markdown
Meziprocesova-komunikace.md
markdown
# Meziprocesová komunikace Napíšeš `ps aux | grep nginx` a nepřijde ti na tom nic zvláštního. Přitom jsi právě spustil dva procesy s oddělenou pamětí, propojil je kanálem, který existuje jen v jádře, a spolehl se na to, že první skončí a druhý to pozná. Roura je nejpoužívanější meziprocesová komunikace na světě a nikdo o ní takhle nepřemýšlí. Píše se jedním znakem, takže vypadá jako gramatika shellu, ne jako systémové volání. Tahle stránka je o tom, jak si dva procesy předají data, kolik ta cesta stojí a co se na ní láme. Neřeší synchronizaci uvnitř jednoho programu - na to je [kritická sekce a zámky](Kriticka-sekce-a-zamky) - ani protokoly nad soketem. Předpokládá [procesy](Procesy) a [systémová volání](Systemova-volani). ## Proč každý bajt musí přes jádro Každý proces má vlastní tabulku stránek. Adresa `0x7f3a0000` v `nginx` ukazuje na jinou fyzickou paměť než tatáž adresa ve `firefoxu` - to je celý smysl [virtuální paměti](Virtualni-pamet-a-strankovani). Poslat druhému procesu ukazatel je proto stejně užitečné jako poslat mu číslo popisné bez obce. Tohle je nejdůležitější věc na celé stránce: **procesy mají z principu oddělenou paměť, takže každá komunikace mezi nimi musí projít jádrem - a jediná výjimka, sdílená paměť, je zároveň jediná rychlá a jediná nebezpečná.** Zbytek stránky je rozvedení téhle věty. Za průchod jádrem platíš systémovým voláním (50-100 ns), kopií dat a často [přepnutím kontextu](Prepinani-kontextu). Dostáváš práva, blokování a probouzení a hlavně to, že jádro ví, kdy druhá strana skončila. [Vlákna](Vlakna) to nemají, protože paměť sdílejí z definice. ## Mapa mechanismů | Mechanismus | Kolik dat | Mezi kým | Synchronizace v ceně | Kdy sáhnout ||---|---|---|---|---|| signál | jedno číslo | kdokoli s právy | ne | „skonči“, „načti konfiguraci“ || roura | proud bajtů | příbuzné procesy | ano, blokuje | řetěz v shellu || pojmenovaná roura | proud bajtů | kdokoli s právy | ano, blokuje | kanál mezi programy || fronta zpráv | zprávy, výchozí 8 KiB | kdokoli s právy | ano, i priorita | potřebuješ priority || sdílená paměť | kolik chceš | kdokoli s právy | **ne, dodáš sám** | megabajty, nanosekundy || **unixový soket** | proud i zprávy | kdokoli s právy | ano, obousměrně | **výchozí volba** || síťový soket | proud i datagramy | i přes stroje | ano, obousměrně | dva stroje || `eventfd` | 64bitový čítač | přes deskriptor | ano, přes `epoll` | jen upozornění | **Když neřešíš nic zvláštního, ber unixový doménový soket.** Je obousměrný, má práva jako soubor, obě strany se dají restartovat nezávisle a druhý klient tě nestojí přepis. ## Mechanismy zblízka ### Signály nejsou kanál Signál je asynchronní oznámení, ne komunikační kanál. Přenáší jedno číslo a nic víc - `kill -l` vypíše, jaká čísla existují. ```bashkill -TERM 1234 # zdvořilá žádost, proces si ji může odchytit a uklidit po soběkill -KILL 1234 # bez diskuse, obsluha neexistuje``` **`SIGKILL` a `SIGSTOP` nejde odchytit, blokovat ani ignorovat.** Kdyby šly, neexistoval by způsob, jak zastavit proces, který si to nepřeje. Proto po `kill -9` nikdy neproběhne úklid. **V obsluze signálu smíš volat jen async-signal-safe funkce.** Spustí se uprostřed libovolné instrukce, klidně uvnitř `mallocu` s rozbitým seznamem bloků - zavoláš-li `malloc` znovu, zasekneš se na zámku, který drží přerušené vlákno. `printf` je na tom stejně, protože uvnitř alokuje a zamyká. Bezpečné je nastavit `volatile sig_atomic_t` a zpracovat ji v hlavní smyčce. **Standardní signály se nefrontují.** Přijdou-li dva `SIGCHLD` dřív, než se obslouží první, obsluha proběhne jednou. Na signály se proto nedá stavět počítání - po `SIGCHLD` se volá `waitpid` ve smyčce, dokud vrací [potomky](Procesy). ### Roury Roura je jednosměrná vyrovnávací paměť v jádře se dvěma deskriptory. V shellu ji vyrobí `|`, v C `pipe()`, a `fork()` ji zdědí - proto funguje jen mezi příbuznými procesy. Kapacita je 64 KiB, tedy šestnáct stránek, a mění se přes `fcntl(F_SETPIPE_SZ)`. **Když je roura plná, zapisovatel se zablokuje.** To je celý mechanismus zpětného tlaku: `find / | grep neco` nezaplní paměť ani na velkém disku, protože `find` čeká na `grep`. **Když zavře poslední čtenář, dostane zapisovatel `SIGPIPE`.** Výchozí obsluha proces ukončí, a proto tohle skončí místo běhu donekonečna: ```bashyes | head -n 3 # head zavře čtecí konec a yes dostane SIGPIPE``` Pojmenovaná roura je tatáž věc se jménem v souborovém systému, takže nepotřebuje příbuznost: ```bashmkfifo /tmp/kanal # soubor typu p, data se do něj neukládajícat /tmp/kanal # první terminál: čeká na zapisovateleecho ahoj > /tmp/kanal # druhý terminál``` ### Fronty zpráv POSIX rozhraní je `mq_open`, `mq_send` a `mq_receive`; fronty uvidíš jako soubory po připojení `/dev/mqueue`. Starší System V varianta `msgget` a `msgsnd` dělá totéž s horším rozhraním. Od roury se fronta liší dvěma věcmi. **Zachovává hranice zpráv** - co pošleš jedním `mq_send`, přečteš jedním `mq_receive`, nikdy ne půlku. A **zprávy mají prioritu**, takže naléhavá předběhne. Navíc přežije odesílatele. Sáhni po ní, když priority skutečně potřebuješ. Většina lidí ji nepoužije, protože unixový soket v režimu `SOCK_SEQPACKET` drží hranice zpráv taky a chová se přitom jako obyčejný deskriptor v `epollu`. ### Sdílená paměť Jediný mechanismus, kde data neprocházejí jádrem vůbec. Jádro jednou nastaví tabulky stránek tak, aby stejný fyzický rám byl namapovaný v obou procesech, a pak už do toho nemluví. ```cint fd = shm_open("/mereni", O_CREAT | O_RDWR, 0600); // vznikne /dev/shm/mereniftruncate(fd, 4096); // bez velikosti dostaneš SIGBUSvoid *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);``` Důležité slovo je `MAP_SHARED`. S `MAP_PRIVATE` bys psal do vlastní kopie a druhá strana by neviděla nic. Totéž jde udělat nad obyčejným souborem - viz [správa paměti](Sprava-pameti). **Cenou je, že synchronizaci si dodáš sám.** Zápis `int` není atomický vůči čtení v jiném procesu a překladač i procesor ti operace přeuspořádají. Do sdílené oblasti proto patří i zámek nebo semafor, což popisují [semafory a monitory](Semafory-a-monitory). Bez toho ti to bude fungovat na notebooku a rozpadne se na osmi jádrech pod zátěží. ### Sokety **Unixový doménový soket (`AF_UNIX`) je doporučená volba pro komunikaci na jednom stroji.** Je obousměrný, umí proud i zprávy, neprochází síťovým zásobníkem, a protože má cestu v souborovém systému, platí pro něj běžná [práva](Prava-a-uzivatele). Vidíš ho všude: `/run/docker.sock`, `/var/run/postgresql/.s.PGSQL.5432`. Umí navíc věc, kterou nedokáže nic jiného: **poslat přes sebe otevřený deskriptor souboru** zprávou `SCM_RIGHTS`. Příjemce dostane vlastní deskriptor na tentýž soubor, i kdyby k němu sám neměl práva. Takhle se předává poslouchající soket při restartu služby bez výpadku a takhle systemd předává službám sokety, které otevřel za ně. Síťový soket použij, teprve když jsou procesy na dvou strojích. Platíš za něj celým zásobníkem TCP/IP, viditelností na síti a nutností řešit šifrování a autentizaci, které u souboru s právy `0600` neřešíš. D-Bus není další mechanismus, je to protokol se jmennou službou postavený nad unixovým soketem. ## Proč ti zprávy chodí slepené Roura a soket typu `SOCK_STREAM` přenášejí **proud bajtů, ne zprávy**. Tři zápisy po deseti bajtech přečte protistrana klidně jedním `read` jako třicet bajtů - jádro neví, kde jsi měl konec zprávy, protože jsi mu to neřekl. Rámování si dodáš sám: délka na začátku, oddělovač na konci, nebo pevná velikost. Fronta zpráv a `SOCK_SEQPACKET` hranice drží, protože každou zprávu berou jako jednotku. ## Kolik to stojí | Mechanismus | Kolečko tam a zpět | Propustnost ||---|---|---|| roura | jednotky až desítky µs | jednotky GB/s || unixový soket | jednotky až desítky µs | jednotky GB/s || sdílená paměť | desítky až stovky ns | omezená jen pamětí | Rozdíl mezi rourou a soketem je v praxi nulový - obojí kopíruje data do jádra a zpět a čas spolkne přepnutí kontextu. **U zpráv do několika kilobajtů je volba mezi nimi otázka pohodlí, ne výkonu, a optimalizovat to je předčasné.** Sdílená paměť je o dva řády jinde, protože nekopíruje a nepřepíná. Sáhni po ní, až budeš mít naměřeno, že tě to bolí. ## System V IPC a proč po něm zůstává nepořádek Fronty, semafory a sdílená paměť existují ve dvou generacích: starší System V (`msgget`, `semget`, `shmget`) a novější POSIX (`mq_open`, `sem_open`, `shm_open`). POSIX varianty jsou lepší, protože pracují s deskriptory a cestami, a chovají se tedy jako zbytek systému. V návodech pořád najdeš System V, protože je starší a používají ho databáze. Diskvalifikuje ho ale jedna vlastnost: **objekty nejsou vázané na proces a přežijí ho.** Program spadne a segment zůstane viset i s daty, dokud ho někdo ručně nesmaže. ```bashipcs -a # co v systému zbylo, včetně vlastníka a počtu připojeníipcrm -m 32769 # smazání segmentu sdílené paměti podle ID``` ## Mars Pathfinder, 1997 Sonda po přistání na Marsu opakovaně restartovala a ztrácela naměřená data. Příčinou byla sdílená oblast chráněná mutexem: nízkoprioritní meteorologická úloha ho držela, vysokoprioritní správa sběrnice na něj čekala a mezitím běžela středně prioritní komunikace. Hlídací obvod usoudil, že správa sběrnice zamrzla, a resetoval počítač. Šlo o [inverzi priorit](Uvaznuti) a tým ji opravil na dálku zapnutím dědění priorit. Poučení: **sdílená paměť nekončí u `mmapu`, končí u zámku** - a chyba v něm se projeví až pod zátěží, kterou při testech nikdo netrefil. ## Diagnostika ```bashlsof -p 1234 # co proces drží: roury, sokety i sdílené segmentyss -xp # unixové sokety a procesy na obou koncíchipcs -a # objekty System V, které v systému visístrace -e trace=read,write,sendto -p 1234 # co proces posílá a komu``` Postup je vždycky stejný: `lsof` řekne, co proces drží, `ss` nebo `ipcs` řekne, kdo je na druhém konci, a `strace` řekne, jestli se vůbec něco děje. Víc nástrojů má [nástroje a diagnostika](Nastroje-a-diagnostika). ## Co se na tom nejčastěji rozbije | Příznak | Kde je problém ||---|---|| Zasekne se při zápisu do roury | roura je plná, čtenář nečte || `Broken pipe` nebo tichý konec procesu | čtenář zavřel konec, přišel `SIGPIPE` || Sdílená paměť po pádu zůstala | System V objekt přežil proces, smaž `ipcrm` || Soket existuje, ale spojení odmítá | zbyl soubor po pádu, server musí před `bind` udělat `unlink` || Zprávy chodí slepené dohromady | proud hranice nezná, chybí rámování || Signál přišel dvakrát, obsloužil se jednou | standardní signály se nefrontují || Obsluha signálu občas zatuhne | volá se v ní `printf` nebo `malloc` | ## Co si odnést **Oddělená paměť je výchozí stav, ne komplikace.** Všechno ostatní z toho plyne. **Unixový soket je správná odpověď, dokud nemáš důvod pro jinou.** Obousměrný, s právy, restartovatelný. **Signál je oznámení, ne kanál.** Jedno číslo, žádná fronta, přísná pravidla v obsluze. **Roura má zpětný tlak a `SIGPIPE`.** Obojí je funkce, ne porucha. **Proud nemá hranice zpráv.** Rámování si dodáš, nebo dostaneš slepené zprávy. **Sdílená paměť je rychlá přesně o to, co si musíš dodělat sám.** Bez zámku je to jen rychlejší způsob, jak mít poškozená data. **System V objekty přežívají procesy.** To je jejich hlavní praktický problém. ## Kam dál - **[Semafory a monitory](Semafory-a-monitory)** - jediné, co dělá ze sdílené paměti použitelnou věc- **[Procesy](Procesy)** - `fork`, `exec` a dědění deskriptorů, na kterém stojí roury- **[Uváznutí](Uvaznuti)** - co se stane, když se dva procesy čekají navzájem- **[Virtualizace a kontejnery](Virtualizace-a-kontejnery)** - proč `/run/docker.sock` v kontejneru znamená root na hostiteli