Axel Rogat
Betriebssysteme und betriebssystemnahes Programmieren
 
8.6: IPC-Äquivalenz Kapitel 9 9.2: IPC-Mechanismen 
 
  9.1 FIFOs  
 

Wir hatten bereits in Abschnitt 6.11 die einfachste Art von Kommunikation kennengelernt: Datenwarteschlangen im Form der Pipelines. Ihr wesentlicher Nachteil ist, daß die kommunizierenden Prozesse einen gemeinsamen Vorfahren besitzen müssen, der die Pipeline anlegt.

Es gibt eine andere Art von Warteschlangen, deren Benutzer nicht direkt miteinander verwandt sein müssen. Sie werden über normale Dateisystem-Namen identifiziert und heißen daher auch Named Pipes, meistens aber einfach FIFOs. Sie können wie Dateien gelesen, beschrieben, gelinkt werden, etc.

9.1.1 Die Bibliotheksfunktion mkfifo

Mit folgender Bibliotheksfunktion (deklariert in sys/stat.h) legt man eine FIFO an:

UNIX
int mkfifo(const char *path, mode_t mode);
  legt eine FIFO mit dem Pfad path im Dateisystem mit den Zugriffsrechten mode&~umask an

Die FIFO existiert danach als spezielle Datei. Der erste Buchstabe in einer Directory-Auflistung ist ein `p' (für Pipe):

prw------- 1 root root 0 Nov 29 01:47 initctl /dev/initctl ist übrigens die FIFO, über die das Kommando init dem init-Prozeß den Befehl schickt, in einen anderen Run Level zu schalten.

Eine FIFO kann nach dem Anlegen wie eine normale Datei behandelt werden. Jeder Prozeß, der auf diese spezielle Datei zugreifen darf, kann die FIFO benutzen. Er öffnet sie dazu mit open - zum Lesen mit dem Flag O_RDONLY, zum Schreiben mit O_WRONLY. Der Datenzugriff erfolgt dann mit read bzw. write.

Prozesse müssen sich natürlich auf den Namen der FIFO einigen. Server-Prozesse benutzen immer Standard-Pfade, damit ihre Clients sie auch finden.

Es ist folgendes zu beachten:

9.1.2 Das Kommando mkfifo

Am einfachsten ausprobieren kann man FIFOs mit dem Kommando mkfifo, das eine Schnittstelle zur gleichnamigen Bibliotheksfunktion ist:
mkfifo [-m mode] pathname(s) Auf einigen Systemen gibt es das Kommando nicht -- dort kann man sich mit mknod behelfen, das FIFOs oder Special Files anlegt (dazu später):
mknod [-m mode] pathname p Von einer Shell aus gestartete Prozesse sind natürlich alle Nachfahren der Shell, so daß man mit Pipes auskäme. Der Vorteil von FIFOs ist, daß man

  • in weiter auseinanderliegenden Kommandos in Shell-Skripten auf die FIFO zugreifen kann,

  • mehrere FIFOs von einem Prozeß ausgehend anlegen kann. d.h. die Ausgabe kann mehrfach verwendet werden Ausgabe und Fehlerkanal können in getrennte FIFOs wandern.

Beispiele:

  • Wir legen in einem Fenster eine FIFO an und versuchen, sie zu füllen:
    makefifo /tmp/testfifo cp test.txt /tmp/testfifo Der cp-Aufruf blockiert, da die FIFO noch keinen Leser hat. In einem Fenster tippen wir nun
    cat /tmp/testfifo Wir erhalten dort den Inhalt von test.txt und kehren sofort zur Shell zurück. Gleichzeitig ist auch der cp-Aufruf im anderen Fenster beendet.

  • So kann man mit zwei FIFOs arbeiten:

    mkfifo /tmp/outfifo /tmp/errfifo testcmd > /tmp/outfifo 2> /tmp/errfifo Das Kommando blockiert so lange, bis beide FIFOs einen Leser gefunden haben.

  • Hier liefert ein schreibender Prozeß writer Daten an drei lesende Prozesse reader1, reader2, reader3:
    mkfifo /tmp/testfifo /tmp/testfifo2 reader2 < /tmp/testfifo & reader3 < /tmp/testfifo2 & writer | tee /tmp/testfifo | tee /tmp/testfifo2 | reader1 rm /tmp/testfifo /tmp/testfifo2

9.1.3 Server und Clients

Wie erwähnt, lösen FIFOs das Kommunikationsproblem zwischen Server- und Client-Prozessen, die nicht direkt miteinander verwandt sind. Wir implementieren zwei einfache solche Beispiele.

Beispiel: Zunächst schreiben wir nur einen C-Server, der darauf wartet, daß er Kommandos von einem Client erhält. Seine Reaktion besteht hier nur aus der Ausgabe der Kommandos auf seine Standard-Ausgabe:

int main() { int fd,err; char c; static char fifoname[]="/tmp/echoserv"; err=mkfifo(fifoname,0x666); fd=open(fifoname,O_RDONLY); if ( err || fd<0 ) { perror(fifoname); exit(0); } while (read(fd,&c,1)==1) putchar(c); close(fd); unlink(fifoname); }
Mehrere Clients können gleichzeitig an den Server senden. Dazu setzen wir einfach in mehreren Fenstern das Kommando
cat > /tmp/echoserv ab. Die danach eingegebenen Zeichen werden (durch die Terminal-Einstellung zeilenweise) an den Server geschickt, der sie (ggf. gemischt) ausgibt. Das cat können wir mit einem EOF (CTRL-D) abbrechen, wodurch die FIFO von diesem Prozeß aus geschlossen wird. Wenn der letzte Client sich verabschiedet, liefert das read im Server eine Null, und der Server bricht ab und löscht die FIFO.

Wenn der Server den Clients Daten liefert, kann dieser Verkehr natürlich nicht über dieselbe FIFO abgewickelt werden wie die Kommandos. In diesem Fall legt jeder Client eine weitere FIFO an, in die der Server die Antwort schreibt.

Üblicherweise enthält der Name dieser FIFO die Prozeßnummer des Clients, damit keine Überschneidungen entstehen können. Diese PID muß natürlich auch im Kommando mitgeschickt werden. Der Client muß nach Erhalt der Antwort die FIFO selbst löschen.

Beispiel: Unser Server ermittelt den kleinsten Teiler (>1) einer natürlichen Zahl, die ihm von einem Client geschickt wird. Zur Identifikation von Primzahlen liefert er 0 (eine 1 bei 0 und 1).

Die Kommandos, die ihm geschickt werden, haben das Format "pid:num", wobei pid die Prozeß-Nummer des Clients und num die zu untersuchende Zahl ist.

Den Namen der Kommando-FIFO und den Namensanfang der Antwort-FIFOs definieren wir in einer Header-Datei fifoname.h, die Client und Server einbinden:

#define REQFIFO "/tmp/divireq" #define ANSFIFO "/tmp/divians" Wir zeigen zunächst das Client-Programm. Ihm können in der Aufrufzeile beliebig viele Zahlen übergeben werden, die es an den Server schickt. Es gibt eine Interpretation der erhaltenen Antwort aus. Wir wollen hier keine Binärdaten, sondern ASCII-Daten schicken. Daher werden zur einfacheren Formatierung mit fdopen C-Ströme um die File-Deskriptoren gelegt.
#include "fifoname.h" int main(int argc, char *argv[]) { int fd_cmd,i; FILE *cmdfifo; if ((fd_cmd=open(REQFIFO,O_WRONLY))<0) { fputs("server not running\n",stderr); exit(0); } cmdfifo=fdopen(fd_cmd,"w"); for ( i=1 ; i<argc ; ++i ) { char ansname[80],buffer[80]; int err,div,fd_ans; long num; pid_t self=getpid(); FILE *ansfifo; sprintf(ansname,ANSFIFO".%d",self); if (mkfifo(ansname,0666) || (fd_ans=open(ansname,O_RDWR))<0 ) { perror("can't create answer fifo"); exit(0); } ansfifo=fdopen(fd_ans,"rw"); num=abs(atol(argv[i])); fprintf(cmdfifo,"%d:%ld\n",self,num); fflush(cmdfifo); if (fscanf(ansfifo,"%d",&div)!=1) perror("no answer"); else { if (div) printf("%d is divisible by %d\n",num,div); else printf("%d is prime\n",num); } fclose(ansfifo); unlink(ansname); } fclose(cmdfifo); } Der Server legt zunächst die Kommando-FIFO an und öffnet sie zum Lesen und zum Schreiben. Dadurch gibt es immer einen schreibenden Prozeß, so daß die FIFO beim Lesen nie ein EOF meldet.

Der Server läuft in einer Endlosschleife und kann nur durch ein Signal abgebrochen werden. Damit auch bei einem CTRL-C die Kommando-FIFO weggeräumt wird, müssen wir einen Signal-Handler schreiben.

#include "fifoname.h" static FILE *cmdfifo; static void sigint_handler(int sig) { fclose(cmdfifo); unlink(REQFIFO); exit(0); } static unsigned long divisor(unsigned long l) { unsigned long d,s; if (l<4) return (l<=1); if ((l&1)==0) return 2; s=(unsigned long)sqrt((double)l); for (d=3;d<=s;d+=2) if (l%d==0) return d; return 0; } int main() { int fd; signal(SIGINT,sigint_handler); if ( mkfifo(REQFIFO,0666) || (fd=open(REQFIFO,O_RDWR))<0 ) { perror("can't create " REQFIFO); sigint_handler(SIGINT); } cmdfifo=fdopen(fd,"rw"); for (;;) { char buffer[256], *P=buffer; fgets(buffer,256,cmdfifo); P=strchr(buffer,':'); if (P==0) fprintf(stderr,"illegal request: %s\n",buffer); else { int fd2; unsigned long num; *P=0; num=atoi(P+1); sprintf(buffer,ANSFIFO".%d",atoi(buffer)); if ((fd2=open(buffer,O_WRONLY))<0) fprintf(stderr,"can't open %s\n",buffer); else { FILE *ansfifo=fdopen(fd2,"w"); fprintf(ansfifo,"%d\n",divisor(num)); fclose(ansfifo); } } } }

 
8.6: IPC-Äquivalenz Startseite 9.2: IPC-Mechanismen