markdown
Procesy.md
markdown
# Procesy, čili co je to vlastně spuštěný program „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ů](Planovani-procesu) - ani mechaniku [přepínání kontextu](Prepinani-kontextu). Předpokládá [systémová volání](Systemova-volani) a [rozdíl mezi režimem jádra a uživatelským](Jadro-a-uzivatelsky-rezim). ## Tři věci, nic víc 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](Sprava-pameti) a [stránkování](Virtualni-pamet-a-strankovani). **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. ## Co si jádro o procesu pamatuje 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](Vlakna) vytvářejí týmž voláním `clone`. Přečteš si ji přes `cat /proc/1234/status`. ### Stavy procesu a přechody mezi nimi 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á. ```mermaidflowchart 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](Vstup-a-vystup). Do zátěže se `D` počítá, takže load average 40 na nevytíženém procesoru je skoro vždycky tohle. ## Jak proces vzniká Nový proces vyrobí jedině jiný proces - kromě toho jediného, který nastartuje jádro při [startu systému](Start-systemu). ### fork a exec: proč dvě volání a ne jedno `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. ```cpid_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ód``` A 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. ### Co tím Windows ztrácí 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. ## Jak proces končí ### Zombie a sirotci 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. ### Proč v kontejneru zůstávají zombie 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](Virtualizace-a-kontejnery). ## Strom procesů, skupiny a sezení 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](Meziprocesova-komunikace). ## Kolik proces stojí a kdy sáhnout po vlákně 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](Vlakna). ## Fork bomba a proč pořád funguje 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`. ```bashsystemd-run --user --scope -p TasksMax=50 bash # shell s tvrdým stropem, na vyzkoušení``` ## Co se dnes už nedělá **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). ## Diagnostika | 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ů: ```bashps -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 Zcat /proc/1234/status # stav, rodič, počet vláken, signály``` Sloupec `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](Nastroje-a-diagnostika). ## Co si odnést **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. ## Kam dál - **[Vlákna](Vlakna)** - co se změní, když si adresní prostor sdílí víc úkolů- **[Plánování procesů](Planovani-procesu)** - kdo z připravených dostane procesor a podle čeho- **[Meziprocesová komunikace](Meziprocesova-komunikace)** - signály, roury a sdílená paměť pořádně- **[Start systému](Start-systemu)** - odkud se vezme PID 1 a co má na starosti