ein Kapitel zurück                                           ein Kapitel weiter

Normalerweise wenn alles glatt verläuft sieht ein normaler Ablauf der Erzeugung eines neuen Prozesses wie folgt aus.......




Die Eltern warten also solange auf das Kind bis es fertig ist, erst dann beenden sich der Elternprozeß.

Was passiert aber wenn sich der Kindprozeß mit einem Fehler schon vor dem Elternprozeß verabschiedet, wie im folgendem Programm......

/*Download:wait1.c*/
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char **argv) { pid_t kind1; while(1) { switch(kind1=fork()) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : sleep(2); printf("Kindprozess ist fertig!!\n\n"); exit(1); default : printf("Elternprozess schläft 30 sec.\n\ "Rufen sie ps x in der neuen Konsole auf!!!\n"); sleep(30); printf("Elternprozess ist fertig\n"); exit(2); } } return 0; }

Öffnen sie während der Ausführung des Programms eine neues Terminal und geben sie ps x ein. Nun sehen sie in etwa folgende Ausgabe (bei unserem Beispiel heißt das Programm fork)....

PID    TTY    STAT    TIME    COMMAND
237    ?      S       0:00    xterm -ls -T Failsave -geometry 80x24-0-0
246    pts/0  SW      0:00    [bash]
.............................................................
.............................................................
1728   ?       S            0:02       kdeinit: kwrite
1894   pts/2   S            0:00       konsole
1895   pts/3   S            0:00       /bin/bash
2080   pts/2   S            0:00       wait
2081   pts/2   Z            0:00       [wait <defunct>]
2082   pts/3   R            0:00       ps x  

Aufällig dürfte Ihnen die Zeile....

2081    pts/2    Z    0:00    [wait ]  

...sein. An dem Status Z können sie erkennen das es sich um einen Zombie-Prozess handelt. Zombie-Prozesse entstehen wenn sich ein Kindprozess beendet hat ,ohne das der Elternprozess auf sie wartet. Oder etwas genauer:

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 Prozeß, 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.




Den Vorgang können sie sich hier noch mal Bildlich ansehen. Zur Erklärung des Init-Prozesses kommen wir gleich noch.

Eine häufige Frage in Foren ist: Sind Zombie-Prozesse schlimm? Häufig bekommt man die Antwort: Zombieprozesse sind nicht schlimm da sie ja keinen Speicherplatz und somit keine Rechenzeit mehr belegen sondern nur einen Eintrag in der Prozesstabelle. Und bei einfachen Programmen ist dies auch weiter nicht schlimm. Nun wird die fork()-Funktion häufig dabei eingesetzt World-Wide-Web-Server (Apache), Chat-Programme oder Mail-Server zu Programmieren. Und bei diesen Client-Server-Anwendungen kann dies Katastrophal sein. Nehmen wir als einfaches Beispiel ein Chat-Programm das Tag und Nacht online ist. Es sind auf Ihrem Server zur Zeit 20 Personen (Clienten) die miteinander chatten. Also haben wir (Server) 20 mal mit fork einen neuen Prozess,sprich Client, erzeugt. 2 Personen haben sich entschieden von dem Chat-Portal sich in eine ruhigere Ecke zurückzuziehen und sich dort one-to-one zu unterhalten. Damit nun die anderen 18 Chatter sie nicht mehr sehen können, sie wollen ja nicht ständig von anderen Unterbrochen werden bei Ihrem Plausch, müssen wir den Prozess beenden und zwei neuen erzeugen in einem anderen Chat-Kanal. Und da sich nun unsere 2 Chatter von den restlichen 18 Chattern verabschiedet haben und berreits in Ihrem one-to-one Kanal chatten müssen sie dafür sorgen das keine Zombies zurückbleiben. Es liegt auf der Hand, Das der Server (Elternprozess) dabei sicherstellen muss, Das keiner seiner Kindprozesse zu einem Zombie-Prozess mutiert, oder das Betriebssystem hat bald keinen Platz mehr in seiner Prozesstabelle. Meist hilft dann nur noch ein Reset des Systems.

Wer nimmt sich eines Zombie's an?
Bevor sie die Funktion kennen lernen, mit der sie Zombieprozesse vermeiden können, wollen wir uns doch den Prozeß ansehen, der sich der sich die Zombies vorknüpft. Einer muss ja die Leichen aus dem Keller holen. Und das trifft hier im warsten Sinne des Wortes zu.

Geben sie in der Konsole doch das Kommando pstree ein.....

pstree | less

Ganz oben im Himmel finden sie den Vater aller Prozesse, init. init befindet sich im Verzeichnis /sbin/init. Der Kernel startet init als ersten Prozeß auf Ihrem System. Alle anderen Prozeße die nach init folgen, werden direkt oder indirekt von init gestartet.

Beim starten des Systems kümmert sich init als Käptain dann um die Basiskonfiguration und Start der unzähligen Dämon-Prozesse. Und wenn ein Kindprozeß mal wieder schneller fertig ist als sein Elternprozeß, dient init als Waisenhaus und nimmt sich dessen an.

Beim Herunterfahren des Systems, geht wie es sich gehört, der Käptain als letztes von Board. init kümmert sich dann als der letzte laufende Prozeß um die Korrekte Beendigung aller noch laufenden Prozesse. Mehr dazu finden sie unter man init.

Also kommen wir wieder zurück zum Thema. Es gibt mehrere Möglichkeiten die Entstehung von Zombie´s zu verhindern. Am meisten wird wohl eingesetzt....

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.

/*Download:wait2.c*/
/*Beispiel 1 ohne Rüchgabewert*/ #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char **argv) { pid_t kind1; while(1) { switch(kind1=fork()) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : sleep(2); printf("Kindprozess ist fertig!!\n\n"); exit(1); default : printf("Elternprozess schläft 30 sec.\n\" "Rufen sie ps x in der neuen Konsole auf!!!\n"); wait(NULL); sleep(30); printf("Elternprozess ist fertig\n"); exit(2); } } return 0; }

Im Gegensatz zum Programm zuvor haben wir hier nur....

wait(NULL);  

....eingefügt.




Anhand diese Bildes können sie nun erkennen, das der Kindprozeß nun in der Lage ist sich selbst zu beenden. Das daher weil er denn Elternprozess informiert, das er fertig ist.

Nun können sie wenn sie wollen das Programm erneut aufrufen und ein zweites Terminal öffnen und ps x eingeben. Und siehe da der Kindprozess unser Zombie zuvor existiert nicht mehr und wurde beendet. Jetzt wollen wir natürlich auch wissen was für einen Rückgabewert das Programm macht....

/*Download:wait3.c*/
/*Beispiel 2 mit Rückgabewert*/ #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char **argv) { pid_t kind1; int status; while(1) { switch(kind1=fork()) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : sleep(5); printf("Kindprozess ist fertig!!\n\n"); exit(1); default : printf("Elternprozess schläft 30 sec.\n"); kind1=wait(&status); printf("Eltern : Kind mit PID: %d ",kind1); if(WIFEXITED(status) !=0) printf("wurde mit status %d beendet\n",WEXITSTATUS(status)); else printf("wurde anormal beendet\n"); sleep(30); printf("Elternprozess ist fertig\n"); exit(2); } } return 0; }

In diesem Fall bekommen wir die Beendigung des Kindes mit der PID und dem exitstatus zurück. Mit....

if(WIFEXITED(status) !=0)  

...warten wir solange bis uns das Makro WIFEXITED(status) ein TRUE oder 1 zurückliefert. Das bedeutet das sie der Kindprozess normal beendet hat. Mit....

printf("wurde mit status %d beendet\n",WEXITSTATUS(status));  

...geben wir dann durch WEXITSTATUS(status) die Nummer des exit-Wertes (1-255) zurück. Weitere Makros währen...

WIFSIGNALED(status)  

Makro liefert TRUE zurück wenn wenn status vom Kindprozess geliefert wird der sich anormal beendet hat durch das Eintreffen eines Signals. Die Nummer des Signals kann mit dem Makro WTERMSIG(status) abgefragt werden.

/*Download:wait4.c*/
/*Beispiel WIFSIGNALED*/ #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char **argv) { pid_t kind1; int status; while(1) { switch(kind1=fork()) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : sleep(20); printf("Kindprozess ist fertig!!\n\n"); exit(1); default : printf("Elternprozess schläft 30 sec.\n"); kind1=wait(&status); printf("Eltern : Kind mit %d ",kind1); if(WIFSIGNALED(status) !=SIGCHLD) printf("wurde mit status %d beendet\n",WTERMSIG(status)); else printf("wurde anormal beendet\n"); sleep(30); printf("Elternprozess ist fertig\n"); exit(2); } } return 0; }

Hiermit wir der Kindprozess beendet wenn das Makro WIFSIGNALED(status) das Signal SIGCHLD erhält. Die Nr. des Signals fragen wir mit WTERMSIG(status) ab. Zu den Signalen kommen wir noch.

Hierzu noch ein Beispiel wie das Programm eben nur fangen wir mit signal() das Signal SIGCHLD selbst auch noch ab.

/*Download:wait5.c*/
/*Beispiel mit signal()*/ #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> typedef void Sigfunc(int); Sigfunc *signal(int, Sigfunc *); static void catchsignal(int sig); int Setsignal(void) { if(signal(SIGCHLD,catchsignal) == SIG_ERR) { fprintf(stderr,"SIGCHLD catch failed.\n"); exit(2); } } static void catchsignal(int sig) { switch(sig) { case SIGCHLD : printf("Eltern :Signal SIGCHLD von %d bekommen!!!\n" ,getpid()); break; default : printf("Unbekanntes Signal erhalten!!!\n"); break; } } int main(int argc, char **argv) { pid_t kind1; int status; while(1) { switch(kind1=fork()) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : sleep(5); printf("Kindprozess ist fertig!!\n\n"); exit(1); default : printf("Elternprozess schläft 30 sec.\n"); Setsignal(); wait(NULL); sleep(30); printf("Elternprozess ist fertig\n"); exit(2); } } return 0; }

In diesem Beispiel setzten wir mit der Funktion Setsignal das Signal....

signal(SIGCHLD,catchsignal)  

...die Funktion catchsignal liefert uns dann falls signal SIGCHLD erhalten hat.....

case SIGCHLD : printf("Eltern :Signal SIGCHLD von %d bekommen!!!\n"
                        ,getpid());  

...zurück. Dies Programm dient nur dazu damit sehen das ein Kindprozess wenn er fertig ist immer an seinen Elternprozess das Signal SIGCHLD liefert. Mit dem Abfangen des Signal könnten sie natürlich theortisch andere Ausführungen machen lassen oder den Elternprozess dazu nötigen das Signal mit SIG_IGN zu ignorieren (siehe Kapitel signal()). Nur hätten sie dann irgendwann lauter Zombie´s.

Es gibt noch ein Makro...

WIFSTOPPED(status)  

Dieser status wird zurückgeliefert wenn der Kindprozess angehalten wurde. Mit dem Makro WSTOPSIG(status) können sie die Nummer des Signals erfahren der den Prozess anhielt.

waitpid

Das Problem von wait ist, das es nur auf die Beendigung eines beliebigen Prozesses warten. Wollen sie bei mehreren fork()-Aufrufen auf einen bestimmten Prozeß warten können sie dies nicht mehr mit wait bewerkstelligen. Genauso kann es manchesmal nicht erwünscht sein das wait den Aufrufenden Prozeß blockiert. Dies geschieht im Fall, wenn der Kindprozeß den Elternprozeß überdauert.

Wenn das Verhalten wie eben beschrieben akzeptierbar ist können sie wait() einsetzen, wenn nicht gibt es die Funktion.........

pid_t waitpid(pid_t pid, int status, int optionen);  

Mit waitpid ist es möglich auf einem bestimmten Prozess zu warten. Dies wir anhand der pid (PID) festgelegt...

  • pid == 0
    Es soll auf alle Kindprozesse gewartet werden, die zur gleichen Prozeßgruppe gehören wie der aufrufende Prozeß
  • pid > 0
    Warten auf Beendigung des Prozesses mit einer bestimmten Prozeß-ID
  • pid == -1
    Warten auf alle Kindprozesse. Entspricht ein das selbe wie wait.

Auch hier schreibt waitpid wie schon bei wait den Beendigungsstatus an die mittels status übergebene Adresse.

Weiterhin können noch folgende Optionen angefügt werden.....

  • WNOHANG - der aufrufende Prozess wird nicht blockiert, falls der Kindprozess mit der PID nicht sofort verfügbar ist. waitpid liefert 0 als Rückgabewert.
  • WUNTRACED - bei Jobkontrolle liefert waitpid den Status des angehaltenen Kindprozesses falls sie diesen bei pid spezifiert haben und der Status seit Anhaltzeitpunkt nicht mehr gefragt wird. Makro WIFSTOPPED(status) liefert TRUE oder 1 wenn der Rückgabewert gleich der PID eines angehalt. Kindprozesses ist.

    Weiter 2 Optionen währen WNOWAIT und WCONTINUED, aber da sie nicht POSIX sind nur SVR4 will ich sie hier nicht durchnehmen.

    /*Download:waitpid1.c*/
    #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char **argv) { pid_t kind; int i,status; int anzahlkinder = 5; for(i=0;i<anzahlkinder;i++) { kind=fork(); switch(kind) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : printf("Kind : Kind Nr.%d erzeugt mit PID %d\n" ,i+1,getpid()); sleep(15); printf("Kindprozess (PID:%d) ist fertig!!\n" ,getpid()); exit(i+1); default : printf("Eltern mit PID %d von Kind mit PID %d\n" ,getpid(),kind); while(kind=waitpid(kind, &status, WNOHANG)==0) { sleep(1); printf("Eltern mit PID %d fertig\n",getpid()); } break; } } return 0; }

    In diesem Beispiel braucht unser Kindprozess einfach mal länger als der Elternprozess. Es werden dabei 5 neue Prozesse erschaffen. Und sie können daran sehen obwohl der Kindprozess dem Elternprozess überdauert wird der Elternprozess nicht blockiert und muss auf dem Kindprozess warten. Und sie wissen ja falls sich der Elternprozess früher verabschiedet als der Kindprozess haben wir einen Waisen. Den übernimmt für uns die Mutter aller Prozesse nämlich der Prozess init.

    Als Gegenbeispiel das selbe Programm nur mit wait.....

    /*Download:waitpid2.c*/
    #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> int main(int argc, char **argv) { pid_t kind; int i,status; int anzahlkinder = 5; for(i=0;i<anzahlkinder;i++) { kind=fork(); switch(kind) { case -1 : printf("Fehler bei fork()..........\n"); exit(0); case 0 : printf("Kind : Kind Nr.%d erzeugt mit PID %d\n" ,i+1,getpid()); sleep(15); printf("Kindprozess (PID:%d) ist fertig!!\n" ,getpid()); exit(i+1); default : printf("Eltern mit PID %d von Kind mit PID %d\n" ,getpid(),kind); kind=wait(&status); if(WIFSIGNALED(status) != SIGCHLD) printf("SIGCHLD\n"); else printf("NOCHLD\n"); sleep(1); printf("Eltern mit PID %d fertig\n",getpid()); break; } } return 0; }

    Sie sehen daran das ganze Programm angehalten wird bis der Elternprozess antwort vom Kindprozess erhält. Erst dann wieder wieder ein neuer Prozess erzeugt.

    ein Kapitel zurück          nach oben           ein Kapitel weiter


    © 2001,2002 Jürgen Wolf