Child-Prozesse

Erzeugung, Steuerung, Kommunikation

Ein Perl-Programm kann Child-Prozesse erzeugen, die asynchron arbeiten - unabhängig voneiander und vom Parent-Programm. Die eigentliche 'Arbeit' wird von beliebig vielen Hintergrund-Prozessen erledigt, während das 'Haupt'-Programm längst wieder bereit ist. Diese eher professionell genutzte Möglichkeit unterscheidet Perl u.a. von der oft ähnlich verwendeten Programmiersprache PHP. Sie kann zur Erstellung besonders schneller und effizienter Programme verwendet werden.
Die Beispiele werden ohne jede Gewähr vorgestellt !
Bei praktischer Anwendung der vorgestellten Beispiele können unerwünschte Folgen auftreten.
Alle hier angeführten Beispiele wurden zwar auf mehreren Betriebssystemen getestet, trotzdem sind Fehler möglich.
Die Anwendung erfolgt daher auf eigenes Risiko.
Die Programme greifen direkt auf System-Funktionen zu. Das Verhalten ist daher stark vom jeweiligen System abhängig.
Zu viele erzeugte Prozesse können jedes System lahmlegen !
Ideale Test-Systeme sind → virtuelle PC !
Perl Scripts für CGI und Systemverwaltung
Child Erzeugung eines Child-(kid)-Prozesses (forking)
Schleifen Child-Prozesse am Fließband, Warten mit wait, Abbruch mit kill
eval-Block Verarbeitung von (Timeout)-Ereignissen
Signale System-spezifische Liste von Abbruch-(kill)-Signalen
Pipes Kommunikation zwischen Parent und Child-Prozessen
Beispiele: Manuelle Eingabe - 1 'überwachter' Child-Prozess
LAN-Ping - 254 parallele Child-Prozesse
Server - Eigener Child-Prozess für jeden Client
Verwandte Themen Timeout,
Links Ausgewählte Links zum Thema Perl-Prozesse

Child-Prozess

Ein Child-Prozess wird u.a. eingesetzt, wenn ein Programm ohne eigene Tätigkeit auf andere Ereignisse warten muss, z.B. auf die Ausführung von System-Befehlen, auf langsame Internet-Kommunikation, auf manuelle Antworten menschlicher AnwenderInnen, etc.

In solchen Fällen kann man den 'wartenden' Programm-Teil an einen Child-Prozess übergeben, während das Haupt-Programm (Parent) ohne Verzögerung weiter arbeitet.
Die Anzahl gleichzeitig arbeitender Child-Prozesse ist nur durch den Arbeits-Speicher und Prozessor-Leistung begrenzt. Je öfter und je längere Wartezeiten auftreten, umso mehr Child-Prozesse können sinnvoll eingesetzt werden.

Server-Programme (demons) erzeugen typisch einen eigenen Child-Prozess zur asynchronen Ausführung jedes angenommenen Auftrags.
Das Beispiel rechts demonstriert den Vorgang:

Zu Beginn läuft nur 1 Prozess, d.h. die Zeile Start wird nur 1mal ausgegeben.
Die Prozess-Nummer PID wird vom Betriebssystem vergeben und ist für jeden Prozess eindeutig. Die globale Perl-Variable $$ enthält stets die eigene PID-Nr jedes Prozesses.

Mit Anweisung fork wird ein Child-Prozess erzeugt. Ab hier wird das gesamte restliche Programm auf den Child-Prozess kopiert, und beide Prozesse werden unabhängig voneinander weitergeführt.

Das fork-Ergebnis wird an eine Variable (hier $cpid) überwiesen. Diese Variable erhält für Parent und Child unterschiedliche Werte und wird daher immer zur Trennung der beiden Prozesse verwendet:
Im Parent enthält $cpid die PID-Nummer des Child-Prozesses.
Im Child ist $cpid=0

Nach der if-Verzweigung laufen beide Prozesse gleichzeitig weiter: Parent und Child verwenden nach der Verzweigung jedoch unterschiedliche Programm-Teile !
Erzeugung eines Child-Prozesses (grün unterlegt) :
print "Gemeinsamer Start \n";
print "PID = $$\n";
$cpid = fork();
if (!defined($cpid)) {
die "Fehler bei fork";
}
if ($cpid) {
print "Parent-Prozess \n";
print "Parent-PID = $$\n";
print "Child-PID = $cpid\n";
}
else {
print "Child-Prozess \n";
print "Child-PID = $$\n";
sleep 1;
print "Child-Ende \n";
exit 0;
}
print "Parent-Ende \n";
Einige Details zum gezeigten Beispiel:

