Programmierung UNIX-Prozesse

Willemers Informatik-Ecke


Weitere Informationen zur UNIX- und Linux-Programmierung finden Sie im umfassenden Handbuch UNIX.


getuid() und geteuid()

Ein gestartetes Programm nennt man Prozess. Ein Prozess wird eindeutig bestimmt durch seine PID. Der Prozess hat die User-ID desjenigen, der den Prozess gestartet hat. Er kann sie mit der Funktion getuid() ermitteln. Gegebenenfalls läuft der Prozess unter einer anderen als der eigenen User-ID, wenn die Programmdatei das Set-User-ID Bit gesetzt hat. Diese ID ermittelt die Funktion geteuid().

getpid() und getppid()

Jeder Prozess hat eine eindeutige Prozess-ID. Diese kann er mit der Funktion getpid() ermitteln. Da ein Prozess immer von einem anderen Prozess gestartet wurde, hat er auch einen eindeutigen Elternprozess und auch dessen ID kann er ermitteln. Dazu gibt es die Funktion getppid().

#include <unistd.h>

   pid_t getpid(void);
   pid_t getppid(void);
   uid_t getuid(void);
   uid_t geteuid(void);

Multiprocessing contra Multithreading

Prozesse sind gegeneinander geschützt

UNIX hat ein durchaus schlankes und effizientes Prozesskonzept, das auf dem Aufruf von fork() basiert. Das Teilen der Prozesse mit allen Ressourcen ermöglicht es leicht, Aufgaben auf mehrere Prozesse zu verteilen. Davon machen die meisten Dämonen auch reichlich Gebrauch. Durch die Aufteilung in zwei Prozesse können sich parallele Jobs nicht so leicht gegenseitig durcheinander bringen. Dieses Konzept ist ideal für parallel laufende Serverprozesse.

Threads teilen sich alle Ressourcen

Im Gegensatz zu einem Prozess arbeitet ein Thread nicht mit einem eigenen Speicherbereich, sondern teilt sich mit dem Vaterprozess alle Ressourcen. Normalerweise besteht ein Thread aus einer Funktion, die man parallel zum Rest des Programms laufen lässt. Threads kommen aus dem Bereich der grafischen Oberflächen. Hier ist es erforderlich, dass ein Ablauf die grafische Darstellung betreut, insbesondere das Verarbeiten der Nachrichten, die über Bildneuaufbau, Mauspositionen und ähnliches berichten. Dieser Ablauf muss auch dann präsent sein, wenn das Programm gerade seiner eigentlichen Aufgabenstellung nachgeht und dabei vielleicht langwierige Berechnungen durchführt.

Threads sind kein Ersatz für Parallelprozesse

Beide Konzepte haben also völlig unterschiedliche Umgebungen, in denen sie arbeiten. Die in Diskussionen manchmal anzutreffende Aussage, dass Threads so unglaublich viel performanter seien, ist also Augenwischerei. Wer einen Server durch Threads parallelisiert, wird zumindest unter UNIX soviel Verwaltungsarbeit durch die Synchronisation der Threads als Überhang bekommen, dass sich der Aufwand nicht lohnt. In der Konsequenz bedeutet Threading, dass jede globale Variable, jeder Dateizeiger oder sonstige Ressource, die gemeinsam genutzt wird, vor dem gegenseitigen Verändern zu schützen ist. Lediglich auf Systemen, die statt dem fork() nur einen kompletten Neustart des Programmes kennen, wird man eventuell auf das Threading ausweichen, weil es dort sehr umständlich ist, die Daten des Vaters an den Sohn zu übermitteln.

Das Konzept mit fork() und exec() ist derart schnell und flexibel, dass eine Thread API erst in den letzten Jahren bei einigen UNIX Systemen Einzug gehalten hat. Da hier auch noch aufgrund der jungen Geschichte Schnittstellenunterschiede existieren und Threads tatsächlich nur für den Programmierer grafischer Oberflächen interessant ist, wird das Thema hier nicht behandelt.

Vervielfältigen von Prozessen: fork

Komplette Prozesskopie

