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