Sehr Umfangreiche Webseite zum Programmieren in C Perl CGI, Skripting, Linux, SystemprogrammierungC C++ C/C++ ANSI C Linux Linuxsystemprogrammierung Unix fork system exec Prozess Prozesse getpid getppid getlogin Linuxsystemprogrammieren fork system exec Prozess Prozesse getpid getppid getlogin 2b. Kreiern und Starten von Prozessen

2.4. Mehrere fork()-Aufrufe hintereinander           zurück Ein Kapitel tiefer zum Inhaltsverzeichnis

Wie viele Prozesse glauben Sie, werden mit dem folgendem Programm erzeugt?

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
   char pid[255];

   fork();
   fork();
   fork();

   sprintf(pid, "PID : %d\n",getpid());
   write(STDOUT_FILENO, pid, strlen(pid));

   exit(0);
}

Mit diesem Programm werden sieben Prozesse erzeugt. Es sind somit also acht Prozesse auf einmal am laufen mit dem Elternprozess. Wie kann man sich das nun vorstellen. Mit dem ersten fork()-Aufruf ergibt sich folgendes Bild ...

Mehrere Prozesse erzeugen mit fork

Der erste fork()-Aufruf erzeugt also einen Kindprozess. Wichtig ist es das Sie wissen, dass ein Kindprozess auch den Instruktions Pointer vom Elternprozess erbt. Diesen kann man sich als Zeiger vorstellen der auf die Adresse zeigt wo sich das Programm gerade befindet. Das bedeutet nach dem ersten fork()-Aufruf, das der Instruktions-Pointer sich im Moment kurz vor dem zweiten fork()-Aufruf befindet. Weiter mit dem zweiten fork()-Aufruf ...

Mehrere Prozesse erzeugen mit fork

Sie sehen hier, dass sowohl der Elternprozess, als auch der Kindprozess jetzt neue Prozesse erzeugen bei denen sich der Instruktions-Pointer (IP) nun vor dem dritten fork()-Aufruf befindet. Jetzt noch den dritten fork()-Aufruf bildlich ...

Mehrere Prozesse erzeugen mit fork

Hiermit erzeugen alle vier laufende Prozesse, bei denen sich der IP beim dritten fork() - Aufruf befindet, weitere vier Prozesse. Würde jetzt ein vierter fork() - Aufruf erfolgen, so hätten Sie damit 15 Prozesse erzeugt. Mit dem Elternprozess laufen dann 16. Insgesamt also 24 Prozesse (2*2*2*2). Daraus ergibt sich das mit n-fork()-Aufrufen 2n-1 Prozesse erzeugt würden.

Ein weiteres Problem bei fork() kann auftreten, wenn Sie mit dem Elternprozess eine Datei zum Schreiben öffnen wollen. Wenn Sie dabei mit fork() einen neuen Kindprozess erzeugen, werden ja auch die Filediskriptoren mitvererbt. Sollte der Kindprozess jetzt in die Datei schreiben und auch der Elternprozess, so müssen Sie sich um die Synchronisation dieser beiden Prozesse kümmern (falls dies erwünscht ist). Dies machen Sie am einfachsten so, in dem der Elternprozess mittels ...

wait(&status);

...auf den Kindprozess wartet. Oder wenn der Kindprozess gar nicht in diese Datei schreiben soll, schließen Sie die geöffnete Datei zur Sicherheit.

2.5. Warten auf's Kind und vermeiden von Zombieprozessen - wait,waitpid  zurück Ein Kapitel höher zum Inhaltsverzeichnis

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

Warten auf Prozesse - wait waitpid

Der Elternprozess wartet so lange auf den Kindprozess, bis dieser fertig ist und erst dann beenden sich der Elternprozess.

Was passiert aber, wenn sich der Kindprozess mit einem Fehler schon von seinem Elternprozess verabschiedet, wie im folgendem Programmbeispiel ...

#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 neuen Konsole und geben sie ps x ein. Nun sehen Sie in etwa folgende Ausgabe (in unserem Beispiel heißt das Programm wait) ...

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

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

2081    pts/2    Z    0:00    [wait <defunct>]

...sein. An dem Status Z können Sie erkennen, dass 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 Prozess, der zwar beendet wurde, dessen Elternprozess den exit-Wert des Kindprozesses aber noch nicht erhalten hat. Erst wenn der Elternprozess beendet wird, wird auch der Zombie-Prozess aus der Prozesstabelle des Betriebssystems entfernt.