Ein neuer Prozess entsteht durch den Aufruf von fork(). Er dupliziert den aktuell laufenden Prozess. Anschließend laufen beide Prozesse parallel. Der neue Prozess ist ein Duplikat der Arbeitsumgebung des Vaters, inklusive des Zustands der CPU, des gesamten Speicherzustands sowie aller offenen Dateien.

#include <unistd.h>

pid_t fork(void);

Rückgabewert informiert, ob Vater oder Sohn

Beide Prozesse stehen nach dem fork() an exakt der gleichen Stelle. Nur am Rückgabewert des fork() erkennt der Prozess, ob er der Vater oder der Sohn ist.

int SohnPID;

  SohnPID=fork();
  if (SohnPID > 0) {
    /* Der Vater ist hier aktiv */
  } else if (SohnPID == 0) {
    /* Der Sohn ist hier aktiv */
  } else {
    /* das war's wohl: Fehler! */
  }

Diese Konstruktion ist ideal, um Serverprozesse zu implementieren. Sobald eine Anfrage vorliegt, teilt sich der Prozess. Beide Prozesse haben die gleichen Informationen, kennen also den Anfrager und haben die Zugriffe auf die benötigten Dateien. Der Vaterprozess kann also hier die Arbeit ohne Zeitverlust dem Sohn überlassen, die Verbindung zum Anfrager schließen und auf neue Anfragen warten.

Geburt eines Dämons

Dämonen sind Kinder des init

Wie schon an anderer Stelle erwähnt, ist ein Dämon ein Prozess, der im Hintergrund läuft und auf ein bestimmtes Ereignis wartet. Serverprozesse sind als Dämonen ausgeprägt oder werden von Dämonen gestartet. Wenn ein Prozess im Hintergrund laufen soll, erzeugt er von sich selbst ein Duplikat und endet, so dass nur noch der Sohn läuft. Das Ergebnis ist, dass dem Sohn der Vater fehlt. Das macht den init-Prozess so traurig, dass er den Sohn adoptiert. Der Code ist sehr kurz:

  if (fork()!=0) exit(0);

Als weiterer Vorteil gilt, dass der Prozeß nicht das SIGHUP-Signal bekommt, falls er von Hand gestartet wurde und der startende Benutzer sich abmeldet. Dieses würde der Vater bekommen. Da der aber nicht mehr lebt...

Der unsterbliche Prozess

In manchen Fällen ist es wichtig, dass ein Prozess zwar aufgrund widriger Umstände auch sterben könnte, aber dann sofort wieder neu erzeugt werden soll. Auch eine solche Konstruktion kann man mit dem fork leicht erzeugen.

for(;;) { /* bis zum nächsten Stromausfall */
    procid = fork();
    if (procid>0) { /* Vater */
        wait(&Zustand);
        /* wenn wir hier sind, ist der Sohn tot */
    } else { /* Sohn */
        for (;;) { /* forever and ever ... */
            /* hier arbeitet der Sohn ewig (fast)... */
        }
    }
}

Vater startet sofort neuen fork()

Der Vaterprozess läuft sofort auf den Aufruf von wait(). Er wartet also, bis der Sohn endet. Da dieser eigentlich endlos arbeiten soll, heißt das, dass der Vaterprozess sofort weiterläuft, wenn der Sohn aus welchem Grund auch immer stirbt. Der Vater wiederholt daraufhin die Schleife und kommt wieder zum Aufruf von fork(), erzeugt also wieder einen neuen Sohn. Muss ein solcher Dämon doch einmal abgeschossen werden, muss natürlich der Vater vor dem Sohn getötet werden.

exec und system

Start eines Kindprozesses

Der Systemaufruf exec() überlädt den aktuellen Prozess mit dem Inhalt einer ausführbaren Datei und startet sie. Da der alte Speicherinhalt überschrieben wurde, gibt es kein Zurück mehr. Vor dem exec() wird typischerweise ein fork() aufgerufen. Ein Programmaufruf der Shell beispielsweise läuft so ab, dass zunächst die Shell einen fork() aufruft. Der Sohnprozess ruft nun exec() mit dem Programmnamen auf, der in der Shell eingegeben wurde. Der Vaterprozess dagegen ruft wait() auf und wartet auf das Ende des Sohnes.

system()

