|
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = getpid();
printf ("Meine PID = %d\n", pid) ;
pid = getppid();
printf ("Meine Eltern-PID = %d\n", pid) ;
return 0;
}
Das Programm aus diesem Listing definiert eine Variable vom Typ pid_t.
Die Werte, die von den Funktionen getpid() und getppid()
zurückgegeben werden, werden dann ausgegeben.
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.
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);
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 der Rückgabewert von fork().
- Im Kindprozess ist der Wert 0.
- Im Elternprozess ist es der Wert der Prozess-ID des Kindprozesses.
- Wenn fork() scheitert, wird -1 zurückgegeben.
Nach dem Aufruf von fork() sind die Daten beider Programme getrennt, so
daß weder der Kind- noch der Elternprozess in der Lage ist, irgendwelche
Variablen oder Daten im jeweils anderen Prozess zu manipulieren.
Beispiel: Mit Hilfe von fork() einen neuen Prozess erzeugen.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
int x = 22;
pid = fork();
if (pid < 0)
{
printf("Fehler: fork()-Rsultat %d.\n", pid);
exit(1);
}
if (pid == 0)
{
printf("Kind: PID = %u. Eltern-PID = %u\n",
getpid(), getppid());
printf("Kind: xalt = %d\n", x);
x = 11;
printf("Kind: xneu = %d\n", x);
sleep(2);
puts ("Kind: Beendet.");
exit(42);
}
else
{
printf("Eltern: PID = %u. Kind-PID = %u\n",
getpid(), pid);
puts("Eltern: 60 Sekunden Pause.");
sleep(60);
puts("Eltern: wieder wach.");
printf("Eltern: x = %d\n", x);
}
return 0;
}
Ausgabe:
Eltern: PID = 1535. Kind-PID = 1536
Eltern: 60 Sekunden Pause.
Kind: PID = 1536. Eltern-PID = 1535
Kind: xalt = 22
Kind: xneu = 11
Kind: Beendet.
Eltern: wieder wach.
Eltern: x = 22
Anhand des Rückgabewertes von fork() wird festgestellt,
ob ein Fehler aufgetreten ist. Sind keine Fehler aufgetreten, werden 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 wird von beiden Prozessen ausgewertet. Der
Kindprozess führt danach den Block nach dem if aus, der Elternprozess
den Block nach dem else.
An der Programmausgabe können Sie erkennen, daß der Elternprozess nach dem
fork()-Aufruf eine Meldung ausgibt und sich dann schlafen legt.
Parallel wird der Kindprozess weiter ausgeführt. Als erstes gibt er seine eigene
PID und die seines Elternprozesses aus. Als Nächstes gibt der Kindprozess den
Wert der Variablen x aus, ändert den Wert und gibt ihn erneut aus.
Schließlich geht auch er für 2 Sekunden Pause.
Da der Elternprozess 60 Sekunden schläft, wacht der Kindprozess vor seinem Eltern
auf und gibt eine Meldung aus. Dann beendet er sich und gibt den Wert 42 zurück.
60 Sekunden später erwacht der Elternprozess von seinem eigenen
sleep()-Aufruf, gibt den Wert der Variablen x aus und beendet sich ebenfalls.
Zombie-Prozesse
Das obige Programm enthält allerdings auch einen dicken Fehler, der in
bestimmten Situationen Probleme verursachen kann. Um zu verstehen, worin dieser
Fehler besteht, führen Sie das Programm noch einmal im Hintergrund aus.
Wenn die "Kind: Beendet"-Meldung erscheint, rufen Sie den
Befehl ps u auf und betrachten den Eintrag des Kindprozesses:
...
jpl 1714 0.0 0.0 0 0 pts/5 Z Jan27 0:00 [kind <defunct>]
...
Der Kind-Prozess wird als erloschen (defunct) gemeldet. In der
STAT-Spalte dieses Prozesses steht ein Z, was bedeutet,
daß 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 Prozesstabelle 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.
Es gibt mehrere Wege, die Entstehung von Zombie-Prozessen zu verhindern. Am
häufigsten wird die Systemfunktion wait() verwendet
(Header-Datei sys/wait.h):
pid_t wait(int *status);
Diese Funktion hat eine int-Variablenparameter 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 status-Variable. Wenn Sie an dem Rückgabewert
des Kindprozesses nicht interessiert sind, übergeben wait() den
Wert NULL. Gibt es keinen Kindprozess, liefert wait() den
Wert -1 zurück.
Beispiel: Mit wait() Zombie-Prozesse verhindern.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int status;
pid = fork();
if (pid < 0)
{
printf("Fehler: fork()-Rsultat %d.\n", pid);
exit(1);
}
if (pid == 0)
{
printf("Kind: PID = %u. Eltern-PID = %u\n",
getpid(), getppid());
sleep(1);
puts ("Kind: Beendet.");
exit(42);
}
else
{
printf("Eltern: PID = %u. Kind-PID = %u\n",
getpid(), pid);
puts("Eltern: 10 Sekunden Pause.");
sleep(10);
puts("Eltern: wieder wach.")
pid = wait(&status);
printf("Eltern: Kind mit PID %u ", pid);
if (WIFEXITED(status) != 0)
printf("wurde mit Status %d beendet\n",WEXITSTATUS(status));
else
printf("wurde mit Fehler beendet.\n");
}
return 0;
}
Dieses Listing entspricht weitgehend dem vorhergehenden Programm. Der Hauptunterschied
liegt darin, daß der Elternprozess nach dem Erwachen die Funktion
wait() aufruft. 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, daß diese Makros dafür sorgen,
daß nur 8-Bit-Werte (1 bis 255) als Exit-Status zurückgeliefert werden.
Die wait()-Funktion ist offensichtlich recht nützlich, wenn man weiß, daß 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 gewünscht, kann man die waitpid()-Funktion
verwenden, die in der Header-Datei sys/wait.h definiert ist:
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, daß 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.
Beispiel: Mit waitpid() Zombie-Prozesse verhindern.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int status;
pid = fork();
if (pid < 0)
{
printf("Fehler: fork()-Rsultat %d.\n", pid);
exit(1);
}
if (pid == 0)
{
printf("Kind: PID = %u. Eltern-PID = %u\n",
getpid(), getppid());
sleep(10);
puts ("Kind: Beendet.");
exit(66);
}
else
{
printf("Eltern: PID = %u. Kind-PID = %u\n",
getpid(), pid);
while ((pid = waitpid (-1, &status, WNOHANG)) == 0)
{
printf("Eltern: Kein Kind beendet.");
puts(" 1 Sekunde Pause.");
sleep(1);
}
printf("Eltern: Kind mit PID %u ", pid);
if (WIFEXITED(status) != 0)
printf("wurde mit Status %d beendet\n", WEXITSTATUS(status));
else
printf("wurde mit Fehler beendet.\n");
}
return 0;
}
Einen Prozess durch einen anderen ersetzen
Die fork()-Funktion ist nur ein Teil der Lösung; der zweite Teil
besteht darin, einen laufenden Prozess durch einen anderen zu ersetzen.
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 Kommandozeile-Parameter werden danach
übergeben. Im Unterschied zu Funktionen wie printf() ist execl() darauf
angewiesen, daß man als letztes Argument einen NULL-Zeiger übergibt, der das Ende
der Argumentenliste anzeigt.
Der zweite an execl() übergebene Parameter ist nicht der erste Kommandozeilen-Parameter, der an das aufzurufende Programm (spezifiziert in
path) übergeben wird. Vielmehr ist er der Name, unter dem der
neue Prozess in der vom ps-Befehl erzeugten Prozessliste aufgeführt
wird. Der erste Parameter, der an das (in path spezifizierte) Programm
übergeben wird, ist also tatsächlich der dritte Parameter von execl().
Wenn Sie beispielsweise das Programm /bin/ls mit dem Parameter -lisa
aufrufen wollen und möchten, daß das Programm in der Prozessliste unter dem
Namen "verz" aufgerufen wird, würden Sie execl() wie folgt aufrufen:
execl("/bin/ls", "verz", "-lisa", NULL);
Dieser Aufruf würde den aktuellen Prozess durch einen Prozess ersetzen, der dem
Aufruf von /bin/ls -lisa von der Befehlszeile entspricht.
Beispiel: Mit execl() einen Prozess durch einen anderen ersetzen.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main(void)
{
pid_t pid ;
pid = getpid();
printf ("Meine PID = %u\n", pid);
execl ("/bin/ps", "ps-proggie", "u", NULL);
puts("Ein Fehler ist aufgetreten.");
return 0;
}
Beachten Sie, daß der ursprüngliche Prozess die gleiche Prozess-ID
trägt wie später der neue Prozess, der ihn ersetzte.
execl() ist nicht die einzige Funktion dieser Art, es gibt eine
ganze Familie mit leicht unterschiedlicher Arbeitsweise.
Alles zusammen
Die C-Funktion system() kann Kommandos an UNIX übergeben - sie vereint
also fork() und exec..(). Sie erhält eine Stringkonstante
(z.B. system("ls -l");) oder eine Stringvariable (z.B. char kommando[
20]; ...; system(kommando);) als Eingabeparameter. Dieser Parameter ist
das Kommando, das dann von UNIX ausgeführt wird.
system() erzeugt einen eigenen Prozeß. Dieser führt das Kommando aus,
was aber keinen Effekt für den aufrufenden Prozeß hat.
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.
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, daß das Programm beendet und eine
"Core Dump"-Datei, erzeugt wird.
In der folgenden Tabelle 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 |
Logoff |
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 |
Prozess suspendiert, ausgelöst durch [Strg)+[Z]. |
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 eine eigene Signal-Bearbeitungsroutine einrichten.
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, daß 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 die Nummer des Signals, dessen Verhalten
Sie verändern oder untersuchen wollen. Man übergibt dem Parameter aber nicht die
tatsächliche Signal-Nummer, sondern die zugehörige symbolische Konstante - 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 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 solche Struktur als
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 daß es später wieder hergestellt werden kann.
- Das Verhalten ändern: sigaction(SIGINT, &neueaktion, NULL);
- Das Verhalten untersuchen: sigaction(SIGINT, NULL, &alteaktion);
- Kopie des aktuellen Verhaltens anlegen und neues Verhalten einrichten:
sigaction(SIGINT, &neueaktion, &alteaktion);
Bei dem ersten Element der sigaction-Struktur, sa_handler, handelt
es sich um einen Zeiger auf eine Funktion, 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, daß 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.
Beispiel: Ein einfaches Beispiel zur Behandlung von Signalen.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static int BEENDEN = 0;
void sig_bearbeiter(int sig)
{
printf("Signal %d empfangen. Programm wird beendet.\n", sig);
BEENDEN = 1;
}
int main(void)
{
struct sigaction sig_struct;
sig_struct.sa_handler = sig_bearbeiter;
sigemptyset(&sig_struct.sa_mask);
sig_struct.sa_flags = 0;
if (sigaction(SIGINT,&sig_struct,NULL) != 0)
{
puts ("Fehler beim Aufruf von sigaction!") ;
exit (1);
}
puts("Programm gestartet, beenden mit [Strg]+[C].");
while (BEENDEN == 0)
{
puts("Programm läuft.");
sleep(1);
}
puts("Erstmal aufraeumen.");
sleeep(5);
puts("Fertig!");
return 0;
}
Wurde die Signal-Bearbeitungsroutine korrekt eingerichtet, gibt das Programm in
eine Meldung aus und tritt in die Schleife des Hauptprogramms ein. Solange die
Variable BEENDEN gleich 0 ist, gibt die
while-Schleife die Meldung "Programm läuft." aus und
legt sich jeweils für eine Sekunde schlafen.
Wenn die Signal-Bearbeitungsroutine sig_bearbeiter() aufgerufen wird,
gibt sie die Meldung "Signal 2 empfangen. Programm wird beendet." auf
den Bildschirm aus und setzt danach den Wert der statischen Variablen
BEENDEN auf 1. Nur das führt zum Beeenden und
nicht das Betätigen von [Ctrl]+[C]. Da Programm könnte
auch einfach weiterlaufen und die Benuterunterbrechung ignorieren.
Hier die Ausgabe eines Beispiel-Laufs:
Beenden mit [Strg]+[C].
Programm läuft.
Programm läuft.
Programm läuft.
Signal 2 empfangen. Programm wird beendet.
Erstmal aufraeumen.
Fertig!
|
|
|