# Přerušení a výjimky

Zdá se, že systém se v cyklu pořád dokola dívá, jestli něco nepřišlo. Klávesa, paket, dokončený zápis.

Nedívá se. Nemá na to kdy. Při deseti tisících událostech za vteřinu by procesor nedělal nic jiného než obcházení zařízení a ptaní se, jestli už.

Hardware si o pozornost řekne sám. Vytáhne signál, řadič ho doručí procesoru a ten kvůli tomu **uprostřed práce odloží, co zrovna počítá**.

Tahle stránka je o tom, co to „odloží" znamená, proč se obsluha musí vejít do mikrosekund a jak poznáš, že jich přišlo víc, než stroj unese. Předpokládá [režim jádra a uživatelský režim](Jadro-a-uzivatelsky-rezim). Ovladače a DMA řeší [vstup a výstup](Vstup-a-vystup).

## Nikdo si nic nevyzvedává

Tohle je nejdůležitější věc na celé stránce: **přerušení není zpráva, kterou by si někdo vyzvedl. Je to násilné odbočení.** Procesor doběhne rozdělanou instrukci, uloží minimum stavu a skočí na adresu, kterou tam jádro předem zapsalo.

Žádná fronta, ze které se čte. Žádné vlákno, které by čekalo. Kód, který zrovna běžel - klidně tvůj `./program` uprostřed násobení - o tom neví.

Z toho, že se odbočuje **cizímu** kódu a bez jeho souhlasu, plyne zbytek stránky.

## Tři druhy událostí, které se pletou

Procesor odbočí ze tří důvodů. Názvy se pro ně používají zaměnitelně, rozdíl je podstatný.

| Druh | Kdo to způsobil | Opakovatelné | Co se s tím dělá |
|---|---|---|---|
| **Přerušení (IRQ)** | zařízení zvenku, asynchronně | ne, přijde kdy chce | obsluha ovladače, potvrzení řadiči |
| **Výjimka / trap** | instrukce, která právě běží | ano, stejný kód znovu | jádro to napraví, nebo přijde signál |
| **Softwarové přerušení** | program sám, záměrně | ano | řízený vstup do jádra |

**Přerušení nemá s běžícím kódem nic společného.** Trefí náhodné vlákno na náhodném jádře a to za to nemůže.

**Výjimka je důsledek instrukce.** Dělení nulou skončí signálem `SIGFPE`. Výpadek stránky vypadá stejně, ale jádro ho tiše vyřídí a **instrukci spustí znovu od začátku** - viz [virtuální paměť](Virtualni-pamet-a-strankovani). Ladicí bod je taky výjimka: debugger přepíše bajt instrukcí `int3`.

**Softwarové přerušení je dobrovolné.** To jsou [systémová volání](Systemova-volani). Na x86-64 už o přerušení technicky nejde: instrukce `syscall` tabulku nepotřebuje. Staré `int 0x80` je jen zpětná kompatibilita, ale v návodech přežívá dál.

### Vektor a tabulka

Adresa se nehledá. Je v **tabulce vektorů** - na x86-64 se jí říká IDT, má **256 položek** a číslo události je index do ní.

Vektory **0-31** má zabrané procesor pro své výjimky:

- `0` - dělení nulou
- `2` - nemaskovatelné přerušení (NMI)
- `3` - ladicí bod (`int3`)
- `6` - neplatná instrukce
- `13` - obecná ochrana
- `14` - výpadek stránky

Zbytek, `32-255`, rozděluje jádro mezi zařízení.

```bash
cat /proc/interrupts     # sloupce jsou jádra, poslední sloupec jméno ovladače
```

Čísla v prvním sloupci **nejsou vektory**, ale Linuxové číslování IRQ. Podstatné je, který řádek roste a kde.

## Co se stane, když linka škubne

Procesor kontroluje přerušení mezi instrukcemi, ne uprostřed nich.

1. **Doběhne rozdělaná instrukce**, nedokončený výsledek se zahodí.
2. **Uloží se minimum stavu** - čítač instrukcí, příznaky, segmenty.
3. **Přepne se na jádrový zásobník.** Uživatelskému se nedá věřit.
4. **Zakážou se další přerušení** na tomhle jádře - průchod branou shodí `IF`.
5. **Zavolá se obsluha**, dosaví registry, udělá práci a potvrdí řadiči převzetí.
6. **Instrukcí `iret` se skočí zpátky** přesně tam, kde se přestalo.

Šestý krok není detail: při návratu jádro kontroluje, jestli nevznikl důvod přepnout na jiné vlákno. Do [plánovače](Planovani-procesu) se dostane jedině vstupem do jádra - přes přerušení, výjimku nebo systémové volání. **Bez tikajícího časovače by nekonečná smyčka procesor nikdy nepustila.** Co [přepnutí kontextu](Prepinani-kontextu) stojí, je jinde.