Da diese Funktionalität recht häufig auftritt, gibt es einen eigenen Aufruf namens system(). Als Parameter erhält er den Programmnamen. Aus Sicht des Programmes wird das angegebene Programm gestartet und nach dem Ende des Programms setzt das aufrufende Programm seine Aktiviät fort. Um genau zu sein, wird eine Shell (/bin/sh) mit dem Kommando gestartet.

int system(const char *Programmname) 

Der Aufruf exec() ist eigentlich eine ganze Funktionsfamilie. Die verschiedenen Verwandten werden gebraucht, um die Parameter ordentlich weiter geben zu können.

execl(char *path, char *arg, ...);
exec mit fester Anzahl von Argumenten. Der letzte Parameter muss NULL sein.
execlp(char *file, char *arg, ...);
exec mit fester Anzahl von Argumenten. Der letzte Parameter muss NULL sein.
execle(char *path, char *arg, ..., char *env[]);
exec mit fester Anzahl von Argumenten. Der vorletzte Parameter muss NULL sein. Der letzte Parameter ist ein Zeiger auf die Umgebungsvariablen.
execv(char *path, *char arg[]);
exec mit Übernahme einer Argumentliste als Vector wie bei main. Das letzte Element des Parameterarrays arg muss NULL sein.
execvp(char *file, *char arg[]);
exec mit Übernahme einer Argumentliste als Vector wie bei main. Das letzte Element des Parameterarrays arg muss NULL sein.

Synchronisation: wait

Der Systemaufruf wait() supendiert einen Prozess solange, bis ein Kindprozess terminiert. War der Kindprozess bereits vor dem Aufruf von wait gestorben, hat das System in der Prozesstabelle noch die Informationen für die Anfrage des Vaters in Form eines so genannten Zombieprozesses aufbewahrt. In diesem Fall wird der Zombie aufgelöst und der wait() kehrt sofort zurück.

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

waitpid() wartet auf besondere Kinder

Während wait() auf das Eintreffen eines beliebigen Kindprozesses wartet, wird bei waitpid() auf einen speziellen Prozess gewartet. Im dritten Parameter options können folgende Konstanten geodert übergeben werden.
Konstante Bedeutung
WNOHANG sofort zurückkehren, wenn kein Kind geendet hat
WUNTRACED kehrt auch zurück, wenn das Kind gestoppt wurde

Prozessumgebung

Die Informationen über Prozessgruppe, Sitzung und Kontrollterminal von Prozessen kann man sich mit ps anschauen, wenn man die Optionen -jx bzw. unter System V -jl angibt. Dabei stehen die wichtigen Informationen unter folgenden Überschriften.
Kürzel Bedeutung
PGID Prozessgruppen ID
SID Sitzung ID
TTY Kontrollierendes Terminal
TPGID Gruppen ID des kontrollierenden Terminal

Prozessgruppen

Jeder Prozess gehört zu einer Prozessgruppe. Die Prozessgruppen-­ID ist die Prozess-ID des Prozessgruppenleiters. Eine Prozessgruppe kann beispielsweise durch eine Pipe verbunden sein. Anders ausgedrückt ist eine Prozessgruppe eine Gruppe von Prozessen, die voneinander abhängig sind und in dieser Abhängigkeit zusammengehören. Durch die folgende Kommandosequenz würde man die in der grafischen Übersicht gezeigten Prozessgruppen erzeugen:

gaston> processA | processB &
[1] 2354
gaston> processD | processG | prozessK &
[2] 2356
gaston> processM | processN

Die Prozessgruppe kann mit dem Aufruf getpgrp() ermittelt werden. Mit dem Aufruf getpgid() kann die Prozessgruppe eines anderen Prozesses ermittelt werden, dessen ID als Parameter übergeben wird. Mit dem Aufruf setpgid() kann einem Prozess eine neue Prozessgruppe zugewiesen werden. Ein Prozess kann setpgid() nur für sich selbst oder einen seiner Kindprozesse aufrufen, solange das Kind noch nicht exec gerufen hat.

#include <unistd.h>

   int setpgid(pid_t pid, pid_t pgid);
   pid_t getpgid(pid_t pid);
   int setpgrp(void);
   pid_t getpgrp(void);

Sitzung

Einer Sitzung (engl. session) können ein oder mehrere Prozessgruppen gehören. Mit setsid() wird eine neue Sitzung eröffnet und damit auch eine neue Prozessgruppe.

#include <unistd.h>

   pid_t setsid(void);

Eine Sitzung besteht aus einer Vordergrundprozessgruppe und einer beliebigen Zahl von Hintergrundprozessgruppen.

Kontrollterminal

Das Kontrollterminal ist das Terminal oder das Pseudoterminal, von dem aus eine Sitzung ursprünglich gestartet wurde. Sofern ein Prozess noch ein Kontrollterminal hat, ist dieses für ihn jeweils über /dev/tty zu erreichen.

Gegenseitige Abhängigkeiten

Versucht ein Hintergrundprozess in einer Umgebung mit Jobkontrolle vom Terminal zu lesen, wird er suspendiert, bis der Prozess durch die Jobkontrolle explizit in den Vordergrund geholt wird. Existiert keine Jobkontrolle, erhält ein Hintergrundprozess beim Versuch, vom Terminal zu lesen, /dev/null zugewiesen. Da dies immer EOF (End Of File) liefert, wird die Eingabe sofort abgeschlossen. Die Ausgabe von Hintergrundprozessen ist grundsätzlich zugelassen, kann aber durch das Kommando stty tostop unterbunden werden. Um die Ausgabe wieder zuzulassen, gibt man das Kommando stty -tostop.

Gemeinsamer Speicher: Shared Memory

Normalerweise sind die Speicherbereiche zweier Prozesse streng getrennt. Der gemeinsame Speicher ermöglicht die Arbeit zweier Prozesse an den gleichen Daten. Neben den hier beschriebenen Prinzipien, wie man sich den Speicher teilt, benötigt man normalerweise eine Form der Synchronisation, mit der die Prozesse sich darüber austauschen, wann wer in den Speicher greifen darf.

shmget()

Die Funktion shmget() legt den gemeinsamen Speicher an, bzw. eröffnet ihn.

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

int shmget(key_t key, int size, int shmflg);

Der Parameter key ist entweder eine Schlüsselzahl oder IPC_PRIVATE. Die shmflg kombiniert die Konstanten IPC_CREAT und IPC_EXCL und neun Berechtigungsbits für den Eigner, die Gruppe und der Welt, wie sie von chmod verwendet werden, indem sie mit dem senkrechten Strich geodert werden. Der Rückgabewert ist -1 im Fehlerfall oder die Shared Memory ID, die für die nächsten Aufrufe benötigt wird.

shmat()

Die Funktion shmat() (shared memory attach) bindet den Speicher ein. Mit shmdt() (shared memory detach) wird die Speicherbindung wieder aufgehoben.

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

void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

Die shmid ist die von shmget() ermittelte ID. Als shmaddr kann eine 0 angegeben werden, dann sucht sich das System eine passende Stelle. Die shmflg ist 0 oder SHM_RDONLY, wenn der Speicher nur lesend zugegriffen werden soll. Nach shmat() steht der Speicher zur Verfügung und kann wie ein normaler Speicherbereich zugegriffen werden. Die Fehlermeldung von shmat() ist (leider) -1. Da der Rückgabewert ein Zeiger ist, der nicht mit einer natürlichen Zahl verglichen werden darf, ergeben sich immer Abfragen wie:

myPtr = shmat(shID, 0, 0);
if (myPtr==(char *)-1) 

shmctl()

Mit der Funktion shmctl() werden bestimmte Eigenschaften des gemeinsamen Speichers verwaltet.

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

int shmctl(int shmid, int kommando, struct shmid_ds *buf);

Als kommando können folgende Konstanten übergeben werden:
Konstante Bedeutung
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 Kommando ipcs kann man einen Überblick über die angeforderten Shared Memory Bereiche bekommen.

Beispiel

Das Programm one erzeugt einen Shared Memory von 30 Byte und schreibt dort ASCII-Zeichen beginnend mit A hinein.

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

#define MAXMYMEM 30

int main(int argc, char **argv)
{
int shID;
char *myPtr;
int i;

    /* Shared Memory erzeugen */
    shID = shmget(2404, MAXMYMEM, IPC_CREAT | 0666);
    if (shID >= 0) {
        /* nun holen wir den Speicher */
        myPtr = shmat(shID, 0, 0);
        if (myPtr==(char *)-1) {
            perror("shmat");
        } else {
            /* Speicher ist zugreifbar: füllen! */
            for (i=0; i<MAXMYMEM; i++) {
                myPtr[i] = 'A'+i;
            }
            getchar(); /* Warte mal auf eine Taste */
            /* gebe den Speicher auf */
            shmdt(myPtr);
        }
    } else { /* shmget lief schief */
        perror("shmget");
    }
}

Das Programm two unterscheidet sich wenig von one. Da Programm one den Speicher erzeugt, braucht two das nicht zu tun. two wird einfach den Inhalt des Speichers auslesen und auf dem Bildschirm ausgeben und damit demonstrieren, dass es sich um denselben Speicher handelt.

#include <sys/ipc.h>
#include <sys/shm.h>
#define MAXMYMEM 30

int main(int argc, char **argv)
{
int shID;
char *myPtr;
int i;

    /* Existierenden Shared Memory zugreifen */
    shID = shmget(2404, MAXMYMEM, 0666);
    if (shID >= 0) {
        myPtr = shmat(shID, 0, 0);
        if (myPtr==(char *)-1) {
            perror("shmat");
        } else {
            for (i=0; i<MAXMYMEM; i++) {
                putchar(myPtr[i]);
            }
            puts("\n");
            shmdt(myPtr);
        }
    } else { /* shmget lief schief */
        perror("shmget");
    }
}

Die Programme brauchen keineswegs parallel zu laufen. Man kann one auch durchlaufen lassen und sieht dann mit dem Befehl ipcs, dass der Shared Memory noch da ist. Um den Speicher zu entsorgen, muss er zerstört werden. Dazu dient das kleine Programm destroy:

#include <sys/ipc.h>
#include <sys/shm.h>
#define MAXMYMEM 30

int main(int argc, char **argv)
{
int shID;
char *myPtr;
int i;

    /* Shared Memory erzeugen */
    shID = shmget(2404, MAXMYMEM, 0666);
    if (shID >= 0) {
        /* zerstöre den Shared Memory */
        shmctl(shID, IPC_RMID, 0);
    } else { /* shmctl lief schief */
        perror("shmctl");
    }
}

Ein interessantes Experiment zeigt das parallele Starten von one und destroy. Man startet one auf einem Terminal. Dieses legt den Shared Memory an und bindet ihn ein. Dann wartet das Programm. Nun startet man destroy auf der anderen Konsole. Der Aufruf zum Zerstören des Speichers hat stattgefunden. Aber ein Blick auf die Ausgabe von ipcs zeigt, dass der Speicher noch da ist. Erst wenn one mit einem Tastendruck weiterläuft und seinen Speicher wieder mit shmdt() freigegeben hat, zeigt auch ipcs das Verschwinden des Shared Memory.

Fehlende Synchronisation

Bei dem Beispiel wird die Synchronisation der Programme durch den zeitlich unterschiedlichen Start erreicht. Bei einem parallelen Start von one und two wäre es aber denkbar, dass one noch gar nicht damit fertig wäre, die Daten in den Speicher zu schreiben, während two bereits mit dem Auslesen beginnt. Um dies zu verhindern, wird ein weiteres Konzept benötigt, das gewährleisten kann, dass der kritische Bereich nur von einem Programm gleichzeitig bearbeitet wird.

Lebensdauer

Der Shared Memory bleibt solange erhalten, bis ein Programm in explizit entfernt (s. o.), bis zum nächsten Shutdown oder bis er mit dem Befehl ipcrm explizit gelöscht wird.

ipcrm shm id

Synchronisation mit Semaphoren

Semaphoren gehen auf den Informatiker E. W. Dijkstra zurück. Sie dienen als Schutz gegen gleichzeitiges Operieren in einem kritischen Bereich. So wird beispielsweise der gemeinsame Speicher (Shared Memory) von zwei oder mehr Prozessen gleichzeitig genutzt. Dabei muss verhindert werden, dass zwei Prozesse gleichzeitig schreiben oder dass ein Prozess liest, während ein anderer Prozess gleichzeitig schreibt.

