Programmieren in C

| ### Programmieren in C von Prof. Jürgen Plate

# 9 Prozesse und Signale in C

## 9.1 Prozesse

Unser Praktikum läuft auf einem System mit dem Betriebssystem UNIX. Dieses Betriebssystem (und auch der frei erhältliche Abkömmling Linux) ist ein Multitasking-System und kann somit mehrere Aufgaben gleichzeitig erledigen. Jedes laufende Programm verhält sich dabei so, als ob es das einzige Programm wäre, das auf dem Computer ausgeführt wird. Wenn unter Linux ein Programm ausgeführt wird, bekommt das Programm eine eindeutige Prozess-Identifikation (PID) zugewiesen, die im Bereich zwischen 1 und 32767 liegt. Anhand dieser PID kann das Betriebssystem in Ausführung befindliche Programme identifizieren und auf diese zugreifen. Wird ein Programm beendet, wird auch seine PID freigegeben und kann später einem anderen Programm zugewiesen werden.

UNIX und Linux stellen, wie viele andere Betriebssysteme auch, eine spezielle Funktion zur Verfügung, mit deren Hilfe man die PID eines Prozesses abfragen kann. Eine zweite Funktion erlaubt es einem Kindprozess, die PID seines Elternprozesses zu ermitteln. Beide Funktion sind in der Header-Datei `unistd.h` definiert:

    pid_t getpid(void);
    pid_t getppid(void);

Die erste Funktion, `getpid()`, liefert die PID des Prozesses zurück, der `getpid()` aufgerufen hat. Die zweite Funktion, `getppid()`, liefert die Eltern-PID des Prozesses. Der Rückgabewert ist jeweils vom Typ `pid_t`, der in einer der in `stdlib.h` eingeschlossenen Header-Dateien als `int` definiert ist.

Beispiel: Die ID des aktuellen Prozesses und seines Elternprozesses ermitteln.

    #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

Die Systemaufrufe `fork()`, `exec()` und `wait()` haben mit der Generierung von Kindprozessen zu tun und erlauben die Synchronisation zwischen Eltern- und Kindprozessen. An dieser Stelle wird nur soweit darauf eingegangen, wie es zum Verständnis der folgenden Abschnitte nötig ist.

- `fork()` erzeugt einen Kindprozeß, der ein vollständiges Abbild des Elternprozesses ist und der beim gleichen Stand des Befehlszählers fortgesetzt wird. Eltern- und Kindprozeß wird jedoch die Möglichkeit geboten, festzustellen, ob es sich um Eltern- oder Kindprozeß handelt: Der Kindprozeß bekommt als Rückgabewert 0, der Elternprozeß die PID (Prozeß-ID) des Kindprozesses.
  - Etliche Systemprozesse schließen stdin, stdout und stderr, treten also in den Hintergrund. Solche Prozesse nennt man Daemonprozesse.
  - Terminiert der Elternprozeß vor dem Kindprozeß, wird dieser zum 'Waisenkind'. Normalerweise wird er dann vom Init-Prozeß 'adoptiert'.
  - Hat der Kindprozeß dann auch noch den Kontakt zum Terminal (Standardausgabe und -eingabe) verloren wird er zum 'Zombie'.
- `wait()` ermöglicht dem Elternprozeß das Warten auf die Beendigung des/der Kindprozess(e). Der Elternprozeß wird verdrängt und erst durch das Ende eines Kindprozesses wieder "aufgeweckt". Zur Unterscheidung mehrerer Kindprozesse liefert die Funktion `wait()` die PID des "gestorbenen" Kindprozesses zurück. Gibt es keinen Kindprozeß, ist das Ergebnis -1.
- Bei `exec()` wird der ursprüngliche Prozeß durch einen neuen Prozeß ersetzt (eine Rückkehr zum aufrufenden Prozeß ist daher logischerweise nicht möglich). `exec()` ist der komplizierteste Aufruf, da der komplette Prozeßadreßraum ersetzt werden muß.

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 erfolgreicher Ausführung von `fork()` gibt es den aufrufenden Prozeß zweimal. Beide Prozesse werden mit dem Befehl fortgesetzt, der auf `fork()` folgt. Zum Beispiel:

    switch (pid = fork())
      {
      case -1: printf("Schief gegangen!\n"); break;
      case 0 : printf("Kindprozess!\n"); break;
      default : printf("Prozeß %d wurde erzeugt!\n",i); break;
      }

Da der Elternprozess eine vollständige Kopie seiner Daten für den Sohn erzeugt, besteht im Anschluss keine Möglichkeit, daß Vater und Sohn über gemeinsame Variablen kommunizieren. Jeder hat von jeder Variablen ja sein eigenes Exemplar.

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.

#### Beenden Prozesses (exit)

Der Aufruf der Systemfunktion `void _exit(int status);` beendet den aufrufenden Prozeß. Üblicherweise wird in einem Anwenderprogramm nicht unmittelbar die Systemfunktion `_exit`, sondern die C-Bibliotheksroutine `exit()` benutzt, da letztere vor dem eigentlichen Beenden dafür sorgt, daß Dateien geschlossen werden.

Der Parameter `status` dient dazu, dem Vaterprozess beispielsweise Informationen über die ordnungsgemäße Abwicklung des Sohnes zukommen zu lassen. Der Vater kann den Status mit der Systemfunktion `wait()` abfragen. Wenn ein Anwenderprogramm keine der Exit-Funktionen explizit aufruft, erfolgt dies implizit nach dem Verlassen der `main()`-Routine.

### 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);

Wenn die Funktion `wait()` aufgerufen wird, hält sie die Ausführung des Elternprozesses so lange an, bis ein Kindprozess beendet wird. Beim Aufruf von "wait" gibt es drei mögliche Ergebnisse:

- `wait()` liefert -1: der Prozeß hat keine Kinder (mehr).
- Der Prozeß hat zwar Kinder, aber alle leben noch - dann schläft der Vater, bis der folgende Fall eintritt.
- Der Prozeß hat mindestens ein Zombie-Kind: eines davon wird ausgewählt, seine Verwaltungsdaten werden freigegeben und seine PID als Rückgabewert der Funktion abgeliefert. Zuvor werden in den Parameter `status` noch die folgenden Informationen eingetragen:
  - Bits 8 bis 15: Der "exit"-Status des Sohnes
  - Bits 0 bis 7: Die Nummer des Signals, das den Tod des Sohnes verursacht hat

Wenn Sie an dem Rückgabewert des Kindprozesses nicht interessiert sind, übergeben Sie `wait()` den Wert `NULL`.

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.

#### Ausführen eines Programmes mit `execve()`

Hier soll nur noch ein weiterer Vertreter der Familie vorgestellt werden:

    int execve (char *filename,char *argv[], char *envp[]);

. Der Parameter `filename` bezeichnet dann entweder ein ausführbares Programm oder ein Skript, das von einem Interpreter ausgeführt wird. `argv` ist ein Feld von Zeichenketten, das die Aufrufparameter enthält, mit denen das Programm versorgt werden soll. Dabei muß `argv[0]` der Name des Programmes selbst sein. `envp` ist ebenfalls ein Feld von Zeichenketten und enthält die Umgebungsvariablen (mit Inhalt) in der Form "NAME=inhalt", die dem Programm übergeben werden sollen. Beide Felder müssen mit einem NULL-Zeiger abgeschlossen sein.

Bei Erfolg kehrt die Funktion `execve()` wie `execl()` nicht zurück. Stattdessen wird das aufrufende Programm durch das aufgerufenen Programmes ersetzt (überschrieben) und dieses gestartet. Das gestartete Programm erhält die gleiche Prozeßnummer wie der aufrufende Prozeß und erbt in der Regel alle "offenen" Dateideskriptoren Im Fehlerfall liefert die Funktion den Wert -1 zurück. Beispiel:

    char *parameter[] = { "ls", "lisa", NULL };
    char *umgebung[]  = { "PATH=/bin:/usr/bin", "HOME=/root", NULL };

    execve("/bin/ls",parameter,umgebung);
    printf("Ooops! ls konnte nicht gestartet werden\n");

#### Dateien und `fork/exec`