Wenn die fork-Anweisung nicht ausgeführt werden kann, dann ist der Wert der zurückgegebenen Variablen (hier $cpid) unbestimmt. Man beendet in diesem Fall meist das Programm (Anweisung die).

Die PID-Nummer (Prozess-ID-Nr) ist eine ganze Zahl, die vom Betriebssystem vergeben wird. Sie ist garantiert eindeutig und kann zur Steuerung der ansonsten 'frei' laufenden Child-Prozesse verwendet werden.

Jeder Parent-Prozess verfügt über die kid-PIDs seiner Child-Prozesse. Im Beispiel wird dieser Wert vom Parent ausgegeben.
Wenn man versucht, die gleiche Variable vom Child auszugeben, erhält man cpid = immer 0

Im Beispiel muss der Child-Prozess mit Anweisung sleep 1 Sekunde warten. Damit wird seine Arbeitszeit simuliert. Die Ausgabe der Zeile Child-Ende erfolgt daher immer zuletzt - nach dem Ende des Parent-Programms !

Die Reihenfolge der gesamten übrigen Ausgabe ist unbestimmt, da die beiden Prozesse gleichzeitig ablaufen. Es ist klug, eine allfällige Ausgabe (print) von Parent und Child immer unterschiedlich zu formatieren, damit man erkennen kann, aus welchem Prozess sie stammt.

Normalerweise wird ein Child-Prozess mit exit eindeutig beendet
Wenn man diese Anweisung entfernt oder mit # abschaltet, dann wird die letzte Zeile Programm-Ende 2mal ausgegeben - zuerst vom Parent, 1 Sekunde später vom Child.
Es ist guter Stil, jedes Child nach der Verzweigung sicher zu beenden. Es kann zu unerwartetem Verhalten führen, wenn man einem Child nach der Verzweigung nochmals erlaubt, den gleichen Code wie der Parent zu verwenden !

Nach dem Ende des Parent-Programms (schnell) läuft das Child-Programm (normalerweise langsamer) weiter.
Wenn das Child nicht sicher beendet wird, dann hat man einen Zombie-Prozess erzeugt der mindestens Speicher belegt, oft auch noch Prozessor-Leistung. Genügend viele solcher Zombies bringen jedes Betriebssystem zum Erliegen...

Abbruch

Child-Prozesse werden meist für länger dauernde Arbeiten eingesetzt. Es ist sehr wichtig, ein Child wieder abzubrechen, wenn es erfolglos wartet.

Kommunikation

Meist wird gewünscht, dass ein Child-Prozess den Erfolg seiner Arbeit mitteilt. Die ↓ Kommunikation mit zwischen Child und Parent ist daher ein weiteres wichtiges Thema.

fork - Schleifen

In diesem Beispiel werden in einer Schleife mehrere Child-Prozesse erzeugt. Es ist wichtig, dabei die Übersicht zu behalten:
Wenn von Child-Prozessen (!) neue Childs erzeugt werden, dann wächst die Anzahl der Prozesse exponentiell, was je nach Betriebssystem und Hauptspeicher früher oder später katastrophal endet.

Im Beispiel dient das → Array @cpid zum Speichern der kid-PIDs mit der Variablen $cc als Zähler. - Das entkoppelt den Child-Zähler von der Schleifen-Variablen $i, die andere Werte annehmen kann, oder z.B. durch eine while-Schleife ersetzt werden kann.

Die fork-Anweisung wird ausschließlich vom Parent ausgeführt, da kein Child-Prozess in die Schleife zurückkehrt.
Die Kid-PIDs der Child-Prozesse werden in die Array-Elemente $cpid[0], $cpid[1], $cpid[2], .. eingetragen.

Die globale Variable $$ enthält stets die eigene PID-Nr, damit kann auch ein Child seine jeweilige PID ermitteln.

Jeder Durchlauf der Schleife enthält getrennte Programm-Teile für Parent und Child.
Das Parent-Programm ist normalerweise minimal - Hier wird die PID-Nr des gerade erzeugten Child-Prozesses ausgegeben.
Im Child-Programmteil wird die eigentliche Arbeit ausgeführt. Dieser Teil wird unabhängig von jedem einzelnen Child-Prozess ausgeführt. Die zur individuellen Steuerung der Prozesse verwendeten Variablen müssen ihre Werte vor der fork-Anweisung erhalten. - Im Beispiel die Variablen $i, $cc, die in jedem Child-Prozess ausgegeben werden.
Erzeugung von 5 unabhängig arbeitenden Child-Prozessen:
my($cc,@cpid,$i);
$cc=0;
print "--p mypid=$$\n";