semget()

Das Erzeugen einer Semaphore erfolgt analog zum Shared Memory. Selbst die Konstante IPC_CREAT ist identisch.

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

int semget(key_t key, int nsems, int semflg);

Der key ist wie bei Shared Memory eine Zahl, die von den Programmen vereinbart wird, die über die Semaphore kommunizieren wollen. Im zweiten Parameter wird angegeben, wieviele Semaphoren erzeugt werden sollen.

Semaphoren kennen Berechtigungen

Der letzte Parameter enthält die Berechtigung, wie man sie von
chmod kennt. Mit dieser wird die Konstante IPC_CREAT geodert, damit die Semaphore angelegt wird, falls sie noch nicht existierte. Soll der Aufruf scheitern, wenn es bereits eine solche Semaphore gibt, odert man auch noch die Konstante IPC_EXCL. Die mit den Semaphoren verbundenen Datenstrukturen werden durch semget() nicht initialisiert. Dies muss durch den Aufruf von semctl() mit dem Kommando SETVAL oder SETALL erfolgen. Der Rückgabewert ist die Semaphoren ID, die für die Identifikation benötigt wird. Scheiterte der Aufruf, gibt er -1 zurück.

semop()

Die Semaphoren werden mit dem Aufruf von semop() bearbeitet.

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

int semop(int semid, struct sembuf *sops, unsigned anzahl);

Als Parameter semid wird der Rückgabewert von semget() verwendet. Der zweite Paramter nimmt die Adresse eines Array von Strukturen sembuf auf, die je eine Semaphorenoperation beschreiben. Wieviele Elemente in dem Array stehen, gibt der dritte Parameter anzahl. Die Struktur sembuf beinhaltet die folgenden Felder:

struct sembuf {
    short sem_num;  /* semaphore number: 0 = first */
    short sem_op;   /* semaphore operation */
    short sem_flg;  /* operation flags */
};

sem_num gibt an, welche Semaphore des Semaphorensets gemeint ist. Die Zählung beginnt bei 0. sem_flag kann die Optionen IPC_NOWAIT und SEM_UNDO annehmen. SEM_UNDO bewirkt, dass die Operation bei Ende des Prozesses zurückgenommen wird. Der Systemaufruf gewährleistet, dass die Operationen nur durchgeführt werden, wenn alle Operationen gelingen. Die Variable sem_op der Struktur sembuf bestimmt die Operation.

semop>0
Der Wert der Semaphore wird um den Wert von semop erhöht. Diese Operation blockiert nie.
semop==0
Wenn der Wert der Semaphore (semval) Null ist, läuft die Operation durch. Ansonsten blockiert der Prozess, bis die Semaphore Null wird.
semop<0
Der Wert der Semaphore wird um den Wert von semop verringert, sofern die Semaphore durch diese Operation nicht negativ wird. Ist semop größer als der Wert der Semaphore, schläft der Prozess bis der Wert hoch genug ist.

semctl()

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

int semctl(int semid, int semnum, int cmd, union semun arg);

Das Kommando kommando gibt an, welche Operation ausgeführt wird. Das wichtigste ist IPC_RMID. Es zerstört sofort alle Semaphoren dieses Semaphorensets. Die auf die Semaphoren wartenden Prozesse werden geweckt und erhalten einen Fehler als Rückgabewert ihres Funktionsaufrufs.

Beispiel

Das Programm sem erzeugt eine Semaphore, sofern nicht schon eine existiert, und wartet auf die Returntaste. Anschließend setzt sie die Semaphore. Das führt zum Blockieren oder das Programm betritt den kritischen Bereich. Das wird am Bildschirm angezeigt. Nach erneutem Drücken der Returntaste verläßt das Programm wieder den kritischen Bereich und gibt ihn für andere frei. Zum Testen kann man das Programm auf mehreren Terminals oder Fenstern starten und sie nacheinander in den kritischen Bereich gehen lassen.

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

