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

```bash
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](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:

```bash
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:

```bash
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í.

```c
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](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.

```bash
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](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

```bash
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](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
