Linux - Meziprocesní komunikace (Interprocess communication, IPC)

Nejjednodušší forma IPC je, že rodič může zjistit, jak skončil jeho potomek. Rodič může potomkovi sdělit informace také přes jeho argumenty a proměnné prostředí. Žádný z těchto mechanizmů ale rodiči neumožňuje s potomkem komunikovat za jeho běhu a už vůbec neumožňuje komunikaci dvou procesů, které nejsou ve vztahu rodič-potomek. Za určitý primitivní druh komunikace se dají považovat signály, ale "skutečná" komunikace znamená výměna informací čili dat. Např. klasické balení souboru dvojicí tar+gzip, tar cf - * | gzip - > soubor.tar.gz, využívá mechanizmu roury (pipes).

Druhy IPC se liší těmito kritérii:

Sdílená paměť (Shared memory)

Jednou z nejjednodušších metod IPC je sdílená paměť, která umožňuje dvěma a více procesům přistupovat ke stejné oblasti paměti.
Sdílená paměť je nejrychlejší formou IPC, protože procesy sdílí stejnou paměť a pro přístup do ní není potřeba žádné systémové volání. Protože ale systém neposkytuje žádnou formu synchronizace, musíme si ji zajistit sami. Např. dva procesy by neměly zapisovat zároveň na stejné místo v paměti. Tradičním nástrojem pro synchronizaci jsou semafory.

Pro použití sdílené paměti ji jeden proces musí alokovat. Všichni si ji pak "připojí". Po používání ji "odpojí" a ten proces, který ji alokoval, ji dealokuje. Sdílená paměť se alokuje po celočíselných násobcích velikosti stránky paměti.

getpagesize(2)
#include <unistd.h>
size_t getpagesize(void);
  • vrací velikost (počet bytů) stránky paměti systému

shmget(2)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int shmflg);
  • je vytvořen úsek paměti velikosti nahoru zaokrouhlené size na násobek stránky, pokud v key je IPC_PRIVATE anebo ke key není asociován žádný úsek sdílené paměti, a v shmflg je IPC_CREAT
  • key je libovolný zvolený číselný klíč
  • v shmflg může být:
    • IPC_CREAT - vytvoří nový úsek, pokud toto není, najde se úsek asociovaný ke klíči key
    • IPC_EXCL - používá se s IPC_CREAT, chyba pokud úsek existuje
    • mode_flags - specifikuje práva k úseku, definována v <sys/stat.h>, může být:
      • S_IRUSR, S_IWUSR - čtení, zápis pro vlastníka úseku
      • S_IROTH, S_IWOTH - pro ostatní
  • vrátí identifikátor sdílené paměti asociovaný ke klíči key, při chybě -1
  • existují limity pro úsek sdílené paměti, viz man

Př. Napište program, který vytvoří sdílenou paměť o velikosti stránky paměti s právy zápisu pro sebe a čtení pro ostatní.

Potomek po fork zdědí připojené úseky, po exec a exit jsou všechny připojené úseky odpojeny, ale ne dealokovány!

shmat(2)
#include <sys/types.h>
#include <sys/shm.h>
void *shmat ( int shmid, const void *shmaddr, int shmflg );
  • připojí úsek sdílené paměti identifikovaný identifikátorem shmid na adresu shmaddr
  • pokud je shmaddr NULL, systém si sám najde volnou adresu
  • pokud je v shmflg SHM_RND, adresa se zaokroulí dolů na násobek stránky, jinak adresa musí být už zaokrouhlená
  • pokud je v shmflg SHM_RDONLY, pameť je připojena jen pro čtení, jinak i pro zápis (pokud to práva dovolují)
  • při chybě vrací -1, jinak adresu připojené paměti

Př. Uložte něco (např. text) do sdílené paměti.

shmdt(2)
#include <sys/types.h>
#include <sys/shm.h>
int shmdt ( const void *shmaddr);
  • odpojí sdílenou paměť na adrese shmaddr, paměť musí být připojená
  • při chybě vrací -1, jinak 0

Př. Před ukončením programu sdílenou paměť odpojte.

shmctl(2)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • zjistí informace o úseku (identifikovaného pomocí shmid) sdílené paměti, mění práva a dealokuje úsek
  • informace vrací do struktury shmid_ds, viz man
  • cmd může být:
    • IPC_STAT - vrací informace, potřebuje právo na čtení úseku
    • IPC_SET - nastavuje práva
    • IPC_RMID - označí úsek pro zrušení, ten se zruší až po posledním odpojení, zrušit úsek může jen ten, kdo ho vytvořil nebo root
  • vrací 0, při chybě -1