Entstehung eines Zombie-Prozesses

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 Diskusionsforen lautet: 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 nicht weiter schlimm. Nun wird die fork()-Funktion häufig dabei eingesetzt World-Wide-Web-Server (wie z.B. den Apache), Chat-Programme oder Mail-Server zu Programmieren. Und bei diesen Client-Server-Anwendungen kann dies Katastrophal sein. Nehmen Sie als einfaches Beispiel ein Chat-Programm, dass Tag und Nacht online ist. Es sind auf Ihrem Server zur Zeit 20 Personen (Clienten) die miteinander chatten. Also haben Sie (Server) 20 mal mit fork einen neuen Prozess, sprich Client, erzeugt. Zwei Personen haben sich entschieden von dem Chat-Portal 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 Sie den Prozess beenden und zwei Neue erzeugen in einem anderen Chat-Kanal. Und da sich nun die zwei Chattern von den restlichen 18 Chattern verabschiedet haben und bereits in Ihrem one-to-one Kanal chatten, müssen Sie dafür sorgen, dass keine Zombies zurückbleiben. Es liegt auf der Hand, dass der Server (Elternprozess) dabei sicherstellen muss, dass keiner seiner Kindprozesse zu einem Zombie-Prozess mutiert. Sonst hat das Betriebssystem 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, lernen Sie noch einen Prozess kennen, der sich die Zombies vorknüpft. Einer muss ja die Leichen aus dem Keller holen. Und das trifft hier im wahrsten Sinne des Wortes zu.

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

pstree | less

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

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

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

Zurück zum Thema. Es gibt mehrere Möglichkeiten die Entstehung von Zombie´s zu verhindern. Am meisten wird wohl folgende 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 diese Funktion aufgerufen wird, hält diese 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, können Sie wait() den Wert NULL übergeben. Gibt es keinen Kindprozess, liefert wait() den Wert -1 zurück.

/*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 zu dem Programm zuvor wurde hier nur ...

wait(NULL);

...eingefügt.

Vermeiden von Zomie-Prozessen mit wait

Anhand diese Bildes können Sie erkennen, dass der Kindprozess in der Lage ist, sich selbst zu beenden. Dies daher weil der Kindprozess denn Elternprozess informiert, das er fertig ist.

Jetzt können Sie das Programm erneut aufrufen, ein zweites Terminal öffnen und ps x eingeben. Und siehe da der Kindprozess des Zombie's existiert nicht mehr und wurde beendet. Jetzt wollen Sie natürlich auch wissen was für einen Rückgabewert das Programm macht ...

/*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 Sie bei Beendigung des Kindes mit der PID und dem exitstatus zurück. Mit ...

if(WIFEXITED(status) !=0)

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

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

...geben Sie dann durch WEXITSTATUS(status) die Nummer des exit-Wertes (1-255) zurück. Ein weiteres Makros das Sie verwenden können währe ...

WIFSIGNALED(status)

Dieses Makro liefert TRUE zurück, 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.

/*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 wird der Kindprozess beendet wenn das Makro WIFSIGNALED(status) das Signal SIGCHLD erhält. Die Nummer des Signals fragen Sie hier mit dem Makro WTERMSIG(status) ab.

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

/*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 Sie mit der Funktion Setsignal das Signal ...

signal(SIGCHLD,catchsignal)

Die Funktion catchsignal liefert dann, falls signal SIGCHLD erhalten hat folgendes zurück ...

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

Dieses Programm dient nur dazu, damit Sie sehen, dass ein Kindprozess wenn er fertig ist immer an seinen Elternprozess das Signal SIGCHLD liefert. Mit dem Abfangen des Signales könnten sie natürlich theoretisch andere Ausführungen machen lassen oder den Elternprozess dazu nötigen, dass 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 angehalten hat.

waitpid
Das Problem von wait ist, dass 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, dass 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. Dieser wird anhand der pid (PID) festgelegt ...

Weiterhin können Sie noch folgende Optionen hinzufügen ...

Weiter zwei Optionen währen WNOWAIT und WCONTINUED, aber da diese nicht dem POSIX-Standard entsprechen, will ich diese hier nicht durchnehmen.

#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 benötigt der 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. Sie wissen ja, falls sich der Elternprozess früher verabschiedet als der Kindprozess, haben wir einen Waisen. Den übernimmt die Mutter aller Prozesse nämlich der Prozess init.

Als Gegenbeispiel das selbe Programm nur mit der Funktion wait() ...

#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 dabei, dass hier das ganze Programm angehalten wird, bis der Elternprozess eine Antwort vom Kindprozess erhält. Erst dann wird wieder ein neuer Prozess erzeugt.

Weiter mit 2.6. getpid,getppid - Hallo Prozeß, wer bist du?