for($i=0;$i<5;$i++) {
$cpid[$cc] = fork();
if($cpid[$cc]) {
### Parent (main) ###
print "--p: cpid[$cc]=$cpid[$cc]\n";
$cc++;
}
else {
### Child ###
print "--c$cc: i=$i\n";
print "--c$cc: mypid=$$\n";
sleep 1;
print "--c$cc Ende\n";
exit 0
}
}
# Warte-Schleife
$r=0;
while($r!=-1) {
$r = wait();
print "--p $r beendet\n";
}
print "--p Ende\n";

Ausgabe

Wenn man das Beispiel praktisch ausprobiert, dann ergibt sich oft ein verwirrendes Bild, insbesondere dann, wenn die simulierte Arbeitszeit sleep durch eine real ausgeführte Arbeit ersetzt wird.

Zur Übersicht sind daher die führenden Zeichen jeder Ausgabe-Zeile markiert: Das Parent-Programm verwendet --p, das erste Child --c1, das zweite Child --c2 usw.

Wenn das Programm eine einzige gemeinsame Ausgabe erzeugt, dann ist die Reihenfolge der einzelnen Beiträge nicht vorhersehbar !
Wenn die Ausgabe für jedes Child getrennt verarbeitet werden soll, dann muss man dafür eigene Kanäle (↓ Pipes) bereitstellen.

Zufall

Ein etwas realistischeres Verhalten ergibt sich bei unterschiedlich langer Ausführungs-Zeit der Child-Prozesse:
Ersetzen sie die fixe sleep-Anweisung durch eine → Zufalls-Zahl 0..$iw..3
$iw = int(rand(4));
print "--c$cc wait $iw\n";
sleep $iw;

In diesem Fall 'arbeiten' die Child-Prozesse 0..3 Sekunden.
Tipp: Für realistische Abbruch-Tests muss man mindestens ein noch laufendes und einen bereits beendetes Child untersuchen !

Warten mit   wait

Im Beispiel wartet das Parent-Programm auf das Ende aller Child-Prozesse.
Jede Anweisung
$r = wait();
wartet auf das Ende eines Child-Prozesses und gibt dessen (abgelaufene) PID zurück.
Wenn kein Child-Prozess mehr läuft, dann wird -1 zurückgegeben.

Diese Variante bietet bereits mehr Sicherheit gegen Zombies: Das (Parent)-Programm wird erst nach dem Ende des letzten Child-Prozesses beendet.

Allenfalls nachfolgende weitere Anweisungen werden vom Parent-Programm sicher ausgeführt.

Abbruch mit   kill

Mit einer kill-Anweisung kann man unterschiedliche ↓ Abbruch-Befehle an ein - durch seine PID identifiziertes - Programm senden.
$r = kill 9,$cpid;

Damit wird Signal Nr. 9 (↓ SIGKILL) an den (Child)-Prozess gesendet. Der Child-Prozess wird sofort beendet.
Andere Abbruch-Befehle sind komplizierter zu verwenden, da sie sonst bis zum Parent aufsteigen und auch den Parent-Prozess beenden.

eval-Block

eval-Blocks haben in Perl eine besondere Verwendung zum Einschluss von Ereignissen (events). Die Ereignis-Meldungen (z.B. Fehlermeldungen) werden abgefangen und können mit eigenen Programmen (handler) verarbeitet werden - ähnlich wie in den try-Blocks anderer Programmiersprachen.

Wenn innerhalb des eval-Blocks ein Abbruch-Ereignis auftritt, dann wird lediglich der eval-Block abgebrochen. Danach wird das nachfolgende Programm ausgeführt.
Es ist verwirrend, dass eval alternativ als alleinstehende Anweisung (ohne Block) anders arbeitet - nämlich ebenso wie in allen anderen modernen Programmiersprachen (z.B. → Javascript, PHP, ...) als Mini-Interpreter.

Ereignisse (events) und Handler

