Operační systémy
Obsah Soubory
markdown

Prepinani-kontextu.md

9.4 kB 126 řádků Změněno Zobrazit na GitHubu Stáhnout
markdown
# 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ů](Planovani-procesu). Předpokládá [procesy](Procesy) a [vlákna](Vlakna) a vyplatí se mít přečtenou [virtuální paměť](Virtualni-pamet-a-strankovani), 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í](Preruseni-a-vyjimky). 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č](Planovani-procesu)- 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í](Systemova-volani) 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](Nastroje-a-diagnostika). ```bashvmstat 1                  # sloupec cs: přepnutí za vteřinu přes celý strojpidstat -w 1              # totéž po procesech, ze sysstat; cswch/s a nvcswch/sperf 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: ```bashgrep 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`: ```bashtaskset -c 2 ./program    # program poběží výhradně na jádře číslo 2taskset -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](Virtualizace-a-kontejnery) 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.