Operační systémy
Obsah Soubory
Základy

Přerušení a výjimky

Aktualizováno 8 min čtení 1 440 slov

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. Ovladače a DMA řeší vstup a výstup.

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ěť. 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í. 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í.

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

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.

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.

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

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.

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