Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Linux-UNIX-Programmierung von Jürgen Wolf
Das umfassende Handbuch – 2., aktualisierte und erweiterte Auflage 2006
Buch: Linux-UNIX-Programmierung

Linux-UNIX-Programmierung
1216 S., mit CD, 49,90 Euro
Rheinwerk Computing
ISBN 3-89842-749-8
gp Kapitel 7 Dämonen, Zombies und Prozesse
  gp 7.1 Was ist ein Prozess?
  gp 7.2 Prozesskomponente
    gp 7.2.1 Prozessnummer (PID)
    gp 7.2.2 Prozessnummer des Vaterprozesses (PPID)
    gp 7.2.3 Benutzer- und Gruppennummer eines Prozesses (UID, EUID, GID, EGID)
    gp 7.2.4 Prozessstatus
    gp 7.2.5 Prozesspriorität
    gp 7.2.6 Timesharing-Prozesse
    gp 7.2.7 Prozessauslagerung
    gp 7.2.8 Steuerterminal
  gp 7.3 Prozesse überwachen – ps, top, kpm
  gp 7.4 Lebenszyklus eines Prozesses
  gp 7.5 Umgebungsvariablen eines Prozesses
    gp 7.5.1 Einzelne Umgebungsvariablen abfragen
    gp 7.5.2 Umgebungsvariable verändern oder hinzufügen – putenv() und setenv()
    gp 7.5.3 Löschen von Umgebungsvariablen – unsetenv() und clearenv()
  gp 7.6 Ressourcenlimits eines Prozesses
    gp 7.6.1 Mehr Sicherheit mit Ressourcenlimits
  gp 7.7 Prozesserkennung
  gp 7.8 Erzeugung von Prozessen – fork()
    gp 7.8.1 Pufferung
    gp 7.8.2 Was wird vererbt und was nicht?
    gp 7.8.3 Einen Prozess mit veränderter Priorität erzeugen
  gp 7.9 Warten auf einen Prozess
  gp 7.10 Die exec-Familie
    gp 7.10.1 execl()
    gp 7.10.2 execve()
    gp 7.10.3 execv()
    gp 7.10.4 execle()
    gp 7.10.5 execlp()
    gp 7.10.6 execvp()
    gp 7.10.7 Kindprozesse mit exec-Aufruf überlagern
  gp 7.11 Kommandoaufrufe aus dem Programm – system()
  gp 7.12 Dämonprozesse
    gp 7.12.1 Wie ein Prozess zum Dämon wird ...
    gp 7.12.2 Dämon, sprich mit uns ...
    gp 7.12.3 Protokollieren von Dämonen – syslog()
    gp 7.12.4 syslog() in der Praxis
    gp 7.12.5 Den Dämon, den ich rief ...
  gp 7.13 Rund um die Ausführung von Prozessen
    gp 7.13.1 Einen Dämon beim Booten mit einem init-Skript starten
    gp 7.13.2 Hintergrundprozesse und Jobkontrolle
    gp 7.13.3 Prozesse zeitgesteuert ausführen (cron-Jobs)
  gp 7.14 Zusammenfassung und Ausblick


Rheinwerk Computing

7.8 Erzeugung von Prozessen – fork()  downtop

Meistens wird zum Erzeugen eines neuen Prozesses der Systemaufruf fork() verwendet:

#include <unistd. h.>
#include  <sys/types.h>
pid_t fork(void);

fork() liefert bei Erfolg dem Elternprozess die Prozess-ID (PID) des Kindprozesses zurück. Dieser hingegen bekommt die Nummer 0 zurückgeliefert. Um seine eigene PID zu finden, ist dann getpid() nötig. Ein typischer Codeausschnitt der Prozessgenerierung sieht häufig so aus:

switch( pid=fork() ) {
  case -1:   /* Fehler bei fork() */ 
     break;
  case  0:   /* Hier befinden Sie sich im Kindprozess   */
     break;
  default:   /* Hier befinden Sie sich im Elternprozess */ 
     break;
}

Oder so:

if((pid = fork()) < 0) {
    fprintf(stderr, "Fehler... %s\n", strerror(errno));
} 
else if(pid == 0) {
    /* Kindprozess */
} 
else {
    /* Elternprozess */
}

Daraus lässt sich auch herauslesen, dass ein Kindprozess nur einen Elternprozess, aber der Elternprozess mehrere Kindprozesse haben kann. Der neue Kindprozess ist nun eine identische Kopie des Elternprozesses. Beide besitzen u. a. dieselben Daten, denselben Befehlszähler, die offenen Dateien, dieselbe User-ID und dasselbe Arbeitsverzeichnis.