### Proč obsluha musí být krátká

Obsluha běží s vypnutými přerušeními a na cizím zásobníku. Každá mikrosekunda navíc je latence, kterou zaplatí někdo nevinný.

Řešením je **horní a dolní polovina**. Horní udělá jen to, co nepočká: potvrdí zařízení, přečte stav, uloží data do fronty.

| Dolní polovina | Kde běží | Smí spát | Kdy ji použít |
|---|---|---|---|
| Softirq | pevná sada v jádře | ne | sám ji nepřidáš |
| Tasklet | nad softirq | ne | nové nepiš, je na odchod |
| Workqueue | jádrové vlákno | ano | práce čekající na zámek nebo paměť |
| **Threaded IRQ** | vlastní jádrové vlákno | ano | **výchozí volba** |

Softirq jsou pevně dané (`NET_RX`, `TIMER`, `BLOCK`) a jejich přetečení odbaví `ksoftirqd`. Tasklety jsou nadstavba na odpis - v návodech pořád jsou, nový kód je nepoužívá.

**Piš threaded IRQ.** Ovladač si přes `request_threaded_irq()` řekne o vlastní vlákno, horní polovina se scvrkne na tři řádky a zbytek běží v kontextu procesu, kde se smí spát. V Linuxu je to od jádra 2.6.30 (2009).

### Co se v obsluze nesmí

**Nesmíš spát.** Není koho uspat - obsluha si vypůjčila kontext náhodného vlákna. Skončí to zatuhnutím nebo panikou jádra.

**Nesmíš alokovat s čekáním.** `GFP_KERNEL` smí zablokovat a čekat na uvolnění paměti, takže patří jen `GFP_ATOMIC` - a ten může selhat.

**Nesmíš vzít mutex.** Mutex uspává, což je zpátky u prvního bodu. Zbývá spinlock ve variantě, která zároveň zakáže přerušení na daném jádře. Jinak si vyrobíš [uváznutí](Uvaznuti) sám se sebou: proces drží zámek, přijde přerušení na stejné jádro a obsluha čeká na zámek, který nikdo nepustí. Podrobnosti u [zámků](Kriticka-sekce-a-zamky).

### Maskování a nemaskovatelné přerušení

Instrukce `cli` shodí příznak `IF`, `sti` ho zvedne. Zakáže se tím doručování maskovatelných přerušení, ale **jen na jádře, které instrukci provedlo**. Ostatních sedm běží dál a klidně vlezou do stejné kritické sekce.

To je omyl z dob jednojádrových strojů, kdy `cli` zastavilo celý stroj. Globální zákaz Linux zrušil v řadě 2.5. Proti ostatním jádrům je zámek, ne `cli`.

**NMI se zakázat nedá.** Má vektor 2 a projde i přes `cli`. Je na to, kde je potřeba signál, který nejde umlčet: hlídací pes na zatuhlé jádro (`/proc/sys/kernel/nmi_watchdog`), profiler a hlášení hardwarových chyb.

## Sdílená IRQ a proč se dnes prakticky nesdílejí

Na starém PC bylo patnáct použitelných linek a karet víc. Sdílení linky znamenalo, že se zavolaly **všechny** obsluhy na ní a každá se musela zeptat „byls to ty?" - tedy číst z registrů přes sběrnici. Jeden vadný ovladač pokazil ostatní.

**MSI to zrušilo.** Zařízení místo tahání za drát zapíše hodnotu na vyhrazenou adresu a tím si řekne o vlastní vektor. MSI umí 32 vektorů na zařízení, MSI-X 2048, takže síťovka má vektor na každou frontu. `IRQF_SHARED` v ukázkách najdeš, ale na PCIe není co sdílet.

## Kam přerušení chodí a kdy ho přišpendlit

Rozdělování mezi jádra má na Debianu na starost démon `irqbalance` a dělá rozumnou věc. Přišpendlit ručně má smysl u úlohy s tvrdou latencí, která potřebuje čistá jádra, nebo když jednofrontová karta zavaluje jádro s něčím důležitým.

```bash
sudo systemctl stop irqbalance                     # jinak ti to démon do minuty vrátí zpátky
cat /proc/irq/24/smp_affinity_list                 # na kterých jádrech smí IRQ 24 běžet
echo 3 | sudo tee /proc/irq/24/smp_affinity_list   # napevno na jádro 3
```

## Kdy je dotazování lepší než přerušení

