-
Notifications
You must be signed in to change notification settings - Fork 0
Meziprocesova 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í.
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.
| 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.
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 neexistujeSIGKILL 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.
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 SIGPIPEPojmenovaná 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álPOSIX 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.
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ěží.
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.
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.
| 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í.
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 IDSonda 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.
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 komuPostup 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.
| 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
|
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.
- Semafory a monitory - jediné, co dělá ze sdílené paměti použitelnou věc
-
Procesy -
fork,execa 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.sockv kontejneru znamená root na hostiteli
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