vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 19

Prozesse und Signale

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:

Prozesse

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.

Mit fork() andere Prozesse starten

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.

Zombie-Prozesse

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.

Was Sie tun sollten

Verwenden Sie wait() oder waitpid(), um beendete Kindprozesse aufzulösen. Besonders wichtig ist dies in Programmen, die viele Kindprozesse erzeugen und für längere Zeit laufen.

Einen Prozess durch einen anderen ersetzen

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.

Was Sie tun sollten

Was nicht

Schließen Sie die Argumentenliste zu execl() mit einem NULL-Zeiger ab.

Vergessen Sie nicht, dass das zweite Argument zu execl() der Name ist, den Sie dem Prozess geben, und nicht das erste Argument zu dem Programm, das von execl() aufgerufen wird.

Signale

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.

Name

Wert

Funktion

SIGHUP

1

Terminal reagiert nicht mehr

SIGINT

2

Benutzer-Interrupt (ausgelöst durch [Strg)+[C])

SIGQUIT

3

Benutzeraufforderung zum Beenden (ausgelöst durch [Strg)+[\])

SIGFPE

8

Fließkommafehler, beispielsweise Null-Division

SIGKILL

9

Prozess killen

SIGUSR1

10

Benutzerdefiniertes Signal

SIGSEGV

11

Prozess hat versucht, auf Speicher zuzugreifen, der ihm nicht zugewiesen war

SIGUSR2

12

Weiteres benutzerdefiniertes Signal

SIGALRM

14

Timer (Zeitgeber), der mit der Funktion alarm() gesetzt wurde, ist abgelaufen

SIGTERM

15

Aufforderung zum Beenden

SIGCHLD

17

Kindprozess wird aufgefordert, sich zu beenden

SIGCONT

18

Nach einem SIGSTOP- oder SIGTSTP-Signal fortfahren

SIGSTOP

19

Den Prozess anhalten

SIGTSTP

20

Terminal anhalten; ausgelöst durch [Strg)+[Z].

SIGWINCH

28

Fenstergröße ändern.

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.

Mit Hilfe von SIGCHLD Zombie-Kindprozesse vermeiden

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.

Was Sie tun sollten

Was nicht

Rufen Sie in Ihren SIGCHLD-Signal- Bearbeitungsroutinen die Funktion waitpid() in einer Schleife auf, für den Fall, dass mehrere Kindprozesse beendet werden, aber nur ein Signal ausgelöst wird.

Packen Sie keinen unnötigen Code in Ihre Signal-Bearbeitungsroutinen. Der Umfang der Signal-Bearbeitungsroutinen sollte auf einem Minimum gehalten werden.

Erwarten Sie nicht, dass für jeden Kindprozess, der beendet wird, ein eigenes SIGCHLD-Signal erzeugt wird. Wenn ein zweiter Kindprozess beendet wird, während die Signal- Bearbeitungsroutine noch mit einem kurz zuvor beendeten Kindprozess beschäftigt ist, wird unter Umständen kein weiteres Signal erzeugt.

Zusammenfassung

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.

Fragen und Antworten

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.

Workshop

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.

Quiz

  1. Was ist eine Prozess-ID?
  2. In welchem Wertebereich liegen gültige Prozess-IDs?
  3. Welcher Unterschied besteht zwischen den Funktionen getpid() und getppid()?
  4. Wie lauten die Rückgabewerte eines fork()-Aufrufs in a) dem Elternprozess und
    b) dem Kindprozess? (Gehen Sie davon aus, dass keine Fehler aufgetreten sind.)
  5. Was ist ein Zombie-Prozess?
  6. Welches ist der wichtigste Unterschied zwischen wait() und waitpid()?
  7. Warum wird Code, der auf einen erfolgreichen execl()-Aufruf folgt, nicht mehr ausgeführt?
  8. Angenommen ein Prozess mit der Prozess-ID 1523 ruft execl() auf. Wie lautet die Prozess-ID des mit execl() gestarteten Programms?
  9. Für welche zwei Signale ist es nicht möglich, dass Standardverhalten durch Installation einer Signal-Bearbeitungsroutine zu verändern?

Übung

Schreiben und testen Sie eine Funktion namens mein_system(), die system() ersetzen kann. Verwenden Sie die Funktionen fork() und execl().



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH