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

```bash
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](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`:

```bash
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í](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.

```bash
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](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:

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

```bash
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í](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.

```bash
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

```bash
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](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čí