Im Beispiel wird eine → Zufalls-Zahl 0..$test..2 erzeugt.
Jedes der 3 möglichen Ergebnisse wird unterschiedlich verarbeitet:
Wenn $test=0 dann versucht das Programm eine (unmögliche) Division durch 0 und erzeugt eine Standard Fehlermeldung. Das führt normalerweise zum Abbruch: Im eval-Block wird lediglich die Fehler-Meldung in der globalen Variablen $@ (oder $EVAL_ERROR) gespeichert.
Wenn $test=1 dann tritt das selbst definierte Ereignis 'Eins' ein. Auf Anweisung die wird (nur) das Programm im eval-Block beendet und die Meldung an $@ übergeben.
Wenn $test=2 tritt kein Abbruch-Ereignis ein und der eval-Block wird vollständig durchlaufen.

In jedem Fall wird das Programm weiter ausgeführt. Zur Demonstration kann man den Wert von $@ ausgeben (hier mit # abgeschaltet).
Der Handler muss prüfen, ob überhaupt ein Ereignis eingetreten ist if($@) und - wenn ja - durch Analyse des Ereignis-Textes ermitteln, um welches Ereignis es sich handelt.
Abfangen und Verarbeiten von Ereignissen:
# eval-Block:
eval {
$test = int(rand(3));
print "test = $test\n";
if($test==1) {die "Eins";}
$n = 10/$test;
};
# Handler:
# print "\$@ = $@";
if($@) {
if ($@=~/Null/) {
print "Handler: Null gefunden !\n";
}
else {
print "Handler: andere Ereignisse...\n";
}
}
Führen sie das Programm mehrmals aus, um die verschiedenen Reaktionen zu beobachten. Das Beispiel funktioniert, ist jedoch stark vereinfacht - in der Praxis werden zusätzliche Sicherungen angebracht.

ALRM Ereignis (timeout)

Dieses Beispiel ist in seinem Aufbau gleich, verwendet jedoch ein anderes Ereignis:

Standard-Signale werden vom → Hash %SIG verwaltet. Als Element ALRM wird ein sub definiert - in diesem Fall enthält es lediglich eine die Anweisung mit einem eindeutig erkennbaren Text (hier 'timeout').
Diese Definition wird mit local auf den eval-Block begrenzt und ist außerhalb unwirksam.

Danach wird mit alarm ein Alarm ausgelöst - in diesem Fall mit einer Vorlaufzeit von 1 Sekunde.

Nun folgt der eigentliche 'Arbeits'-Teil. Er wird durch eine sleep Anweisung simuliert, die mit einem → Zufallswert 0..3 Sekunden andauert. Die Arbeit wird mit Ausgabe von komplett abgeschlossen, das laufende Alarm-Ereignis mit alarm(0) abgeschaltet.
Maximale Laufzeit für ein Programm:
eval {
local $SIG{ALRM}=sub{die 'timeout';};
alarm(1); # nach 1 Sekunde
$w = int(rand(4));
print "wait $w\n";
sleep $w;
alarm(0);
print "komplett\n";
};
if ($@=~/timeout/) {
print "Abbruch\n";
}
else {
print "normaler Ablauf\n";
}
Wenn die Arbeitszeit 0..1 Sekunden beträgt, dann erreicht das Programm sein Ziel und der Alarm wird vor seiner Auslösung abgeschaltet.
Die Variable $@ bleibt leer und der nachfolgende Handler gibt normaler Ablauf aus.
Wenn die Arbeitszeit länger dauert, dann wird der Alarm noch vor Erreichen der letzten Anweisung ausgelöst und das Programm im eval-Block abgebrochen.
In der Ausgabe fehlt der Text komplett
Der nachfolgende Handler findet in der Variablen $@ den Text timeout und gibt Abbruch aus.
Die Analyse der Variablen $@ wird meist mit Regulären Ausdrücken (RegExp) durchgeführt. Details zum Thema Reguläre Ausdrücke. sowie zu den Perl-Funktionen alarm, sleep

Abbruch-Signale

Es gibt viele verschiedene Möglichkeiten, um einen Prozess zu beenden.
Das Linux Konzept von Abbruch-Signalen ist der Maßstab für moderne Betriebssysteme.
Jedem Abbruch-Signal ist ein Name und eine ganze Zahl zugeordnet. Leider ist die Signal-Liste nicht einheitlich: Verschiedene Systeme bieten unterschiedlich viele Signale, teilweise unter anderen Signal-Nummern.

Linux

Ein einfacher Konsolen-Befehl genügt zur Ausgabe der gesamten Liste von Abbruch-Signalen.
Das System-Verhalten wird normalerweise in dieser Text-Datei konfiguriert:
/usr/include/signal.h
Vorsicht - Eine Änderung dieser Datei kann das gesamte System unbrauchbar machen ! Solche Eingriffe erfordern umfangreiches Wissen und Erfahrung !
Ausgabe einer Liste aller Abbruch-Signale an der Shell-Konsole:
(die Option ist ein kleines L, keine Ziffer !)
# kill -l
Vorsicht: Bei Angabe einer Zahl wird der entsprechende Prozess abgebrochen !
So wird das Manual der kill-Anweisung angezeigt:
# man kill
Im Manual navigiert man mit den Pfeil-Tasten. Mit Taste q wird das Manual wieder verlassen.

Windows

bietet kein vergleichbare Signal-Liste. Die Abbruch-Anweisungen werden vom Perl-Interpreter übersetzt. Zur Anzeige verwendet man ein kleines Perl-Programm (rechts).

$i=0;
defined $Config{sig_name} || die "keine Signale ?";
foreach $sn (split(' ', $Config{sig_name})) {
print "signal $i = $sn\n";
$i++;
}

Abbruch

Mit Anweisung kill wird ein Prozess abgebrochen.
Das 1. Argument ist die Nummer des Abbruch-Signals
Das 2. Argument ist die PID-Nr des Prozesses, z.B. aus der Rückgabe einer fork-Anweisung.
Die Funktion gibt die Anzahl tatsächlich beendeter Prozesse zurück.
Tipp: Stellen sie vor Anwendung sicher, ob und mit welcher Nr. das Signal unterstützt wird !
Child-Prozesse lassen sich vom Parent-Programm immer löschen. Für alle anderen Prozesse braucht man jedoch die entsprechenden Zugriffs-Rechte, die auf Linux streng geregelt sind.
Abbruch mit Signal Nr. 9 SIGKILL
$cpid = fork();

kill 9 $cpid;
# kill KILL => $cpid;
# kill('KILL',$cpid);
Das Signal SIGKILL ist bei den meisten Systemen an Nr. 9 gebunden - Das ist allerdings nicht garantiert.

System-unabhängige Varianten geben den Signal-Namen an (hier abgeschaltet). Es ist jedoch nicht garantiert, dass ein bestimmtes Signal auf einem System unterstützt wird, außerdem reagieren Perl Programme auf die gezeigten Syntax-Varianten teilweise etwas unterschiedlich ...

Alle aktiven Child-Prozesse beenden

Als 2. Argument kann auch ein → Array mit den PIDs aller Child-Prozesse übergeben werden. In diesem Fall werden alle noch aktiven Child-Prozesse beendet.

$r = kill 9 @cpid;
print "$r Child-Prozesse beendet\n";

Existiert ein Child ?

Abbruch-Signal   0   führt nicht zu einem Abbruch. Damit kann man feststellen, ob ein Child-Prozess (noch) existiert oder bereits beendet ist.
$r = kill 0,$cpid;
if($r) {print "Child existiert\n";}
else {print "Child existiert nicht\n ";}

Ausnahme für Parent

Je nach System und Konfiguration wird ein Abbruch-Signal nach erfolgtem Abbruch an den jeweiligen Parent-Prozess wie eine aufsteigende Luftblase (bubble) weitergegeben:
In diesem Fall wird nach dem Child auch der Parent-Prozess abgebrochen.
Wenn das unerwünscht ist, kann man den Parent-Prozess mit dem Wert 'IGNORE' vom jeweiligen Handler ausnehmen (Beispiel rechts)
Child löschen, Parent weiterführen, am Beispiel des Abort-Signals:
eval {
local $SIG{ABRT}='IGNORE';
...
kill 6,$cpid[$i]; # Linux
# kill 22,$cpid[$i]; # Windows
};
Vorsicht - Die Signal-Nummern unterscheiden sich je nach System und können auf ihrem System andere Werte aufweisen.
Details zu Ereignissen (events) und Handlern im Kapitel ↑ eval-Block.

Kommunikation über Pipes

Perl bietet mehrere Möglichkeiten zur Kommunikation zwischen Parent und Childs.
Hier werden Pipes vorgestellt, direkte 'Schläuche' oder 'Röhren' für die zu transportierenden Daten.

Wie für → Input/Output allgemein üblich, ist eine Deklaration erforderlich, ähnlich einer open-Anweisung: Damit wird jeweils ein Paar von Pipes erzeugt und mit Handle-Variablen verknüpft. Es ist üblich, diese Variablen mit Großbuchstaben zu bezeichnen.

Anweisung pipe erzeugt je einen (uni-direktionalen) Kanal zum Lesen und zum Schreiben.
Danach können die Pipes von Parent und allen Childs verwendet werden.

Wichtig: (Pipe)-Handles werden beim fork-Prozess im Gegensatz zu allen anderen Variablen nicht kopiert.
Es gibt daher für jede pipe-Anweisung nur ein eindeutiges Paar von Kanälen.

Das Parent-Programm kann daher mittels Pipe nicht einem bestimmten Child selektiv 'zuhören' sondern nur allen Childs gleichzeitig.
Das ist für parallele Prozesse sinnvoll, denn andernfalls braucht man keine Child-Prozesse, sondern arbeitet sequentiell. Um ein bestimmtes Child zu in einer Meldung bezeichnen, deklariert es am besten in den übergebenen Daten selbst seine Identität.

Standard-Kanäle

Die → I/O-Kanäle STDIN, STDOUT, STDERR sind ohne besondere Maßnahmen in jedem Programm geöffnet.

Sie werden beim fork ausnahmsweise nicht in den Child-Prozess kopiert, d.h. der Parent-Prozess und jeder erzeugte Child-Prozess verwenden die gleichen Standard-Kanäle.

Alle hier vorgestellten Beispiele sind Konsolen-Programme, d.h. alle print-Anweisungen geben Text an das gleiche Konsolen-Fenster aus.
Parent und alle Childs arbeiten gleichzeitig und unabhängig voneinander, daher stammen die Ausgabe-Texte in bunter Mischung aus allen Prozessen. Daher ist die Reihenfolge der Ausgabe-Texte weitgehend unbestimmt !

Pipe

Mit Anweisung pipe wird ein Paar von Kanälen zum Lesen (PREAD) und Schreiben (PWRITE) geöffnet. Auch diese beiden Handles werden beim fork nicht in den Child-Prozess kopiert.

Das Parent-Programm hat zunächst keine andere Aufgabe, als Child-Prozesse zu erzeugen. Hier dokumentiert es zusätzlich deren PID-Nummern.

Jedes Child-Programm führt seine eigentliche Aufgabe aus (mit #... angedeutet) und sendet allfällige Meldungen (rot) an den Parent.

Vor dem Ende des Child-Programms muss der verwendete Kanal geschlossen werden.

Nach Erzeugung aller Child-Prozesse liest das Parent-Programm (hier in einer while-Schleife) alle eintreffenden Meldungen.
Meldungen Child-ProzesseParent (vereinfacht !):
pipe(PREAD,PWRITE);
PWRITE->autoflush(1);
$cc = 0;
for($i=0;$i<3;$i++) {
$cpid[$cc] = fork();
if($cpid[$cc]) {
# Parent
print "-p: \$cpid[$cc]=".$cpid[$cc]."\n";
$cc++;
}
else {
# Child
print "-c$cc pid=$$\n";
close PREAD;
# ...
print PWRITE "-c$cc send\n";
# ...
close PWRITE;
exit 0;
}
}
# Parent read loop
close PWRITE;
while (chomp($msg = <PREAD>)) {
print "-p read: $msg\n";
}
close PREAD;
Anmerkungen:

Anweisung autoflush sorgt nach jeder einzelnen print-Anweisung für die sofortige Übertragung der Daten dieses Kanals (andernfalls werden die Daten gespeichert und in größeren Blöcken gesendet).

Nicht benötigte Kanäle werden vor der Kommunkation geschlossen (close PREAD von jedem Child), benutzte Kanäle nach der Kommunikation (close PWRITE von jedem Child am Ende seines Programms.
Wenn der PWRITE-Kanal von einem einzigen Child nicht geschlossen wird, dann entkommt das Parent-Programm nicht aus seiner (unendlichen) Lese-Schleife !
In seiner Lese-Schleife erhält das Parent-Programm die auf <PREAD> eintreffenden Meldungen aller Childs. Dabei wird keine Reihenfolge eingehalten, d.h. die Daten können von jedem Child stammen.
Im Beispiel markiert jedes Child seine Meldung mit seiner fortlaufenden Nummer ($cc), alternativ mit seiner PID-Nummer $$.

Das Beispiel funktioniert, muss jedoch für die Praxis abgesichert werden:
Ein ↑ Timeout muss sicherstellen, dass nach Ablauf einer sinnvollen Arbeitszeit die Lese-Schleife verlassen wird und alle noch aktiven Childs (mit kill) beendet werden. (nachfolgende Beispiele).

Wenn eine bidirektionale Kommunikation gewünscht wird, dann öffnet man mit einer weiteren pipe-Anweisung ein zusätzliches Paar von Kanälen. In diesem Fall kann man auch Meldungen vom Parent an die (noch aktiven) Childs senden. Das ist nur dann notwendig, wenn die Daten erst nach dem fork-Prozess vorliegen.
Normalerweise wird ein Child bereits bei der Erzeugung mit allen Daten zu seiner Steuerung versorgt. Dazu genügt es, den betreffenden Variablen vor dem fork Werte zuzuweisen.

Modul IO

Dieses Perl-Modul bietet zahlreiche spezialisierten Funktionen für versierte EntwicklerInnen.

Hier wird lediglich das Sub-Modul IO::Pipe vorgestellt. Das Beispiel arbeitet genauso wie das oben vorgestellte Beispiel.

Weitere Funktionen bieten u.a. die Möglichkeit für non-blockin Pipes.
In diesem Fall wird das Lesen von einem Pipe-Kanal nicht blockiert, wenn der Kanal nicht bereit ist.
Meldungen Child-ProzesseParent (vereinfacht !):
use IO::Pipe;
my $pipe=new IO::Pipe;
$cc = 0;
for($i=0;$i<3;$i++) {
$cpid[$cc] = fork();
if($cpid[$cc]) {
# Parent
print "-p: \$cpid[$cc]=".$cpid[$cc]."\n";
$cc++;
}
else {
# Child
print "-c$cc pid=$$\n";
$pipe->writer();
$pipe->autoflush(1);
# ...
print $pipe "-c$cc send\n";
# ...
exit 0;
}
}
# Parent read loop
$pipe->reader();
while (chomp($msg = <$pipe>)) {
print "-p read: $msg\n";
}
undef $pipe;

Private Kanäle

Wenn beliebige → I/O-Kanäle nach dem fork-Verzweigung geöffnet werden, dann dienen sie dem jeweiligen Programm-Teil als exklusive private Kanäle.

Wenn z.B. im Child Programm-Teil eine Datei zum Lesen oder Schreiben geöffnet wird
open(READ,'test.dat');
dann benutzt jeder Child-Prozess mit dem Handle READ seinen eigenen Kanal, meist zu unterschiedlichen Dateien. Jeder Child-Prozess sollte offene Kanäle nach Verwendung unbedingt schließen.

Non-Blocking

Ohne weitere Maßnahmen sind alle I/O-Anweisungen (<KANAL>-Zuweisung, print) 'Blocking', d.h. jede I/O-Anweisung wartet mit der Ausführung, bis der entsprechende Kanal 'ready' meldet.

Dieses Verhalten kann ein Programm lahmlegen, wenn der Kanal niemals bereit (fertig) wird.

Dieser Fall tritt bei Child-Prozessen öfters auf. Dagegen helfen 2 übliche Strategien:
Blockierte Child-Prozesse durch das Parent-Programm abbrechen (folgendes Beispiel).
Kanal 'Non-Blocking' setzen (rechts).
use IO::Handle;
$io = new IO::Handle;
pipe(PREAD,PWRITE);
$cc=0;
for $i (0..3) {
$cpid[$cc] = fork();
if($cpid[$cc]) {
# Parent
print "-p: \$cpid[$cc]=".$cpid[$cc]."\n";
$cc++;
}
else {
# Child
close PREAD;
$io->fdopen(fileno(PWRITE),'w');
$io->autoflush (1);
$w = int(rand(5));
$io->print("-c$cc: wait $w\n");
print "-c$cc: wait $w\n";
sleep($w); # Simul.Arbeitszeit
$io->print("-c$cc: finished\n");
$io->close;
exit 0;
}
}

# Parent
close PWRITE;
sleep(1);
kill(9,@cpid);
$io->fdopen(fileno(PREAD),'r');
$io->blocking(0);
while(chomp($msg=$io->getline)) {
print "-p read: $msg\n";
}
$io->close;
close PREAD;
Das Beispiel zeigt eine Variante des mit Pipe gezeigten Beispiels am Anfang dieses Kapitels. Der Code wurde möglichst wenig modifiziert.

Zunächst wird ein Handle ($io) erzeugt, dem später mit fdopen die Pipe-Kanäle zugewiesen werden, und zwar PWRITE in den Child-Prozessen und PREAD im Parent-Prozess.

Jeder Child-Prozess sendet 2 Meldungen ('wait', 'finished'), dazwischen wird eine zufällige 'Arbeitszeit' simuliert.

Der Parent-Prozess wartet (sleep) 1 Sekunde und beendet danach (kill) alle noch arbeitenden Child-Prozesse.

Der Eingangs-Kanal kann mit $io->blocking(0) non-blocking gesetzt werden.
Das Beispiel verhält sich - wie einige dieser Seite - je nach System unterschiedlich:

Linux:
Anweisung $io->blocking(0) funktioniert. Der Lese-Vorgang wird durch noch laufende Child-Prozesse nicht mehr blockiert.

Mit kill(9,@cpid) werden alle noch laufenden Child-Prozesse beendet. Auch das genügt, um die Blockierung des Pipe-Kanal aufzuheben.
Spätestens am Ende des Parent-Programms sollten damit alle noch laufenden Zombies beendet werden.

Windows:

Anweisung $io->blocking(0) funktioniert derzeit nicht. Der Lese-Vorgang wird durch jeden noch laufenden Child-Prozess blockiert.

Mit kill(9,@cpid) werden alle noch laufenden Child-Prozesse beendet. Das genügt normalerweise, um die Blockierung des Pipe-Kanal aufzuheben.

Beispiel: Eingabe mit Timeout

Dieses Beispiel demonstriert die Verwendung eines einzelnen Child-Prozesses.

Eine manuelle Eingabe ist typisch für einen Vorgang, dessen Dauer unbekannt ist, der jedoch nach einer angemessenen Wartezeit abgebrochen werden muss.
Das Programm wird in jedem Fall ausgeführt und kann reagieren, je nachdem ob Daten eingegeben wurden oder nicht.
Strategie:
Der unsichere Vorgang (Warten auf die Eingabe) wird in einen Child-Prozess verlagert, den man notfalls abbrechen kann.
Der Parent-Prozess enthält nur sichere Programm-Teile und kann daher nicht in eine endlose Warteschleife geraten.

Das Beispiel kombiniert Elemente aus den meisten Kapiteln dieser Seite.
Zur Kommunikation (Übergabe des erhaltenen Eingabe-Textes) wird eine ↑ Pipe geöffnet.

Der Child-Prozess wartet an der rot bezeichneten Stelle auf die Eingabe <STDIN> über die Tastatur
Nach Eingabe wird ein nachfolgendes \n Newline-Zeichen entfernt und der erhaltene Text über die Pipe an den Parent-Prozess gesendet.
Wenn keine Eingabe erfolgt, dann wartet der Child-Prozess unendlich lange oder bis zu seinem Abbruch durch den Parent-Prozess.

Der Parent-Prozess darf nur dann von der Pipe <PREAD> lesen (rot), wenn der Kanal vom Child-Prozess geschlossen wurde (close PWRITE) - Andernfalls würde der Parent-Prozess endlos lange versuchen, die Daten der Pipe zu lesen.

Der Parent-Prozess beginnt mit einem eval-Block. Darin wird ein Timeout-Ereignis mit der gewünschten Wartezeit gesetzt. Danach wartet der Prozess mit wait auf das reguläre Ende des Child-Prozesses. Wenn dieses eintritt, dann wird der alarm abgeschaltet, andernfalls wird das Timeout-Ereignis ausgelöst.

Der eval-Block wird spätestens nach Eintritt des alarm-Events verlassen. Bei erfolgter Eingabe wird das Warten wait beendet und der eval-Block sofort verlassen.

Nach Ende des eval-Blocks ist geklärt, ob der Child-Prozess mit einer Eingabe beendet wurde ($@ ist leer) oder ob ein Timeout erfolgte ($@='timeout').
Wenn der Child-Prozess noch auf die Eingabe wartet, dann wird er mit kill beendet.

Die weitere Vorgangsweise ist nur angedeutet:
Wenn ein Text eingegeben wurde, dann wird er vom Parent ausgegeben, andernfalls wird auf den Timeout hingewiesen.
use IO::Handle;
my $ts = 5; # 5 Sekunden
my ($cpid,$txt);
pipe(PREAD,PWRITE);
PWRITE->autoflush(1);
$cpid=fork();
if (!$cpid) {
# Child
close PREAD;
print "Eingabe ";
print "(Timeout nach $ts Sekunden)\n";
print "--> ";
$txt = <STDIN>; # Child wartet
$txt=~s/\n$//;
print PWRITE $txt;
close PWRITE;
exit 1;
}
# Parent
close PWRITE;
eval{
local $SIG{ALRM}=sub{die "timeout";};
alarm($ts);
wait();
alarm(0);
};
if ($@=~/timeout/) {
kill 9,$cpid;
$txt = '# Timeout #';
}
else {
# Darf nicht warten !
$txt = <PREAD>;
}
close PREAD;
print "Text = $txt\n";