fork, exec, wait und exitDie wunderbare Welt von Isotopp
Sonntag, 7. Januar 2007fork, exec, wait und exitIn de.comp.os.unix.linux.misc fragte jemand:
Werden in einem Skript die Befehle streng sequentiell ausgeführt, d.h.der nächste erst bearbeitet, wenn der Vorgänger vollständig ausgeführtist, oder wird automatisch bei unvollständiger Auslastung des Systemsbereits der nächste Befehl angefangen?Läßt sich das Standardverhalten wie auch immer es sein mag beiBedarf ändern?
Wenn man in ein Shellbuch schaut, wird einem an der einen oder anderen Stellemöglicherweise erläutert, daß die Shell jeden Befehl in einem eigenen Prozeß abarbeitet.Dann wiederum fängt man möglicherweise an zu denken und fragt sich, wie das alleszusammenhängt. Sobald man dort angekommen ist, kann man sich mit dem UnixProzeßzyklus beschäftigen.Prozeß und Programm
Ein Programm ist in Unix eine Serie von ausführbaren Maschineninstruktionen auf derPlatte. Man kann mit dem Befehl size einen sehr oberflächlichen Blick auf die Struktur desProgrammes werfen oder mit objdump sehr viel mehr Detailinformation bekommen. DerAspekt, der uns hier interessieren soll: Ein Programm ist eine Folge von Anweisungen undDaten (auf der Platte), die möglicherweise einmal ausgeführt werden.
Ein Prozeß ist ein in Ausführung befindliches Programm. Er besteht aus dem Programmselbst (also der Versammlung von Anweisungen und Daten) und dem aktuellen Zustandder Ausführung. Dazu gehört neben der MemoryMap, die sagt wie das Programm undseine Daten im Speicher angeordnet sind auch der Programmzähler, die Prozessorregisterund der Stack des Prozesses, aber auch sein RootDirectory, sein aktuelles Verzeichnis,die Umgebungsvariablen und alle offenen Dateien sowie einigen weiteren Dingen.
Unix behandelt Prozesse und Programme als die verschiedenen Dinge, die es sind: Es istmöglich, ein Programm mehr als einmal auszuführen es ist zum Beispiel möglich, mehrals eine Kopie des Texteditors vi offen zu haben, die zwei unterschiedliche Textebearbeiten. Programm und (initiale) Daten beider Prozesse sind gleich, aber der Zustandbeider Prozesse ist verschieden. Es ist auch möglich, im selben Prozeß nacheinandermehr als ein Programm auszuführen dazu schmeißt sich das aktuelle Programm in demProzeß selbst weg und ersetzt sich durch ein zweites, in diesen Prozeß nachgeladeneProgramm.
Unix regelt all diese Dinge mit vier sehr einfachen Systemkonzepten fork(), exec(), wait()und exit().
Usermode und Kernel
Prozeßwechsel: Es wird ein Stück Prozeß 1abgearbeitet, dann (1) auf Prozeß 2 umgeschaltet.Nach einer Weile wird (2) wieder auf Prozeß 1zurück geschaltet. Die Ausführung von Prozeß 1erscheint lückenlos, erfolgt aber in zeitlich nicht
zusammenhängenden Intervallen.
Wenn ein UnixProzeß eine Systemfunktion aufruft (und noch bei ein paar anderen
Gelegenheiten), dann verläßt der betreffende Prozeß
seinen Userkontext und betritt den privilegierten
Betriebsystemkern, den Kernel. Dort wird die
aufgerufene Systemfunktion ausgeführt und danach
landet jede dieser Funktionen im Scheduler. Der
Scheduler entscheidet dann, welcher Prozeß als
nächstes dran kommt und kehrt aus dem Kernel in
diesen Prozeß zurück. Das kann unser
Ausgangsprozeß oder nach Entscheidung des
Schedulers ein anderer Prozeß sein.
Wir halten für die Zwecke diese Textes fest: Jeder
Systemaufruf wechselt vom Userkontext in den
Kernel. Der einzige Weg aus dem Kernel in einen
Userkontext führt durch den Scheduler, und dann
kehren wir unter Umständen nicht in den Ausgangsprozeß zurück. Bei jedem Systemaufruf
kann ein Prozeß also seine CPU verlieren.
Das ist nicht schlimm, weil dieser andere Prozeß auch irgendwann einmal die CPU
aufgeben muß und wir dann in unseren eigenen Prozeß zurück kehren als sei nichts
gewesen.
Unser Programm wird also nicht linear abgearbeitet, sondern in kurzen linearen
Segmenten, zwischen denen Pausen liegen können. In den Pausen arbeitet die CPU an
den Segmenten der anderen Prozesse, die ebenfalls lauffähig sind.
fork() und exit()
In traditionellem Unix ist der Systemaufruf fork() die einzige Methode, einen neuen Prozeß
zu erzeugen. Der neue Prozeß enthält eine Kopie des laufenden Programmes. Er hat eine
neue ProzeßID und die ProzeßID des Erzeugers ist als seine Parent ProzeßID (PPID)
eingetragen. Im ParentProzeß kehrt fork() mit der PID des neuen Prozesses als Ergebnis
zurück. Der neue Prozeß kehrt ebenfalls aus dem Systemaufruf fork() zurück, liefert dort
aber das Resultat 0.
Der Systemaufruf fork() ist also insofern besonders, als daß er einmal betreten wird, aber
zweimal verlassen wird: Einmal im Elternprozeß und einmal im neu erzeugten Kindprozeß.
fork() erhöht die Anzahl der laufenden Prozesse im System um eins. Jeder UnixProzeß
beginnt seine Existenz also, indem er aus einem fork()Systemaufruf spontan zurückkehrt
und ein Programm ausführt, daß eine Kopie des Elternprogrammes ist.Sein Schicksal
unterscheidet sich vom Schicksal des Elternprozesses, weil das Ergebnis des fork()
Aufrufes unterschiedlich ist (0 statt der PID des Kindes) und man dies zur Verzweigung
nutzen kann.
In Code:
CODE:
kris@linux:/tmp/kris> cat probe1.c
#include <stdio.h>#include <unistd.h>#include <stdlib.h>
main(void) pid_t pid = 0;
pid = fork(); if (pid == 0) printf("Ich bin der Kindprozess.\n"); if (pid > 0) printf("Ich bin der Elternprozess, das Kind ist %d.\n",pid);
if (pid < 0) perror("In fork():");
exit(0);kris@linux:/tmp/kris> make probe1cc probe1.c o probe1kris@linux:/tmp/kris> ./probe1Ich bin der Kindprozess.Ich bin der Elternprozess, das Kind ist 16959.
Wir vereinbaren also eine Variable pid vom Typ pid_t. Diese Variable speichert dasErgebnis des Systemaufrufes fork() und mit Hilfe dieses Wertes aktivieren wir entweder daseine ("Ich bin der Kindprozeß") oder das andere ("Ich bin der Elternprozeß") if(). Starten wirdas Programm, erhalten wir zwei Ausgaben. Da innerhalb eines Prozesses nur ein Statusexistieren kann und nur eines der beiden if() betreten werden kann, wir aber zweiAusgaben erhalten haben, müssen wir zwei Prozesse erzeugt haben. Indem wir dasErgebnis von getpid() druckten, könnten wir das sogar noch anschaulicher zeigen.
Der Systemaufruf fork() wird einmal betreten, aber zweimal verlassen und erhöht dieAnzahl der Prozesse im System um eins. Nach dem Ablauf unseres Programmes ist dieAnzahl der Prozesse im System aber wieder genauso hoch wie vor dem Aufruf desProgrammes. Es muß also einen weiteren Systemaufruf geben, der die Anzahl derProzesse im System um eins erniedrigt.
Dieser Aufruf ist exit(). exit() wird einmal betreten und nie verlassen. Er verkleinert dieAnzahl der Prozesse im System um eins. exit() liefert außerdem einen Exitstatus, den derElternprozeß abholen kann (oder gar muß) und der ihn über das Schicksal seines Kindesinformiert.
In unserem Beispiel enden alle Varianten unseres Programmes mit exit() wir rufen exit()also im Elternprozeß und im Kindprozeß auf, beenden also zwei Prozesse. Das können wirnur deswegen tun, weil unser Elternprozeß auch ein Kindprozeß ist und zwar ein Kind derShell. Die Shell arbeitet also genau wie wir:
CODE:bash (16957) erzeugt durch fork() > bash (16958) wird zu > probe1 (16958)
probe1 (16958) erzeugt durch fork() > probe1 (16959) > exit() | +> exit()
exit() schließt alle Dateien und Internetverbindungen eines Prozesses, gibt allen Speicherfrei und beendet dann den Prozeß. Der Parameter von exit(), der Exitstatus, wird an denElternprozeß zurückgegeben.
wait()
Der Kindprozeß endet durch ein exit(0). Die 0 ist der Exitstatus unseres Programmes undsteht nun zur Abholung bereit. Wir müssen im Elternprozeß den Exitstatus abholen. Diesgeschieht mit der Systemfunktion wait().
In Code:
CODE:kris@linux:/tmp/kris> cat probe2.c
#include <stdio.h>#include <unistd.h>#include <stdlib.h>
#include <sys/types.h>#include <sys/wait.h>
main(void) pid_t pid = 0;
int status;
pid = fork(); if (pid == 0) printf("Ich bin der Kindprozess.\n"); sleep(10); printf("Ich bin der Kindprozess 10 Sekunden spaeter.\n"); if (pid > 0) printf("Ich bin der Elternprozess, das Kind ist %d.\n",pid); pid = wait(&status); printf("Ende des Prozesses %d: ", pid); if (WIFEXITED(status)) printf("Der Prozess wurde mit exit(%d) beendet.\n",WEXITSTATUS(status)); if (WIFSIGNALED(status)) printf("Der Prozess wurde mit kill %d beendet.\n",WTERMSIG(status)); if (pid < 0) perror("In fork():");
exit(0);kris@linux:/tmp/kris> make probe2cc probe2.c o probe2kris@linux:/tmp/kris> ./probe2Ich bin der Kindprozess.Ich bin der Elternprozess, das Kind ist 17399.Ich bin der Kindprozess 10 Sekunden spaeter.Ende des Prozesses 17399: Der Prozess wurde mit exit(0) beendet.
Die Variable status wird dem Systemaufruf wait() als Referenzparameter mit übergebenund von diesem überschrieben. Neben dem Exitstatus finden wir dort auch noch weitereInformationen über den Grund des Programmendes hinterlegt. Zur Decodierung stellt dasSystem eine Reihe von Prädikaten wie WIFEXITED() oder WIFSIGNALED() zur Abfragebereit und Extraktoren wie WEXITSTATUS() und WTERMSIG(). wait() gibt außerdem dieProzeßID des Prozesses zurück, der beendet wurde.
wait() hängt im Elternprozeß so lange bis entweder ein Signal eintrifft oder ein Kindprozeßbeendet wird.
Das Programm init mit der PID 1 macht übrigens den ganzen lieben langen Tag nixanderes: Es hängt im wait() und frühstückt die ihm zugeworfenen Exitstati ab, um sie zuverwerfen. Außerdem liest es die /etc/inittab und startet die dort konfigurierten Programme.Ist eines dieser Programme auf Respawn gesetzt und wird beendet, wird es von init neugestartet.
Beendet sich ein Kindprozeß, ohne daß der Elternprozeß ein wait() macht, zerstört exit()schon einmal alle Datenstrukturen des Kindprozesses, kann jedoch denProzeßlisteneintrag des Prozesses noch nicht wegwerfen, denn hier steht der Exitstatusdes Kindes drin. Es könnte ja nun sein, daß der Elternprozeß sich irgendwann entschließt,ein wait() auszuführen und dann muß der Exitstatus ja bereitstehen.
Der Kindprozeß ist also bereits tot er hat exit() ausgeführt und alle Ressourcenfreigegeben, kann aber noch nicht sterben, weil ja der Elternprozeß den Status noch nichtabgeholt hat. Unix nennt so einen Prozeß einen ZombieProzeß. Zombies werden in derProzeßliste sichtbar, wenn ein Prozeßerzeuger falsch programmiert ist und nichtausreichend wait() aufruft.
Anders herum ist es auch möglich, daß ein Kindprozeß weiter läuft, während einElternprozeß beendet wird. Dann wird die Parent ProzeßID (PPID) des Kindes von der PIDdes Elternprozesses auf die Konstante 1 geändert, oder in anderen Worten init erbt denProzeß. Beendet sich das Kind, empfängt init den Exitstatus des Kindes, denn init hängt jasowieso dauernd im wait. Dadurch wird die Entstehung eines Zombies in diesem Fallverhindert.
Wenn die Anzahl der Prozesse im System über die Laufzeit des System im Mittel konstantist, dann ist die Anzahl der fork(), exit() und wait()Aufrufe im System ebenfalls im Mittelgleich, denn für jedes fork() muß irgendwann einmal ein exit() gemacht werden und fürjedes exit() muß der Elternprozeß einmal ein wait() machen (In Wirklichkeit ist die Situationwegen einiger anderer Regeln noch ein wenig komplizierter, aber erst einmal soll dies hiergenügen). Wir haben also ein sauberes forkexitwaitDreieck.
exec()
So wie fork() Prozesse erzeugt, so lädt exec() Programme in einen Prozeß. In Code:CODE:kris@linux:/tmp/kris> make probe3
cc probe3.c o probe3kris@linux:/tmp/kris> cat probe3.c
#include <stdio.h>#include <unistd.h>#include <stdlib.h>
#include <sys/types.h>#include <sys/wait.h>
main(void) pid_t pid = 0; int status;
pid = fork(); if (pid == 0) printf("Ich bin der Kindprozess.\n"); execl("/bin/ls", "ls", "l", "/tmp/kris", (char <strong>) 0); perror("In exec(): "); if (pid > 0) printf("Ich bin der Elternprozess, das Kind ist %d.\n",pid); pid = wait(&status); printf("Ende des Prozesses %d: ", pid); if (WIFEXITED(status)) printf("Der Prozess wurde mit exit(%d) beendet.\n",WEXITSTATUS(status)); if (WIFSIGNALED(status)) printf("Der Prozess wurde mit kill %d beendet.\n",WTERMSIG(status)); if (pid < 0) perror("In fork():");
exit(0);kris@linux:/tmp/kris> ./probe3Ich bin der Kindprozess.Ich bin der Elternprozess, das Kind ist 17690.total 36rwxrxrx 1 kris users 6984 20070105 13:29 probe1rwrr 1 kris users 303 20070105 13:36 probe1.crwxrxrx 1 kris users 7489 20070105 13:37 probe2rwrr 1 kris users 719 20070105 13:40 probe2.crwxrxrx 1 kris users 7513 20070105 13:42 probe3rwrr 1 kris users 728 20070105 13:42 probe3.cEnde des Prozesses 17690: Der Prozess wurde mit exit(0) beendet.
Hier wird im Sohnprozeß der Code von probe3 weggeworfen (Das perror("In exec():") wirdniemals ausgeführt) und durch den angegebenen Aufruf von "ls" ersetzt. In der Ausführungerkennen wir, daß probe3 wartet, bis das "ls" sich mit exit() beendet hat und dann seineeigene Ausführung danach fortsetzt.
Als Shellscript
Die Beispiele oben operieren in C. In bash sieht es so aus:CODE:kris@linux:/tmp/kris> cat probe1.sh
#! /bin/bash
echo "Starte Kindprozess"sleep 10 &
echo "Der Kindprozess hat die ID $!"echo "Der Elternprozess hat die ID $$"echo "$(date): Elternprozess geht schlafen."waitecho "Der Kindprozess $! hat den ExitStatus $?"echo "$(date): Elternprozess ist aufgewacht."
kris@linux:/tmp/kris> ./probe1.shStarte KindprozessDer Kindprozess hat die ID 18071Der Elternprozess hat die ID 18070Fri Jan 5 13:49:56 CET 2007: Elternprozess geht schlafen.Der Kindprozess 18071 hat den ExitStatus 0Fri Jan 5 13:50:06 CET 2007: Elternprozess ist aufgewacht.
Und hier beobachten wie die Shell bei der Ausführung von Kommandos:
CODE:kris@linux:~> strace f e execve,clone,fork,waitpid bash
kris@linux:~> lsclone(Process 30048 attachedchild_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,child_tidptr=0xb7dab6f8) = 30048[pid 30025] waitpid(1, Process 30025 suspended <unfinished ...>[pid 30048] execve("/bin/ls", ["/bin/ls", "N", "color=tty", "T", "0"],[/</strong> 107 vars */]) = 0...Process 30025 resumedProcess 30048 detached<... waitpid resumed> [WIFEXITED(s) && WEXITSTATUS(s) == 0], WSTOPPEDWCONTINUED) = 30048 SIGCHLD (Child exited) @ 0 (0) ...
Linux verwendet eine Verallgemeinerung von fork() mit dem Namen clone() um einenKindprozeß zu erzeugen. Daher sehen wir keinen fork(), sondern einen clone()Aufruf miteinigen Parametern. Linux verwendet außerdem die Variante waitpid() von wait(), um aufeine bestimmte PID zu warten. Linux startet außerdem das Programm mit execve() statt mitexecl(), aber das ist nur eine andere Anordnung von Parametern. Nach dem Ende von "ls"(PID 30048) wird der Prozeß 30025 aus dem wait() erweckt und fortgesetzt.
Und hier ist der CCode der Originalshell aus dem Jahre 1979, mit dem fork() (Man suchenach "case TFORK:" und wundere sich nicht über den Programmierstil von Herrn Bourne).Ja, bash ist schöner GNU Code oder nicht.
(nach einem NewsArtikel von mir)Geschrieben von Kristian Köhntopp in Schulung um 01:09 | Kommentare (6) | Trackbacks (5)
TrackbacksTrackbackURL für diesen Eintrag
Kris erklärt die Welt heute: fork, exec, wait und exitKris hat einen ein sehr guten Artikel über die Abläufe beim Starten und Beenden vonProzessen in einem unixoiden Operating Environment geschrieben: fork, exec, wait und exit.Eindeutige Leseempfehlung.Weblog: c0t0d0s0.orgAufgenommen: Jan 07, 06:40
fork und exec vs. CreateProcessDisclaimer: Meine WindowsKenntnisse sind beschränkt, veraltet und ausschließlichtheoretischer Natur. Im Zweifel erzählt dieser Artikel Unsinn nach Hörensagen. Nach demArtikel form, exec, wait und exit habe ich mir aber einmal meine Kopie von JeffreyWeblog: Die wunderbare Welt von IsotoppAufgenommen: Jan 07, 09:07
Prozesse am Sonntag
Spannende Sonntagslektüre beim Kris: wie funktioniert das mit dem Entstehen & und
Vergehen von Prozessen auf UnixSystemen? – Sehr lesenswert für Einsteiger und
zur Auffrischung!
Weblog: [juergenluebeck.de]
Aufgenommen: Jan 07, 10:44
Serendipitäten?
In einem früheren Beitrag verlinkte ich einen Artikel von Isotopp. Dabei versuchte mein S9y
natürlich, einen Trackback zu generieren. Bei einem anderen, auf das S9y von Balrog
verlinkenden Artikel klappte das einwandfrei nicht jedoch bei dem, der auf Is
Weblog: Ingo Heinscher Blog
Aufgenommen: Jan 09, 15:53
Immer wieder lesenwert
Kris schreibt heute in seinem Blog einen wunderbaren Artikel zu Prozess und Programm.
Danke, Kris. Wobei hier eine Merkwürdigkeit auftrat. Siehe weiter oben.
Weblog: Ingo Heinscher Blog
Aufgenommen: Jan 09, 16:07
KommentareAnsicht der Kommentare: (Linear | Verschachtelt)
Verblüffend bei den Beispielen ist, daß nach dem 'fork()' der Kindprozeß von Scheduler stets
vor dem Elternprozeß ausgefüht wird. Das war nicht immer und überall so, mir wollen die
Argumente für und wider nicht einfallen. Vielleicht möchtest Du darauf eingehen.
Prima Artikel, aber jetzt nicht schlappmachen: Wozu brauche ich "Threads", was
unterscheidet sie von einer "gefork()ten" Instanz ?
Gruß Hans Bonfigt
#1 Hans Bonfigt am 07.01.2007 12:57
Der Unterschied zwischen Prozessen und Threads ist, dass sich Prozesse gekapselt in
einem eigenständigen Adressraum befinden, wohingegen Threads alle auf den gleichen
Adressraum zugreifen.
#1.1 peter am 07.01.2007 19:04
Threads und Prozesse sind unter Linux sogar (was die Behandlung im KernelCode
angeht) dasselbe: Wie Peter schon sagte, sind Threads schlicht Prozesse die miteinander
auf dieselben Datenstrukturen zugreifen, wohingegen Prozesse jeweils ihre eigenen
haben.
#1.2 Fred (Link) am 09.01.2007 07:40
Kris: Schöner Artikel, danke. Es freut mich auch, dass du wenig gekünstelt klingende,
deutsche Worte verwendest :)
PS: Der Plural von "status" ist nicht "stati" sondern "status" (mit langem u)...
#2 Fred (Link) am 09.01.2007 07:43
Ich bin ja für Statussis.
#2.1 Isotopp (Link) am 09.01.2007 09:23
Wenn, dann richtig: Statussen.
#2.1.1 Axel (Link) am 10.01.2007 22:04
Kommentar schreiben
Name
EMail
Homepage
Antwort zu
Kommentar
Umschließende Sterne heben ein Wort hervor (*wort*), per _wort_ kann ein
Wort unterstrichen werden.
Um maschinelle und automatische Übertragung von Spamkommentaren zu
verhindern, bitte die Zeichenfolge im dargestellten Bild in der Eingabemaske
eintragen. Nur wenn die Zeichenfolge richtig eingegeben wurde, kann der
Kommentar angenommen werden. Bitte beachten Sie, dass Ihr Browser
Cookies unterstützen muss, um dieses Verfahren anzuwenden.
Hier die Zeichenfolge der SpamschutzGrafik eintragen:
BBCodeFormatierung erlaubt Daten merken?