markdown
Vstup-a-vystup.md
markdown
# Vstup a výstup: ovladače, DMA a vyrovnávací paměti Zapíšeš gigabajt na disk a příkaz skončí za dvě vteřiny. Pět set megabajtů za vteřinu, sedí to na papíře i na specifikaci disku. Pak spustíš ten samý příkaz s `oflag=direct` a trvá pětkrát dýl. Na disku je přitom v obou případech totéž. ```bashdd if=/dev/zero of=test bs=1M count=1024 # dvě vteřinydd if=/dev/zero of=test bs=1M count=1024 oflag=direct # a teď poctivě``` Rozdíl není v disku ani v `dd`. Poprvé jsi lhal sám sobě: data byla v paměti, ne na disku. `dd` skončilo, když je převzalo jádro, ne když je potvrdilo zařízení. Tahle stránka je o tom, kudy data mezi programem a železem tečou a kde se zdržují. Neřeší uspořádání souborů ([souborový systém](Souborovy-system)) ani to, co přežije výpadek proudu ([žurnálování](Zurnalovani-a-konzistence)). Předpokládá [systémová volání](Systemova-volani) a [přerušení](Preruseni-a-vyjimky). ## Tři vyrovnávací paměti, ne jedna Tohle je nejdůležitější věc na celé stránce: **mezi tvým programem a plotnou nebo čipem stojí nejméně tři vyrovnávací paměti - v knihovně, v jádře a v samotném zařízení.** Skoro každé překvapení kolem vstupu a výstupu je v tom, že jsi na jednu z nich zapomněl. | Vrstva | Kde je | Kdo ji vyprázdní | Čím ji obejdeš ||---|---|---|---|| Buffer v libc | v procesu | `fflush()`, zaplnění | `write()` napřímo || Page cache | v jádře, v RAM | zpětný zápis, `fsync()` | `O_DIRECT` || Cache zařízení | uvnitř disku | FLUSH z `fsync()` | vypnutá zápisová cache | Když `printf` nic nevypíše, je to první řádek. Když je druhé čtení souboru okamžité, je to druhý. Když po výpadku proudu chybí zapsaná data, je to třetí. ## Znakové a blokové zařízení Zkus `ls -l /dev`: ```bashls -l /dev/nvme0n1 /dev/ttyUSB0 /dev/urandombrw-rw---- 1 root disk 259, 0 /dev/nvme0n1crw-rw---- 1 root dialout 188, 0 /dev/ttyUSB0crw-rw-rw- 1 root root 1, 9 /dev/urandom``` První písmeno je typ: **`c` je znakové zařízení, `b` blokové.** Místo velikosti jsou tam dvě čísla - **major** říká, který ovladač zařízení obsluhuje, **minor** které konkrétní zařízení to je. **Znakové zařízení je proud.** `/dev/ttyUSB0` ani `/dev/urandom` nemají „blok číslo 4 000", nedá se v nich skákat a jádro pro ně nedrží mezipaměť. **Blokové zařízení má adresovatelné bloky pevné velikosti.** `/dev/nvme0n1` přečte libovolný blok kdykoli, a proto se vyplatí si přečtené pamatovat. Data v tom souboru nejsou, drží se jen typ a dvojice čísel - zbytek řeší [VFS a montování](VFS-a-montovani). ## Cesta dat ze zařízení do paměti ### Dotazování, přerušení a DMA | Způsob | Kdo přesouvá data | Cena pro procesor | Kdy se hodí ||---|---|---|---|| Dotazování (polling) | procesor v cyklu | celé jádro po dobu čekání | velmi krátká čekání || Přerušení | procesor v obsluze | přepnutí kontextu na událost | **řídké události** || DMA | řadič zařízení sám | jedno přerušení na konci | **každý větší přenos** | **DMA není alternativa k přerušení, je jeho doplněk.** Ovladač zapíše řadiči fyzickou adresu a délku, řekne „jeď" a uspí se. Řadič si data přesune do paměti sám, bez účasti procesoru, a přerušením ohlásí až konec. Proto **kopírování gigabajtu z disku stojí procesor pár desítek mikrosekund**. Zbytek udělalo železo. ### Proč DMA obchází ochranu paměti Řadič nepíše do virtuálních adres, píše do fyzických. Nejde přes MMU, takže **stránkovací tabulky ani [oddělení jádra a uživatelského režimu](Jadro-a-uzivatelsky-rezim) na něj neplatí**. Špatné číslo v jeho registru znamená zápis kamkoli do RAM včetně kódu jádra. Proto existuje IOMMU (Intel VT-d, AMD-Vi): MMU pro zařízení. Každé dostane vlastní tabulku a smí jen tam, kam mu ovladač dovolil. Bez ní se nedá bezpečně předat zařízení do virtuálního stroje. ## Ovladač je překladatel, který běží v jádře Ovladač převádí jednotnou sadu operací - `open`, `read`, `write`, `ioctl` - na registry konkrétního kusu železa. Jádro tvůj disk nezná, zná blokové zařízení a věří, že si ovladač poradí. **Chyba v ovladači je chyba v jádře.** V monolitickém jádře běží ovladač na stejné úrovni oprávnění jako plánovač, viz [architektury jádra](Architektury-jadra). Neexistuje segfault modulu, existuje panika stroje. Devatenáctého července 2024 rozeslal CrowdStrike vadný soubor pro svého agenta, který na Windows běží jako ovladač v režimu jádra. Následovalo čtení mimo rozsah a **zhruba 8,5 milionu strojů na modré obrazovce**. Kód uvnitř jádra nemá jak selhat jen sám za sebe. ```bashlsmod # načtené moduly a kdo je používámodinfo nvme # původ modulu a jeho parametrylspci -k # který ovladač obsluhuje které zařízení``` ## Co si jádro nechává: page cache ### Každý blok zůstane, každý zápis chvíli počká **Každý přečtený blok zůstane v paměti, dokud je místo, a každý zapsaný tam chvíli počká.** Proto je druhé čtení téhož souboru okamžité a proto `free` ukazuje málo volné paměti - `buff/cache` se uvolní, jakmile je potřeba, viz [správa paměti](Sprava-pameti). Zapsaná stránka se označí jako špinavá a na disk ji dostanou až vlákna zpětného zápisu, v `ps` viditelná jako `kworker`. Řídí to dvě hranice: ```bashsysctl vm.dirty_background_ratio # bývá 10 - odtud zápis na pozadísysctl vm.dirty_ratio # bývá 20 - odtud zapisující proces zablokuje``` Nad druhou hranicí jádro přestane předstírat a `write()` se zastaví, dokud se stránky nevyprázdní. Odtud ten zámrz na vteřinu po zkopírování něčeho velkého. ```bashsync # špinavé stránky se nezahazují, nejdřív je dopišecho 3 > /proc/sys/vm/drop_caches # vyhodí page cache; jen jako root``` Na produkci to nedělej - zahodíš mezipaměť celého stroje a pár minut bude všechno pomalé. ### Kdy mezipaměť obejít `O_DIRECT` pošle DMA rovnou do bufferu tvého procesu a page cache přeskočí. Platíš za to zarovnáním: buffer, offset i délka musí být násobky logické velikosti bloku, jinak dostaneš `EINVAL`. Používají ho databáze, které si dělají vlastní mezipaměť a nechtějí data v paměti dvakrát, typicky InnoDB v MySQL. PostgreSQL naopak na page cache staví záměrně. **Pro běžný program je `O_DIRECT` chyba, ne optimalizace.** Zahodíš čtení napřed, sdílení stránek mezi procesy i slučování zápisů a dostaneš za to požadavek na zarovnání. ## Blokující, neblokující a asynchronní vstup a výstup ### Od `read`, který čeká, k `epoll` Výchozí `read()` blokuje: proces se uspí a plánovač dá procesor někomu jinému. `O_NONBLOCK` to změní. `read()` se vrátí hned, buď s daty, nebo s `-1` a `errno` nastaveným na `EAGAIN` - to není chyba, to je „zatím nic". U obyčejných souborů na disku ale `O_NONBLOCK` nedělá nic a čtení zablokuje tak jako tak. S tisíci deskriptory se ptáš na všechny naráz. `select` to umí, jenže má pevný strop `FD_SETSIZE`, tedy **1024 deskriptorů**, a sady musíš před každým voláním sestavit znovu. `poll` strop nemá, zato pokaždé projde celý seznam. `epoll` (od jádra 2.5.44, rok 2002) drží sadu uvnitř jádra a vrací jen to, co se stalo. ### io_uring `io_uring` (od jádra 5.1, rok 2019) je dnešní odpověď: dva sdílené kruhové prstence mezi procesem a jádrem, na požadavky a na výsledky. V dobrém případě k tomu nepotřebuješ ani [systémové volání](Systemova-volani) a na rozdíl od `epoll` to umí i disk. **Doporučení: `epoll` na síť, `io_uring` na disk, kde jde opravdu o výkon, `select` nikdy.** Není ale všude - Google `io_uring` v roce 2023 kvůli sérii zranitelností vypnul na svých produkčních strojích, v Androidu i v ChromeOS. ## Plánování požadavků na disk, které se dnes už nedělá U plotnového disku rozhodovalo vystavení hlavy. Přeuspořádat frontu podle pozice na plotně a projet ji jako výtah - jedním směrem, cestou obsloužit vše - šetřilo desítky milisekund na dávce. Linux na to měl plánovače `cfq`, `deadline` a `noop`. U NVMe to neznamená skoro nic. Zařízení nemá hlavu, má vlastní fronty a tisíce operací zpracovává souběžně. Přeuspořádání v jádře už jen přidává latenci. ```bashcat /sys/block/nvme0n1/queue/scheduler # [none] mq-deadline kyber bfq``` V hranatých závorkách je aktivní volba. **U NVMe je `none` správně a není to nedbalost.** `mq-deadline` patří k SATA a plotnovým diskům, `bfq` na desktop. Staré jednofrontové plánovače jádro odstranilo ve verzi 5.0 (rok 2019) i s původní blokovou vrstvou. Návody, které radí `echo noop > .../scheduler`, jsou starší a dnes skončí chybou. ## IOPS versus propustnost - **Propustnost** - megabajty za vteřinu. Rozhoduje u velkých souvislých přenosů.- **IOPS** - počet operací za vteřinu. Rozhoduje u malých náhodných přístupů. Plotnový disk musí na náhodný přístup vystavit hlavu a počkat na otáčku, dohromady kolem 10 ms, tedy **zhruba 100 IOPS**. Souvisle přitom čte stovky megabajtů za vteřinu. NVMe zvládne **statisíce IOPS** a gigabajty za vteřinu. Odtud to, co vidíš v praxi: **databáze, která na NVMe létá, se na plotnovém disku zadrhne, i když rozdíl v propustnosti není zdaleka tak velký jako rozdíl v IOPS.** Dělá totiž malé náhodné zápisy do žurnálu a indexů, ne souvislé streamy. ## Diagnostika ```bashiostat -xz 1 # ze sysstat; -z vynechá nečinná zařízeníiotop # který proces čte a zapisuje; jen jako rootvmstat 1 # sloupce bi, bo, wa, bcat /proc/pressure/io # PSI, od jádra 4.20 (rok 2018)dd if=/dev/zero of=test bs=1M count=1024 oflag=direct # poctivé měření zápisu``` `%util` je podíl času, kdy mělo zařízení rozdělaný aspoň jeden požadavek. **U NVMe to není míra vytížení** - sto procent znamená „pořád něco dělá", ne „už nemůže". `await` je průměrná doba požadavku včetně fronty, `aqu-sz` průměrná délka fronty. Ve `vmstat` jsou `bi` a `bo` přečtené a zapsané bloky za vteřinu, `wa` procento času, kdy procesor **čeká na disk a nemá co dělat**, a `b` počet procesů v nepřerušitelném spánku. | Příznak | Kde je problém ||---|---|| vysoké `wa`, nízké `us` | úzké hrdlo je disk, ne procesor || `%util` 100 % a malá propustnost | mnoho malých náhodných operací || zápis rychlý, pak vteřina zámrzu | dosažen `vm.dirty_ratio`, vyprazdňují se špinavé stránky || proces trvale ve stavu `D` | čeká v jádře na I/O, `kill -9` nezabere, viz [procesy](Procesy) || druhé měření desetkrát rychlejší | měříš page cache; přidej `oflag=direct` | ## Co si odnést **Mezi programem a železem stojí tři vyrovnávací paměti.** V knihovně, v jádře a v zařízení. **Rychlý zápis obvykle znamená, že jsi změřil RAM.** Poctivé měření má `oflag=direct` nebo `fsync` na konci. **DMA obchází ochranu paměti, protože píše do fyzických adres.** Proto existuje IOMMU. **Chyba v ovladači je chyba v jádře.** CrowdStrike 2024, 8,5 milionu strojů. **`O_DIRECT` je nástroj databází, ne optimalizace tvého programu.** **`select` nepoužívej.** `epoll` na síť, `io_uring` na disk. **U disku měř IOPS, ne jenom megabajty.** Náhodné operace rozhodují o tom, jestli poběží databáze. ## Kam dál - **[Přerušení a výjimky](Preruseni-a-vyjimky)** - jak zařízení ohlásí dokončený DMA přenos- **[Správa paměti](Sprava-pameti)** - odkud se bere paměť, kterou si page cache bere- **[Žurnálování a konzistence](Zurnalovani-a-konzistence)** - co `fsync` doopravdy slibuje- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - zbytek arzenálu, když `iostat` nestačí