Př. Vytvořte potomka, který si zjistí informace o této paměti a z nich vypíše její velikost, pak přečte, co tam rodič zapsal a vypíše to.

Každý úsek sdílené paměti musí být explicitně zrušen, protože systém ji nezruší ani po ukončení procesů!

Př. Před ukončením programu sdílenou paměť zrušte.

ipcs(8)
ipcs -m
  • vypíše úseky sdílené paměti

Př. Podívejte se, zda v systému nezůstaly úseky nepoužívané sdílené paměti.

ipcrm(8)
ipcrm shm id...
  • zruší úseky sdílené paměti specifikované pomocí id

Př. Zrušte úseky nepoužívané sdílené paměti.

PŘÍKLAD

Mapovaná paměť (Mapped memory)

Mapovaná paměť umožňuje různým procesům komunikovat přes sdílený soubor. Může se zdát, že je to stejné jako sdílená paměť, ale jsou zde technické rozdíly. Mapovaná paměť se dá použít jak pro komunikaci, tak pro jednoduchý přístup do souboru. Linux rozdělí soubor na paměťové stránky a ty zkopíruje do paměti, takže proces k nim může přistupovat jako do paměti, číst i zapisovat.

mmap(2)
#include <unistd.h>
#include <sys/mman.h>
caddr_t mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
  • namapuje length bytů na offsetu offset ze souboru specifikovaném deskriptorem fd na adresu start (v praxi se ovšem dává NULL - systém sám vybere adresu)
  • prot specifikuje práva:
    • PROT_EXEC - z paměti lze spouštět
    • PROT_READ - číst
    • PROT_WRITE - zapisovat
  • flags určuje typ mapování, může být jen jedno z:
    • MAP_SHARED - sdílení mapování souboru s ostatními procesy, které ho mapují také, zápis je nebufferovaný, okamžitý
    • MAP_PRIVATE - privátní mapování
  • vrací ukazatel na mapovanou paměť, při chybě -1

munmap(2)
#include <unistd.h>
#include <sys/mman.h>
int munmap(void *start, size_t length);
  • zruší mapování paměti na adrese start délky length
  • vrací 0, při chybě -1
  • mapovaná paměť se automaticky zruší po skončení procesu

Různé procesy mohou komunikovat použitím mapované paměti téhož souboru a použití MAP_SHARED.
Stejně jako u sdílené paměti je potřeba explicitně zajistit synchronizaci, např. semafory nebo zamykáním souboru.
Časté použití mapované paměti je také pro rychlé čtení a zápis do souboru nebo ukládání datových struktur do souboru.

OTÁZKA: Když ukládaná datová struktura obsahuje ukazatele, po načtení této struktury budou tyto ukazatele neplatné. Proč? Jaké shody okolností by musely nastat, aby byly platné?

PŘÍKLAD

Další funkce týkající se sdílené nebo mapované paměti jsou:

Roury (Pipes)

Roura umožňuje jednosměrnou komunikaci. Data zapsaná do "zapisovacího konce" jsou čtena z "čtecího konce". Roury jsou sériová zařízení, data jsou čtena ve stejném pořadí, v jakém byla zapsána. Používají se ke komunikaci dvou vláken jednoho procesu nebo mezi rodičovským procesem a potomky.
V shellu symbol | vytvoří rouru. Např. ls | less vytvoří dva potomky shellu, ls a less. Shell také vytvoří rouru spojující standardní výstup ls se standardním vstupem less.
Kapacita roury je omezená. Pokud zapisující proces zapisuje rychleji než čtecí proces čte a roura už nemůže uchovat data, zapisovací proces je blokován, dokud se neuvolní místo. Pokud se proces pokouší číst z roury, ale není co číst, je blokován, dokud nebude co číst. Roura tedy automaticky synchronizuje dva procesy.

pipe(2)
#include <unistd.h>
int pipe(int filedes[2]);
  • vytvoří dva deskriptory souboru, ukazující na konce roury, a uloží je do filedes
  • filedes[0] je pro čtení, filedes[1] pro zápis
  • vrací 0, pří chybě -1

Deskriptory souboru, které vytvoří pipe, jsou platné jen v procesu a jeho potomcích. Při volání fork jsou deskriptory kopírovány do potomka, proto roura může spojovat pouze příbuzné procesy. Po volání fork mají jak rodič tak i potomek oba konce roury. Roura jako komunikační zařízení má však jen dva konce, proto se musí v rodiči i v potomkovi nepoužívané konce ihned uzavřít. Zvláště existence dvou zapisovacích konců roury způsobuje podivné chování.
Při čtení z roury, která má zapisovací konec uzavřený, vrací read hodnotu 0. A při zápisu do roury, která má uzavřený čtecí konec, obdrží proces signál SIGPIPE.

Př. Napište program, který vytvoří rouru a potomka. Jeden proces pak do roury něco zapíše, druhý to přečte a vypíše. K otevření konce roury ve formě streamu (FILE *) použijte funkci fdopen, k uzavření zase close.

Často je potřeba vytvořit potomka tak, aby jeden konec roury byl jeho standardní vstup nebo výstup.

dup(2)
dup2(2)
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
  • funkce vytvoří kopii deskriptoru oldfd
  • starý i nový deskriptor jsou zaměnitelné, sdílejí zámky, pozici, nesdílejí příznak uzavření při ukončení procesu
  • dup2 vytvoří kopii jako newfd, který popřípadě nejdřív uzavře
  • vrací nový deskriptor, při chybě -1

Př. Přesměrujte std. vstup potomka na čtecí konec roury a změňte program potomka na sort. V rodiči do roury zapište několik vět. Roura má buffer určité velikosti, proto po zápisu vět zavolejte fflush. Deskriptor std. vstupu je STDIN_FILENO.

Běžné použítí rour je zasílání nebo příjímání dat od programu, který běží jako potomek. K vytvoření tohoto stavu je potřeba volat postupně funkce pipe, fork, dup2, exec a fdopen. Volání všech těchto funkcí nahrazují funkce popen a pclose.

popen(3)
#include <stdio.h>
FILE *popen(const char *command, const char *type);
  • vytvoří proces voláním pipe, fork a spuštěním shellu
  • type může být jen jedno z:
    • "r" - proces bude číst data od potomka, funkce vrátí standardní výstup potomka
    • "w" - proces bude zapisovat data pro potomka, funkce vrátí standardní vstup potomka
  • command je příkaz pro shell, spuštěn pomocí /bin/sh -c command
  • vytvořený stream by měl být uzavřen pomocí pclose, ne fclose
  • při chybě (fork, pipe nebo alokace paměti) vrací NULL

pclose(3)
#include <stdio.h>
int pclose(FILE *stream);
  • uzavře stream vytvořený pomocí popen, čeká, až se spuštěný příkaz ukončí
  • vrací návratový kód spuštěného příkazu, při chybě -1

Př. Poslední úkol (sort) proveďte pomocí popen a pclose.

PŘÍKLAD

FIFO

FIFO (First In, First Out) je roura, která je souborem. Jakýkoliv proces může číst nebo zapisovat do FIFO, procesy nemusí být příbuzné. FIFO musí být otevřená pro čtení i zápis dřív, než se z ní může něco číst nebo do ní zapisovat. Otevření pro čtení proces blokuje, dokud ji neotevře jiný proces pro zápis a opačně.
FIFO se někdy nazývá pojmenovaná roura.

mkfifo(1)
mkfifo [OPTION] NAME...
  • vytvoří FIFO se jménem NAME
  • volbou -m (--mode) se nastavují práva, která musí obsahovat práva čtení a zápisu

Př. Vytvořte FIFO, např. /tmp/fifo. V jednom terminálu z ní čtěte pomocí cat < /tmp/fifo. V jiném do ní zapisujte pomocí cat > /tmp/fifo a zadávejte věty (ukončené ENTER). Sledujte, jak se věty vypisují v prvním terminálu. Zadávání vět ukončete pomocí C-D. Smažte FIFO.

mkfifo(3)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo ( const char *pathname, mode_t mode );
  • vytvoří FIFO se jménem pathname
  • mode specifikuje práva, která musí obsahovat práva čtení a zápisu, stejné jako u funkce open
  • s FIFO se pracuje jako s obyčejným souborem, tj. open, read, write, close (low-level I/O) nebo fopen, fread, fwrite, fclose (I/O C knihovny)
  • vrací 0, při chybě -1

Z FIFO může číst nebo do ní zapisovat více procesů. Data z každého zapisujícího procesu jsou zapisována atomicky.

Další funkce týkající se rour nebo FIFO jsou:



Jan Outrata
outrata@phoenix.inf.upol.cz