Operační systémy
Obsah Soubory
Procesy

Přepínání kontextu

Aktualizováno 7 min čtení 1 228 slov

Přepínání kontextu a co doopravdy stojí

Na osmi jádrech ti běží dvě stě procesů a všechny vypadají, že běží současně. Neběží. Procesor dělá v každém okamžiku nejvýš osm věcí a mezi zbytkem přepíná tak rychle, že to nepoznáš.

Ta iluze má cenu a platí se strojovým časem, ze kterého nemá nikdo nic. Když ji přeženeš - postavíš proti osmi jádrům dvě stě pracovních vláken - stroj stráví víc času přepínáním než prací. Vytížení procesoru ukáže sto procent a hotového bude míň.

Tahle stránka říká, co se při přepnutí doopravdy stane, kolik to stojí a jak si tu cenu změříš. Neřeší, koho plánovač vybere jako dalšího - to je plánování procesů. Předpokládá procesy a vlákna a vyplatí se mít přečtenou virtuální paměť, protože podstatná část ceny je schovaná právě tam.

Co se ukládá a co obnovuje

Přepnutí je výměna obsahu procesoru. Uloží se čítač instrukcí, aby proces věděl, kam se vrátit, ukazatel zásobníku, stavový registr s příznaky a univerzální registry. Mezi dvěma procesy k tomu přibývá cr3, registr s fyzickou adresou kořene tabulek stránek - tedy celý adresní prostor naráz.

Jádro to neskládá registr po registru. Většina jich už leží na zásobníku jádra z toho, jak se do jádra vstoupilo - odložil je tam vstupní kód přerušení nebo systémového volání. Vlastní přepnutí pak prohodí ukazatele zásobníků, doplní hrstku registrů a přepíše cr3.

Zvlášť se přenáší stav plovoucí čárky a vektorových registrů instrukcemi XSAVE a XRSTOR - u AVX-512 přes dva kilobajty na vlákno. I tak jde o stovky nanosekund práce v teplé paměti.

Nejdražší je to, co po přepnutí zůstane studené

Tohle je nejdůležitější věc na celé stránce: přepnutí kontextu není uložení registrů. Registry jsou na celé operaci to nejlevnější. Draho stojí to, co po přepnutí zůstane studené - vyrovnávací paměti a TLB.

Proces, který doteď běžel, měl v L1 a L2 svoje data a v L3 megabajty svojí pracovní množiny. Ten po něm tam najde cizí obsah. Každý minutý přístup stojí cestu do RAM, tedy zhruba 80 ns, a takových přístupů jsou tisíce.

Stejně dopadne TLB, vyrovnávací paměť překladů adres. Má řádově tisíce položek a při zápisu do cr3 se zahazuje, protože její obsah patřil jinému adresnímu prostoru. Než se naplní, platí každý první dotek stránky průchodem tabulkami stránek.

Přímá cena se dá spočítat. Nepřímou platíš ještě dlouho poté, co je přepnutí dávno hotové.

Vlákno téhož procesu, nebo cizí proces

Rozdíl mezi těmi dvěma případy je hlavní praktický důsledek předchozí sekce.

Co se mění Vlákno téhož procesu Jiný proces
Registry a zásobník ano ano
cr3 a tabulky stránek ne ano
TLB zůstává platná zahodí se, pokud nepomůže PCID
L1 a L2 část přežije, data jsou společná vychladne

Tohle je skutečný důvod, proč jsou vlákna „levnější" než procesy. Ne že by šla rychleji vytvořit, ale že přepnutí mezi nimi nechá adresní prostor na pokoji.

PCID tu ztrátu zmírňuje, neruší ji. Je to značka u položek TLB, která odliší překlady dvou adresních prostorů, takže se zahazovat nemusí; Linux ji na x86-64 používá od jádra 4.14 (2017). Aktivních značek si ale jádro drží na jádro jen několik, takže kolotoč desítek procesů se do nich nevejde.

Že je změna cr3 vidět v penězích, ukázal rok 2018. Po zveřejnění Meltdownu přibylo do jádra oddělení tabulek stránek jádra od uživatelských (KPTI, Linux 4.15) a cr3 se od té doby přepisuje i při každém vstupu do jádra. U zátěží s hodně systémovými voláními se hlásily propady od jednotek procent po desítky, nejhůř na strojích bez PCID. Poučení: za přepnutí adresního prostoru se neplatí v nanosekundách, ale v procentech výkonu celé serverovny.

Kdy k přepnutí vůbec dojde

Situací je pět:

  • vypršelo časové kvantum, které procesu přidělil plánovač
  • proces se zablokoval na vstupu nebo výstupu
  • přišlo přerušení a probudilo něco přednějšího
  • proces skončil
  • proces se procesoru sám vzdal, typicky čekáním na zámek nebo voláním sched_yield()

Všech pět má společné jedno a je to podstatnější než ten výčet: přepnutí může vyvolat jedině přerušení nebo systémové volání. Jinak jádro neběží a nemá jak zasáhnout. Program zacyklený v čistém výpočtu odejde z procesoru až s tikem časovače.

Časové kvantum dnes není konstanta a ze sysctl se nenastavuje. Návody z doby jádra 3.x radí zvětšit kernel.sched_min_granularity_ns. V jádře 6.12 takový přepínač není - s plánovačem EEVDF (jádro 6.6, rok 2023) ho nahradilo ladicí rozhraní v /sys/kernel/debug/sched/ a délku úseku si plánovač počítá sám.

Kolik to stojí a jak to změříš

Přímá cena

Uložení stavu, průchod plánovačem a obnovení vyjdou na jednotky mikrosekund. Pro srovnání: obyčejné systémové volání stojí 50-100 ns, s mitigacemi Spectre a Meltdown i 500 ns. Přepnutí je tedy o řád dražší než vstup do jádra a zpátky.

Nepřímá cena

Zahřát zpátky vyrovnávací paměti a TLB stojí klidně řádově víc než přepnutí samo. Přímo se to změřit nedá; poznáš to tak, že tentýž kód po návratu na procesor běží pomaleji než před odchodem.

Počty přepnutí ti řeknou tři nástroje, každý na jiné úrovni; víc o nich je na stránce o nástrojích a diagnostice.

vmstat 1                  # sloupec cs: přepnutí za vteřinu přes celý stroj
pidstat -w 1              # totéž po procesech, ze sysstat; cswch/s a nvcswch/s
perf stat -e context-switches,cpu-migrations ./program   # jen za dobu běhu programu

Dobrovolně, nebo pod nátlakem

Nejužitečnější číslo ale nedává ani jeden z nich. Dává ho jádro u každého procesu zvlášť a dělí přepnutí na dva druhy:

grep ctxt_switches /proc/1234/status   # dvě čísla, obě od startu procesu

Vysoké nonvoluntary_ctxt_switches znamená rvačku o procesor. Proces byl práceschopný a jádro mu procesor sebralo, protože o něj stálo něco jiného. Ubírej vlákna, nepřidávej je.

Vysoké voluntary_ctxt_switches znamená čekání na vstup a výstup. Proces se procesoru vzdal sám, neměl co počítat. Jádra navíc nepomůžou, problém je na disku, na síti nebo na zámku.

Poměr těch dvou čísel řekne o procesu víc než průměrné vytížení procesoru, protože rozliší „nestíhá" od „čeká".

Migrace mezi jádry a připoutání

Migrace je přepnutí, po kterém proces pokračuje na jiném jádře. L1 a L2 má každé jádro vlastní, takže se z nich nepřenese nic, a mezi paticemi zmizí i sdílená L3. Proto je horší než obyčejné přepnutí, i když se v číslech tváří stejně.

Připoutat proces na konkrétní jádro umí taskset:

taskset -c 2 ./program    # program poběží výhradně na jádře číslo 2
taskset -cp 2 1234        # totéž pro už běžící proces s PID 1234

Pomůže to jedné dlouho běžící úloze, které jde o latenci, a jen když jí to jádro zároveň uklidíš. Když na stejné jádro připoutáš tři procesy, uškodíš si: proces bude čekat na obsazeném jádře, zatímco vedle se sedm jader nudí, protože je plánovač nesmí použít.

V kontejneru je přepínání úplně stejné, protože kontejner je pořád obyčejný proces. Ve virtuálním stroji přibývá patro navíc: hostitel přepíná vlákna virtuálních procesorů a host o tom neví.

Co se na tom nejčastěji rozbije

Příznak Kde je problém
Desítky tisíc cs za vteřinu na osmi jádrech víc práceschopných vláken než jader, nebo příliš jemné zamykání
Vysoké sy, nízké us ve vmstat práci dělá jádro - přepínání, systémová volání, obsluha přerušení
Výkon spadne po přidání vláken vlákna se předhánějí o táž jádra, každé přepnutí vychladí mezipaměť
Latence roste při stejné zátěži migrace mezi jádry nebo cizí zátěž na stroji
Vysoké nvcswch/s u jednoho procesu rvačka o procesor, proces je odsouván
Vysoké cswch/s a nízké vytížení jader čeká se na vstup a výstup

Praktická pravidla

Počet práceschopných vláken drž blízko počtu jader. Osm jader neobslouží dvě stě vláken lépe než osmnáct, jen u toho víc přepíná.

Nejdřív se podívej, jestli jsou přepnutí dobrovolná, nebo ne. Rozhoduje to, jestli ubrat vlákna, nebo hledat problém na disku a na síti. Bez toho laduješ naslepo.

Co se často střídá, drž ve stejném adresním prostoru. Dvě vlákna si ušetří zahození TLB, které dva procesy platí při každém přepnutí.

Připoutání na jádro nasazuj až jako poslední krok. Plánovač to skoro vždycky umí líp a při špatném odhadu si jím výkon rozbiješ.

Neškrť přepínání laděním plánovače. Přepnutí je následek, ne příčina. Sniž počet souběžných vláken nebo zkrať kritické sekce a cs spadne samo.