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.

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().
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:

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.

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:

SignalSignal-Quelle
SI_USERkill(), sigsend(), or raise()
SI_KERNELThe kernel
SI_QUEUEsigqueue()
SI_TIMERPOSIX timer expired
SI_MESGQPOSIX message queue state changed (since Linux 2.6.6)
SI_ASYNCIOAIO completed
SI_SIGIOqueued SIGIO
SI_TKILLtkill() or tgkill() (since Linux 2.4.19)
SIGILLillegal opcode
SIGFPEfloating point unit error
SIGSEGVsegmentation fault
SIGBUSbus error
SIGTRAPbreakpoint, trace
SIGCHLDsignal from child process
SIGPOLLpoll 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: 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. 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: 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