Die Tabelle der Dateideskriptoren gehört ebenfalls zu den Daten des Prozesses. Hat der Elternprozess "offene" Dateideskriptoren, hat sie ebenfalls der Kindprozess und sie zeigen auf dieselben Einträge in der Datetabelle, da diese nicht zu den Prozeßdaten gehört und damit nicht kopiert wird. Beide Prozesse können somit gemeinsam auf offene Dateien zugreifen und sie benutzen dabei den selben Dateioffset. Da das Schreiben aber asynchron erfolgt, ist die Nutzung einer gemeinsamen Datei zur Prozeßkommunikation keine besonders gute Idee. Besser werden dazu Pipes verwendet (Näheres siehe weiter unten).

### 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.

## 9.2 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);
      void (*sa_sigaction)(int, siginfo_t *, void *);
      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. Die Signalhandler-Funktion hat dann nur einen Integer-Parameter, der beim Auslösen mit der Signalnummer belegt ist (sihe Beispiel unten):

Das zweite Element der `sigaction`-Struktur, `sa_sigaction` verweist ebenfalls auf einen Signalhandler, nur dass dieser drei Parameter besitzt, die weitere Informationen wie z. B. die Prozessnummer oder die User-ID enthalten. Ob `sa_handler` oder `sa_sigaction` verwendet wird, steuert eines der Flags in `sa_flags`. Wird `SA_SIGINFO` in `sa_flags` angegeben, so wird der Pointer `sa_sigaction` für die Signalhandler-Funktion verwendet.

Für das `sa_flags`-Element gibt es eine ganze Reihe möglicher Einstellungen, die uns bis auf das oben erwähnte `SA_SIGINFO` aber nicht weiter interessieren sollen; wir werden das Element in den Beispielen jeweils auf `0` bzw. `SA_SIGINFO` setzen. Über das `sa_mask`-Element kann man angeben, welche anderen Signale während der Ausführung des Signal-Handlers 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);