Der Unterprozess (Kindprozess) sollte an seinem logischen Ende im Code möglichst irgendwo ein exit() haben, sonst macht er unterhalb des switch/if weiter wie der Elternprozess!

Gewöhnlich werden aber nicht gleich das Daten-, das Stacksegment und der Heap kopiert. Denn nicht selten wird ein Kindprozess erzeugt und mit einer Überlagerung eines exec-Funktionsaufrufes mit anderweitigen Daten und Code versorgt. Daher wird häufig im Kernel das Copy on write-Verfahren angewandt. Das bedeutet, dass erst dann wirklich eine Kopie einer Page der Datensegmente erfolgt, wenn in diese geschrieben wird. Dadurch erspart man sich häufig zeitaufwändiges Kopieren, was bei knappem RAM zum Swapping führen kann.

Nicht kopiert wird hingegen das Textsegment, das von beiden Prozessen gleichermaßen benutzt wird. Beide Prozesse arbeiten mit unterschiedlichen Befehlszeigern.

Dazu am besten ein Beispiel, das Sie zum einen verwirren sollte, aber zum anderen die Prozesse, wenn verstanden, in einem ganz anderen Licht erscheinen lassen. Hierzu das Listing:

/* child.c */
#include <unistd. h.>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
static int global_var = 1;
int main (void) {
   pid_t pid;
   int lokal_var = 1;
   switch (pid = fork ()) {
   case -1:
      printf ("Fehler bei fork()\n");
      break;
   case 0:
      sleep (1);   /* Kurze Pause */
      printf ("--- Im Kindprozess ---\n");
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse : %p\n",
                lokal_var, &lokal_var);
      ++global_var;
      ++lokal_var;
      printf ("--- Im Kindprozess ---\n");
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse : %p\n",
                lokal_var, &lokal_var);
      break;
   default:
      printf ("--- Im Elternprozess ---\n");
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse : %p\n",
                lokal_var, &lokal_var);
      sleep (2);
      printf ("--- Im Elternprozess ---\n");
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse : %p\n",
                lokal_var, &lokal_var);
      break;
   }
   return EXIT_SUCCESS;
}

Das Programm bei der Ausführung:

$ gcc -o child child.c
$ ./child
--- Im Elternprozess (2152) ---
global_var = 1 Speicheradresse : 0x804976c
lokal_var  = 1 Speicheradresse : 0xbffff380
--- Im Kindprozess (2153) ---
global_var = 1 Speicheradresse : 0x804976c
lokal_var  = 1 Speicheradresse : 0xbffff380
--- Im Kindprozess (2153) ---
global_var = 2 Speicheradresse : 0x804976c
lokal_var  = 2 Speicheradresse : 0xbffff380
--- Im Elternprozess (2152) ---
global_var = 1 Speicheradresse : 0x804976c
lokal_var  = 1 Speicheradresse : 0xbffff380

Wie Sie bereits erfahren haben, wird beim Erzeugen eines neuen Prozesses eine Kopie erstellt (s. o.). Somit dürfte Ihnen auch klar sein, dass der neue Kindprozess ein Ebenbild des Elternprozesses ist, was Ihnen auch an der Ausgabe der Adressen bestätigt wird. Nachdem aber jetzt im Kindprozess die lokale sowie die globale Variable inkrementiert wird, werden Sie jetzt (sofern Sie keine Erfahrung diesbezüglich haben) sicherlich die Frage stellen, warum dies beim anschließenden Elternprozess nicht verändert wurde? Nein, es handelt sich hierbei nicht um ein Pufferungs- oder Synchronisationsproblem. Wie ist es also möglich, dass trotz derselben Adresse die Werte im Elternprozess unverändert bleiben? Und genau hierbei haben viele Anfänger der Linux-Programmierung ein Problem, das gar nicht kompliziert ist.

Nehmen Sie am besten diese Seiten des Buches zur Hand und legen diese auf den Kopierer (Copyright-Rechte bitte beachten), und machen Sie zwei Kopien. Beide Kopien sehen, abhängig vom Kopierer, jetzt exakt gleich aus. Jetzt nehmen Sie eine Kopie zur Hand und bessern den Rechtschreibfehler Copyright-Rechte im Buch aus (in der Hoffnung, dass dies nicht schon von den fleißigen Lektoren gemacht wurde), hierbei kann ich auch gleich überprüfen, ob Sie überhaupt noch bei der Sache sind. Jetzt haben Sie ein Blatt mit der korrigierten Fassung und eines ohne Korrektur. Zugegeben, das ist weit hergeholt, aber das Prinzip, worauf ich hinauswill, ist, dass eine Kopie eines Prozesses bedeutet, dass Sie einen weiteren Prozess erzeugt haben, der ebenfalls einen eigenen Adressraum (virtueller Speicher) besitzt. Letztendlich handelt es sich eben um zwei eigene Prozesse, was Ihnen die Ausgabe der Prozess-ID (PID) auch bestätigt.


