-
Notifications
You must be signed in to change notification settings - Fork 0
Procesy
„Program" a „proces" se běžně používají zaměnitelně a je to zdroj nedorozumění, který se táhne až do produkce. Program je soubor na disku. Leží tam, nedělá nic a bude tam ležet i za rok.
Proces je běžící instance toho souboru. Stejný program může běžet padesátkrát naráz a každá instance má vlastní paměť, vlastní stav a vlastní osud. Napiš si ps aux | grep firefox - neuvidíš jeden proces, ale dvacet a víc, jeden rodičovský a k tomu další pro karty, grafiku a zvuk. „Jeden Firefox" je kolonie.
Stránka je o tom, co proces doopravdy je, jak vzniká, jak umírá a co se na tom láme. Neřeší, kdo dostane procesor - to je plánování procesů - ani mechaniku přepínání kontextu. Předpokládá systémová volání a rozdíl mezi režimem jádra a uživatelským.
Tohle je nejdůležitější věc na celé stránce: proces je adresní prostor plus stav procesoru plus tabulka otevřených souborů.
To je celé. Zbytek stránky je jen rozvedení téhle věty.
Adresní prostor je mapa toho, co proces vidí v paměti - kód, data, halda, zásobník, namapované soubory. Detaily má správa paměti a stránkování.
Stav procesoru jsou registry, hlavně ukazatel instrukcí a ukazatel zásobníku. Odchází z procesoru - uloží se. Vrací se - obnoví se.
Tabulka otevřených souborů je pole deskriptorů: 0 vstup, 1 výstup, 2 chyby, dál všechno, co si proces otevřel.
Všechno, co jádro s procesy dělá, je ukládání a obnovování téhle trojice.
Struktura, kde je ta trojice zapsaná, se jmenuje PCB (Process Control Block), v Linuxu struct task_struct. Je v ní PID a PPID, stav, uložené registry, ukazatel na tabulky stránek, tabulka deskriptorů, UID, priorita, naběhaný čas, maska a fronta signálů a ukazatel na rodiče. Na x86-64 vyjde řádově na deset kilobajtů podle konfigurace jádra a ke každému úkolu patří navíc 16 KiB zásobníku pro běh v jádře.
V Linuxu je task_struct na jeden úkol, ne na proces. Proces je skupina úkolů, které sdílejí adresní prostor - proto se procesy i vlákna vytvářejí týmž voláním clone. Přečteš si ji přes cat /proc/1234/status.
Klasických stavů je pět: nový, připravený, běžící, čekající a ukončený. Procesor je jeden na jádro, takže drtivá většina procesů nikdy neběží, jen čeká.
flowchart TD
N[Nový] --> P[Připravený]
P -->|plánovač vybere| B[Běžící]
B -->|vyprší kvantum| P
B -->|čeká na I/O nebo zámek| C[Čekající]
C -->|data dorazila| P
B -->|exit| U[Ukončený, čeká na rodiče]
U -->|rodič zavolá wait| K[Uklizeno]
Nejzajímavější je poslední hrana: proces po exit ještě chvíli existuje. V ps odpovídají stavům písmena ve sloupci STAT:
R - běží nebo je připravený; ps to nerozlišuje.
S - přerušitelný spánek, sem patří skoro všechno.
D - nepřerušitelný spánek uvnitř jádra.
T - zastavený po Ctrl+Z, rozběhne ho kill -CONT.
Z - zombie: skončil a nikdo nepřečetl návratový kód.
I - nečinné jádrové vlákno, do zátěže se nepočítá.
Stav D se vyplatí vysvětlit, protože se o něj lidi pravidelně zabíjejí. Proces sedí v místě jádra, které signály vůbec nekontroluje, typicky uprostřed čtení z disku. kill -9 na něj funguje jen zdánlivě: signál se uloží do fronty a čeká, až se proces vrátí do uživatelského režimu. Dokud operace neskončí nebo neselže, nevrátí se a nezabiješ ho ničím.
Proces v D není zaseknutý program, je to zaseknuté zařízení. Umírající NVMe, odpojený NFS server - hledej příčinu v I/O. Do zátěže se D počítá, takže load average 40 na nevytíženém procesoru je skoro vždycky tohle.
Nový proces vyrobí jedině jiný proces - kromě toho jediného, který nastartuje jádro při startu systému.
fork udělá kopii volajícího procesu. exec v aktuálním procesu zahodí adresní prostor a nahradí ho jiným programem. Spuštění programu je tedy fork a po něm exec.
fork kopíruje adresní prostor, tabulku deskriptorů, pracovní adresář, masku signálů a prostředí. Nekopíruje PID, ostatní vlákna, zámky souborů ani naběhaný čas.
Adresní prostor se přitom doopravdy nekopíruje. Obě strany dostanou tytéž fyzické stránky jen pro čtení a teprve zápis vyrobí soukromou kopii jedné stránky. Tomu se říká copy-on-write a je to důvod, proč jde forknout i proces s osmi gigabajty haldy.
Návratová hodnota je celý trik: potomek dostane nulu, rodič PID potomka. Jedno volání, dva návraty.
pid_t pid = fork();
if (pid == 0) {
close(1); // zavře standardní výstup
open("vystup.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); // dostane deskriptor 1
execlp("ls", "ls", "-l", NULL); // odsud dál je to jiný program
_exit(127); // sem se dojde, jen když exec selhal
}
waitpid(pid, &status, 0); // rodič si vyzvedne návratový kódA tady je pointa. Mezi fork a exec je okamžik, kdy potomek existuje, ale nový program ještě neběží - a v něm se dá s procesem cokoliv udělat. Přesně to dělá shell u ls -l > vystup.txt. Program ls o přesměrování neví a nemusí umět nic než psát na výstup.
Windows nemá fork. Má CreateProcess, které postaví proces a rovnou do něj nahraje program - jedno volání s deseti parametry. Přesměrování se řeší vyplněním struktury STARTUPINFO a povolením dědění deskriptorů.
Ztrácí se ten mezikrok: co nejde vyjádřit parametry CreateProcess, nejde vůbec. Model fork a exec je obecnější, ale platí se za něj kopírováním tabulek stránek.
Když proces skončí, jádro uvolní paměť, zavře soubory a zahodí adresní prostor. Nechá si ale task_struct s návratovým kódem, protože ta informace patří rodiči. Do té doby je proces zombie.
Zombie se nedá zabít, protože už je mrtvý; kill -9 na něj nemá žádný efekt. Uklidí ho jediná věc: rodič zavolá wait nebo waitpid a přečte si návratový kód.
Když rodič skončí dřív než potomek, je z potomka sirotek a jádro ho přepojí pod PID 1, který volá wait ve smyčce. Odtud plyne zdánlivý paradox: hromada zombie zmizí, jakmile zabiješ jejich rodiče.
V kontejneru je PID 1 tvoje aplikace, ne init. Node ani Java o té roli nic nevědí, wait ve smyčce nevolají, a tak se pod nimi zombie hromadí, dokud nevyčerpají kvótu úkolů.
Druhá půlka problému je horší: PID 1 má potlačené výchozí obsluhy signálů. Bez vlastní obsluhy SIGTERM aplikaci docker stop nezabije a čeká deset vteřin na SIGKILL. Řešení je pro obojí stejné - dej jako vstupní bod docker run --init nebo tini. Zbytek má virtualizace a kontejnery.
Každý proces má rodiče, takže procesy tvoří strom. Kořenem je PID 1, na Debianu systemd; podívej se na něj přes pstree -p 1234.
Nad stromem leží druhé členění, o kterém většina lidí neví, dokud je nekopne. Procesy se sdružují do skupin a skupiny do sezení. Jedna roura je jedna skupina - sort data.txt | uniq -c | head jsou tři procesy v jedné skupině a Ctrl+C míří na celou skupinu popředí. Proto se roura zastaví celá.
Sezení je svázané s terminálem. Když spadne SSH, dostane skupina popředí SIGHUP a implicitně umře. Proto existuje nohup, který SIGHUP ignoruje, a setsid, který proces posadí do vlastního sezení. U dlouhoběžících věcí je ale správná odpověď udělat z toho službu systemd.
Signály jsou nejjednodušší způsob, jak procesu něco říct: jedno číslo, žádná data. SIGTERM znamená ukonči se, SIGKILL nejde odchytit, SIGCHLD ohlásí konec potomka. Výklad je na meziprocesové komunikaci.
Prázdný proces stojí v jádře řádově desítky kilobajtů. Triviální céčkový program přidá pár set kilobajtů obsazené paměti a vytvoření vyjde na desítky až stovky mikrosekund.
| Vlastnost | Proces | Vlákno |
|---|---|---|
| Adresní prostor | vlastní | sdílený |
| Tabulka deskriptorů | kopie při fork
|
sdílená |
| Cena vytvoření | desítky až stovky µs | jednotky až desítky µs |
| Pád jedné části | ostatní běží dál | strhne celý proces |
Doporučení: procesy tam, kde spolu části komunikují málo, vlákna jen tam, kde sdílejí velkou datovou strukturu. Nginx obsluhuje spojení procesy, jeden na jádro - pád workeru nepoloží server. Výpočet nad dvougigabajtovou maticí naopak dělej vlákny.
Nejznámější ukázka toho, že proces je zdroj jako každý jiný, se vejde na řádek:
:(){ :|:& };:
Funkce : zavolá dvakrát sama sebe a obě volání pošle na pozadí. Počet procesů roste exponenciálně, tabulka procesů se zaplní a systém přestane reagovat. Zabít to nejde, protože i kill potřebuje ke spuštění fork, který už neprojde.
2. listopadu 1988 vypustil Robert Tappan Morris červa, který se šířil po tehdejším internetu. Ochranu proti opakované nákaze měl, ale schválně děravou - v jednom případě ze sedmi se nainstaloval i tam, kde už kopie běžela. Stroje se plnily kopiemi, dokud nebyly nepoužitelné; postihlo to zhruba 6 000 z nich. Poučení: nepoložila je chyba v kódu, ale nekontrolované vytváření procesů.
Obrana je dvojí a zapni obojí. ulimit -u omezí počet procesů na uživatele, trvale přes /etc/security/limits.conf. Služby a kontejnery hlídá kontrolér pids v cgroups - v systemd TasksMax=, v Dockeru --pids-limit.
systemd-run --user --scope -p TasksMax=50 bash # shell s tvrdým stropem, na vyzkoušeníDémonizace dvojitým fork. Návody pořád ukazují dvojí fork, setsid a zápis PID do souboru. Pod systemd napiš Type=simple a zůstaň na popředí.
kill $(cat /var/run/foo.pid). PID se recykluje a číslo může mezitím dostat někdo jiný; strop ukáže sysctl kernel.pid_max. Používej systemctl stop, v kódu pidfd_open od jádra 5.3 (rok 2019).
| Příznak | Kde je problém |
|---|---|
Proces v D, kill -9 nezabírá |
čeká v jádře na I/O, signál dojde až po návratu |
| Tisíce zombie | rodič nevolá wait
|
| Zombie v kontejneru | aplikace je PID 1 a nesklízí; nasaď --init
|
| Proces po odhlášení zmizel | dostal SIGHUP; chce to nohup nebo službu |
kill zabil něco jiného |
recyklovaný PID, nespoléhej na PID soubory |
ps a top hlásí jiný počet |
ps aux počítá procesy, top -H vlákna |
fork: Resource temporarily unavailable |
vyčerpaný ulimit -u nebo pids.max
|
Tři příkazy pokryjí většinu případů:
ps -eo pid,ppid,stat,wchan:20,comm --sort=stat # kdo v jakém stavu a v čem visí
ps -eo stat= | sort | uniq -c # rozložení stavů, hledej D a Z
cat /proc/1234/status # stav, rodič, počet vláken, signálySloupec wchan říká, ve které funkci jádra proces čeká, a u stavu D je to jediná užitečná informace. Zbytek má nástroje a diagnostika.
Proces je adresní prostor, stav procesoru a tabulka deskriptorů. Zbytek je jen správa téhle trojice.
Program je soubor, proces je jeho instance. Jeden program, padesát procesů, padesát osudů.
fork vrací nulu potomkovi a PID rodiči. Mezikrok před exec je důvod, proč v shellu funguje přesměrování.
Copy-on-write dělá fork použitelným. Paměť se kopíruje až při zápisu a po stránkách.
Zombie není proces, je to nevyzvednutá účtenka. Sklidí ji wait rodiče, nebo po jeho smrti PID 1.
Stav D znamená zaseknuté zařízení. kill -9 je tam ztráta času.
Počet procesů je zdroj a musí mít strop. ulimit -u a pids v cgroups nastav předem.
- Vlákna - co se změní, když si adresní prostor sdílí víc úkolů
- Plánování procesů - kdo z připravených dostane procesor a podle čeho
- Meziprocesová komunikace - signály, roury a sdílená paměť pořádně
- Start systému - odkud se vezme PID 1 a co má na starosti
Našel jsi chybu nebo něco chybí? Založ issue nebo pošli pull request.
Základy
- Co dělá operační systém
- Režim jádra a uživatelský režim
- Systémová volání
- Přerušení a výjimky
- Architektury jádra
Procesy
Souběh
Paměť
Soubory a zařízení
Systém v provozu
Praxe