Skip to content

Meziprocesova komunikace

Martin Skalicky edited this page Aug 7, 2026 · 1 revision

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 - ani protokoly nad soketem. Předpokládá procesy a systémová volání.

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. 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. Dostáváš práva, blokování a probouzení a hlavně to, že jádro ví, kdy druhá strana skončila. Vlákna 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í.

kill -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.

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:

yes | 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:

mkfifo /tmp/kanal        # soubor typu p, data se do něj neukládají
cat /tmp/kanal           # první terminál: čeká na zapisovatele
echo 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í.

int fd = shm_open("/mereni", O_CREAT | O_RDWR, 0600);  // vznikne /dev/shm/mereni
ftruncate(fd, 4096);                                   // bez velikosti dostaneš SIGBUS
void *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.

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. 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. 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.

ipcs -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 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

lsof -p 1234                       # co proces drží: roury, sokety i sdílené segmenty
ss -xp                             # unixové sokety a procesy na obou koncích
ipcs -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.

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 - jediné, co dělá ze sdílené paměti použitelnou věc
  • Procesy - fork, exec a dědění deskriptorů, na kterém stojí roury
  • Uváznutí - co se stane, když se dva procesy čekají navzájem
  • Virtualizace a kontejnery - proč /run/docker.sock v kontejneru znamená root na hostiteli

Clone this wiki locally