Deset gigabitů za vteřinu je při plných rámcích zhruba **812 tisíc paketů za vteřinu**, při nejmenších **14,88 milionu**. Jedno přerušení na paket je nesmysl: při statisících přerušení na jádro nezbude výkon na nic jiného.

Proto se to při zátěži otočí. **NAPI** na první paket přerušení té fronty **vypne** a data odebírá v cyklu, dokud mu nedojde rozpočet. Když fronta vyschne, přerušení zase zapne.

```bash
sysctl net.core.netdev_budget net.core.netdev_budget_usecs   # výchozí 300 paketů / 2000 µs
ethtool -c enp1s0                                            # koalescence: kolik smí karta odkládat
```

**Přerušení jsou lepší při nízké zátěži, dotazování při vysoké.** Nečinné přerušení nestojí nic, dotazování žere jádro naprázdno. NAPI přepíná automaticky.

## Bouře přerušení: stroj, který žije a nic nedělá

Nejzákeřnější porucha nevypadá jako porucha. Zařízení drží linku aktivní, obsluha se zavolá, nic nezmění, vrátí se - a linka je pořád aktivní, takže se zavolá znova. Do nekonečna.

Stroj přitom odpovídá na ping, `uptime` ukazuje nízkou zátěž a přihlásíš se přes SSH. Jen se nic neděje - zátěž je nízká právě proto, že **nikdo neběží**.

Jádro má pojistku: když na jedné lince napočítá 99 900 nepřevzatých přerušení ze sta tisíc, vypíše do logu `irq 16: nobody cared` a linku **vypne**.

**Přestupná sekunda, 30. června 2012.** Vložení sekundy nechalo v jádře nesrovnaný základ pro časovače, takže časovače s absolutním koncem expirovaly okamžitě a pořád dokola. Vlákna čekající s časovým limitem se budila ve smyčce a procesory vyskočily na sto procent. Servery reagovaly a nic nedělaly - odneslo to mimo jiné Reddit a odbavování Qantasu. Léčilo se to přenastavením hodin příkazem `date -s "$(date)"`, bez restartu. **Poučení: stroj, který odpovídá, nemusí nic dělat.**

## Diagnostika

Sleduj, jak čísla rostou v čase, ne jak vypadají teď.

```bash
watch -n1 'grep -E "nvme|enp" /proc/interrupts'   # roste jen jeden sloupec? IRQ visí na jednom jádře
vmstat 1                                          # in = přerušení/s, cs = přepnutí kontextu/s
cat /proc/softirqs                                # měkká přerušení podle typu a jádra
sudo dmesg | grep -i irq                          # nobody cared, chyby ovladačů
```

| Příznak | Kde je problém |
|---|---|
| Jedno jádro na 100 % v `si` | měkká přerušení jedné fronty na jednom jádře |
| Rostoucí `hi` v `top` | moc tvrdých přerušení, nasaď koalescenci |
| `nobody cared` a mrtvé zařízení | obsluha linku nezhasla, jádro IRQ vypnulo |
| `in` ve statisících, `cs` nízké | bouře, na plánování nezbývá čas |
| Řádek v `/proc/interrupts` má nuly | obsluha se nezaregistrovala |
| Ztráty paketů a `ksoftirqd` na stropě | vyčerpaný rozpočet NAPI |

Širší souvislosti jsou na [nástrojích a diagnostice](Nastroje-a-diagnostika).

## Co si odnést

**Přerušení je odbočení, ne zpráva.** Nikdo si ho nevyzvedává, je do kódu vražené zvenku.

**Výjimka je synchronní a opakovatelná, přerušení není.** Výjimku umíš reprodukovat, přerušení ne.

**V horní polovině nesmíš spát, alokovat s čekáním ani brát mutex.** Zbývá spinlock.

**Nové obsluhy se píší jako threaded IRQ.** Vlastní vlákno, kontext procesu, žádné tasklety.

**`cli` platí na jedno jádro.** Proti ostatním chrání zámek.

**Při vysoké zátěži vyhrává dotazování.** Milion přerušení za vteřinu se odbavit nedá, od toho je NAPI.

**Bouře přerušení vypadá jako zdravý stroj.** Nízká zátěž, plná odezva, nulová práce.

## Kam dál

- **[Vstup a výstup](Vstup-a-vystup)** - co obsluha obsluhuje: ovladače, DMA, page cache
- **[Přepínání kontextu](Prepinani-kontextu)** - co stojí přepnutí, které obsluha spustila
- **[Systémová volání](Systemova-volani)** - dobrovolný vstup do jádra
- **[Kritická sekce a zámky](Kriticka-sekce-a-zamky)** - proč je v obsluze jediná možnost spinlock