Rheinwerk Computing

7.8.1 Pufferung  downtop

Einen nicht ganz unbedeutenden Seiteneffekt muss ich hierbei noch ansprechen. Verwenden Sie das Beispiel von oben, und leiten Sie die Ausgabe in eine Datei um.

$./child > out.txt

Wenn Sie nun die Datei out.txt betrachten, wird Ihnen auffallen, dass die Reihenfolge der Ausgabe nicht mehr der entspricht, wie die, die auf der Konsole zurückgegeben wurde. Das Problem, das für sich eigentlich gar keines ist, ist die Pufferung der Daten. Da Sie hier die Ausgabe in eine Datei umleiten, wird aus Performance-Gründen die Vollpufferung verwendet. Wir haben hier aber auch zwei Pufferungen, da ja zwei Prozesse vorhanden sind. Jeder Prozess hat seinen eigenen Puffer. Somit schreiben sowohl Kind- als auch Elternprozess zuerst jeder in seinen eigenen Puffer, bis dieser voll ist (mindestens BUFSIZ Bytes) oder bis sich der Prozess beendet. Erst dann wird von zwei separaten Puffern in die Datei out.txt geschrieben. Wollen Sie so etwas vermeiden, können Sie entweder den unbequemen Weg mit der Funktion write(), die ja ungepuffert arbeitet, gehen oder den etwas bequemeren mit der Standardfunktion fflush(). Ein fflush(stdout) leert den Puffer der Ausgabe sofort und schreibt diesen gleich in die Datei. Im Listing sind dies vier Zeilen, die hinzugefügt werden müssen:

/* child2.c */
#include <unistd. h.>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
static int global_var = 1;
int main (void) {
   pid_t pid;
   int lokal_var = 1;
   switch (pid = fork ()) {
   case -1:
      printf ("Fehler bei fork()\n");
      break;
   case 0:
      sleep (1);   /* Kurze Pause */
      printf ("--- Im Kindprozess (%d) ---\n",getpid());
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse ; %p\n",
                lokal_var, &lokal_var);
      fflush(stdout);
      ++global_var;
      ++lokal_var;
      printf ("--- Im Kindprozess (%d) ---\n",getpid());
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse ; %p\n",
                lokal_var, &lokal_var);
      fflush(stdout);
      break;
   default:
      printf ("--- Im Elternprozess (%d) ---\n",getpid());
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse ; %p\n",
                lokal_var, &lokal_var);
      fflush(stdout);
      sleep (2);
      printf ("--- Im Elternprozess (%d) ---\n",getpid());
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse ; %p\n",
                lokal_var, &lokal_var);
      fflush(stdout);
      break;
   }
   return EXIT_SUCCESS;
}

Hinweis   Über die Reihenfolge der Ausgabe von Eltern- bzw. Kindprozess haben Sie hier noch keinen Einfluss, abgesehen von der primitiven Methode mittels sleep(), die allerdings nicht die Prozesse synchronisiert, sondern aufhält. Für die Ausgabe der Reihenfolge ist ein bestimmter Algorithmus vorhanden, der hier allerdings nicht von Interesse ist. Wie Sie Prozesse aufeinander abstimmen (synchronisieren) können, erfahren Sie im Kapitel über Interprozesskommunikation (IPC) und die Signale.


Natürlich sollte Ihnen auch klar sein, dass die Kopie dem Kindprozess entspricht, wenn der Kindprozess selbst einen weiteren Prozess erzeugt, , da ja in diesem Fall der Kindprozess der Elternprozess und der Elternprozess vom Kindprozess des Großelternprozesses des neu erzeugten Prozesses vom Kindprozess ist. Dann haben Sie bereits drei Generationen von Prozessen. Das Beispiel dazu zum besseren Verständnis:

/* child3.c */
#include <unistd. h.>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
static int global_var = 1;
int main (void) {
   pid_t pid;
   int lokal_var = 1;
   switch (pid = fork ()) {
   case -1:
      printf ("Fehler bei fork()\n");
      break;
   case 0:
      sleep (1);   /* Kurze Pause */
      printf ("--- Im Kindprozess (%d) ---\n",getpid());
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse : %p\n",
                lokal_var, &lokal_var);
      fflush(stdout);
      ++global_var;
      ++lokal_var;
      if(fork() == -1)
         printf("Fehler bei fork() (2)\n");
      else {
         printf ("--- Im (Kind-)Kindprozess(%d) ---\n",
            getpid());
         printf ("global_var = %d Speicheradresse : %p\n",
                   global_var, &global_var);
         printf ("lokal_var  = %d Speicheradresse : %p\n", 
                   lokal_var, &lokal_var);
         break;
      }      
      break;
   default:
      printf ("--- Im Elternprozess (%d) ---\n",getpid());
      printf ("global_var = %d Speicheradresse : %p\n",
                global_var, &global_var);
      printf ("lokal_var  = %d Speicheradresse : %p\n",
                lokal_var, &lokal_var);
      fflush(stdout);
      break;
   }
   return EXIT_SUCCESS;
}

Das Beispiel bei der Ausführung:

$ gcc -o child3 child3.c
$ ./child3
--- Im Elternprozess (2512) ---
global_var = 1 Speicheradresse : 0x804982c
lokal_var  = 1 Speicheradresse : 0xbffff380
 --- Im Kindprozess (2513) ---
global_var = 1 Speicheradresse : 0x804982c
lokal_var  = 1 Speicheradresse : 0xbffff380
--- Im (Kind-)Kindprozess (2516) ---
global_var = 2 Speicheradresse : 0x804982c
lokal_var  = 2 Speicheradresse : 0xbffff380
--- Im (Kind-)Kindprozess (2513) ---
global_var = 2 Speicheradresse : 0x804982c
lokal_var  = 2 Speicheradresse : 0xbffff380

Nach der Freude, dies verstanden zu haben, folgt bei dieser Ausgabe wohl gleich wieder Ernüchterung. Natürlich will ich Sie nicht dem Knock-out überlassen. Auch für dieses Beispiel gilt wieder der Satz vom Anfang: Mit fork() erzeugen Sie eine Kopie des aktuellen Prozesses. Und der fork()-Aufruf im Kindprozess (hier mit der PID 2513) erzeugt nun mal mit dem fork()-Aufruf eine Kopie von sich selbst, und diese beinhaltet ja im Code einen weiteren fork()-Aufruf, womit automatisch und logischerweise ein weiterer Prozess gestartet wird. Dies kann bei mehreren fork()-Aufrufen hintereinander sehr komplex und unübersichtlich werden. Hierzu ein Beispiel:

/* child4.c */
#include <unistd. h.>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX 4094
int main (void) {
   char log[MAX];   
   printf("1.fork()\n");   fflush(stdout);
   fork();
   printf("2.fork()\n");   fflush(stdout);
   fork();
   printf("3.fork()\n");   fflush(stdout);
   fork();   
   sleep(2);   
   snprintf(log, MAX, "PID:%d PPID:%d\n",
      getpid(), getppid());
   printf("%s",log);
   fflush(stdout);   
   return EXIT_SUCCESS;
}

So sieht´s aus:

$ gcc -o child4 child4.c
$ ./child4
1.fork()
2.fork()
3.fork()
3.fork()
2.fork()
3.fork()
3.fork()
PID:2873 PPID:2866
PID:2866 PPID:1958
PID:2870 PPID:2867
PID:2867 PPID:1
PID:2872 PPID:2871
PID:2871 PPID:1
PID:2869 PPID:2868
PID:2868 PPID:1

Sie sehen hier, dass der erste fork()-Aufruf nur einmal, der zweite zweimal und der dritte bereits viermal aufgerufen wird.


Rheinwerk Computing

7.8.2 Was wird vererbt und was nicht?  downtop

Hiermit möchte ich die Gelegenheit wahrnehmen aufzulisten, was die Kinder alles so von ihren Eltern vererbt bekommen.

Das bekommen die Kinder von ihren Eltern ...

... alle offenen Filedeskriptoren. Daher sollten Sie stets darauf achten, wenn beide Prozesse um eine Datei schreibend ringen. Aber dazu wird in der Interprozesskommunikation noch ausführlich eingegangen.

gp  Arbeits- und Wurzelverzeichnis
gp  Dateierstellungsmaske
gp  Signalmaske und Handler
gp  UID, EUID, GID, EGID, Prozessgruppen-, Session-, Setuser- und Setgroup-ID etc.
gp  Steuerterminal (CTTY)
gp  Umgebungsvariablen
gp  Ressourcenlimits
gp  Shared-Memory-Segmente (sofern verwendet)