Man kann die Maske aber auch von Hand setzen. 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;

    /* Signal-Handler */
    void  sig_bearbeiter(int sig)
      {
      printf("Signal %d empfangen. Programm wird beendet.\n", sig);
      BEENDEN = 1;
      }

    int main(void)
      {
      struct sigaction sig_struct;

      /* Signal-Handler aktivieren */
      memset (&sig_struct, '\0', sizeof(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].");

      /* Warteschleife */
      while (BEENDEN == 0)
        {
        puts("Programm läuft.");
        sleep(1);
        }

      puts("Erstmal aufraeumen.");
      sleep(1);
      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!

`sigaction()` kann mit NULL als zweiten Parameter aufgerufen werden, um den aktuellen Signalhandler zu ermitteln. Die Funktion kann auch verwendet werden, um zu überprüfen, ob ein bestimmtes Signal für die aktuelle Maschine gültig ist, indem er sie mit NULL für den zweiten und dritten Parameter aufgerufen wird. Es ist nicht möglich, SIGKILL oder SIGSTOP über `sa_mask` zu blockieren.

Mehr Info bekommt man, wenn die Variante `sa_sigaction` verwendet wird. Hier hat der Signal-Handler drei Parameter: die Signal-Nummer als ersten Parameter, einen Zeiger auf eine Struktur vom Type `siginfo_t` als zweiten Parameter und einem Zeiger auf einen Typ `(void *) ucontext_t` als letzten Parameter, der hier nicht weiter betrachtet wird. Der Parameter `siginfo_t` ist definiert als:

    siginfo_t {
        int      si_signo;     /* Signal number */
        int      si_errno;     /* An errno value */
        int      si_code;      /* Signal code */
        int      si_trapno;    /* Trap number that caused
                                  hardware-generated signal
                                  (unused on most architectures) */
        pid_t    si_pid;       /* Sending process ID */
        uid_t    si_uid;       /* Real user ID of sending process */
        int      si_status;    /* Exit value or signal */
        clock_t  si_utime;     /* User time consumed */
        clock_t  si_stime;     /* System time consumed */
        sigval_t si_value;     /* Signal value */
        int      si_int;       /* POSIX.1b signal */
        void    *si_ptr;       /* POSIX.1b signal */
        int      si_overrun;   /* Timer overrun count;
                                  POSIX.1b timers */
        int      si_timerid;   /* Timer ID; POSIX.1b timers */
        void    *si_addr;      /* Memory location which caused fault */
        long     si_band;      /* Band event (was int in
                                  glibc 2.3.2 and earlier) */
        int      si_fd;        /* File descriptor */
        short    si_addr_lsb;  /* Least significant bit of address
                                  (since Linux 2.6.32) */
        void    *si_call_addr; /* Address of system call instruction
                                  (since Linux 3.5) */
        int      si_syscall;   /* Number of attempted system call
                                  (since Linux 3.5) */
        unsigned int si_arch;  /* Architecture of attempted system call
                                  (since Linux 3.5) */
      }

`si_signo, si_errno` und `si_code` sind für alle Signale definiert, wobei `si_signo` unter Linux nicht verwendet wird. Der Rest der Struktur kann auch eine Union sein, so dass man nur die Felder lesen kann, die für das gegebene Signal sinnvoll sind. POSIX.1b-Signale und SIGCHLD liefern `si_pid` und `si_uid`. SIGCHLD liefert auch `si_status, si_utime` und `si_stime`. SIGILL, SIGFPE, SIGSEGV und SIGBUS füllen `si_addr` mit der Adresse des Fehlers.

possible for any signal are listed in this table:

Signal | Signal-Quelle
--- | ---
SI_USER | kill(), sigsend(), or raise()
SI_KERNEL | The kernel
SI_QUEUE | sigqueue()
SI_TIMER | POSIX timer expired
SI_MESGQ | POSIX message queue state changed (since Linux 2.6.6)
SI_ASYNCIO | AIO completed
SI_SIGIO | queued SIGIO
SI_TKILL | tkill() or tgkill() (since Linux 2.4.19)
SIGILL | illegal opcode
SIGFPE | floating point unit error
SIGSEGV | segmentation fault
SIGBUS | bus error
SIGTRAP | breakpoint, trace
SIGCHLD | signal from child process
SIGPOLL | poll function

Das folgende Programm installiert eine Signalhandler für SIGTERM, der einige zusätzliche Infos ausgibt. Das Programm wird beendet, indem von einem zweiten Terminals aus erst die Prozessnummer ermittelt (mit `ps`, `top` etc.) und mit dieser dann das Kommando `kill ≪Prozessnummer>` abschickt.

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    #include <string.h>

    /* Signal-Handler mit mehr Info */
    void sig_hdl (int sig, siginfo_t *siginfo, void *context)
      {
      printf("Signal %d empfangen. Programm wird beendet.\n", sig);
      printf("PID: %ld, UID: %ld\n",
                    (long)siginfo->si_pid, (long)siginfo->si_uid);
      exit(1);
      }

    int main (void)
      {
      struct sigaction sig_struct;

      /* Signalhandler einsetzen, diesmal mit sa_sigaction */
      memset(&sig_struct, '\0', sizeof(sig_struct));
      sigemptyset(&sig_struct.sa_mask);
      sig_struct.sa_sigaction = &sig_hdl;
      sig_struct.sa_flags = SA_SIGINFO; /* damit sa_sigaction verwendet wird */
      if (sigaction(SIGTERM, &sig_struct, NULL) < 0)
        {
        perror ("sigaction");
        return 1;
            }

      puts("Programm gestartet.");

      /* Warteschleife */
      while (1)
        {
        puts("Programm l&auml;uft.");
        sleep(1);
        }

      puts("Fertig!");
      return 0;
      }

Die Programmausgabe ist ähnlich wie beim vorhergehenden Programm:

    Programm gestartet.
    Programm läuft.
    Programm läuft.
    Programm läuft.
    Programm läuft.
    Programm läuft.
    Signal 15 empfangen. Programm wird beendet.
    PID: 29911, UID: 1000
    Fertig!

Im Folgenden werden weitere Betriebssystemfunktionen beschrieben, die im Zusammenhang mit der Steuerung von Prozessen von Bedeutung sind.

#### Setzen eines Timers (alarm)

Mit der Systemfunktion `unsigned int alarm(unsigned int seconds);` kann ein "Wecker" aufgezogen werden, der nach "seconds" Sekunden das Signal "SIGALRM" an den aufrufenden Prozeß sendet. Wird keine benutzerspezifische Signalreaktion vereinbart, so bricht der Prozeß nach Empfang des Signals ab. Ein eventuell bereits vorher "aktiver" Wecker wird zurückgesetzt. Wenn der Rückgabewert der Funktion `alarm()` 0 ist, dann war zuvor kein "Wecker" aktiv. Wenn der Wert ungleich 0 ist, dann gibt er an, nach wieviel Sekunden ein zuvor eingestellter Wecker abgelaufen wäre.

#### Warten auf ein Signal (pause)

Die Systemfunktion `int pause(void);` bewirkt, daß der aufrufende Prozeß in den Schlafzustand versetzt wird und dort solange verharrt, bis irgendein Signal eintrifft. Damit ist allerdings noch nicht festgelegt, welche Reaktion im Anschluss erfolgen soll. Ohne entsprechende Maßnahmen kehrt "pause" bei den meisten Signalen nicht zurück, sondern bricht das Programm ab. Davon abweichendes Verhalten kann mit der Systemfunktion "signal" erreicht werden.

#### Warten auf das Ende eines Zeitintervalls (nanosleep)

Mit der Systemfunktion `int nanosleep(const struct timespec *req, struct timespec *rem);` kann sich ein Prozeß für eine vorgegebene Zeitspanne in den Schlafzustand versetzen. Die Parameter "req" und "rem" verweisen auf Datenstrukturen mit den Komponenten "tv_sec" (Sekunden) und "tv_nsec" (Nanosekunden). In "*req" wird das gewünschte Schlafintervall angegeben. Falls der Parameter "rem" ungleich NULL ist, dann wird in "*rem" vom System die noch nicht verstrichene Restzeit abgelegt, wenn der Schlafzustand durch ein vorzeitig eintreffendes Signal beendet wird. In diesem Fall liefert "nanosleep" den Wert -1 zurück. Auf Intel-PC's ist die Genauigkeit des Intervalls auf ein Vielfaches von 10 Millisekunden beschränkt. Das folgende Beispiel definiert eine Funktion `delay`, der man die Wartezeit in Millisekunden übergibt. Wenn im Programm noch auf die 10 ms-Beschränkung geachtet wird, sollte die Wartezeit relativ genau reproduzierbar sein.

    int delay(unsigned long millis)
      {
      struct timespec ts;
      int err;

      ts.tv_sec = millis / 1000;
      ts.tv_nsec = (millis % 1000) * 1000000L;
      err = nanosleep(&ts, (struct timespec *)NULL);
      return (err);
      }

Etwas einfacher zu benutzen ist die C-Bibliotheksfunktion `unsigned int sleep(unsigned int seconds);` wenn eine Sekundengenauigkeit ausreicht. Diese liefert im Falle eines vorzeitigen Abbruches die Restzeit als Funktionsergebnis zurück.

#### Vereinbarung einer Signalreaktion (signal)

Die Vereinbarung einer Reaktion auf ein bestimmtes Signal kann nicht nur mit der oben gezeigten Funktion `sigaction()` erfolgen, sondern auch - bei einfachen Anwendungen - mit dem Systemaufruf `void (*signal(int signum, void (*handler)(int)))(int);` Da dieser Funktionsprototyp etwas verwirrend ist, hier die Auflösung: Erster Parmeter ist eine Signalnummer, der zweite Parameter die Adresse der Signal-Handler-Funktion. Dazu ein Beispiel:

    #include <signal.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <ctype.h>

    void tick (int dummy) // nur Wecker neu aufziehen
      { alarm(1); }

    void beenden(int signal_nummer)
      { // Signal-Bearbeitungsroutine
      char c;
      if (signal_nummer == SIGINT)
        {
        printf("Prozeß wirklich beenden ?");
        c = getchar();
        if (c == 'j' || c == 'J') exit(1);
        else                      return;
        }
      else
        {
        printf("unerwartetes Signal %d\n");
        exit(1);
        }
      }

    int main(void)
      {
      signal(SIGINT,beenden);
      signal(SIGALRM,tick);
      alarm(1);  // Wecker aufziehen
      for (;;)
        {
        pause();  // auf Signal warten
        putchar('.');
        fflush(stdout); // sonst sieht man nichts
        };
      }

Das Hauptprogramm plant für das Signal "SIGINT" (z.B. Drücken von Ctrl-C) die Bearbeitungsroutine "beenden" und für das Signal "SIGALRM" (Timer-Signal) die Routine "tick" ein. Im Anschluss daran wird der Timer mit "alarm(1)" auf 1 Sekunde gesetzt.
Nun folgt eine Endlosschleife, in der mit "pause()" auf ein beliebiges Signal gewartet wird. Falls der Benutzer innerhalb der nächsten Sekunde nichts tut, wird beim Eintreffen von "SIGALRM" die Funktion "tick" aufgerufen, in der nur der Timer neu gesetzt wird. Die Folge ist eine regelmäßige Ausgabe der Zeichenfolge "tick" auf dem Terminal.
Wird allerdings Ctrl-C betätigt (was das Signal "SIGINT" auslöst), dann wird "beenden" aufgerufen, wo der Benutzer gefragt wird, ob er das Programm tatsächlich abbrechen möchte. Wenn er dann nicht mit "j" oder "y" antwortet, wird das Programm einfach fortgesetzt.
Die Signal-Bearbeitungsroutinen müssen void-Funktionen mit einem int-Parameter sein. Dieser repräsentiert die Nummer des Signals, das den Aufruf verursacht hat. Dadurch ist es möglich, eine Routine für verschiedene Signale einzuplanen und in der Routine die auslösende Ursache zu ermitteln.
Innerhalb einer Signal-Bearbeitungsroutine wird ein erneutes Eintreffen des gleichen Signals ignoriert. In unserem Beispiel heißt dies, daß das wiederholte Drücken von Ctrl-C (während des Dialoges) keine Wirkung hat.
Statt des Namens einer Bearbeitungsfunktion kann an der Position des zweiten Parameters von "signal" auch eine von zwei vordefinierten Konstanten angegeben werden:

- `SIG_IGN` bedeutet, daß das Signal ignoriert werden soll.
- `SIG_DFL` bedeutet, daß die Standardbearbeitung für das Signal eingestellt werden soll.

Schreibt man z. B. `signal(SIGINT,SIG_IGN);`, wird das Drücken von Ctrl-C grundsätzlich ignoriert.

Möchte man wissen, welche Signalbearbeitung vor dem Aufruf von "signal" eingestellt ist, dann muss man den Rückgabewert von "signal" auswerten. Dieser repräsentiert die "alte" Signalreaktion. Damit kann beispielsweise eine Signalbearbeitung vorübergehend modifiziert werden, um sie im Anschluss wieder auf den vorherigen Mechanismus zurückzusetzen. Mit `void (*old)(int) = signal(SIGINT,beenden);` holt man die "alte" Signalreaktion und mit `signal(SIGINT,old);` wird sie wieder eingesetzt.

#### Senden eines Signales (kill)

Die Systemfunktion `int kill(int pid, int signal);` wird verwendet, um einem Prozeß ein Signal zuzusenden. Wenn der Parameter "pid" größer als 0 ist, wird das Signal dem Prozeß mit der entsprechenden Nummer zugestellt. Wenn "pid" gleich 0 ist, wird es allen Prozessen übermittelt, die zur gleichen Gruppe wie der Aufrufer gehören. Im Falle "pid" gleich -1 werden alle existierenden Prozesse (außer "init") adressiert, und bei "pid" kleiner -1 wird es an eine andere Gruppe geschickt, deren Nummer gleich dem Absolutwert von "pid" ist. Wenn der aufrufende Prozeß keine Superuser-Rechte besitzt, kann ein Signal nur an einen Prozeß desselben Benutzers geschickt werden.
Zum gezielten Abbrechen eines Prozesses sollte möglichst das Signal "SIGINT" benutzt werden. Der Empfänger kann daßelbe "abfangen" und hat damit die Chance, vor dem eigentlichen Beenden Aufräumarbeiten durchzuführen (z.B. Daten abspeichern). Allerdings kann er sich auch dafür entscheiden, das Signal zu ignorieren, wodurch "SIGINT" keine sichere Maßnahme zum "killen" eines Prozesses darstellt.

Die folgende Tabelle fasst die Funktionen für Signale zusammen:

`exit` | Beendet den Prozess. Prototyp: `void exit (int status);` Parameter:`status`: Status der zurückgegeben wird (0 = OK)
`fork` | Starten einen neuen Prozess. Prototyp: `int fork(void);` Rückgabewert: 0 an Kindprozess, Prozess-ID (PID) des Kindes an Elternprozess, -1 bei Fehler.
`getpid` | Liefert die Prozess-ID (PID) des aufrufenden Prozesses bzw. -1 bei einem Fehler. Prototyp: `int getpid(void);`
`getppid` | Liefert die Prozessidentifikationsnummer des Vaterprozesses (PPID) bzw. -1 bei einem Fehler. Prototyp: `int getppid(void);`
`kill` | Das Signal `sig` wird durch diese Funktion an den Prozess mit der Prozessidentifikationsnummer `pid` geschickt. Includes: `#include <signal.h> Prototyp: `int kill(int pid, int sig);` Parameter:`pid` des Empfänger-Prozesses.`
`pause` | Der Prozess wird angehalten und wartet auf ein Signal. Prototyp: `int pause(void);`
`signal` | Diese Funktion bindet das Signal sig an einen Signal Handler. Includes: `#include <signal.h>` Prototyp: `void signal(int sig, int *sighand);` Parameter: `sig`: Signal, das an den Signal Handler gebunden werden soll. `*sighand`: Signal Handler der ausgeführt werden soll.
`sleep` | Der aufrufende Prozess blockiert für eine bestimmte Zeit. Prototyp: `unsigned sleep(int sec);` Parameter: `int sec`: Dauer in Sekunden.
`wait` | Es wird auf die Beendigung eines Sohnprozesses gewartet. Haben einer oder mehrere Sohnprozesse bereits terminiert, so kehrt der Aufruf sogleich zurück. Ist dies nicht der Fall wird auf die Beendigung des nächsten Sohnprozesses gewartet. Prototyp: `int wait(int *statusp);` Parameter: `*statusp`: Zeiger auf Variable, in der der Terminierungsstatus des Sohnes zurückgegeben wird. Benötigt man den Rückgabestatus nicht, kann 0 als Parameter benutzt werden. Rückgabewert: PID des terminierten Sohnes bzw. -1 wenn kein Sohnprozess existiert oder bereits terminierte Söhne durch frühere `wait(void)`-Aufrufe entgegengenommen wurden.
`waitpid` | Es wird auf die Beendigung eines bestimmten Sohnprozesses gewartet. Prototyp: `int waitpid(int pid, int *statusp, int optionen);` Parameter: `*statusp` wie bei `wait`. Ist der Wert von `pid` > 0, handelt es sich um die PID des Prozesses, auf den gewartet werden soll. Bei `pid == -1` wird auf die Beendigung eines beliebigen Sohnprozesses gewartet. Der Parameter `optionen` bestimmt wie und worauf gewartet werden soll. Er ist aber abhänig davon ob das System z.B. eine Job-Kontrolle unterstützt.

Zum Schluss ein Reaktionstest in C.

    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <time.h>
    #include <math.h>
    #include <unistd.h>

    clock_t start, ende, differenz, record=1000000;

    /* Signal-Handler-Routinen */
    void  strgbackslash_faenger(int sig)
      {
      signal(SIGQUIT, SIG_IGN);
      printf("......Die schnellste STRG-C Tastenfolge dauerte %7.3f\n",
              record/(double)CLOCKS_PER_SEC);
      exit(0);
      }

    void  strgc_faenger(int sig)
      {
      /* Fuer die Dauer dieser Funktionsausführung muessen weitere  */
      /* SIGINT-Signale ignoriert werden.                           */
      signal(SIGINT, SIG_IGN);

      /* Gebrauchte Zeit berechnen und ausgeben */
      ende = clock();
      differenz = ende - start;
      printf("Gebrauchte Zeit:  %10.3f Sek\n", differenz/(double)CLOCKS_PER_SEC);
      if (differenz < record)
        {
        record = differenz;
        printf("...........Neuer Rekord %10.3f Sek\n", record/(double)CLOCKS_PER_SEC);
        }
      sleep(rand()%2+1);
      printf("\nDruecke so schnell wie moeglich STRG-C.......\n");
      start = clock();

      /* Signal-Handler wieder fuer SIGINT installieren */
      signal(SIGINT, SIG_IGN);
      if (signal(SIGINT, strgc_faenger) == SIG_ERR)
        {
        printf("Fehler: SIGINT-Handler nicht installiert!\n");
        exit(1);
        }
      }

    int main(void)
      {
      /* Startwert für Pseude-Zufallszahlen erzeugen und   */
      /* Signal Handler installieren                       */
      srand( time(NULL) );
      if (signal(SIGQUIT, strgbackslash_faenger) == SIG_ERR)
        {
        printf("Fehler: SIGQUIT-Handler nicht installiert!\n");
        exit(1);
        }
      signal(SIGINT, SIG_IGN);
      sleep(rand()%2+1);
      printf("Bitte merk Dir: Beenden mit STRG-\\\n");
      printf("\nDruecke so schnell wie moeglich STRG-C.......\n");
      if (signal(SIGINT, strgc_faenger) == SIG_ERR)
        {
        printf("Fehler: SIGINT-Handler nicht installiert!\n");
        exit(1);
        }
      start = clock();
      while (1);
      return(0);
      }

Denken Sie auch daran, dass Sie den Signal-Handler schnell und effizient halten sollten, wie bei Unterbrechungen im Kernel. Auch sind etliche C-Funktionen nicht reentrant und gehören daher nicht in einen Signal-Handler (so sind die Beispiele mit `printf()` eigentlich ungünstig. Wenn der Signal-Handler etwas ausgeben soll, nehmen Sie z. B. `write()`. Auch muss nicht alles, was eine längere Zeit in Anspruch nimmt, im Signal-Handler abgearbeitet werden. Oft reicht das Setzen eines Flags, das dann im Hauptprogramm bearbeitet wird.

Es gibt noch einen weiteren Bereich der zeitabhängigen Programmierung, die sogenannten Timer, die im Prinzip eine starke Erweiterung der Alarm-Funktion darstellen. Mehr darüber erfahren Sie in der Abteilung "Raspberry Pi" (auch wenn sie generell auf vielen Sytemen verfügbar sind):

Sleep- und Timerfunktionen in C

## 9.3 Prozess-Synchronisation

### Synchronisation durch Semaphore

Ein Semaphor ist ein Zähler der mehreren Prozessen gleichzeitig den Zugriff auf ein gemeinsames Datenobjekt ermöglicht. Es wird geregelt, wieviele Prozesse gleichzeitig eine gemeinsame Ressource benutzen dürfen. Ein Beispiel aus dem täglichen Leben ist z.B. die Benutzung eines Autobusses, in dem maximal 50 Personen Platz haben. Ein Semaphor wäre hier ein Zähler, der mit 50 initialisiert wird. Jedesmal, wenn eine Person in den Bus einsteigt, wird der Zähler um 1 vermindert (dekrementiert). Verlässt eine Person den Bus, wird der Zähler um 1 erhöht (inkrementiert). Ist der Zähler 0, so darf keine weitere Person den Bus betreten. Um eine gemeinsam genutzte Ressource zu verwenden, muß ein Prozess folgende Schritte ausführen:

1. Abfragen des Semaphors, der die Ressource kontrolliert.
2. - Hat der Semaphor einen Wert größer 0, dekrementieren des Semaphors und Verwenden der Ressource.
   - Hat der Semaphor den Wert 0, Prozess anhalten, bis ein anderer Prozess, der zur Zeit die Ressource benutzt, diese freigibt.
3. Beim Freigeben der Ressource incrementieren des Semaphors. Eventuell wartende Prozesse werden jetzt fortgesetzt.

Oft werden auch binäre Semaphore eingesetzt. Sie bewirken den wechselseitigen Ausschluß von einer Ressource. Falls ein kritischer Bereich belegt ist, warten die Prozesse im Zustand blockiert, bis der kritische Bereich frei ist. Das System wird durch die wartenden Prozesse nicht belastet.

Das folgende Programmbeispiel erzeugt einen Semaphor, der auf 3 initialisiert wird, und 10 Sohnprozesse, die über den Semaphor synchronisiert werden. Die ersten drei Prozesse können sofort arbeiten. Alle anderen Prozesse müssen warten, bis ein anderer Prozess den Semaphor wieder inkrementiert.

Da bei verschiedenen UNIX-Varianten die Semaphor-Operationen unterschiedlich sind, läuft das Beispiel nur unter Linux.

    #include<stdio.h>
    #include<stdlib.h>
    #include<signal.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/sem.h>

    #if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
       /* union semun is defined by including <sys/sem.h> */
    #else
       /* according to X/OPEN we have to define it ourselves */
       union semun
         {
         int val;                    /* value for SETVAL */
         struct semid_ds *buf;       /* buffer for IPC_STAT, IPC_SET */
         unsigned short int *array;  /* array for GETALL, SETALL */
         struct seminfo *__buf;      /* buffer for IPC_INFO */
         };
    #endif

    struct sembuf sem_p[1];      /* Strucktur fuer */
                                 /* P-Operation auf Semaphor */

    struct sembuf sem_v[1];      /* Strucktur fuer */
                                 /* V-Operation auf Semaphor */

    int main(void)
      {
      int prozess_pid[10];       /* Feld um die PID´s der  */
                                 /* Sohn-Prozesse zu speichern */
      int anzahl;                /* Anzahl der Sohn-Prozesse */
      int semid;                 /* ID der Semaphorengruppe */
      ushort initarray[1];       /* Initialisierungsfeld */
      ushort outarray[1];        /* Ausgabefeld */
      union semun para;
      union semun para2;          /* Variablen zum Arbeiten unter Linux */
      para.array = initarray;
      para2.array = outarray;

      initarray[0] = 3;
      semid = semget(IPC_PRIVATE,1,IPC_CREAT|0777);
              /* Erzeugung einer Semaphorgruppe mit einem Semaphor */

      semctl(semid,0,SETALL,para);

      sem_p[0].sem_num = 0;         /* Vorbereitung der P-Operation */
      sem_p[0].sem_op  = -1;
      sem_p[0].sem_flg = 0;

      sem_v[0].sem_num = 0;         /* Vorbereitung der V-Operation */
      sem_v[0].sem_op  = 1;
      sem_v[0].sem_flg = 0;

      printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n");
      while (getchar() != '\n');

      for (anzahl=0; anzahl<10; anzahl++)     /* 10 Sohn-Prozesse */
        {                                     /* werden erzeugt */
        if((prozess_pid[anzahl] = fork()) == 0)
          {
          printf("Kunde %i betritt den Laden  \n",anzahl);
          semop(semid,sem_p,1);             /* Semaphor dekrementieren */
          printf("Kunde %i wird bedient \n",anzahl);
          sleep(5);
          printf("Kunde %i verlaesst den Laden\n",anzahl);
          semop(semid,sem_v,1);             /* Semaphor inkrementieren */
          exit(0);
          }
        }
      while(getchar() != '\n');
      for (anzahl=0; anzahl<10; anzahl++)    /* terminiere die restlichen  */
        kill(prozess_pid[anzahl],9);         /* Sohn-Prozesse */

      semctl(semid,0,IPC_RMID,para);
      return 0;
      }

Die Ausgabe des Programms könnte folgendermaßen aussehen:

    Zum Starten und Beenden bitte Eingabetaste druecken

    Kunde 0 betritt den Laden
    Kunde 0 wird bedient
    Kunde 1 betritt den Laden
    Kunde 1 wird bedient
    Kunde 2 betritt den Laden
    Kunde 2 wird bedient
    Kunde 3 betritt den Laden
    Kunde 4 betritt den Laden
    Kunde 5 betritt den Laden
    Kunde 6 betritt den Laden
    Kunde 7 betritt den Laden
    Kunde 8 betritt den Laden
    Kunde 9 betritt den Laden
    Kunde 0 verlaesst den Laden
    Kunde 2 verlaesst den Laden
    Kunde 1 verlaesst den Laden
    Kunde 5 wird bedient
    Kunde 4 wird bedient
    Kunde 3 wird bedient
    Kunde 3 verlaesst den Laden
    Kunde 4 verlaesst den Laden
    Kunde 5 verlaesst den Laden
    Kunde 7 wird bedient
    Kunde 6 wird bedient
    Kunde 8 wird bedient
    Kunde 8 verlaesst den Laden
    Kunde 6 verlaesst den Laden
    Kunde 7 verlaesst den Laden
    Kunde 9 wird bedient
    Kunde 9 verlaesst den Laden

### Synchronisation durch Signale

Signale wurden schon weiter oben vorgestellt. Hier greifen wir sie nochmals auf - unter dem Aspekt der Prozeß-Synchronisation. Bei der Synchronisation/Steuerung von Prozessen durch Signale kann man die Reihenfolge festlegen, in der bestimmte Prozesse bearbeitet werden. Die Funktionen `signal()`, `pause()` und `kill()` werden hierfür verwendet. Durch die Funktion `signal()` wird ein Signal-Handler, der beim Eintreffen des Signals ausgeführt wird, an das Signal gebunden. Bei größeren Programmen, die mehrere Prozesse haben, wird es allerdings schwierig den Überblick zu behalten. Bei dieser Methode wird die Reihenfolge festgelegt in der Prozesse bzw. Teile von Prozessen ausgeführt werden. Die "parallele" Bearbeitung von Prozessen wird dadurch eingeschränkt. Ein weiteres Problem bei der Arbeit mit Signalen ist, daß Signale nicht vom System gespeichert werden. Erhält ein Prozess ein Signal bevor dieser selbst die Funktion `pause()` aufgerufen hat geht dieses Signal verloren und der Prozess wartet, wenn er später die Funktion `pause()` aufruft, vergeblich auf ein Signal.

Beispiel für die Synchronisation durch Signale:

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

    void sighand(void) /* Signal Handler wird beim Eintreffen */
      {                /* des Signales SIGUSR1 ausgefuehrt */
      signal(SIGUSR1,&sighand);
      /* Hier wird die Bindung des Signals */
      /* an den Signal Handler sighand() erneuert.  */

      puts("Signalhandler aktiv!\n");
      }

    int main(void)
      {
      int vater_pid, prozess1_pid, prozess2_pid;  /* PIDs der Soehne */
      signal (SIGUSR1,&sighand);                  /* Bindung des Signals SIGUSR1 */
                                                 /* an den Signal Handler sighand() */
      if ((prozess1_pid = fork()) == 0)           /* Sohnprozess 1 wird erzeugt */
        {                                         /* und gestartet */
        vater_pid = getppid();                    /* Sohnprozess erfragt die */
                                                  /* PID des Vaters */
        printf("Sohn 1 laeuft\n");
        sleep(3);
        kill(vater_pid,SIGUSR1);                  /* Dem Vaterprozess wird das */
                                                  /* Signal SIGUSR1 gesendet */
        printf("Sohn 1 terminiert\n");
        exit(0);
        }

      if ((prozess2_pid = fork()) == 0)           /* Sohnprozess 2 wird */
        {                                         /* erzeugt und gestartet */
        printf("Sohn 2 gestartet - wartet\n");
        pause();                                  /* Sohnprozess 2 wartet */
                                                  /* auf ein Signal */

        printf("Sohn 2 terminiert\n");
        exit(0);
        }
      printf("Vater wartet auf Signal von Sohn 1\n");
      pause();
      printf("Vater: Signal von Sohn 1, kille Sohn 2\n");
      kill(prozess2_pid,SIGUSR1);
      putchar('\n');
      return(0);
      }

Die Ausgabe des Programms:

    Vater wartet auf Signal von Sohn 1
    Sohn 1 laeuft
    Sohn 2 gestartet - wartet
    Sohn 1 terminiert
    Signalhandler aktiv!
    Vater: Signal von Sohn 1, kille Sohn 2
    Signalhandler aktiv!
    Sohn 2 terminiert

### Wechselseitiger Ausschluß mit Spinlocks

Der Begriff "Spinlock" (engl. Spin = drehen, kreisen) entstand, weil bei dieser Art des wechselseitigen Ausschlußes eine `while`-Schleife der Hauptbestandteil ist. Diese wird so oft durchlaufen, bis der Prozess Zugriff auf die benötigte Ressource erhält. Man unterscheidet zwei Arten.

1. Spinlocks durch Maschinenbefehle
   Hier wird durch Aufruf einer Funktion innerhalb einer `while`-Schleife (`while(TEST_AND_SET(&lock));`) getestet, ob ein kritischer Bereich frei ist oder nicht. Dies wird anhand einer Sperrvariablen festgestellt. Wenn der kritische Bereich besetzt ist, liefert die Funktion z.B. den Wert `1` zurück, so daß die `while`-Schleife erneut durchlaufen wird.
   Ist der kritische Bereich frei, so erhält man den Wert `0` und die Sperrvariable wird auf `1` gesetzt. Hierbei ist wichtig, daß die Überprüfung und das Setzen der Variable ohne Unterbrechung also "atomar" erfolgt.
   Beim Verlassen des kritischen Bereiches setzt der Prozess die Sperrvariable einfach auf `0` (`lock = 0`).
2. Spinlocks durch Lock Files
   Bei dieser Art von Spinlock wird anstelle einer Sperrvariable eine sogenannte Lock-File verwendet. Diese Lock-File ist eine Datei mit einem bestimmten Namen. Dieser Name muß allen Prozessen bekannt sein die sich einen kritischen Bereich teilen. Ein Prozess versucht vor dem Eintritt in einen kritischen Bereich diese Datei anzulegen. Existiert diese Datei, ist der kritische Bereich bereits von einem anderen Prozess belegt. Verläßt dieser den kritischen Bereich, löscht er die Datei, die er beim Eintritt in diesen Bereich angelegt hat. Durch die `while`-Schleife versuchen die Prozesse solange diese Datei anzulegen, bis es ihnen gelingt. Das Erzeugen der Lock-Files muß "atomar" erfolgen.

Die Verwendung von Spinlocks hat diverse Nachteile. Es wird keine Reihenfolge festgelegt, so dass einige Prozesse ggf. sehr lange warten müssen. Nach Freigabe des kritischen Bereiches besitzt jeder Prozess, auch jener, der den kritischen Bereich gerade verlassen hat, die selbe Wahrscheinlichkeit den kritischen Bereich als nächstes zu erhalten. Der größte Nachteil ist jedoch, daß die Prozesse nicht in den Zustand blockiert übergehen sondern immer wieder die `while`-Schleife durchlaufen, was zu einer höheren Belastung des Systems führt.

## 9.4 Prozesskommunikation

### Pipes

Eine Pipe ist ein Datenkanal, der wie eine Datei behandelt wird. Ein Prozess schreibt die Daten in diese Pipe und ein anderer Prozess kann diese Daten in der Reihenfolge auslesen, in der sie vom anderen Prozess geschrieben wurden. Eine Pipe in Unix/Linux ist unidirektional, so daß die Daten nur in eine Richtung übermittelt werden. Eine Pipe ist aus Sicht des Prozesses eine Datei, auf die er sequentiell schreibt oder von der er sequentiell liest. Ein Prozess, der aus einer leeren Pipe lesen will, muß warten, bis von einem anderen Prozess in die Pipe geschrieben wurde. Ein Prozess, der in eine Pipe schreiben will, muß warten, wenn der Pipe-Buffer voll ist.

#### Unbenannte Pipe

Die (unbenannte) Pipe ist eingeschränkt.Ihre Lebensdauer ist abhängig von der Lebensdauer der Prozesse die mit ihr arbeiten. Sind all diese Prozesse beendet, wird die Pipe gelöscht. Die Kommunikation über eine unbenannte Pipe ist nur für Prozesse möglich, die im gleichen Prozeßbaum liegen. Mit dem `pipe()`-Aufruf besitzt ein Prozess zunächst eine Pipe zu sich selbst, aus der er mit Filehandle 0 Daten lesen kann. Mit dem Filehandle 1 kann er Daten in diese Pipe schreiben. Sinnvoll wird das erst, wenn der Vaterprozess durch einen `fork()`-Aufruf einen Sohnprozess erzeugt, der mit dem Vaterprozess Daten austauscht. Dieser Sohnprozess erbt die Pipe seines Vaters. Die Richtung des Datenstromes wird dadurch beeinflußt welcher Prozess die Lese-bzw. Schreibseite der Pipe schließt.

Sollen zwei Söhne durch eine unbenannte Pipe miteinander kommunizieren, müssen folgende Schritte ausgeführt werden.

1. Vaterprozess richtet durch den Aufruf `pipe()` eine Pipe ein.
2. Der Vaterprozess erzeugt mit `fork()` einen "Schreib-Sohn".
3. Der Vaterprozess schließt die Schreibseite der Pipe.
4. Der "Schreib-Sohn" schließt die Leseseite der Pipe.
5. Der Vaterprozess erzeugt nun mittels `fork()` einen "Lese-Sohn".
6. Der Vaterprozess schließt nun auch die Leseseite der Pipe.
7. Dieser "Lese-Sohn" schließt die Schreibseite der Pipe.

Die so erstellte Pipe bildet nun eine Verbindung zwischen dem ersten Sohn (Schreibprozess) und dem zweiten Sohn (Leseprozess). Der Vaterprozess hat nach dem Erstellen keinen Einfluß auf die Pipe, da er die Lese-und Schreibseite geschlossen hat.

Das folgende Beispiel demonstriert, wie eine Shell prinzipiell vorgeht, wenn sie eine "Prozeß-Pipeline" ausführt. Angenommen, das Kommando `ls | sort` wird eingegeben. Dann läuft - vereinfacht dargestellt - der folgende Mechanismus ab:

    int Pipe[2];
    int status;
    char *parls[]   = { "/bin/ls", NULL };
    char *parsort[] = { "/usr/bin/sort", NULL };

    int main(void)
      {
      ...
      pipe(Pipe);           // Pipe erzeugen
      if (fork() == 0)      // erster Sohn: "ls"
        {
        dup2(Pipe[1],1);    // Pipeausgabe->Standardausgabe
        close(Pipe[0]);     // Pipeeingabe nicht benötigt
        execve("/bin/ls",parls,NULL);
        }
      else
        {
        if (fork() == 0)    // zweiter Sohn: "sort"
          {
          dup2(Pipe[0],0);  // Pipeeingabe->Standardeingabe
          close(Pipe[1]);   // Pipeausgabe nicht benötigt
          execve("/usr/bin/sort",parsort,NULL);
          }
        else                // Vater (Shell)
          {
          close(Pipe[0]);
          close(Pipe[1]);
          wait(&status);
          wait(&status);
          }
        }
      ...
      }

#### Benannte Pipe

Eine benannte Pipe (named pipe) besitzt einen Geräteeintrag vom Typ FIFO (First In First Out) und hat einen Namen, mit dem sie von jedem Prozeß durch `open()` angesprochen werden kann. Eine benannte Pipe wird vom System nicht automatisch gelöscht, wenn alle Prozesse beendet sind. Durch den Aufruf `unlink()` muß der Anwender die benannte Pipe innerhalb eines Prozesses selber löschen. Für beannte Pipes gibt es folgende Schnittstellenfunktionen:

`close` | Schließt ein Schreib-oder Leseende einer Pipe. Prototyp: `int close(int fd);` Parameter: `fd`: Lese-bzw. Schreibdeskriptor einer Pipe
`mkfifo` | Erzeugt eine benannte Pipe. Prototyp: `int mkfifo (char *name, int mode);` Parameter: `*name`: Name bzw. Pfad der Pipe, `mode`: Bitmaske für Zugriffsrechte auf die Pipe. Die Positon und Bedeutung dieser Bits sind so wie beim numerischen `chmod`-Kommando (z.B. 0755 [führende Null wg. Oktalangabe]). Rückgabewert: 0 bei erfolgreicher Ausführung, sonst -1.
`open` | öffnet eine Pipe bzw. Datei. Prototyp: `open (char *name, int flag, int mode); Parameter: `*name`: Name bzw. Pfad der Pipe, `flag`: Bitmuster für Zugriff auf die Pipe (`O_RDONLY` Lesezugriff, `O_WRONLY` Schreibzugriff, `O_NONBLOCK` Prozessverhalten) Wird `O_NONBLOCK` nicht angegeben (Normalfall), blockiert der Leseprozess, bis ein anderer Prozess die Pipe zum Schreiben öffnet und umgekehrt. Rückgabewert: -1 bei Fehler oder Dateideskriptor für die Pipe.`
`pipe` | Erzeugt eine unbenannte Pipe. Prototyp: `int ipe (int fd[2]);` Parameter: `fd[2]`: zwei Dateideskriptoren, die zurückgegeben werden, wobei `fd[0]` der Dateideskriptor für die Leseseite und `fd[1]` Dateideskriptor für die Schreibseite der Pipe ist.
`read` | Lesen der Daten aus einer Pipe. Ist die Pipe leer, blockiert die Funktion. Prototyp: `int read (int fd, char *outbuf, unsigned bytes);` Parameter: `fd`: Diskriptor der Pipe, `*outbuf`: Zeiger auf den Speicherbereich, in dem die Daten gespeichert werden und `bytes`: Maximale Anzahl der Bytes, die gelesen werden. Rückgabewert: Anzahl der tatsächlich gelesenen Bytes, -1 bei einem Fehler und 0, wenn die Schreibseite der Pipe geschlossen wurde.
`unlink` | Löscht die benannte Pipe. Prototyp: `int unlink (char *name);<` Parameter: `*name`: Name/Pfad der Pipe.
`write` | Schreibt Daten in eine Pipe. Ist der Pipe-Buffer voll, blockiert diese Funktion. Prototyp: `int write (int fd, char *outbuf, unsigned bytes);` Parameter: `fd`: Diskriptor der Pipe, `*outbuf`: Zeiger auf den Speicherbereich, in dem die zu schreibenden Daten stehen und `bytes`: Anzahl der Bytes, die geschrieben werden.

Beispiel: Named Pipe für zwei getrennte Prozesse In Unix/Linux können benannte Pipes auch für die Kommunikation zwischen Prozesse eingesetzt werden, die nicht miteinander "verwandt" sind:

    /* Empfaenger */
    #include<stdio.h>
    #include<signal.h>
    #include <unistd.h>
    #include<fcntl.h>

    int main(void)
      {
      int ein;               /* Hilfsvariable fuer Programmstart */
      int hilf;
      char outbuffer[2];	 /* Buffer zum Auslesen der Pipe */
      int fd;	             /* Dateideskriptor fuer Pipe */
      int gelesen;           /* speichert die Anzahl der gelesenen Bytes */

      printf("Empfaengerprozess wurde gestartet\n\n");
      do
        {
        fd = open("TESTPIPE",O_RDONLY);  /* Oeffnen der Pipe zum Lesen */
        if (fd == -1) printf("Prozess zum Schreiben in die Pipe starten!\n");
        sleep(2);
        }
      while (fd == -1);
      do
        {
        gelesen = read(fd,outbuffer,2);	/* 2 Bytes werden ausgelesen */
        if (gelesen != 0) printf("Lese %c aus der Pipe\n",outbuffer[0]);
        sleep(2);
        }
      while (gelesen > 0);
      unlink("TESTPIPE");		/* benannte Pipe wird geloescht */
      return(0);
      }

    /* Sender */
    #include<stdlib.h>
    #include <unistd.h>
    #include<stdio.h>
    #include<fcntl.h>

    int main(void)
      {
      int hilf;
      char inbuffer[2];  /* Buffer zum Schreiben in die Pipe */
      int fd;            /* Dateideskriptor fuer Pipe */

      system("mkfifo TESTPIPE -m 666");   /* benannte Pipe wird erzeugt */
      printf("Sendeprozess wurde gestartet\n\n");

      mkfifo("TESTPIPE",0666);           /* benannte Pipe wird erzeugt */
      fd = open("TESTPIPE",O_WRONLY);    /* Oeffnen der Pipe zum */
      for(hilf=0; hilf<10; hilf++)       /* Zaehler zum Schreiben */
        {
        inbuffer[0] = (int)'0' + hilf;
        inbuffer[1] = '\0';
        write(fd,inbuffer,2);            /* 2 Bytes werden geschrieben */
        printf("Schreibe %c in die Pipe\n",inbuffer[0]);
        sleep(1);
        }
      return(0);
      }

### Message Queues

Bei dieser Art der Kommunikation werden die Daten an Nachrichtenspeicher gesendet und können dort von anderen Prozesssen abgeholt werden. Die Message Queeues werden mit Hilfe einer Message-Queue-Tabelle vom Betriebssystem verwaltet. Die Nachrichten bestehen aus einem Nachrichtenkopf (Message Header) und einem Nachrichtentext (Message Body). Im Header sind Informationen, wie Typ, Größe der Nachricht und ein Zeiger auf den Speicherbereich, wo die Nachricht steht, enthalten.

Eine solche Queue kann mit Nachrichten verschiedenen Typs arbeiten. Der Typ wird durch die Anwendung bestimmt und ist einfach eine Zahl. Ein Prozess kann Nachrichten an die Warteschlange senden. Beim Erreichen der Kapazität der Schlange kann der Prozess per Parameter bestimmen, ob er blockieren will bis die Nachricht abzuliefern ist oder mit einem Fehler zurückkehren möchte. Auf der anderen Seite kann ein Prozess eine Nachricht bestimmten Typs anfordern. Auch hier kann der Prozess warten, bis er eine passende Nachricht bekommt, oder mit einer Fehlermeldung sofort zurückkehren.

Die folgenden Funktionen `msgsnd()` und `msgrcv()` verwenden eine Struktur `msgbuf` für ihre Nachrichten:

    struct msgbuf
      {
      long mtype;     /* von der Anwendung definierbar > 0 */
      char mtext[1];  /* Nachrichtendaten beginnen hier */
      };

Es kann als Typ eine beliebige Zahl größer Null verwendet werden, die allein von der Applikation festgelegt werden. Für die eigenen Nachrichten werden Sie im mtext vermutlich mehr als ein Zeichen versenden wollen. Dazu definieren Sie sich eine eigene Struktur mit entsprechend größerem Datenpuffer. Die Größe wird beiden Funktionen als Parameter übergeben.

An Include-Dateien werden benötigt:

    #include <sys/ipc.h>
    #include <sys/msg.h>

`msgget` | Die Funktion legt eine Message Queue an. Prototyp: `int msgget(key_t key, int msgflg);` Parameter: `key` ist entweder eine Schlüsselzahl oder `IPC_PRIVATE`, `msgflg` kombiniert die Konstanten `IPC_CREAT` und `IPC_EXCL` und deren Oder-Verknüpfung mit neun Berechtigungsbits für den Eigner, die Gruppe und der Welt, wie sie vom Kommando `chmod` verwendet werden. Rückgabewert: -1 im Fehlerfall oder die Message-Queue-ID, die für die nächsten Aufrufe benötigt wird.
`msgsnd` | Versenden von Nachrichten. Prototyp: `int msgsnd(int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg);` Parameter: `msqid` ist der Rückgabewert der Funktion `msgget().` `msgp` ist die Adresse der Datenstruktur mit dem Nachrichtentyp und den Daten. `msgsz` ist so groß wie das Array `mtext` in der Datenstruktur für die Nachricht. `msgflg` kann mit der Optionen `IPC_NOWAIT` besetzt werden, wenn die Funktion bei einer übervollen Message-Queue nicht blockieren und warten soll, bis wieder Platz ist, sondern mit einem Fehler zurückkehren.
`msgrcv` | Nachrichten empfangen. Prototyp: `int msgrcv(int msqid, struct msgbuf *msgp, size_t msgsz, long msgtyp, int msgflg);` Parameter: `msqid` ist der Rückgabewert der Funktion `msgget().` `msgp` ist die Adresse der Datenstruktur mit dem Nachrichtentyp und den empfangenen Daten. `msgsz` ist so groß wie das Array `mtext` in der Datenstruktur für die Nachricht. `msgtyp` legt fest, auf welchen Nachrichtentyp `msgrcv()` warten soll. Alle anderen Typen werden von `msgrcv()` ignoriert. Wird hier 0 angegeben, nimmt `mgsrcv()` jeden Typ entgegen. `msgflg` kann mit der Optionen `IPC_NOWAIT` besetzt werden, wenn die Funktion nicht blockieren und warten soll, bis eine Nachricht vorliegt, sondern bei leerer Queue mit einem Fehler zurückkehrt.
`msgctl` | Eigenschaften der Nachrichten verwaltet. Prototyp: `int msgctl(int msqid, int kommando, struct msqid_ds *buf);` Parameter: `msqid` ist der Rückgabewert der Funktion `msgget().` Mit `kommando` können folgende Konstanten übergeben werden: `IPC_STAT:` Die Informationen über die Message Queue einlesen `IPC_SET:` ändere die Benutzerrechte in `mode` `IPC_RMID:` Zerstört die Message Queue und weckt alle darauf wartenden Prozesse

Mit dem Kommando `ipcs` erhalten Sie einen Überblick über angeforderte Message-Queues.

Beispiel:
Das Programm `rcv.c` wartet auf eine Nachricht in einer Message-Queue.

    #include <stdio.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <sys/types.h>

    #define MSGSIZE 20

    int main(void)
      {
      key_t Key = 666;
      long Msgtyp = 4711;

      int MsgID;
      struct myMsg
        {
        long mtype;
        char mtext[MSGSIZE];
        } MsgData;

      MsgID = msgget(Key, IPC_CREAT | 0666);   /* Messagequeue oeffnen/erzeugen */
      if (MsgID >= 0)
        {
        printf("Warte auf Message Type %ld\n", Msgtyp);
        if (msgrcv(MsgID, &MsgData, MSGSIZE, Msgtyp, 0) == -1)
          { printf("Fehler in msgrcv\n"); }
        else
          { printf("Daten empfangen: %s\n", MsgData.mtext); }
        }
      else
        { printf("Fehler in msgget\n"); }
      return(0);
      }

Das Programm `snd.c` sendet Nachrichten.

    #include <stdio.h>
    #include <string.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <sys/types.h>

    #define MSGSIZE 20

    int main(void)
      {
      key_t Key = 666;
      long Msgtyp = 4711;

      int MsgID;
      struct myMsg
        {
        long mtype;
        char mtext[MSGSIZE];
        } MsgData;

      MsgData.mtype = Msgtyp;
      strncpy(MsgData.mtext, "Hello World", MSGSIZE);  /* Datenpuffer fuellen */

      MsgID = msgget(Key, IPC_CREAT | 0666);     /* Messagequeue oeffnen/erzeugen */
      if (MsgID >= 0)
        {
        printf("Sende Messagetyp %ld\n", MsgData.mtype);
        if (msgsnd(MsgID, &MsgData, MSGSIZE, 0) == -1)
          { printf("Fehler in msgsnd\n"); }
        else
          { printf("Daten gesendet: %s\n", MsgData.mtext); }
        }
      else
        { printf("Fehler in msgget\n"); }
      return(0);
      }

Die Message-Queue bleibt solange erhalten, bis ein Programm sie explizit per `msgctl()` mit dem Kommando `IPC_RMID` entfernt, oder bis sie mit dem Befehl `ipcrm` gelöscht wird (siehe `man ipcrm`, `man ipcs`).

### Shared Memory

Hier benutzen die Prozesse einen gemeinsamen Speicherbereich auf den sie zugreifen. Dieser Speicherbereich muß durch das Beriebssystem zur Verfügung gestellt und registriert werden. Erfolgt der Zugriff auf diesen Speicherbereich durch mehrere Prozesse, müssen diese sich synchronisieren.

An Include-Dateien werden benötigt:

    #include <sys/ipc.h>
    #include <sys/shm.h>

`shmget` | Legt den gemeinsamen Speicher an bzw. eröffnet ihn. Prototyp: `int shmget(key_t key, int size, int shmflg);` Parameter: `key` ist entweder eine Schlüsselzahl oder `IPC_PRIVATE`. `shmflg` bildet eine Oder-Verknüpfung der Konstanten `IPC_CREAT` bzw. `IPC_EXCL` mit den neun Berechtigungsbits für den Eigner, die Gruppe und der Welt (oktal!, wie bei `chmod`). Rückgabewert: -1 im Fehlerfall oder die Shared-Memory-ID, die für die nächsten Aufrufe benötigt wird.
`shmat` | (shared memory attach) bindet den Speicher ein. Prototyp: `void *shmat(int shmid, const void *shmaddr, int shmflg);` Parameter: `shmid` ist die von `shmget()` ermittelte ID. `shmaddr` ist normalerweise 0, dann sucht sich das System eine passende Stelle. `shmflg` ist entweder 0 oder `SHM_RDONLY,` wenn auf den Speicher nur lesend zugegriffen werden soll. Rückgabewert: Der Fehlerwert von `shmat()` ist -1 und nicht `NULL`, wie man erwarten sollte. Daher ergeben sich Abfragen wie:
    myPtr = shmat(shID, 0, 0);
    if (myPtr == (char *)-1) ...
`shmdt` | (shared memory detach) hebt die Speicherbindung wieder auf. Prototyp: `int shmdt(const void *shmaddr);`Parameter: `shmaddr` ist der Rückgabewert von `shmat()`. Rückgabewert: -1 im Fehlerfall, sonst 0.``
`shmctl` | Bestimmte Eigenschaften des gemeinsamen Speichers verwaltet. Prototyp: `int shmctl(int shmid, int kommando, struct shmid_ds *buf);` Parameter: `shmid` ist der Rückgabewert der Funktion `shmget().` Mit `kommando` können folgende Konstanten übergeben werden: `IPC_STAT:` Die Informationen über den Speicher einlesen `IPC_SET:` Ändere die Benutzerrechte in `mode` `IPC_RMID:` Markiere das Segment als zerstört

Mit dem Kommandozeilenbefehl `ipcs` bekommen Sie, wie bei den Message-Queues, einen Überblick über die angeforderten Shared-Memory-Bereiche.

Beispiel: Das erste Programm erzeugt einen Shared-Memory-Block von 128 Bytes und schreibt dort ASCII-Zeichen hinein.

    #include <stdio.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>

    #define MAXMYMEM 100

    int main(void)
      {
      int ShmID;
      key_t Key = 1234;
      char *ShmPtr;
      int i;

      /* Shared Memory erzeugen */
      ShmID = shmget(Key, MAXMYMEM, IPC_CREAT | 0666);
      if (ShmID >= 0)
        {
        /* nun holen wir den Speicher */
        ShmPtr = shmat(ShmID, 0, 0);
        if (ShmPtr == (char *)-1)
          { printf("Fehler bei shmat\n"); }
        else
          {
          for (i=0; i<95; i++)
            ShmPtr[i] = (char)((int)' ' + i);
          while (getchar() != '\n'); /* Warte auf Enter */
          shmdt(ShmPtr);
          }
        }
      else
        { printf("Fehler bei shget\n"); }
      return(0);
      }

Das zweite Programm unterscheidet sich wenig vom vorhergehenden. Da das erste Programm den Speicher reserviert, braucht das zweite dies nicht zu tun, es liest einfach den Inhalt des Speichers aus und gibt ihn auf dem Bildschirm aus.

    #include <stdio.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>

    #define MAXMYMEM 100

    int main(void)
      {
      int ShmID;
      key_t Key = 1234;
      char *ShmPtr;
      int i;

      /* Existierenden Shared Memory zugreifen */
      ShmID = shmget(Key, MAXMYMEM, 0666);
      if (ShmID >= 0)
        {
        ShmPtr = shmat(ShmID, 0, 0);
        if (ShmPtr == (char *)-1)
          { printf("Fehler bei shmat\n"); }
        else
          {
          for (i=0; i<95; i++)
            putchar(ShmPtr[i]);
          putchar('\n');
          shmdt(ShmPtr);
          }
        }
      else
        { printf("Fehler bei shget\n"); }
      return(0);
      }

Man kann die Programme nacheinander laufen lassen. Nach dem ersten Programmlauf sieht man mit `ipcs`, daß der Shared-Memory-Block noch vorhanden ist.

### Sockets

Sockets ermöglichen eine bidirektionale Kommunikation sowohl lokal als auch innerhalb eines Netzwerkes. Der vom Benutzer aus sichtbare Teil der Kommunikation besteht aus drei Teilen:

- dem Socket-Kopf (Socket Layer),
- dem Protokollteil (Protocol Layer),
- dem Gerätetreiber ( Device Layer).

Der Socket-Kopf bildet die Schnittstelle zwischen den Betriebssystemaufrufen und den weiter unten liegenden Schichten. Sockets mit gleichen Charakteristika bezüglich Adressierung und Protokolladreßformat werden zu Bereichen, sogenannten Domains, zusammengefaßt. Die Unix-System-Domain dient dabei zur lokalen Kommunikation zwischen Prozessen. Die Internet-Domain dient zur Kommunikation über ein Netzwerk.

Sockets werden an anderer Stelle behandelt.

Zum Inhaltsverzeichnis | Zum nächsten Abschnitt

###### Copyright © FH München, FB 04, Prof. Jürgen Plate