int main(int argc, char **argv)
{
int semID;
struct sembuf sema;

    /* Semaphore erzeugen */
    semID = semget(2404, 1, IPC_CREAT | 0666);
    if (semID >= 0) {
        puts("Semaphore erzeugt. Vor Anfrage");
        getchar();
        /* Bereite die Semaphore vor und starte */
        sema.sem_num = 0;
        sema.sem_flg = SEM_UNDO;
        sema.sem_op  = -1;
        if (-1==semop(semID, &sema, 1)) {
            /* Fehler */
            perror("semop");
        }
        puts("bin im kritischen Bereich");
        getchar();
        sema.sem_op  = 1;
        if (-1==semop(semID, &sema, 1)) {
            /* Fehler */
            perror("semop");
        }
        puts("und nun wieder draußen");
    } else {
        perror("semget");
    }
}

Auch die Semaphore bleibt nach Verlassen des Programms bestehen und muss explizit gelöscht werden. Dazu reicht das folgende Programmbeispiel aus:

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

int main(int argc, char **argv)
{
int semID;
char *myPtr;
int i;

    semID = semget(2404, 1, 0666);
    if (semID >= 0) {
        /* zerstöre die Semaphore */
        semctl(semID, 1, IPC_RMID, 0);
    } else { /* semctl lief schief */
        perror("semget");
    }
}

Lebensdauer

Die Semaphore bleibt solange erhalten, bis ein Programm in explizit entfernt (s. o.), bis zum nächsten Shutdown oder bis er mit dem Befehl ipcrm explizit gelöscht wird.

ipcrm sem id

Message Queues

Message Queues dienen zum Senden und Empfangen von Nachrichten. Eine solche Nachrichtenschlange kann Nachrichten verschiedenen Typs behandeln. 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 lieber mit einem Fehler zurückkehren möchte. Auf der anderen Seite kann ein Prozess eine Nachricht bestimmten Typs anfordern. Auch hier steht es dem Programmierer frei, ob er möchte, dass der Prozess wartet, bis er eine passende Nachricht bekommt oder ob er mit einer Fehlermeldung sofort zurückkehren soll.

msgget()

Die Funktion msgget() legt eine Message Queue an.

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

int msgget(key_t key, int msgflg);

Der Parameter key ist entweder eine Schlüsselzahl oder IPC_PRIVATE. Der Parameter msgflg kombiniert die Konstanten IPC_CREAT und IPC_EXCL und neun Berechtigungsbits für den Eigner, die Gruppe und der Welt, wie sie von chmod verwendet werden, indem sie mit dem senkrechten Strich geodert werden. Der Rückgabewert ist -1 im Fehlerfall oder die Message Queue ID, die für die nächsten Aufrufe benötigt wird. Die Funktionen msgsnd() und msgrcv() verwenden eine Struktur msgbuf für ihre Nachrichten, die den Typ und den Puffer enthält.

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. Auf diese Weise kann man leicht verschiedene Arten von Daten austauschen und sie über den Nachrichtentyp trennen. Für die eigenen Nachrichten wird man im mtext vermutlich mehr als ein Zeichen versenden wollen. Dazu definiert man sich eine eigene Struktur mit entsprechend größerem Datenpuffer. Die Größe wird beiden Funktionen als Parameter übergeben.

msgsnd()

Mit der Funktion msgsnd() werden Nachrichten versandt.

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

int msgsnd(int msqid, struct  msgbuf  *msgp,  size_t msgsz, 
           int msgflg);

Der erste Parameter ist der Rückgabewert der Funktion msgget(). Es folgt die Adresse der Datenstruktur mit dem Nachrichtentyp und den Daten. Der Parameter msgsz ist so groß wie das Array mtext in der Datenstruktur für die Nachricht. msgflg kann mit der Optionen IPC_NOWAIT besetzt werden. Dann wird die Funktion bei einer übervollen Message Queue nicht blockieren und warten, bis wieder Platz ist, sondern mit einem Fehler zurückkehren.

msgrcv()

Mit der Funktion msgrcv() werden werden Nachrichten empfangen.

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

int msgrcv(int msqid, struct  msgbuf  *msgp,  size_t msgsz, 
           long msgtyp, int msgflg);

Der erste Parameter ist der Rückgabewert der Funktion msgget(). Es folgt die Adresse der Datenstruktur, in der sich nach erfolgreichem Empfang der Nachrichtentyp und die Daten wiederfinden. Der Parameter msgsz ist so groß wie das Array mtext in der Datenstruktur für die Nachricht. Im Parameter msgtyp kann festgelegt werden, auf welchen Nachrichtentyp msgrcv() warten soll. Alle anderen Typen werden von msgrcv() ignoriert. Wird als Parameter hier 0 angegeben, nimmt mgsrcv() jeden Typ entgegen. msgflg kann mit der Optionen IPC_NOWAIT besetzt werden. Dann wird die Funktion nicht blockieren und warten, bis eine Nachricht vorliegt, sondern bei leerer Message Queue mit einem Fehler zurückkehren.

msgctl()

Mit der Funktion msgctl() werden bestimmte Eigenschaften der Nachrichten verwaltet.

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

int msgctl(int msqid, int kommando, struct msqid_ds *buf);

Als kommando können folgende Konstanten übergeben werden:
Konstante Bedeutung
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 kann man einen Überblick über die angeforderten Message Queues bekommen.

Beispiel

Das Programm rcvmsg.c wartet auf eine Nachricht in einer Message Queue. Die Nummer des Typs wird als erster Parameter beim Aufruf übergeben. Wird nichts übergeben wartet das Programm auf eine beliebige Nachricht.

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

#define MSGSIZE 20

int main(int argc, char **argv)
{
int msgID;
struct myMsg {
  long mtype;
  char mtext[MSGSIZE];
} dataMsg;
long msgTyp = 0;

    /* hole die Messagetypnummer aus dem ersten Parameter */
    if (argc>1) {
        msgTyp = atol(argv[1]);
    }

    /* Messagequeue oeffnen bzw. erzeugen */
    msgID = msgget(2404, IPC_CREAT | 0666);
    if (msgID >= 0) {
        printf("Warte auf Message Type %ld\n", msgTyp);
        if (-1==msgrcv(msgID, &dataMsg, MSGSIZE, msgTyp, 0)) {
            perror("msgrcv"); /* Fehler */
        } else {
            /* Wir sind durchglaufen */
            printf("Daten empfangen: %s\n", dataMsg.mtext);
        }
    } else {
        perror("msgget");
    }
}

Das Programm sndmsg.c sendet Nachrichten. Der Nachrichtentyp wird wie bei rcvmsg.c als erster Parameter übergeben. Als zweiter Parameter kann ein String übergeben werden, der dann als Daten in der Messages Queue abgestellt wird und den rcvmsg dann empfängt.

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

#define MSGSIZE 20

int main(int argc, char **argv)
{
int msgID;
struct myMsg {
  long mtype;
  char mtext[MSGSIZE];
} dataMsg;
long msgTyp = 0;

    /* hole die Messagetypnummer aus dem ersten Parameter */
    if (argc>1) {
        dataMsg.mtype = atol(argv[1]);
    }
    if (argc>2) {
        strncpy(dataMsg.mtext, argv[2], MSGSIZE);
    } else {
        *dataMsg.mtext = 0;
    }
    /* Messagequeue oeffnen bzw. erzeugen */
    msgID = msgget(2404, IPC_CREAT | 0666);
    if (msgID >= 0) {
        printf("Sende Messagetyp %ld\n", dataMsg.mtype);
        if (-1==msgsnd(msgID, &dataMsg, MSGSIZE, 0)) {
            perror("msgsnd"); /* Fehler */
        } else {
            /* Wir sind durchglaufen */
            printf("Daten gesendet: %s\n", dataMsg.mtext);
        }
    } else {
        perror("msgget");
    }
}

Mit Hilfe der beiden Programme läßt sich das Verhalten der Message Queue leicht testen. Eine Anpassung an eigene Befürfnisse dürfte eine leichte Übung sein.

Lebensdauer

Die Message Queue bleibt solange erhalten, bis ein Programm sie explizit per msgctl() mit dem Kommando IPC_RMID entfernt, bis zum nächsten Shutdown oder bis sie mit dem Befehl ipcrm gelöscht wird.

ipcrm msg id


Homepage (C) Copyright 2002 Arnold Willemer