... und das bekommen die Kinder NICHT von ihren Eltern vererbt ...

gp  Prozess-ID (PID) und Elternprozess-ID (PPID)
gp  die Dateisperren (Lockings)
gp  Zeitschaltuhren, die z. B. mit alarm() gesetzt wurden
gp  noch nicht ausgeführte Signale (hängende Signale)
gp  Die Zeiten utime, stime, cutime und ustime werden beim Kindprozess auf 0 gesetzt.

Rheinwerk Computing

7.8.3 Einen Prozess mit veränderter Priorität erzeugen  toptop

Auf das Verändern und Abfragen der Prozesspriorität wurde bereits ausführlich eingegangen. Wollen Sie jetzt einen Prozess mit verringerter (oder eventuell erhöhter) CPU-Priorität ausführen, müssen Sie lediglich mit fork() einen neuen Kindprozess starten, mit der Funktion setpriority() im Elternprozess entsprechende Prioritäten vergeben und diesen mithilfe einer exec*-Funktion (mehr zu den exec*-Funktionen in Kürze) überlagern. Beachten Sie allerdings, dass ein Erhöhen der Priorität nur mit Superuser-Rechten möglich ist.

Im folgenden Beispiel wird das Programm mit dem Makro PROGRAMM im Kindprozess mithilfe von execvp() überlagert und im Elternprozess mit der Funktion setpriority() auf die Priorität gesetzt, die mit dem Makro PRIORITY angegeben wird. Passen Sie die Angaben der beiden Makros PROGRAMM und PRIORITY bitte Ihren Bedürfnissen an. Im Beispiel wird find mit der niedrigsten Priorität ausgeführt. Natürlich können dabei weitere Argumente aus der Kommandozeile für find mit übergeben werden, die ebenfalls ausgewertet und verwendet werden.

/* prio_child.c */
#include <stdlib.h>
#include <unistd. h.>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
/* Bitte anpassen */
#define PROGRAM "/usr/bin/find"
/* Bitte anpassen */
#define PRIORITY 19
/* Max. Argumente aus der Kommandozeile */
#define MAX_ARGS 1024
int main (int argc, char **argv) {
   int i;
   pid_t pid;
   char *args[MAX_ARGS + 1] = { PROGRAM, NULL };
   seteuid (getuid ());  /* Privilegien entfernen */
    /* Argumente kopieren */   
    for (i = 1; i < argc && i < MAX_ARGS; i++)   
      args[i] = argv[i];
   args[i] = NULL;
   /* Neuen Prozess für PROGRAMM erzeugen */
   pid = fork ();   
   if (pid == 0) {
      /* neuen Prozess PROGRAMM starten */
      execvp (PROGRAM, args);   
   }
   else {   
      /* Privilegien wieder einführen */
      seteuid (0); 
      /* Priorität für PROGRAMM (pid) auf PRIORITY setzen */
      setpriority (PRIO_PROCESS, pid, PRIORITY);
   }
   return EXIT_SUCCESS;
}

Das Programm bei der Ausführung:

$ gcc –o prio_child prio_child.c
$ ./prio_child /home -name child\*
/home/tot/Kap6/child
/home/tot/Kap6/child2.c
/home/tot/Kap6/child3.c
/home/tot/Kap6/child4.c
/home/tot/Kap6/child2
/home/tot/Kap6/child3
/home/tot/Kap6/child4
/home/tot/Kap6/child.c
/home/tot/backups/21_April_04/Listings/kap5/child1.c
/home/tot/backups/21_April_04/Listings/kap5/child2.c
/home/tot/backups/21_April_04/Listings/kap5/child3.c
/home/tot/backups/21_April_04/Listings/kap5/child.c
 << zurück
  
  Zum Rheinwerk-Shop
Neuauflage: Linux-UNIX-Programmierung
Neuauflage:
Linux-UNIX-
Programmierung

bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Linux-Server






 Linux-Server


Zum Rheinwerk-Shop: Das Komplettpaket LPIC-1 & LPIC-2






 Das Komplettpaket
 LPIC-1 & LPIC-2


Zum Rheinwerk-Shop: Linux-Hochverfügbarkeit






 Linux-
 Hochverfügbarkeit


Zum Rheinwerk-Shop: Shell-Programmierung






 Shell-
 Programmierung


Zum Rheinwerk-Shop: Linux Handbuch






 Linux Handbuch


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
Info





Copyright © Rheinwerk Verlag GmbH 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern