Předchozí kapitola ukazuje primitiva nutná pro řízení procesů. Jestliže ale tyto procesy již běží, je nutné zajistit mezi nimi komunikaci. Unix podporuje širokou škálu prostředků pro komunikaci mezi procesy (IPC -- InterProcess Communination).
Roury (pipes) jsou nejstarším komunikačním prostředkem v unixu. Jsou podporovány všemi typy unixů za těchto omezujících podmínek:
V kapitole "Funkce socketpair si popíšeme proudové roury, které eliminují první omezení, a před pojmenovanými rourami neobstojí ani druhé omezení, viz kap. "Funkce fifo".
Roura se vytvoří vyvoláním funkce pipe.
| #include <unistd.h> |
| int pipe (int filedes[2]); |
| Vrací: 0 když OK, -1 při chybě |
Funkce vrátí dva deskriptory pomocí argumentu filedes. Deskriptor filedes[0] je otevřen pro čtení a filedes[1] pro zápis. Pro schematické znázornění roury můžeme použít dva způsoby, jak je vidno z obr. 11.
Obrázek 11: Dva způsoby zobrazení unixové roury
Roura je u jednoho procesu bezvýznamná. Proto se často používá volání pipe před fork. Běžný scénář ilustruje obrázek 12.
Obrázek 12: Poloduplexní roura po fork
Procesy pak musí zabezpečit uzavření nepoužívaných konců. Viz obr. 13.
Obrázek 13: Roura od rodiče k potomkovi
Když je jeden konec roury zavřený, děje se následující:
Příklad:
int
main(void)
{
int n, fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) { /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
Nyní jsme již schopni nahradit funkce TELL_WAIT, ...z kap. "Podmínky závodu" konkrétní implementací. Pro komunikaci tentokrát využijeme roury.
Příklad:
static int pfd1[2], pfd2[2];
void
TELL_WAIT()
{
if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
err_sys("pipe error");
}
void
TELL_PARENT(pid_t pid)
{
if (write(pfd2[1], "c", 1) != 1)
err_sys("write error");
}
void
WAIT_PARENT(void)
{
char c;
if (read(pfd1[0], &c, 1) != 1)
err_sys("read error");
if (c != 'p')
err_quit("WAIT_PARENT: incorrect data");
}
void
TELL_CHILD(pid_t pid)
{
if (write(pfd1[1], "p", 1) != 1)
err_sys("write error");
}
void
WAIT_CHILD(void)
{
char c;
if (read(pfd2[0], &c, 1) != 1)
err_sys("read error");
if (c != 'c')
err_quit("WAIT_CHILD: incorrect data");
}
Ačkoliv se většinou pipe využívá pro meziprocesní komunikaci, lze roury s úspěchem použít i pro I/O operace. Tyto funkce zajišťují špinavou práci -- vytvoření pipe, fork potomka, uzavření nadbytečných konců a volání shellu pro vykonání příkazu.
| #include <stdio.h> |
| FILE *popen (const char *cmdstring, const char *type); |
| Vrací: ukazatel když OK, NULL při chybě |
| int pclose (FILE *fp); |
| Vrací: status ukončení dle cmdstring nebo -1 při chybě |
Činnost je nejlépe patrná z obrázků 14 a 15.
Obrázek 14: Výsledek po fp=popen(cmdstring, "r")
Obrázek 14: Výsledek po fp=popen(cmdstring, "w")
Příkazový řetězec je interpretován shellem. Jestliže nemůže být shell vyvolán, nastaví se návratový kód na 127. Řetězec se vlastně provádí jako:
sh -c cmdstring
Proto lze při tomto zpracování plně využívat hvězdičkovou konvenci i ostatní substituce shellu.
fp = popen ("ls *.c", "r");
Jestliže tedy chceme posílat výstupy našeho programu do nějakého stránkovače, můžeme využít IPC a např. program more.
Příklad:
#include <sys/wait.h>
#define PAGER "${PAGER:-more}"
/* environment variable, or default */
int
main(int argc, char *argv[])
{
char line[MAXLINE];
FILE *fpin, *fpout;
if (argc != 2)
err_quit("usage: a.out <pathname>");
if ( (fpin = fopen(argv[1], "r")) == NULL)
err_sys("can't open %s", argv[1]);
if ( (fpout = popen(PAGER, "w")) == NULL)
err_sys("popen error");
/* copy argv[1] to pager */
while (fgets(line, MAXLINE, fpin) != NULL) {
if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
}
if (ferror(fpin))
err_sys("fgets error");
if (pclose(fpout) == -1)
err_sys("pclose error");
exit(0);
}
Unixové filtry jsou takové programy, které čtou standardní vstup a zapisují do standardního výstupu. Filtry se normálně spojují lineárně za sebou. Filtr se stane koprocesem, když nějaký program generuje jeho vstup a zároveň čte jeho výstup.
Koprocesy (spolupracující oboustranně komunikující procesy) nabízí také Korn shell.
Na obr. 16 je vidět, jak probíhá komunikace s koprocesem.
Obrázek 16: Ovládání koprocesu
Celou činnost lze také pochopit z příkladu.
Příklad:
int
main(void)
{
int n, int1, int2;
char line[MAXLINE];
while ( (n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
line[n] = 0; /* null terminate */
if (sscanf(line, "%d%d", &int1, &int2) == 2) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(STDOUT_FILENO, line, n) != n)
err_sys("write error");
} else {
if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
err_sys("write error");
}
}
exit(0);
}
Tento program nedělá nic jiného, než že sečte dvojici čísel ze vstupu a výsledek pošle na výstup. Pro ilustraci koprocesů musíme přidat jakýsi driver pro obsluhu tohoto programu.
Příklad:
#include <signal.h>
static void sig_pipe(int); /* our signal handler */
int
main(void)
{
int n, fd1[2], fd2[2];
pid_t pid;
char line[MAXLINE];
if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
err_sys("signal error");
if (pipe(fd1) < 0 || pipe(fd2) < 0)
err_sys("pipe error");
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) { /* parent */
close(fd1[0]);
close(fd2[1]);
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd1[1], line, n) != n)
err_sys("write error to pipe");
if ( (n = read(fd2[0], line, MAXLINE)) < 0)
err_sys("read error from pipe");
if (n == 0) {
err_msg("child closed pipe");
break;
}
line[n] = 0; /* null terminate */
if (fputs(line, stdout) == EOF)
err_sys("fputs error");
}
if (ferror(stdin))
err_sys("fgets error on stdin");
exit(0);
} else { /* child */
close(fd1[1]);
close(fd2[0]);
if (fd1[0] != STDIN_FILENO) {
if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout");
close(fd2[1]);
}
if (execl("./add2", "add2", (char *) 0) < 0)
err_sys("execl error");
}
}
static void
sig_pipe(int signo)
{
printf("SIGPIPE caught\n");
exit(1);
}
Toto schéma je v unixu často využíváno. Je však třeba si dát pozor na bufferování standardních I/O operací.
V našem koprocesu jsme použili funkce read a write. Kdybychom přepsali program pomocí fgets a printf, tak by program nefungoval, pokud bychom nenastavili nulový buffer pomocí:
if (setvbuf(stdin, NULL, _IOLBF, 0) != 0)
err_sys ("setvbuf error");
if (setvbuf(stdout, NULL, _IOLBF, 0) != 0)
err_sys ("setvbuf error");
Tímto bychom sice vyřešili problém pro náš případ, ale co máme dělat, když jako koproces chceme tento program pro awk, který řeší stejný problém?
#! /bin/awk -f
{ print $1 + $2 }
Zde, protože nemůžeme předělat zdrojový kód awku, je jediným řešením připojit standardní vstup a výstup k tzv. pseudoterminálu (o něm viz literaturu [7], kapitola 19).
Tento způsob meziprocesní komunikace se také někdy nazývá pojmenované roury. Standardní roury mohou požívat pouze procesy se shodným předchůdcem. FIFO mohou používat i procesy, které spolu jinak nesouvisí. V kapitole o filesystemu (kap. "Soubory a adresáře") jsme ukázali, že FIFO je typ souboru. Vytvoření FIFO je tedy obdobné vytvoření souboru.
| #include <sys/types.h> #include <sys/stat.h> |
| int mkfifo (const char pathname, mode_t mode); |
| Vrací: 0 když OK, -1 při chybě |
Specifikace argumentu mode je shodná s funkcí open (viz kap. Funkce open). Po vytvoření FIFO jej musíme otevřít funkcí open. Pak už s ním můžeme pracovat jako s každým jiným souborem (read, write, ...).
V unixu se standardně FIFO používá pro:
Příklad:
FIFO můžeme využít pro duplikování výstupního toku dat. S použitím příkazu tee můžeme pak posílat vystup jednoho programu dvěma dalším. (Program tee kopíruje standardní vstup na standardní výstup a zároveň do daného souboru.)
Zápis v shellu bude vypadat takto:
mkfifo fifo1
prog3 < fifo1&
prog1 < infile | tee fifo1 | prog2
Výsledek můžete vidět na obrázku 17.
Obrázek 17: Posílání proudu dvěma procesům pomocí FIFO a tee
Příklad použití v sytému client-server nám dává schéma na obr. 18.
Obrázek 18: Příklad komunikace klient-server pomocí FIFO
Jednotlivé prostředky pro meziprocesní komunikaci mají mnoho společného. V Systemu V existují tři základní typy IPC -- zprávy, fronty a semafory. Někdy se také zahrnuje do výčtu sdílená paměť. Poprvé byly tyto rysy implementovány do interní verze Unixu Columbus v sedmdesátých letech.
Každá IPC struktura v jádře je referencována pomocí nezáporného celého čísla. Toto číslo se nazývá identifikátor. Na rozdíl od popisovačů souborů unix nezajišťuje alokaci nejmenšího volného identifikátoru, ale postupně identifikátory zvyšuje.
Kdykoliv vytvoříme IPC strukturu, musíme specifikovat klíč. Klíč slouží k přístupu k dané struktuře. Pro přístup k existující struktuře musí být klíč shodný s klíčem zadaným při vytváření struktury.
Existuje několik variant komunikačních scénářů:
Tři IPC funkce mají jednoduché argumenty -- key a flag. Nová IPC struktura je vytvořena, jestliže:
Odkaz na klíč (obvykle zajišťuje klient) musí být shodný se specifikovaným klíčem a bit IPC_CREAT nemusí být nastaven.
Základním problémem IPC v Systemu V spočívá v tom, že struktury jsou volně přístupné a nemají referenční počítadlo. Např. jestliže si vytvoříte frontu a pak ji ukončíte, tato fronta a její obsah zůstanou zachovány až do té doby, než je obsah přečten nebo vymazán.
Dalším problémem je to, že IPC struktury nejsou pod svým jménem známy filesystému. Proto nemůžeme modifikovat jejich zdroje, ani na ně použít nástroje z předchozích kapitol.
V tabulce 14 jsou shrnuty hlavní výhody jednotlivých typů IPC. Z tabulky není patrna další nesporná výhoda a totiž to, že IPC provádí unikátní spojení pro každého klienta.
Typ IPC
bez spojení
spolehlivé
řízení toku
záznamy
typy zpráv
či priorityfronty
ne
ano
ano
ano
ano streams
ne
ano
ano
ano
ano stream socket
ne
ano
ano
ne
ne datagram socket
ano
ano
ne
ano
ne FIFO
ne
ano
ano
ne
ne
Sokety i proudové roury patří již do kapitoly síťového programování (viz kapitolu "Síťování").
Fronta je seznam zpráv navzájem provázaných odkazy, ukládán pomocí jádra. Každá fronta má unikátní identifikátor. Nová fronta se vytvoří funkcí msgget. Stejnou funkcí se otevře i již existující fronta. Informace se do fronty zapisují funkcí msgsnd a vybírají pomocí msgrcv. Nemusíme nutně vybírat zprávy metodou FIFO, ale můžeme je vybírat v závislosti na jejich typu. Ke každé frontě je přidělena odpovídající struktura msqid_ds (popsána v <sys/msg.h>).
První funkcí zpravidla bývá msgget, která zajistí otevření nebo vytvoření fronty.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
| int msgget (key_t key, int flag); |
| Vrací: identifikátor fronty když OK, -1 při chybě |
Kapitola "System V IPC" popisuje způsob konverze klíče do identifikátoru. V případě vytvoření nové fronty jsou inicializovány některé důležité prvky struktury msqid_ds.
Se založenou frontou lze provádět některé řídící operace. K tomu slouží funkce msgctl.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
| int msgctl (int msqid, int cmd, struct msqid_ds *buf); |
| Vrací: 0 když OK, -1 při chybě |
Parametr cmd určuje, jaká operace se bude provádět. Tyto tři akce lze provádět i pro semafory a sdílenou paměť:
| IPC_STAT | Načtení řídící struktury do buf. |
| IPC_SET | Nastavení některých polí struktury IPC. |
| IPC_RMID | Odstranění fronty ze systému. |
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
| int msgsnd (int msqid, const void *ptr, size_t nbytes, int flag); |
| Vrací: 0 když OK, -1 při chybě |
Argumenty specifikují identifikátor fronty, ukazatel na začátek fronty, velikost zprávy a příznaky. Příznak může být nastaven na IPC_NOWAIT, což je vlastně ekvivalent neblokujícího příznaku používaného u I/O funkcí.
Je výhodné zavést jakési typy zpráv (pro priority). Proto struktura naší zprávy může vypadat např. takto:
struct mymesg {
long mtype;
char mtext[512];
}
Zprávy z fronty vybírá funkce msgrcv.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> |
| int msgrcv (int msqid, void *ptr, size_t nbytes, long type, int flag); |
| Vrací: délku zprávy když OK, -1 při chybě |
Stejně jako u msgsnd, ukazatel ptr ukazuje na long, kde je uložen typ zprávy. Argument type může nabývat následujících hodnot:
| type == 0 | Vrátí první zprávu z fronty. |
| type > 0 | Vrátí první zprávu daného typu. |
| type < 0 | Vrátí první zprávu nejnižšího typu, nižšího, než abs(type). |
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
| int semget (key_t key, int nsems, int flag); |
| Vrací: identifikátor semaforu když OK, -1 při chybě |
Argument nsems určuje počet semaforů v množině. Při vytváření semaforu se musí tato položka specifikovat. Při odkazu na existující semafor stačí dosadit 0.
Řídící operace nad semaforem zajišťuje funkce semctl.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
| int semctl (int semid, int semnum, int cmd, union semun arg); |
| Vrací: závisí na operaci |
Všimněte si, že poslední argument je union, nikoli ukazatel na union.
union semun {
int val; /* pro SETVAL */
struct semuid_ds *buf; /* pro IPC_STAT a IPC_SET */
ushort *array; /* pro GATALL a SETALL */
}
V následujícím přehledu jsou vyjmenovány některé z řídících operací nad semaforem.
| IPC_STAT | Načtení řídící struktury semaforu. |
| IPC_SET | Nastavení některých polí struktury IPC. |
| IPC_RMID | Odstranění semaforu ze systému. |
| GETVAL | Zjištění hodnoty semaforu. |
| SETVAL | Nastavení hodnoty semaforu. |
| GETPID | Zjištění sempid. |
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
| int semop (int semid, struct sembuf semoparray[], size_t nops); |
| Vrací: 0 když OK, -1 při chybě |
Argument semarr je vlastně ukazatel na pole operací nad semaforem.
struct sembuf {
ushort sem_num; /* číslo semaforu v sadě */
short sem_op; /* operace nad semaforem */
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */
}
Je-li semafor kladný a větší, než absolutní hodnota sem_op, pak se odečte sem_op a proces běží dál.
Jinak ale
Jestliže je semafor 0, funkce se vrátí okamžitě.
Pokud je nenulový, pak
Atomická povaha funkce sem_op je zaručena.
Zvláštní problém představuje definování hodnoty semaforu při ukončení procesu. Existuje totiž možnost zablokování ostatních procesů nesprávným ukončením jednoho z procesů sdílejících semafor.
Tento problém řeší příznak SEM_UNDO. V případě ukončení procesu jádro rozpozná, kolik zdrojů proces alokoval a upraví hodnotu semaforu.
Sdílená paměť umožňuje dvěma nebo i více procesům sdílet část adresního prostoru. Tato forma IPC je ze všech nejrychlejší, protože není nutné kopírovat data při komunikaci typu client-server. Jediné úskalí této metody spočívá v nutnosti synchronizování přístupu k paměti.
Proces o přidělení paměti požádá funkcí shmget.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
| int shmget (key_t key, int size, int flag); |
| Vrací: identifikátor sdílené paměti když OK, -1 při chybě |
Nad blokem sdílené paměti lze opět provádět řídící operace:
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
| int shmctl (int shmid, int cmd, struct shmid_ds *buf); |
| Vrací: 0 když OK, -1 při chybě |
Popis struktury shmid_ds je v <sys/shm.h>.
Parametr cmd opět určuje, jaká operace se bude provádět.
| IPC_STAT | Načtení řídící struktury sdílené paměti. |
| IPC_SET | Nastavení některých polí struktury IPC. |
| IPC_RMID | Odstranění sdílené paměti ze systému. |
| SHM_LOCK | Uzamčení sdílené paměti (jen superuživatel). |
| SHM_UNLOCK | Odemčení sdílené paměti (jen superuživatel). |
Jakmile je jednou sdílený blok paměti vytvořen, je možné ho připojit do adresního prostoru funkcí shmat.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
| void *shmat (int shmid, void *addr, int flag); |
| Vrací: ukazatel na sdílenou paměť když OK, -1 při chybě |
Připojení segmentu je závislé na hodnotě argumentu addr a nastavení příznaku.
Obecně mohou nastat tyto příznaky:
Pro ukončení práce se sdílenou pamětí ji můžeme odpojit.
| #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> |
| int shmdt (void *addr); |
| Vrací: 0 když OK, -1 při chybě |
Příklad:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define ARRAY_SIZE 40000
#define MALLOC_SIZE 100000
#define SHM_SIZE 100000
#define SHM_MODE (SHM_R | SHM_W) /* user read/write */
char array[ARRAY_SIZE]; /* uninitialized data = bss */
int
main(void)
{
int shmid;
char *ptr, *shmptr;
printf("array[] from %x to %x\n", &array[0], &array[ARRAY_SIZE]);
printf("stack around %x\n", &shmid);
if ( (ptr = malloc(MALLOC_SIZE)) == NULL)
err_sys("malloc error");
printf("malloced from %x to %x\n", ptr, ptr+MALLOC_SIZE);
if ( (shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
err_sys("shmget error");
if ( (shmptr = shmat(shmid, 0, 0)) == (void *) -1)
err_sys("shmat error");
printf("shared memory attached from %x to %x\n",
shmptr, shmptr+SHM_SIZE);
if (shmctl(shmid, IPC_RMID, 0) < 0)
err_sys("shmctl error");
exit(0);
}
Ladislav Dobias