Operační systémy
Obsah Soubory
Soubory a zařízení

Vstup a výstup

Aktualizováno 8 min čtení 1 463 slov

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éž.

dd if=/dev/zero of=test bs=1M count=1024               # dvě vteřiny
dd 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) ani to, co přežije výpadek proudu (žurnálování). Předpokládá systémová volání a přerušení.

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:

ls -l /dev/nvme0n1 /dev/ttyUSB0 /dev/urandom
brw-rw---- 1 root disk    259, 0 /dev/nvme0n1
crw-rw---- 1 root dialout 188, 0 /dev/ttyUSB0
crw-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í.

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 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. 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.

lsmod          # načtené moduly a kdo je používá
modinfo nvme   # původ modulu a jeho parametry
lspci -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.

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:

sysctl 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.

sync                               # š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í 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.

cat /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

iostat -xz 1      # ze sysstat; -z vynechá nečinná zařízení
iotop             # který proces čte a zapisuje; jen jako root
vmstat 1          # sloupce bi, bo, wa, b
cat /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
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