Sicherlich haben Sie schon gehört, dass es sich bei Linux um ein Multitasking- und Mehrbenutzer-Betriebssystem handelt. Beides sind Konzepte, die unabdingbar dafür sind, dass man Linux sowohl auf Server- als auch auf Client-Rechnern einsetzen kann. Heute lernen Sie:
fork()
neue Prozesse startet
Wenn die Leute davon sprechen, dass Linux ein Multitasking-Betriebssystem sei, meinen Sie damit, dass Linux mehrere Aufgaben gleichzeitig erledigen kann. Wenn Sie beispielsweise die Übungen aus diesem Buch bearbeiten, so verwenden Sie einen Texteditor zum Aufsetzen des Quellcodes, ein xterm-Fenster zum Kompilieren und Ausführen Ihrer Programme und unter Umständen auch den Gnome-Hilfe-Browser oder ein ähnliches Programm zum Lesen der Online-Dokumentationen. Jedes dieser Programme verhält sich dabei so, als ob es das einzige Programm wäre, das auf dem Computer ausgeführt wird.
Ermöglicht wird dies durch Linux, das sehr schnell - etwa 100-mal pro Sekunde oder
mehr - zwischen den Programmen hin- und herschaltet. Beim Anwender, also Ihnen,
entsteht dadurch der Eindruck, dass die Programme gleichzeitig ausgeführt werden.
Wenn Sie sich einige der gerade in Ausführung befindlichen Programme anzeigen
lassen möchten, tippen Sie hinter dem Befehlseingaben-Prompt den Befehl ps u
ein.
Danach wird Ihnen eine Aufstellung angezeigt, die ähnlich wie die nachfolgende Liste
aufgebaut sein dürfte:
[erik@coltrane erikd]$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
erik 914 0.0 0.2 1776 652 pts/5 S Sep24 0:01 -bash
erik 11359 0.0 1.1 4516 2928 pts/5 S Sep25 0:00 nedit list1901.c
erik 11362 0.0 0.9 4516 2928 pts/5 S Sep25 0:00 nedit list1902.c
erik 11368 0.0 0.3 2528 908 pts/5 R Sep25 0:00 ps u
Es ist extrem unwahrscheinlich, dass Sie genau die gleichen Daten angezeigt
bekommen. Statt meiner User-ID, erik
, werden Sie Ihre eigene User-ID sehen, und
auch die Werte in den anderen Spalten dürften sich von den hier abgebildeten Daten
unterscheiden. In obigem Beispiel zeigt der ps
-Befehl drei Programme, aber vier
Prozesse an: bash
(der Kommandozeilen-Interpreter), zwei Instanzen des Texteditors
nedit
und den eigenen ps
-Prozess. Welcher Unterschied besteht zwischen einem
Programm und einem Prozess? Ein Programm ist eine ausführbare Datei, die auf der
Festplatte (Diskette etc.) abgespeichert ist. Ein Prozess dagegen ist die Instanz eines
Programms, die gerade unter dem Betriebssystem ausgeführt wird.
Die obige Prozessliste zeigt wirklich nur einen kleinen Teil der Programme, die auf
dem Rechner ausgeführt werden. Wenn Sie sich eine vollständige Liste anzeigen
lassen wollen, tippen Sie den Befehl ps aux
ein. Diese Liste kann gut und gerne 50
oder mehr Programme enthalten.
Uns interessieren im Moment allerdings mehr die Spalten der Liste, insbesondere die zweite Spalte mit der Überschrift PID, was für »process identifier« steht. Wenn unter Linux ein Programm ausgeführt wird, bekommt das Programm eine eindeutige Prozess-ID oder -kennung zugewiesen, die im Bereich zwischen 1 und 32767 liegt. Anhand dieser Prozess-ID - und nicht etwa anhand des möglicherweise mehrdeutigen Programmnamens (unter Umständen werden zwei Programme mit dem gleichen Namen ausgeführt) - kann das Betriebssystem in Ausführung befindliche Programme identifizieren und auf diese zugreifen. Wird ein Programm beendet, wird seine Prozess-ID freigegeben und kann später einem anderen Programm zugewiesen werden.
Am Tag 12, »Fortgeschrittene Programmsteuerung«, haben wir uns die Funktion
system()
angeschaut, die es einem Programm ermöglicht, ein anderes Programm
aufzurufen. In so einem Fall bezeichnet man das Programm, das die Funktion
system()
aufruft, als Elternprozess und das Programm, das durch den Aufruf gestartet
wird, als Kindprozess. Beide Prozesse erhalten eigene, eindeutige Prozess-IDs.
Linux stellt, wie die anderen Betriebssysteme der Unix-Familie auch, eine spezielle
Funktion zur Verfügung, mit deren Hilfe man die PID eines Prozesses abfragen kann.
Eine zweite Funktion erlaubt es einem Kindprozess, die PID seines Elternprozesses zu
ermitteln. Beide Funktion sind in der Header-Datei unistd.h
definiert:
pid_t getpid(void);
pid_t getppid(void);
Die erste Funktion, getpid()
, liefert die PID des Prozesses zurück, der getpid()
aufgerufen hat. Die zweite Funktion, getppid()
, liefert die PID des Elternprozesses.
Der Rückgabewert ist jeweils vom Typ pid_t
, der in einer der in stdlib.h
eingeschlossenen Header-Dateien als int
definiert ist.
Listing 19.1 zeigt ein einfaches Beispiel für den Einsatz dieser Funktionen.
Listing 19.1: Die ID des aktuellen Prozesses und seines Elternprozesses ermitteln.
1 : /* Verwendung von getpid() und getppid(). */
2 : #include <stdio.h>
3 : #include <stdlib.h>
4 : #include <unistd.h>
5 :
6 : int main(void)
7 : {
8 : pid_t pid;
9 :
10: pid = getpid();
11: printf ("Meine PID = %d\n", pid) ;
12:
13: pid = getppid();
14: printf ("Die PID meines Eltern = %d\n", pid) ;
15:
16: return 0;
17: }
Meine PID = 12179
Die PID meines Eltern = 914
Das Programm aus diesem Listing definiert in Zeile 8 eine Variable vom Typ pid_t
.
Die Funktion getpid()
wird in Zeile 10 aufgerufen, die Funktion getppid()
in Zeile
13. Die Werte, die von den Funktionen zurückgegeben werden, werden in den Zeilen
11 und 14 ausgegeben. Denken Sie daran, dass sich hinter dem Datentyp pid_t
eigentlich int
verbirgt, weshalb in den beiden printf
-Aufrufen der
Konvertierungsspezifizierer %d
verwendet wird.
Wenn Sie das Programm mehrmals im gleichen Konsolenfenster ausführen, erhalten
Sie jedes Mal eine andere Prozess-ID, während die ID für den Elternprozess immer die
gleiche bleibt. Wenn Sie den ps
-Befehl ausführen, werden Sie feststellen, dass die
Prozess-ID des Elternprozesses mit der Prozess-ID des Bash-Befehlsinterpreters des
Konsolenfensters, in dem sowohl der ps
-Befehl als auch das Programm aus Listing
19.1 ausgeführt werden, übereinstimmt.
Wie kann ein Programm wie der Bash-Befehlsinterpreter die Ausführung eines anderen Programms anstoßen und dabei selbst weiter ausgeführt werden? Nun, wir werden dies peu à peu klären. Damit ein Programm als Befehlsinterpreter fungieren kann, muss es zwei Dinge können: Es muss andere Prozesse starten und einen Prozess durch einen anderen ersetzen können.
Die Fähigkeit des Bash-Befehlsinterpreters, andere Programme zu starten und selbst
weiter ausgeführt zu werden, kann man leicht mit Hilfe des Programms aus Listing
19.2 demonstrieren. Nachdem Sie dieses Programm kompiliert haben, sollten Sie es
zuerst ohne das kaufmännische Und (&
) und danach mit dem &
-Symbol am Ende der
Befehlszeile aufrufen. Wenn Sie ein Programm mit dem &
-Symbol starten, weisen Sie
den Befehlsinterpreter an, das Programm im Hintergrund auszuführen. Dies
ermöglicht es dem Befehlsinterpreter, sofort zurückzukehren, so dass Sie weitere
Befehle eingeben können. Ohne das &
-Symbol wartet der Befehlsinterpreter darauf,
dass das Programm beendet wird, bevor er die Eingabe weiterer Befehle erlaubt.
Wenn Sie das Programm mit und ohne &
-Symbol ausführen, versuchen Sie jeweils,
einen anderen Befehl, beispielsweise ls
, einzugeben. Vergleichen Sie die Ergebnisse
und versuchen Sie diese zu interpretieren.
Listing 19.2: Ein Programm, das im Hintergrund ausgeführt werden kann.
1 : /* Ein Programm, das im Hintergrund ausgeführt werden kann. */
2 : #include <stdio.h>
3 : #include <unistd.h>
4 :
5 : int main(void)
6 : {
7 : int count;
8 :
9 : for(count = 0; count < 10; count ++)
10: {
11: sleep(2);
12: puts("Immer noch am Laufen!");
13: }
14:
15: puts("Jetzt bin ich fertig!");
16: return 0;
17: }
[erik@coltrane day19]$ ./list1902
Immer noch am Laufen!
Immer noch am Laufen!
ls
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Jetzt bin ich fertig!
[erik@coltrane day19]$ ls
list1901.c list1902 list1902.c list1903.c list1904.c
[erik@coltrane day19]$
[erik@coltrane day19]$
[erik@coltrane day19]$ ./list1902 &
[1] 12345
[erik@coltrane day19]$ Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
[erik@coltrane day19]$ ls
list1901.c list1902 list1902.c list1903.c list1904.c
[erik@coltrane day19]$ Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Immer noch am Laufen!
Jetzt bin ich fertig!
[1]+ Done ./list1902
Das Programm selbst ist sehr einfach aufgebaut und enthält nichts, was Sie nicht
schon von früheren Lektionen her kennen. Was uns hier interessiert, ist die Ausgabe
des Programms. Der hier abgedruckten Ausgabe können Sie entnehmen, dass das
Programm zuerst ohne &
-Symbol ausgeführt wurde. Nachdem das Programm seine
Meldung zweimal ausgegeben hat, tippt der Anwender den ls
-Befehl ein und drückt
die Eingabetaste. Normalerweise würde dieser Befehl sofort ausgeführt, aber unser
Programm läuft noch und solange es läuft, kann der ls
-Befehl nicht ausgeführt
werden. Wie Sie sehen können, erscheint die Ausgabe des ls
-Befehls erst, nachdem
unser Programm seine »Jetzt bin ich fertig«-Meldung ausgegeben hat.
Wenn das Programm mit dem &
-Symbol in der Befehlszeile aufgerufen wird, ist das
Verhalten ein anderes. Zuerst gibt der Bash-Befehlsinterpreter zwei Zahlen aus, von
denen die erste in Klammern gesetzt ist. Die Zahl in Klammern gibt die Anzahl der
Prozesse an, die derzeit im Hintergrund ausgeführt werden. Die andere Zahl ist die
Prozess-ID des Prozesses, der soeben gestartet wurde. Danach gibt der Bash-
Befehlszeileninterpreter seinen Eingabe-Prompt aus, und unser Programm, das im
Hintergrund ausgeführt wird, gibt seine Meldungen aus. Wenn der Anwender den ls
-
Befehl eingibt, wird dieser augenblicklich vom Bash-Interpreter ausgeführt. Danach
gibt unser Programm weiter seine »Immer noch am Laufen«-Meldungen aus, bis es
schließlich beendet wird. Der Bash-Interpreter gibt daraufhin eine Done
-Meldung aus,
um den Anwender darüber zu informieren, dass das Programm beendet wurde.
Linux und andere Mitglieder der Unix-Familie verfügen über eine Standardmethode
zum Starten anderer Prozesse, die auf der Funktion fork()
basiert. Ebenso wie
getpid()
liefert fork()
eine Prozess-ID zurück und ist in der Header-Datei unistd.h
definiert. Ihr Prototyp sieht wie folgt aus:
pid_t fork(void);
Wenn fork()
aus irgendeinem Grund scheitert, liefert die Funktion den Wert -1
zurück. Tritt kein Fehler auf, erzeugt fork()
einen neuen Prozess, der mit dem
aufrufenden Prozess identisch ist. Sowohl der alte als auch der neue Prozess werden
danach - ab der Anweisung hinter dem fork()
-Aufruf - parallel ausgeführt. Obwohl
beide Prozesse das gleiche Programm ausführen, verfügen sie über eigene Kopien
aller Daten und Variablen. Eine dieser Variablen ist die Variable vom Typ pid_t
, die
von der fork()
-Funktion zurückgeliefert wurde. Im Kindprozess ist der Wert dieser
Variablen 0, im Elternprozess ist es der Wert der Prozess-ID des Kindprozesses. Nach
dem Aufruf von fork()
sind die Daten beider Programme getrennt, so dass weder der
Kind- noch der Elternprozess in der Lage ist, irgendwelche Variablen oder Daten im
jeweils anderen Prozess zu manipulieren. Listing 19.3 demonstriert die Verwendung
der fork()
-Funktion.
Listing 19.3: Mit Hilfe von fork() einen neuen Prozess erzeugen.
1 : /* Startet mit fork() einen neuen Prozess. */
2 : #include <stdio.h>
3 : #include <stdlib.h>
4 : #include <unistd.h>
5 :
6 : int main(void)
7 : {
8 : pid_t pid;
9 : int x=13;
10:
11: pid = fork();
12:
13: if (pid < 0)
14: {
15: printf("Fehler : fork () lieferte %u.\n", pid);
16: exit(1);
17: }
18:
19: if (pid == 0)
20: {
21: printf("Kind : PID = %u. PID des Eltern = %u\n",
22: getpid(), getppid());
23: printf("Kind : x = %d, ", x);
24: x = 10;
25: printf("neues x = %d\n", x);
26: sleep(2);
27: puts ("Kind : wird jetzt beendet.");
28: exit(42);
29: puts ("Kind : Diese Meldung werden Sie nie sehen.");
30: }
31: else
32: {
33: printf("Eltern: PID = %u. PID des Kindes = %u\n",
34: getpid(), pid);
35: puts("Eltern: lege mich für 60 Sekunden schlafen.");
36: sleep(60);
37: puts("Eltern: wacht wieder auf.");
38: printf("Eltern: x = %d\n", x);
39: }
40:
41: return 0;
42: }
Eltern: PID = 16525. PID des Kindes = 16526
Eltern: lege mich für 60 Sekunden schlafen.
Kind : PID = 16526. PID des Eltern = 16525
Kind : x = 13, neues x = 10
Kind : wird jetzt beendet.
Eltern: wacht wieder auf.
Eltern: x = 13
Das Einzige, was in diesem Listing neu für Sie ist, ist der Aufruf der fork()
-Funktion.
Die Zeilen 2 bis 4 binden die notwendigen Header ein, und die Zeilen 8 und 9
definieren zwei Variablen, von denen die zweite, x
, auf 13 gesetzt wird. Die fork()
-
Funktion wird in Zeile 11 aufgerufen. Anhand ihres Rückgabewertes wird festgestellt,
ob ein Fehler aufgetreten ist (Zeile 13). Sind keine Fehler aufgetreten, werden von
Zeile 12 an zwei Prozesse ausgeführt. Im Kindprozess ist der Wert von pid
0, im
Elternprozess enthält die Variable eine Prozess-ID im Bereich zwischen 1 und 32767.
Die if
-Anweisung in Zeile 21 wird von beiden Prozessen ausgewertet. Der
Kindprozess führt danach den Block in den Zeilen 21 bis 29 aus, der Elternprozess
den Block von Zeile 33 bis 38.
An der Programmausgabe können Sie erkennen, dass der Elternprozess nach dem
fork()
-Aufruf seine eigene PID und die PID des Kindprozesses (Zeilen 33 und 34) und
eine Meldung ausgibt und sich dann mit Hilfe der Funktion sleep()
, die am Tag 12
behandelt wurde, für 60 Sekunden schlafen legt.
Wenn sich der Elternprozess schlafen legt, wird der Kindprozess weiter ausgeführt. Als
erstes gibt er in den Zeilen 21 und 22 seine eigene PID, die seines Elternprozesses
aus. Anhand der Ausgabe können Sie sich vergewissern, dass diese Werte mit den
Werten des Elternprozesses übereinstimmen, d.h. die PID des Eltern des Kindes ist die
PID des Elternprozesses. Als Nächstes gibt der Kindprozess den Wert der Variablen x
aus, ändert den Wert und gibt ihn erneut aus. Schließlich ruft auch der Kindprozess
die Funktion sleep()
auf. Da sich der Kindprozess aber nur für 2 Sekunden schlafen
legt, während der Elternprozess ganze 60 Sekunden schläft, wacht der Kindprozess
vor seinem Eltern auf und gibt in Zeile 27 eine Meldung aus. Dann ruft er in Zeile 28
exit()
mit dem ArgumeNT 42 auf. Beachten Sie, dass die Meldung aus Zeile 29
wegen des vorangehenden exit()
-Aufrufs nicht mehr ausgeführt wird. Wie Sie von
Tag 12 wissen, beendet die exit()
-Funktion ein Programm und übergibt den Wert
ihres Integer-Arguments an das Betriebssystem. 60 Sekunden später erwacht der
Elternprozess von seinem eigenen sleep()
-Aufruf, gibt den Wert der Variablen x aus
und beendet sich selbst durch die return
-Anweisung in Zeile 41.
Beachten Sie, dass die Variable x
vor dem fork()
-Aufruf im Elternprozess auf den Wert
13 gesetzt wurde. Der Kindprozess hat x
zwar später auf 10 gesetzt, doch als der
Elternprozess den Wert von x
nach der Beendigung des Kindprozesses ausgab, war der
Wert immer noch gleich 13. Der Grund hierfür ist schlichtweg, dass nach dem fork()
-
Aufruf beide Prozesse ihre eigene Kopie der Variablen x
besitzen und verwenden.
Bisher waren alle vordefinierten Funktionen, mit denen wir es zu tun hatten, Teil der C-Bibliothek. Bei der
fork()
-Funktion und einigen anderen Funktionen, die wir heute kennen lernen werden, handelt es sich aber im Grunde um Systemfunktionen, da sie vom Betriebssystem definiert sind und als Schnittstelle zu diesem dienen.
Das Programm aus Listing 19.3 zeigt die einfachstmögliche Verwendung der fork()
-
Funktion. Das Programm enthält allerdings auch einen kleinen Makel, der in
bestimmten Situationen Probleme verursachen kann. Um zu verstehen, worin dieser
Fehler besteht, führen Sie das Programm noch einmal mit dem &
-Symbol aus, so dass
es im Hintergrund ausgeführt wird. Wenn die »Kind : wird jetzt beendet
«-Meldung
erscheint, rufen Sie den Befehl ps u
auf und betrachten die Liste, die ungefähr so
aussehen dürfte:
[erik@coltrane day19]$ ./list1903 &
Eltern: lege mich für 60 Sekunden schlafen.
Kind : PID = 16714. PID des Eltern = 16713
Kind : x = 13, neues x = 10
Kind : wird jetzt beendet.
[erik@coltrane day19]$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
erik 914 0.0 0.2 1784 708 pts/5 S Sep24 0:01 -bash
erik 16490 0.0 1.2 4640 3184 pts/5 S Sep27 0:00 nedit list1903.c
erik 16713 0.0 0.1 1052 332 pts/5 S Sep27 0:00 ./list1903
erik 16714 0.0 0.0 0 0 pts/5 Z Sep27 0:00 [list1903 <defunct>]
erik 16715 0.0 0.3 2556 936 pts/5 R Sep27 0:00 ps u
Schauen Sie sich die vierte Zeile der Ausgabe des ps
-Befehls an. Eine Kopie des
Programms list1903
wird als erloschen (defunct
) gemeldet. In der STAT
-Spalte dieses
Prozesses steht ein Z
, was bedeutet, dass es sich um einen so genannten Zombie-
Prozess handelt.
Prozesse verwenden zum Beenden die return
-Anweisung oder rufen die Funktion
exit()
mit einem Wert auf, der an das Betriebssystem zurückgeliefert wird. Das
Betriebssystem lässt den Prozess so lange in seiner internen Datentabelle eingetragen,
bis entweder der Elternprozess des Prozesses den zurückgelieferten Wert liest oder der
Elternprozess selbst beendet wird. Ein Zombie-Prozess ist in diesem Sinne ein
Prozess, der zwar beendet wurde, dessen Elternprozess den Exit-Wert des Kindes aber
noch nicht gelesen hat. Erst wenn der Elternprozess beendet wird, wird auch der
Zombie-Prozess aus der Prozesstabelle des Betriebssystems entfernt.
Was ist so schlimm an den Zombie-Prozessen? Nun, eine der häufigsten
Einsatzbereiche für die Systemfunktion fork()
sind Server-Anwendungen, die über
Netzwerkverbindungen mit Client-Programmen kommunizieren. Mail-Server oder
World-Wide-Web-Server sind gute Beispiele für diese Art von Server-Anwendungen,
die üblicherweise direkt beim Booten des Rechners gestartet und nicht vor dem
Herunterfahren des Rechners beendet werden - was unter Umständen Wochen,
Monate oder sogar Jahre dauern kann. In der Zwischenzeit erzeugt der Server mittels
fork()
für jedes Client-Programm, das sich über das Netzwerk mit dem Server
verbindet, einen Kindprozess, der die Kommunikation mit dem Client übernimmt.
Wenn der Client die Kommunikation mit dem Server abgeschlossen hat, schließt er
die Verbindung, und der Kindprozess des Servers wird beendet. Es liegt auf der Hand,
dass der Server dabei sicherstellen muss, dass keiner seiner Kindprozesse zu einem
Zombie-Prozess mutiert, oder das Betriebssystem hat bald keinen Platz mehr in seiner
Prozesstabelle. Das Löschen der Zombie-Prozesse bezeichnet man im Übrigen auch
als Reaping - eine Anspielung auf den Sensemann, der im Englischen »The Grim
Reaper« genannt wird.
Es gibt mehrere Wege, die Entstehung von Zombie-Prozessen zu verhindern. Am
häufigsten wird die Systemfunktion wait()
verwendet, die in der Header-Datei sys/
wait.h
wie folgt definiert ist:
pid_t wait(int *status);
Diese Funktion definiert einen int
-Zeiger als Parameter und liefert einen Wert vom
Typ pid_t
zurück. Wenn die Funktion aufgerufen wird, hält sie die Ausführung des
Elternprozesses so lange an, bis ein Kindprozess beendet wird. Tritt dieser Fall ein
oder liegt ein Kindprozess als Zombie-Prozess vor, liefert wait()
die Prozess-ID des
Kindes zurück und kopiert den Exit-Wert des Kindprozesses in die Adresse, auf die das
Zeigerargument *status
verweist. Wenn Sie an dem Rückgabewert des Kindprozesses
nicht interessiert sind, sollten Sie wait()
den Wert NULL
übergeben. Gibt es keinen
Kindprozess, liefert wait()
den Wert -1
zurück. Listing 19.4 demonstriert, wie man
mit wait()
einen bereits beendeten Kindprozess auflöst.
Listing 19.4: Mit wait() Zombie-Prozesse verhindern.
1 : /* Mit wait() auf einen Kindprozess warten. */
2 : #include <stdio.h>
3 : #include <stdlib.h>
4 : #include <unistd.h>
5 : #include <sys/types.h>
6 : #include <sys/wait.h>
7 :
8 : int main(void)
9 : {
10: pid_t pid;
11: int status;
12:
13: pid = fork();
14:
15: if (pid < 0)
16: {
17: printf("Fehler : fork () lieferte %u.\n", pid);
18: exit(1);
19: }
20:
21: if (pid == 0)
22: {
23: printf("Kind : PID = %u. PID des Eltern = %u\n",
24: getpid(), getppid());
25: sleep(1);
26: puts ("Kind : wird jetzt beendet.");
27: exit(42);
28: }
29: else
30: {
31: printf("Eltern: PID = %u. PID des Kindes = %u\n",
32: getpid(), pid);
33: puts("Eltern: lege mich für 10 Sekunden schlafen.");
34: sleep(10);
35: puts("Eltern: wacht wieder auf.");
36:
37: pid = wait(&status);
38: printf("Eltern: Kind mit PID %u ", pid);
39: if (WIFEXITED(status) != 0)
40: printf("wurde mit Status %d beendet\n",WEXITSTATUS(status));
41: else
42: printf("wurde anomal beendet.\n");
43: puts("Eltern: lege mich für 30 Sekunden schlafen.");
44: sleep(30);
45: }
46:
47: return 0;
48: }
Eltern: PID = 19301. PID des Kindes = 19302
Eltern: lege mich für 10 Sekunden schlafen.
Kind : PID = 19302. PID des Eltern = 19301
Kind : wird jetzt beendet.
Eltern: wacht wieder auf.
Eltern: Kind mit PID 19302 wurde mit Status 42 beendet
Eltern: lege mich für 30 Sekunden schlafen.
Dieses Listing entspricht weitgehend dem Programm aus Listing 19.3. Der
Hauptunterschied liegt darin, dass der Elternprozess nach dem Erwachen die Funktion
wait()
aufruft (Zeile 37). Da der Kindprozess schon vorher beendet wurde, kehrt
wait()
sofort nach dem Aufruf zurück und setzt die Variable pid
auf die Prozess-ID des
beendeten Kindprozesses. Des Weiteren kopiert die Funktion den Exit-Wert des
Prozesses in die Variable status
, deren Adresse der Funktion als Argument übergeben
wurde. Der Elternprozess gibt die Prozess-ID des Kindes aus und verwendet die
Makros, WIFEXITED()
and WEXITSTATUS()
, die in sys/wait.h
definiert sind, um den
Rückgabestatus des Kindprozesses abzufragen und ebenfalls auszugeben. Auf der
Manpage zur wait()
-Funktion können Sie nachlesen, dass diese Makros dafür sorgen,
dass nur 8-Bit-Werte (1 bis 255) als Exit-Status zurückgeliefert werden.
Wenn Sie das Programm im Hintergrund ausführen (&
-Symbol in Befehlszeile
verwenden), können Sie den Befehl ps u
während der Laufzeit des Programms
aufrufen. Wenn Sie den Befehl nach dem exit()
-Aufruf des Kindprozesses (Zeile 27),
aber noch vor dem Erwachen des Elternprozesses (Zeile 34) aufrufen, können Sie
sehen, dass der Kindprozess - wie zuvor - zum Zombie-Prozess mutiert ist. Wenn Sie
den ps u
-Befehl noch einmal ausführen, wenn der Elternprozess die sleep()
-Funktion
zum zweiten Male aufruft (Zeile 43), werden Sie feststellen, dass das Betriebssystem
den Zombie-Prozess aufgelöst hat.
Die wait()
-Funktion ist offensichtlich recht nützlich, wenn man weiß, dass der
Kindprozess bereits beendet wurde. Sollte dies nicht der Fall sein, hält die wait()
-
Funktion den Elternprozess so lange an, bis der Kindprozess beendet wird. Wenn
dieses Verhalten nicht akzeptierbar ist, kann man die waitpid()
-Funktion verwenden,
die zusammen mit wait()
in der Header-Datei sys/wait.h
definiert ist und wie folgt
aussieht:
pid_t waitpid(pid_t pid, int *status, int options);
Mit waitpid()
können Sie auf einen bestimmten Prozess (spezifiziert durch seine
Prozess-ID) oder einen beliebigen Kindprozess (falls für pid
der Wert -1
übergeben
wird) warten. Der Exit-Status des Kindprozesses wird im zweiten Argument
zurückgeliefert. Dem letzten Parameter, options
, kann man eine der Konstanten
WNOHANG
, WUNTRACED
oder 0
(waitpid()
verhält sich dann wie wait()
) übergeben. Die
erste dieser Konstanten ist die interessanteste, da sie dafür sorgt, dass waitpid()
sofort mit einem Wert von 0
- einer ungültigen Prozess-ID - zurückkehrt, wenn kein
Kindprozess beendet wurde. Der Elternprozess kann dann mit der Ausführung
fortfahren und waitpid()
zu einem späteren Zeitpunkt wieder aufrufen. Listing 19.5
zeigt, wie man mit Hilfe von waitpid()
beendete Kindprozesse auflöst.
Listing 19.5: Mit waitpid() Zombie-Prozesse verhindern.
1 : /* Mit waitpid() auf einen Kindprozess warten. */
2 : #include <stdio.h>
3 : #include <stdlib.h>
4 : #include <unistd.h>
5 : #include <sys/types.h>
6 : #include <sys/wait.h>
7 :
8 : int main(void)
9 : {
10: pid_t pid;
11: int status;
12:
13: pid = fork();
14:
15: if (pid < 0)
16: {
17: printf("Fehler : fork () lieferte %u.\n", pid);
18: exit(1);
19: }
20:
21: if (pid == 0)
22: {
23: printf("Kind : PID = %u. PID des Eltern = %u\n",
24: getpid(), getppid());
25: sleep(10);
26: puts ("Kind : wird jetzt beendet.");
27: exit(33);
28: }
29: else
30: {
31: printf("Eltern: PID = %u. PID des Kindes = %u\n",
32: getpid(), pid);
33:
34: while ((pid = waitpid (-1, &status, WNOHANG)) == 0)
35: {
36: printf("Eltern: Kein Kind beendet.");
37: puts(" Lege mich für 1 Sekunde schlafen.");
38: sleep(1);
39: }
40:
41: printf("Eltern: Kind mit PID %u ", pid);
42: if (WIFEXITED(status) != 0)
43: printf("wurde mit Status %d beendet\n", WEXITSTATUS(status));
44: else
45: printf("wurde anormal beendet.\n");
46: }
47:
48: return 0;
49: }
Kind : PID = 19454. PID des Eltern = 19455
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Kind : PID = 19455. PID des Eltern = 19454
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Eltern: Kein Kind beendet. Lege mich für 1 Sekunde schlafen.
Kind: wird jetzt beendet.
Eltern: Kind mit PID 19455 wurde mit Status 33 beendet
Dieses Listing gleicht den beiden vorangehenden Programmen insofern, als es fork()
zur Erzeugung eines Kindprozesses verwendet, der vor dem Elternprozess beendet
wird. In Zeile 34 ruft der Elternprozess die Funktion waitpid()
auf. Liegt kein
Kindprozess vor, der aufzulösen ist, gibt der Elternprozess eine entsprechende
Meldung aus und legt sich für eine Sekunde schlafen. Wurde der Kindprozess
zwischenzeitlich beendet, gibt der Elternprozess seine ID und seinen Exit-Wert aus.
Verwenden Sie |
Erinnern Sie sich: Wir wollten herausfinden, wie der Bash-Befehlsinterpreter andere
Prozesse erzeugen und dabei selbst weiter ausgeführt werden kann. Die fork()
-
Funktion ist nur ein Teil der Lösung; der zweite Teil besteht darin, einen laufenden
Prozess durch einen anderen zu ersetzen. Der Befehlsinterpreter funktioniert nämlich
so, dass er einen Kindprozess als Kopie seiner selbst erzeugt. Der Kindprozess
wiederum ersetzt sich selbst durch den Befehl, den Sie - der Anwender - im
Befehlsinterpreter aufgerufen haben.
Unter Linux/Unix gibt es gleich eine ganze Reihe von Systemfunktionen, die so
genannte exec
-Familie, mit denen man einen Prozess unter Beibehaltung der Prozess-
ID auf ein anderes Programm umschalten kann. In der exec
-Manpage finden Sie
ausführliche Informationen zu den verschiedenen Mitgliedern der exec
-Familie. Wir
werden uns jetzt auf die Funktion execl()
konzentrieren, die in der Header-Datei
unistd.h
wie folgt definiert ist:
int execl( const char *path, const char *arg, ...);
Diese Funktion kehrt nur dann zurück, wenn ein Fehler auftritt. Andernfalls wird der
aufrufende Prozess vollständig durch den neuen Prozess ersetzt. Den
Programmnamen des Prozesses, der den aufrufenden Prozess ersetzen soll, übergibt
man im Argument zu path
, etwaige Befehlszeilenparameter werden danach
übergeben. Im Unterschied zu Funktionen wie printf()
ist execl()
darauf
angewiesen, dass man als letztes Argument einen NULL
-Zeiger übergibt, der das Ende
der Argumentenliste anzeigt.
Die execl()
-Funktion führt vor allem bei Programmierern, die die Funktion das erste
Mal nutzen, zu Verwirrung. Dies liegt daran, dass das zweite an execl()
übergebene
Argument nicht das erste Kommandozeilenargument ist, dass an das aufzurufende
Programm (spezifiziert in path
) übergeben wird. Vielmehr ist das zweite Argument der
Name, unter dem der neue Prozess in der vom ps
-Befehl erzeugten Prozessliste
aufgeführt wird. Das erste Argument, das an das (in path
spezifizierte) Programm
übergeben wird, ist also tatsächlich das dritte Argument, das an execl()
übergeben
wird. Wenn Sie beispielsweise das Programm /bin/ls
mit dem Parameter -al
aufrufen wollen und möchten, dass das Programm in der Prozessliste unter dem
Namen »verz
« aufgerufen wird, würden Sie execl()
wie folgt aufrufen:
execl("/bin/ls", "verz", "-al", NULL);
Dieser Aufruf würde den aktuellen Prozess durch einen Prozess ersetzen, der dem
Aufruf von /bin/ls -al
von der Befehlszeile entspricht. Listing 19.6 demonstriert den
Einsatz der execl()
-Funktion. Im Programm rufen wir ps
statt ls
auf, um zu beweisen,
dass der aktuelle Prozess tatsächlich ersetzt und nicht etwa nur ein neuer, zusätzlicher
Prozess erzeugt wurde.
Listing 19.6: Mit execl() einen Prozess durch einen anderen ersetzen.
1 : /* Mit execl() einen Prozess durch einen anderen ersetzen. */
2 : #include <stdio.h>
3 : #include <stdlib.h>
4 : #include <unistd.h>
5 : #include <errno.h>
6 :
7 : int main(void)
8 : {
9 : pid_t pid ;
10:
11: pid = getpid();
12: printf ("Meine PID = %u\n", pid);
13:
14: puts ("/bin/ps ausführen.");
15: execl ("/bin/ps", "* prog *", "u", NULL);
16:
17: puts("Ein Fehler ist aufgetreten.");
18: perror("list1906");
19: return 0;
20: }
Meine PID = 19569
/bin/ps ausführen.
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
erik 914 0.0 0.2 1784 716 pts/5 S Sep24 0:01 -bash
erik 19569 0.0 0.3 2544 924 pts/5 R Sep27 0:00 * prog * u
Dieses Listing ist äußert einfach. Nach dem Einbinden der erforderlichen Header-
Dateien (Zeilen 2 bis 5) ermittelt das Programm mit Hilfe der Funktion getpid()
seine
Prozess-ID und gibt sie aus. Es folgt eine weitere Meldung (Zeile 14) und dann wird in
Zeile 15 die execl()
-Funktion aufgerufen. Der Rest des Programms wird nicht mehr
ausgeführt, es sei denn der Aufruf von execl()
wäre aus irgendeinem Grunde
gescheitert, in welchem Fall in den Zeilen 17 und 18 eine Fehlermeldung ausgegeben
wird. Wie man an der Programmausgabe ablesen kann, ist die execl()
-Funktion nicht
gescheitert, und der Befehl ps u
wurde ausgeführt. Beachten Sie, dass das zweite
Argument aus der execl()
-Funktion, * prog *
, in der Prozessliste als Name des
Prozesses auftaucht. Beachten Sie weiterhin, das der ursprüngliche Prozess, dessen ID
von dem Programm in Zeile 12 ausgegeben wurde, die gleiche Prozess-ID trug wie
später der neue Prozess, der ihn ersetzte.
Ein weiteres wichtiges Element der Unix-ähnlichen Betriebssysteme stellen - neben der Möglichkeit, neue Prozesse zu starten oder einen Prozess durch einen anderen Prozess zu ersetzen - die Signale dar, die vielfach auch als Software Interrupts bezeichnet werden. Signale sind Meldungen, die vom Betriebssystem an einen laufenden Prozess geschickt werden. Manche Signale werden durch Fehler im Programm selbst ausgelöst, andere sind Anforderungen, die der Anwender beispielsweise über die Tastatur auslöst und die vom Betriebssystem an den laufenden Prozess weitergeleitet werden.
Signale, die das Programm über einen aufgetretenen Fehler informieren, können dadurch ausgelöst worden sein, dass versucht wurde, eine Zahl durch 0 zu teilen, oder dadurch, dass auf Speicher zugegriffen wurde, der nicht zu dem Prozess gehört. Diese Art von Signalen nennt man auch synchrone Signale, da sie bei jeder Ausführung des Programms an den immer gleichen Stellen auftreten.
Andere Signale werden durch Ereignisse ausgelöst, die außerhalb des Programms entstehen - beispielsweise wenn der Anwender von der Konsole Steuerzeichen eingibt oder das Betriebssystem allen Programmen ein Signal schickt, dass sie sich wegen des bevorstehenden Herunterfahrens des Systems beenden sollen. Diese Signale bezeichnet man als asynchrone Signale, da es äußerst unwahrscheinlich ist, dass sich das Programm jedes Mal, wenn es eines dieser Signale empfängt, im gleichen Zustand und bei Ausführung des gleichen Maschinenbefehls befindet.
Alle Signale, die an ein Programm gesendet werden, verfügen über ein vordefiniertes Verhalten, das durch das Betriebssystem festgelegt wird. Einige Signale, insbesondere die Signale, die aufgrund irgendwelcher aufgetretener Fehlerbedingungen an das Programm geschickt werden, führen dazu, dass das Programm beendet und eine »Core Dump«-Datei, erzeugt wird. (Eine »Core Dump«-Datei ist eine Datei, die vom Betriebssystem erzeugt wird und in die der komplette Inhalt des Speichers, den das Programm zum Zeitpunkt des Signalempfangs belegte, geschrieben wird.) Die »Core Dump«-Datei kann zum Debuggen des Fehlers, der den Core Dump ausgelöst hat, verwendet werden und ist vor allem nützlich, wenn ein Programm wegen einer Null- Division oder eines unerlaubten Speicherzugriffs ein entsprechendes Signal empfangen hat.
In Tabelle 19.1 finden Sie eine Liste der am häufigsten unter Unix-Systemen
ausgelösten Signale. Eine vollständige Liste der für Linux definierten Signale finden
Sie in der Header-Datei /usr/include/bits/signum.h
.
Tabelle 19.1: Die symbolischen Fehlerkonstanten für die Signale (wie definiert in bits/signum.h).
Abgesehen von SIGSTOP
und SIGKILL
kann man das Standardverhalten jedes Signals
durch Installation einer Signal-Bearbeitungsroutine anpassen. Eine Signal-
Bearbeitungsroutine ist eine Funktion, die vom Programmierer implementiert wurde
und die jedes Mal aufgerufen wird, wenn der Prozess ein entsprechendes Signal
empfängt. Abgesehen von SIGSTOP
und SIGKILL
können Sie für jedes Signal aus
Tabelle 19.1 eine eigene Signal-Bearbeitungsroutine einrichten. Es ist allerdings auch
möglich, wenn auch selten empfehlenswert, für alle zu bearbeitenden oder
abzufangenden Signale eine gemeinsame Signal-Bearbeitungsroutine aufzusetzen.
Eine Funktion, die als Signal-Bearbeitungsroutine fungieren soll, muss einen einzigen
Parameter vom Typ int
und einen void
-Rückgabetyp definieren. Wenn ein Prozess
ein Signal empfängt, wird die Signal-Bearbeitungsroutine mit der Kennnummer des
Signals als Argument aufgerufen.
Um Signale abfangen und mit einer geeigneten Signal-Bearbeitungsroutine bearbeiten
zu können, muss der Programmierer dem Betriebssystem mitteilen, dass es bei jedem
Auftreten des betreffenden Signals für das Programm die zugehörige Signal-
Bearbeitungsroutine aufrufen soll. Zwei Funktionen gibt es, mit denen man unter Unix
eine Signal-Bearbeitungsroutine verändern oder untersuchen kann: signal()
und
sigaction()
, die beide in der Header-Datei signal.h
definiert sind. Die zweite
Funktion, sigaction()
, ist die aktuellere und wird auch häufiger eingesetzt. Sie ist wie
folgt definiert:
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
Im Erfolgsfall liefert die Funktion 0
zurück, im Fehlerfall -1
. Der erste Parameter von
sigaction()
ist für die Kennnummer des Signals, dessen Verhalten Sie verändern
oder untersuchen wollen. Sie sollten dem Parameter aber nicht die tatsächliche
Kennnummer, sondern lieber die zugehörige symbolische Konstante übergeben - also
beispielsweise SIGINT
statt der Zahl 2
. Der zweite und der dritte Parameter sind Zeiger
auf eine sigaction
-Struktur. Diese Struktur ist in signal.h
wie folgt definiert:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
Indem Sie dem zweiten Parameter der sigaction()
-Funktion einen Zeiger auf eine
korrekt eingerichtete sigaction
-Struktur übergeben, können Sie das Verhalten für das
zugehörige Signal verändern. Indem Sie einen Zeiger auf eine ähnliche Struktur als
Argument für den dritten Parameter übergeben, fordern Sie die sigaction()
-Funktion
auf, die Daten, die das aktuelle Verhalten zu dem Signal bestimmen, in die
übergebene sigaction
-Struktur zu kopieren. Beiden Parametern kann man auch NULL
-
Zeiger übergeben.
Es ist also möglich, das aktuelle Verhalten zu ändern, sowie das aktuelle Verhalten zu untersuchen, ohne es zu ändern, das aktuelle Verhalten zu untersuchen und vor dem Ändern abzuspeichern, so dass es später wieder hergestellt werden kann. Alle drei Möglichkeiten sind in den folgenden Code-Fragmenten implementiert:
/* Das Verhalten ändern. */
sigaction(SIGINT, &neueaktion, NULL);
/* Verhalten untersuchen. */
sigaction(SIGINT, NULL, &alteaktion);
/* Kopie des aktuellen Verhaltens anlegen */
/* und neues Verhalten einrichten. */
sigaction(SIGINT, &neueaktion, &alteaktion);
Schauen wir uns die sigaction
-Struktur etwas genauer an. Beachten Sie, dass es sich
bei dem ersten Element der Struktur, sa_handler
, um einen Zeiger auf eine Funktion
handelt, die ein int
-Argument übernimmt. Dieses Element dient als Zeiger auf die
Funktion, die als Signal-Bearbeitungsroutine für das zu bearbeitende Signal fungieren
soll. Sie können diesem Strukturelement auch die symbolischen Konstanten SIG_DFL
oder SIG_IGN
zuweisen. SIG_DFL
stellt das Standardverhalten für das Signal wieder her,
SIG_IGN
bewirkt, dass das Signal ignoriert wird. Für das sa_flags
-Element gibt es eine
ganze Reihe möglicher Einstellungen, die uns aber nicht weiter interessieren sollen;
wir werden das Element in den Beispielen jeweils auf 0
setzen. Über das sa_mask
-
Element kann man angeben, welche anderen Signale während der Ausführung der
Signal-Bearbeitungsroutine blockiert werden sollen. Meist wird dieses Strukturelement
mit Hilfe der Funktion sigemptyset()
gesetzt, die in signal.h
wie folgt definiert ist:
int sigemptyset(sigset_t *set);
Das letzte Element der Struktur, sa_restorer
, wird heute nicht mehr verwendet.
Listing 19.7 enthält ein einfaches Programm, das eine Signal-Bearbeitungsroutine für
das SIGINT
-Signal einrichtet.
Listing 19.7: Ein einfaches Beispiel zur Behandlung von Signalen.
1 : /* Ein einfaches Beispiel zur Signal-Behandlung. */
2 : #include <stdio.h>
3 : #include <unistd.h>
4 : #include <signal.h>
5 :
6 : void sig_bearbeiter(int sig);
7 :
8 : static int jetzt_beenden = 0;
9 :
10: int main(void)
11: {
12: struct sigaction sig_struct ;
13:
14: sig_struct.sa_handler = sig_bearbeiter;
15: sigemptyset(&sig_struct.sa_mask);
16: sig_struct.sa_flags = 0;
17:
18: if (sigaction(SIGINT,&sig_struct,NULL) != 0)
19: {
20: perror ("Fehler") ;
21: exit (1);
22: }
23:
24: puts("Beenden mit Strg+C.");
25: while (jetzt_beenden == 0)
26: {
27: puts("Programm läuft.");
28: sleep(1);
29: }
30:
31: puts("Daten auf Festplatte schreiben.");
32:
33: return 0;
34: }
35:
36: void sig_bearbeiter(int sig)
37: {
38: printf("Signal %d empfangen. Programm wird beendet.\n", sig);
39: jetzt_beenden = 1;
40: }
Beenden mit Strg+C.
Programm läuft.
Programm läuft.
Programm läuft.
Signal 2 empfangen. Programm wird beendet.
Daten auf Festplatte schreiben.
In den Zeilen 2 bis 5 werden die erforderlichen Header-Dateien eingebunden. Zeile 6
definiert den Prototyp unserer Signal-Bearbeitungsroutine, die in den Zeilen 36 bis 40
definiert ist. Beachten Sie, dass dieser Prototyp zu dem sa_handler
-Element der
sigaction
-Struktur passt. Zeile 8 definiert eine statische Variable namens
jetzt_beenden
, die für die Kommunikation zwischen der Signal-Bearbeitungsroutine
und dem Hauptprogramm verwendet und mit dem Wert 0
initialisiert wird. In Zeile 12
wird eine Strukturvariable vom Typ sigaction
definiert, die in den Zeilen 14 bis 16
mit Werten gefüllt wird. Zeile 14 weist dem sa_handler
-Strukturelement die Adresse
der sig_bearbeiter()
-Funktion zu, deren Prototyp bereits in Zeile 6 deklariert wurde.
Zeile 15 ruft die Funktion sigemptyset()
zum Setzen des sa_mask
-Elements auf, und in
Zeile 16 wird das sa_flags
-Elements auf 0
gesetzt. Nach der Einrichtung der
Strukturvariablen kann die sigaction()
-Funktion aufgerufen werden (Zeile 18), wobei
man nicht vergessen sollte, den Rückgabewert zu prüfen, um sicherzugehen, dass kein
Fehler aufgetreten ist. Falls doch ein Fehler aufgetreten ist, wird eine Meldung
ausgegeben und das Programm wird in Zeile 21 beendet.
Wurde die Signal-Bearbeitungsroutine korrekt eingerichtet, gibt das Programm in
Zeile 24 eine Meldung aus und tritt in die Schleife des Hauptprogramms ein (Zeilen
25 bis 29). Solange die Variable jetzt_beenden
gleich 0
ist, gibt die while
-Schleife
eine Meldung aus (Zeile 27) und legt sich jeweils für 1 Sekunde schlafen (Zeile 28).
Aufgabe dieser Schleife ist es, in dem Programm eine komplexere Berechnung zu
simulieren.
Die Signal-Bearbeitungsroutine sig_bearbeiter()
ist in den Zeilen 36 bis 40
implementiert. Wenn die Funktion aufgerufen wird, gibt sie eine Meldung auf den
Bildschirm aus, die anzeigt, welches Signal empfangen wurde, und setzt danach den
Wert der statischen Variablen jetzt_beenden
auf 1
.
Wenn man sich die Ausgabe des Programms anschaut, sieht man die Meldung von
Zeile 24 und drei Meldungen, die auf die Zeile 27 zurückgehen. Die nächste Zeile der
Ausgabe stammt von der printf()
-Anweisung aus Zeile 38. Dies liegt daran, dass ich
nach der dritten »Programm läuft
«-Meldung bei gedrückt gehaltener [Strg]-Taste die
Taste [C] betätigt habe. Die Tastenkombination bewirkt, dass das Betriebssystem ein
SIGINT
-Signal an den Prozess sendet, das von der Funktion sig_bearbeiter()
in den
Zeilen 36 bis 40 abgefangen wird. Die Funktion sig_bearbeiter()
gibt dann in Zeile
38 ihre eigene Meldung aus und setzt die globale Variable jetzt_beenden
. Nach
Beendigung der Signal-Bearbeitungsroutine kehrt die Programmausführung zur
main()
-Funktion zurück. Der Wert der Variablen jetzt_beenden
ist nun nach der
Änderung in der Signal-Bearbeitungsroutine nicht mehr länger gleich Null, so dass die
while
-Schleife beendet und der Code der Zeilen 30 bis 34 (inklusive der puts()
-
Anweisung) ausgeführt wird.
Inwieweit hat die Einrichtung der Signal-Bearbeitungsroutine das Verhalten des
Programms eigentlich geändert? Kommentieren Sie zur Probe die Zeilen 17 bis 22
aus, indem Sie in Zeile 17 /*
einfügen und Zeile 22 mit */
abschließen. Der gesamte
Code innerhalb der Klammern wird damit zu einem Kommentar, der vom Compiler
bei der Übersetzung des Programms ignoriert wird. Wenn Sie den Code mit dem
Kommentar neu kompilieren und ausführen, erhalten Sie eine andere Ausgabe:
Beenden mit Strg+C.
Programm läuft.
Programm läuft.
Programm läuft.
Dieses Mal wird beim Drücken der Tastenkombination [Strg]+[C] das
Standardverhalten des SIGINT
-Signals ausgeführt, d.h. das Programm wird sofort
beendet. Die Meldung »Daten auf Festplatte schreiben
« wird nicht ausgegeben.
Wenn Sie die Kommentare wieder entfernen und das Programm neu kompilieren und
starten, zeigt es wieder sein altes Verhalten.
Zu Anfang dieser Lektion haben wir uns mit der Systemfunktion fork()
beschäftigt
und herausgearbeitet, wie wichtig es ist, Zombie-Kindprozesse zu löschen. Zwei
verschiedene Methoden zur Behandlung von Zombie-Prozessen wurden erwähnt, aber
nur eine wurde Ihnen vorgestellt. In diesem Abschnitt werden wir uns die zweite
Methode anschauen.
Wann immer ein Prozess beendet wird, wird ein SIGCHLD
-Signal an den Elternprozess
des Kindes gesendet.
Das Auflösen von Zombie-Kindprozessen kann also auch dadurch erledigt werden,
dass man das Standardverhalten der SIGCHLD
-Signal-Bearbeitungsroutine ändert.
Häufig sieht man Code, in dem zu diesem Zweck SIG_IGN
als Signal-
Bearbeitungsroutine eingesetzt wird:
sig_struct.sa_handler = SIG_IGN;
sigemptyset(&sig_struct.sa_mask);
sig_struct.sa_flags = 0;
sigaction(SIGCHLD,&sig_struct,NULL);
Dieser Ansatz funktioniert unter Linux und vielleicht auch noch einigen anderen Mitgliedern der Unix-Familie, aber er ist mit zwei Problemen behaftet. Erstens lässt er dem Programm keine Möglichkeit, den Exit-Status des Kindprozesses zu lesen. Zweitens funktioniert er nicht für alle Mitglieder der Unix-Familie. Wenn Sie möchten, dass Ihre Programme portierbar sind, sollten Sie diesen Ansatz daher nicht verwenden.
Der beste Weg, Zombie-Kindprozesse aufzulösen, besteht darin, eine SIGCHLD
-Signal-
Bearbeitungsroutine einzurichten und in der Bearbeitungsroutine waitpid()
aufzurufen. Listing 19.8 demonstriert diesen Ansatz.
Listing 19.8: SIGCHLD zum Löschen von Zombie-Kindprozesse abfangen.
1 : /* SIGCHLD zum Löschen von Zombie-Kindprozesse abfangen. */
2 : #include <stdio.h>
3 : #include <stdlib.h>
4 : #include <unistd.h>
5 : #include <signal.h>
6 : #include <sys/types.h>
7 : #include <sys/wait.h>
8 :
9 : void sigchld_bearbeiter (int);
10:
11: int main(void)
12: {
13: pid_t pid;
14: struct sigaction sig_struct;
15: int k;
16:
17: sig_struct.sa_handler = sigchld_bearbeiter;
18: sigemptyset(&sig_struct.sa_mask);
19: sig_struct.sa_flags = 0;
20:
21: if (sigaction(SIGCHLD,&sig_struct,NULL) != 0)
22: {
23: perror ("Fehler") ;
24: exit (1);
25: }
26:
27: pid = fork();
28:
29: if (pid < 0)
30: {
31: printf("Fehler : fork () lieferte %u zurück.\n", pid);
32: exit(1);
33: }
34:
35: if (pid == 0)
36: {
37: printf("Kind : PID = %u. PID des Eltern = %u\n",
38: getpid(), getppid());
39: sleep(1);
40: puts ("Kind : wird jetzt beendet.");
41: exit(42);
42: }
43: else
44: {
45: printf("Eltern: PID = %u. PID des Kindes = %u\n",
46: getpid(), pid);
47:
48: puts("Eltern: lege mich für 30 Sekunden schlafen.");
49: for (k=0; k<30; k++)
50: sleep(1);
51: puts("\nEltern: wird beendet.");
52: }
53:
54: return 0;
55: }
56:
57: void sigchld_bearbeiter (int sig)
58: {
59: pid_t pid;
60: int status;
61:
62: while ((pid = waitpid (-1, &status, WNOHANG)) > 0)
63: {
64: printf("SIGCHLD: Kind mit PID %u ", pid);
65: if (WIFEXITED(status) != 0)
66: printf("wurde mit Status %d beendet \n", WEXITSTATUS(status));
67: else
68: printf("wurde anomal beendet.\n");
69: }
70:
71: }
[erik@coltrane day19]$ ./list1908 &
[2] 1914
[erik@coltrane day19]$ Eltern: PID = 1914. PID des Kindes = 1915
Kind : PID = 1915. PID des Eltern = 1914
Eltern: lege mich für 30 Sekunden schlafen.
Kind : wird jetzt beendet.
SIGCHLD: Kind mit PID 1915 wurde mit Status 42 beendet
[erik@coltrane day19]$ ps u
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
erik 1110 0.0 0.3 1720 980 pts/3 S 02:24 0:00 -bash
erik 1849 0.0 1.2 4616 3136 pts/3 S 05:43 0:00 nedit list1908.c
erik 1914 0.0 0.1 1052 332 pts/3 S 05:49 0:00 ./list1908
erik 1916 0.0 0.3 2536 916 pts/3 R 05:49 0:00 ps u
[erik@coltrane day19]$
Eltern: wird beendet.
[2]+ Done ./list1908
In den Zeilen 17 bis 25 wird nach dem gleichen Muster, das Sie schon aus Listing
19.7 kennen, eine Signal-Bearbeitungsroutine eingerichtet - diesmal allerdings zum
Abfangen von SIGCHLD
und nicht für SIGINT
(wie in Listing 19.7). Der Rest der main()
-
Funktion besteht aus einem fork()
-Aufruf (Zeile 27) und Code für den Kindprozess
(Zeilen 36 bis 42). Der Elternprozess führt den Code in den Zeilen 44 bis 51 aus.
Beachten Sie, dass im Elternprozess keine wait()
- oder waitpid()
-Aufrufe wie in den
Listings 19.4 und 19.5 benötigt werden.
Die Funktion, die uns als Signal-Bearbeitungsroutine dienen soll, ist in Zeile 9
deklariert und in den Zeilen 57 bis 71 definiert. Die Signal-Bearbeitungsroutine
enthält zwei lokale Variablen, pid
und status
, die in den Zeilen 59 und 60 definiert
sind. In Zeile 62 wird waitpid()
in einer while
-Schleife immer wieder aufgerufen, bis
waitpid()
eine ungültige Prozess-ID zurückliefert. Liefert waitpid()
eine gültige
Prozess-ID zurück (also einen Wert im Bereich 1 bis 32767), wird der
Anweisungsblock der while
-Schleife ausgeführt, der die Prozess-ID und den Exit-
Status des Kindprozesses ausgibt. Wenn waitpid()
irgendeinen Wert zurückliefert, der
keine gültige Prozess-ID darstellt, wird die while
-Schleife nicht mehr ausgeführt, und
die Signal-Bearbeitungsroutine kehrt zurück. Jetzt ist es aber so, dass waitpid()
bei
Auftreten eines Fehlers einen negativen Wert zurückliefert. Warum prüfen wir nicht,
ob ein solcher Fehler aufgetreten ist? Wenn die Funktion waitpid()
in einem Prozess
aufgerufen wird, zu dem es keine Kindprozesse gibt, liefert die Funktion den Wert -1
zurück und setzt die globale Variable errno
auf ECHILD
. Wir prüfen also deshalb nicht
auf etwaige Fehler, weil der häufigste Fehler dann eintritt, wenn keine Kindprozesse
vorliegen.
Ein weiterer Punkt, den es zu beachten gilt, ist die for
-Schleife in den Zeilen 49 und
50. In den vorangegangenen Beispielen wurde stets eine einzelne sleep()
-Anweisung
verwendet, die in diesem Programm plötzlich durch eine Schleife ersetzt wurde.
Warum? Nun, die Funktion sleep()
ist eine von mehreren Systemfunktionen, die
automatisch beendet werden, wenn ein Signal empfangen wird. Würde man in Listing
19.8 einfach einen einzelnen sleep()
-Aufruf verwenden, der das Programm für 30
Sekunden schlafen legt, würde die sleep()
-Funktion abgebrochen, sowie der
Kindprozess beendet wird. Auch das Programm würde dann beendet und könnte
nicht mehr bestätigen, dass der Kindprozess korrekt aufgelöst wurde.
Es gibt noch eine weitere Falle, die bei Einrichtung einer SIGCHLD
-Signal-
Bearbeitungsroutine zu beachten ist. Wenn es mehr als einen Kindprozess gibt und die
Signal-Bearbeitungsroutine für einen gerade beendeten Kindprozess aufgerufen wird,
kann es passieren, dass ein zweites Kind beendet wird, bevor die Signal-
Bearbeitungsroutine mit dem ersten Kind fertig ist. In diesem Fall kann es sein, dass
kein zweites SIGCHLD
-Signal erzeugt wird. Anders ausgedrückt, der Programmierer
kann nicht davon ausgehen, dass er für jeden Kindprozess, der beendet wird, ein
eigenes SIGCHLD
-Signal erhält. Aus diesem Grund wird waitpid()
in Listing 19.8 in
einer while
-Schleife aufgerufen.
In der heutigen Lektion haben wir uns mit den Prozessen und Signalen auf Linux- und
anderen Unix-ähnlichen Systemen befasst. Sie haben gelernt, dass ein Prozess eine
Instanz eines Programms ist, das gerade vom Betriebssystem ausgeführt wird.
(Programme stehen also auf der Festplatte, Prozesse befinden sich im
Arbeitsspeicher.) Jeder Prozess verfügt über eine eigene Prozess-ID und kann mit
Hilfe der Systemfunktion fork()
Kindprozesse abspalten. Sie haben auch erfahren,
wie man einen Prozess mit Hilfe von execl()
durch einen anderen Prozess ersetzen
kann. Beide Funktionen werden auch vom Bash-Befehlszeilen-Interpreter zum
Ausführen von Programmen und Betriebssystembefehlen verwendet. Schließlich
wurde Ihnen erklärt, was Signale sind und wie Sie mit Hilfe der sigaction
-Funktion
Signal-Bearbeitungsroutinen zum Abfangen der Signale einrichten können.
Frage:
Haben alle Prozesse einen Elternprozess?
Antwort:
Nein, doch es gibt nur einen einzigen Prozess, der keinen Eltern hat. Dieser
Prozess heißt init
und hat die Prozess-ID 1
. Sie können ihn sich mit dem
Befehl ps u
anzeigen lassen. Der init
-Prozess ist der erste Prozess, der beim
Hochfahren Ihrer Linux-Maschine gestartet, und der letzte Prozess, der beim
Herunterfahren beendet wird. Darüber hinaus gibt es auch die Möglichkeit,
dass ein Kindprozess seinen Elternprozess verliert, aber nicht beendet wird.
Ein solcher Kindprozess wird dann vom init
-Prozess adoptiert.
Frage:
Wofür werden Signale unter Linux hauptsächlich eingesetzt?
Antwort:
Es gibt kein hauptsächliches Anwendungsgebiet; Signale werden für viele
Aufgaben eingesetzt. Sie werden von Kommandozeilen-Interpretern zur
Kontrolle der Programme eingesetzt (SIGINT
, SIGTSTP
, SIGSTOP
und SIGCONT
).
Sie werden vom Betriebssystem eingesetzt, um Prozesse darüber zu
informieren, dass einer ihrer Kindprozesse beendet wurde (SIGCHLD
). Viele
Server-Anwendungen sind so implementiert, dass sie ihre
Konfigurationsdateien neu auswerten, wenn sie ein SIGHUP
-Signal empfangen.
Wenn ein Rechner heruntergefahren wird, sendet der init
-Prozess zuerst
SIGTERM
- und später SIGKILL
-Signale an alle laufenden Prozesse. SIGSEGV
,
SIGFPE
und SIGILL
werden an Prozesse gesendet, die einen Fehler ausgelöst
haben. Und so weiter und so fort; die Liste der möglichen Einsatzbereiche ist
nahezu endlos.
Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.
getpid()
und getppid()
?
fork()
-Aufrufs in a) dem Elternprozess und wait()
und waitpid()
?
execl()
-Aufruf folgt, nicht mehr
ausgeführt?
1523
ruft execl()
auf. Wie lautet die
Prozess-ID des mit execl()
gestarteten Programms?
Schreiben und testen Sie eine Funktion namens mein_system()
, die system()
ersetzen
kann. Verwenden Sie die Funktionen fork()
und execl()
.