6.11.1 pipe
Für das Anlegen der Kommunikationskanäle auf Systemebene ist der
Systemaufruf pipe (deklariert in unistd.h) verantwortlich:
UNIX
|
int pipe(int fildes[2]);
| |
| legt eine Pipe an und füllt das Array fildes mit zwei
passenden File-Deskriptoren
| |
Über die File-Deskriptoren kann die Pipe danach angesprochen werden.
fildes[0] dient zum Lesen aus der Pipe, fildes[1] zum
Schreiben in die Pipe (analog zu stdin=0, stdout=1).
Wenn man bidirektionale Kommunikation braucht, muß man
zwei Pipes öffnen.
Ein Analogon bei der Kommunikation zwischen verschiedenen Rechnern
ist übrigens die BSD-Funktion socketpair.
-
Zeichen, die man nach fildes[1] schreibt, kann man nach FIFO-Art
aus fildes[0].
Man kann in einer Pipe nicht wie bei normalen Dateien mit der
Schreib-/Leseposition herumwandern (etwa mit seek). Einmal gelesene
Zeichen werden aus der FIFO entfernt und sind verloren.
-
Sinnvoll wird das ganze natürlich erst, wenn
ein Prozeß das Schreiben und ein anderer das Lesen übernimmt.
Wenn ein Prozeß aus der Pipe zu lesen versucht, wenn sie gerade leer ist,
wird er bis zum Eintreffen eines Zeichens (oder EOF) blockiert. Auch
beim Schreiben kann ggf. blockiert werden, nämlich wenn die Pipe sehr
voll ist, d.h. die interne Maximalgröße erreicht ist. Die Pipe
muß dann erst wieder ein wenig leergelesen werden.
-
Ein Prozeß kann nicht die ganze Pipe schließen, wohl aber mit einem
close die beiden Kanäle, die sich ja wie Dateien verhalten.
Wenn kein Prozeß mehr die Schreib-Hälfte der Pipe geöffnet hat,
blockiert ein Lesen aus der Pipe nicht mehr, sondern liefert ein End-of-File
(read liefert 0 zurück).
Wenn es niemand mehr gibt, der aus der Pipe lesen könnte, werden
danach geschriebene Daten weggeworfen, und der schreibende Prozeß
erhält das Signal SIGPIPE. Wenn dieses Signal ignoriert
wird (was voreingestellt ist), schlägt der write-Befehl fehl
(Rückgabewert -1, mit errno=EPIPE).
Man sollte immer darauf achten, nicht mehr benutzte Kanäle einer Pipe
zu schließen, da sie sonst eventuell unnötig im System verbleiben!
Beachte: Die größte Einschränkung von Pipes ist,
daß die kommunizierenden Prozesse sich die Filedeskriptoren
teilen müssen. Dazu benötigen sie einen
gemeinsamen Vorfahr, der die Pipe anlegt, und von dem sie die
Deskriptoren erben. Pipes sind also nur zwischen Elter- und Kind-Prozeß,
zwischen Bruder-Prozessen, etc. möglich. Server, die
beliebigen anderen Prozessen Daten liefern, sind so nicht realisierbar!
Beispiel: Wir erzeugen mit pipe eine Pipe und verdoppeln
danach mit fork unseren Prozeß. Genau wie offene Dateien werden
auch Pipes geerbt, d.h Elter und Kind haben beide Zugriff auf die Pipe.
Der jeweils nicht gebrauchte Kanal wird sofort geschlossen (Eingabe
beim Kind, Ausgabe beim Elter).
Danach schreibt das Kind die Zahlen 1 bis 20 in die Pipe (jeweils als
mit Linefeed abgeschlossene Zeile), und der Elter
kopiert einfach zeichenweise aus der Pipe auf die Standard-Ausgabe.
Da das Kind nur schreibt und der Elter nur liest, schließen wir zu Beginn
den jeweils nicht benötigten Kanal.
Wenn das Kind fertig ist (return) wird seine Seite der Pipe
geschlossen. Der Elter erhält dann irgendwann beim Lesen ein End-of-File,
d.h. read liefert 0 als Anzahl gelesener Zeichen zurück. Dann
beendet sich der Elter.
int main()
{
int fildes[2];
pipe(fildes);
if (fork())
{
close(fildes[1]);
char c;
while (read(fildes[0],&c,1)>0) write(1,&c,1);
int s;
waitpid(-1,&s,0);
}
else
{
close(fildes[0]);
for (int i=1;i<=20;++i)
{
char buf2[8];
sprintf(buf2,"%d\n",i);
write(fildes[1],buf2,strlen(buf2));
}
}
return 0;
}
Wenn man Pipes, wie man sie aus der Shell kennt, darstellt, ist es sinnvoll,
daß der ursprüngliche Prozeß der letzte in der Kette
ist und das zuletzt erzeugte Kind das erste. Normalerweise beendet
sich ein Prozeß, wenn seine Standard-Eingabe zur Neige gegangen ist, d.h.
wenn der Prozeß eins weiter links in der Kette sich beendet hat. Das
Prozeß-Sterben schreitet also von links nach rechts fort.
Beispiel: Wir simulieren eine Pipe-Konstruktion wie beim
`|' in der Shell. Wir wollen folgende Filter-Kombination mit einem
Programm zusammenbauen:
grep "^\ *#"   |   sed -e "s/^\ *//" -e "s/^#\ */#/"
Wir nennen unser Programm praep. Ein Aufruf wie
"cat *.c | praep" würde alle Präprozessorzeilen der
C-Quelltexte im aktuellen Verzeichnis ausgeben. Spaces vor und
nach dem `[[#]]' werden dabei eliminiert.
Der Elter-Prozeß ist der, der rechts vom `|'
steht, der Kind-Prozeß der linke. Beide Prozesse
überschreiben sich mit execlp durch ein anderes Programm, der
Elter durch sed, das Kind durch grep.
Nun schreibt aber grep auf seine Standard-Ausgabe, und
sed liest von seiner Standard-Eingabe. Wie müssen
mittels dup2 diese Kanäle mit unseren Pipe-Kanälen verbinden!
int main()
{
int fildes[2];
pipe(fildes);
if (fork()==0)
{
close(fildes[0]);
dup2(fildes[1],1);
execlp( "grep" , "grep" , "^\\ *#" , 0 );
}
else
{
close(fildes[1]);
dup2(fildes[0],0);
execlp( "sed" , "sed" , "-e" , "s/^\\ *//" , "-e" , "s/^#\\ */#/" , 0 );
}
}
Jeder Prozeß schließt vor dem execlp zwei
Kanäle: den nicht benötigten der Pipe und implizit den durch
dup2 überschriebenen!
Bemerkung 1: Wir bauen hier Pipes nicht in unsere eigene Shell
ein, da die Handhabung beliebig vieler Prozesse den Quelltext zu
unübersichtlich macht. Das Prinzip ist aber genau das im vorangegangenen
Beispiel beschriebene.
Die Aufrufe der Einzelprozesse könnten wieder mittels vector
gespeichert werden. Beim Untersuchen der Aufrufzeile dürften wir aber
beispielsweise nicht mehr strtok benutzen. Wir müßten ja auch
bei `;' und `&' unterteilen, die Information über
das jeweilige Trennzeichen geht bei strtok aber verloren.
Am besten definiert man die Syntax einer solchen Kommandozeile in Form
einiger grammatischer Regeln und baut dann (per Hand oder mit einem
Parser-Generator) zunächst einen vernünftigen Parser. Die technische
Seite der Prozeß-Erzeugung sollte erst danach darauf aufgesetzt werden.
Bemerkung 2: In Linux werden Pipes durch das normale Dateisystem VFS
(dazu später) dargestellt. Die Daten werden werden aber nicht auf einem
Sekundärspeicher, sondern mit Hilfe von
mmap
im Hauptspeicher gelagert, in einem von beiden Prozessen zugreifbaren
Segment.
6.11.2 popen
Es gibt in der C-Bibliothek (deklariert in stdio.h) eine
weitere nützliche Funktion, die mit Pipes arbeitet:
UNIX
|
FILE *popen(const char *command, const char *type);
| |
| die Shell bin/sh führt den String command, als
Befehlszeile interpretiert, aus und koppelt seine Ein- oder Ausgabe an eine
Pipe
| |
int pclose(FILE *stream);
| |
| schließt eine mit popen geöffnete Pipe
| |
Der Aufruf ist also mit dem system-Aufruf verwandt.
Es wird aber intern automatisch eine Pipe angelegt und der Prozeß mit
fork dupliziert, bevor die Shell gestartet wird.
Entweder die Eingabe oder die Ausgabe der
Shell wird mit der Pipe gekoppelt. Dafür setzt man type="w"
(zum Schreiben in die Eingabe des Programms) bzw.
type="r" (zum Lesen aus seiner Ausgabe).
Über den zurückgelieferten File-Pointer kann man
dann z.B. die Ausgabe des Programms zeichenweise
lesen und bearbeiten. Am Schluß schließt man diese Art von
Pipe mit pclose auf diesen Pointer ( nicht
fclose verwenden)!
Beispiel: Wir führen in einem kurzen
C-Programm ls aus, numerieren die angezeigten Dateien dabei
aber durch.
#include <stdio.h>
int main()
{
int c,line=0,numout=1;
FILE *pipe=popen("ls","r");
while (c=fgetc(pipe), c!=EOF)
{
if (numout) { printf("%d: ",++line); numout=0; }
putchar(c);
if (c=='\n') numout=1;
}
pclose(pipe);
}
Die Konstruktion mit dem numout ist notwendig, damit wir am
Ende nicht eine Zeilennummer zuviel ausgeben.