9.5 Semaphore
Um es gleich von vornherein klarzustellen, die Semaphore nach System V haben nichts mit den Kernel-Semaphoren zu tun. Rein theoretisch könnten Sie Semaphore selbst nachbilden. Hierzu würden eine globale Variable und eine Funktion ausreichen. Will man nun einen kritischen Codebereich für andere Prozesse sperren, müsste die Funktion die globale Variable auf 0 setzen. Alle anderen Prozesse, die jetzt ebenfalls in diesen kritischen Bereich kommen, müssen nun warten, bis die globale Variable wieder einen positiven Wert bekommt (1). Dies ist natürlich nur rein theoretisch gesehen.
Meistens werden Semaphore binärer Natur verwendet (wie eben beschrieben). Das sind Semaphore, die man für gewöhnlich verwendet, um kritische Ressourcen zu schützen. Dazu reichen die zwei Zustände (bi) 1 und 0 aus. Es lassen sich aber auch allgemeine Semaphore verwenden – die dann allerdings die Werte 0, 1, 2, 3 ... n annehmen können. Der höchste Wert zeigt dann an, wie viele Prozesse den kritischen Bereich noch verwenden.
In System V sind die Semaphore mittlerweile mehr als nur globale Variablen geworden. Um mit diesen Semaphoren zu arbeiten, steht dem Entwickler ein ganzer Satz an Funktionen zur Verfügung. Es ist damit auch möglich, nicht nur auf einem, sondern auf einem ganzen Satz von Semaphoren Operationen durchzuführen.
9.5.1 Lebenszyklus eines Semaphors
Der Titel ist ein wenig zweischneidig und soll Ihnen mehr oder weniger den üblichen Weg bzw. die Verwendung eines Semaphors zeigen. Hier die einzelnen Schritte.
|
Zuerst wird ein Semaphor erzeugt, oder, falls es existiert, das bereits erzeugte wird geöffnet. Beides geschieht mit der Funktion semget(). Ein erzeugtes Semaphor sollte zu Beginn gleich auf einen positiven Wert (1) gesetzt werden – was die Funktion semctl() erledigt. Für alle anderen Prozesse, die ein bereits erzeugtes Semaphor verwenden, sollte dies unbedingt unterbleiben. Denn würde jeder Prozess das Semaphor auf einen positiven Wert setzen, so würde vielleicht ein Prozess, der auf einen anderen Prozess wartet, der gerade in einem kritischen Codeausschnitt arbeitet, ebenfalls Zugriff auf den kritischen Codeausschnitt bekommen. |
|
Wird jetzt eine Sperre für einen kritischen Codebereich benötigt, wird das Semaphor auf den Wert 0 gesetzt – dies erledigen Sie mit der Funktion semop(). |
|
Wurde jetzt ein weiterer Prozess gestartet, der nun ebenfalls auf diesen kritischen Bereich zugreifen will, wird das Semaphor überprüft, das für die Synchronisation des Bereichs verantwortlich ist. Ist der Wert des Semaphors positiv, so kann der Prozess auf diesen Bereich zugreifen. Dabei dekrementiert logischerweise der Prozess dieses Semaphor, so dass hier wiederum kein anderer Prozess zugreifen kann. Ist hingegen das Semaphor gleich 0, wartet der Prozess so lange, bis der kritische Codebereich freigegeben wird – genauer, bis der Wert des Semaphors wieder positiv (1) ist. |
|
Ist ein Prozess mit dem kritischen Codebereich fertig, muss der Wert des Semaphors wieder auf 1 gesetzt werden, um es den anderen Prozessen zu gestatten, ebenfalls auf diesen Bereich zuzugreifen. Dies wird ebenfalls wieder mit der Funktion semop() gemacht. |
Diesen einfachen Vorgang will ich Ihnen nun anhand eines Listings mit anschließender Erklärung demonstrieren:
/* sem.c */
#include <stdio.h>
#include <unistd. h.>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define LOCK -1
#define UNLOCK 1
#define PERM 0666 /* Zugriffsrechte */
#define KEY 123458L
static struct sembuf semaphore;
static int semid;
static int init_semaphore (void) {
/* Testen, ob das Semaphor bereits existiert */
semid = semget (KEY, 0, IPC_PRIVATE);
if (semid < 0) {
/* ... existiert noch nicht, also anlegen */
/* Alle Zugriffsrechte der Dateikreierungsmaske */
/* erlauben */
umask(0);
semid = semget (KEY, 1, IPC_CREAT | IPC_EXCL | PERM);
if (semid < 0) {
printf ("Fehler beim Anlegen des Semaphors ...\n");
return -1;
}
printf ("(angelegt) Semaphor-ID : %d\n", semid);
/* Semaphor mit 1 initialisieren */
if (semctl (semid, 0, SETVAL, (int) 1) == -1)
return -1;
}
return 1;
}
static int semaphore_operation (int op) {
semaphore.sem_op = op;
semaphore.sem_flg = SEM_UNDO;
if( semop (semid, &semaphore, 1) == -1) {
perror(" semop ");
exit (EXIT_FAILURE);
}
return 1;
}
int main (void) {
int res;
res = init_semaphore ();
if (res < 0)
return EXIT_FAILURE;
printf ("Vor dem kritischen Codeausschnitt ...\n");
semaphore_operation ( LOCK );
/* Kritischer Codeausschnitt */
printf ("PID %d verwendet Semaphor %d\n",
getpid(), semid);
printf ("Im kritischen Codeabschnitt ...\n");
sleep (10);
semaphore_operation ( UNLOCK );
printf ("Nach dem kritischen Codeausschnitt ...\n");
//semctl (semid, 0, IPC_RMID, 0);
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
tty1 $ gcc -o sem sem.c
tty1 $ ./sem
Semaphor-ID : 1048577
Vor dem kritischen Codeausschnitt ...
PID 3582 verwendet Semaphor 1048577
Im kritischen Codeabschnitt ...
[inzwischen vor Ablauf der 10 Sekunden in eine weitere Konsole]
tty2 $ ./sem
Semaphor-ID : 1048577
Vor dem kritischen Codeausschnitt ...
[Wartet auf Freigabe des Semaphors von PID 3582]
[ inzwischen tty0 ]
Nach dem kritischen Codeausschnitt ...
$ tty1
[Jetzt ist das Semaphor frei für tty1]
PID 3583 verwendet Semaphor 1048577
Im kritischen Codeabschnitt ...
[nach 10 Sek.]
Nach dem kritischen Codeausschnitt ...
Sie können hierzu jetzt beliebig viele Prozesse starten. Es kann aber immer nur ein Prozess in dem kritischen Codebereich zugreifen. Einen Schönheitsfehler hat dieses Beispiel allerdings dann doch noch. Geben Sie in der Kommandozeile Folgendes ein:
$ ipcs
Schlüssel shmid Besitzer Rechte Bytes nattch Status
0x0000000 131074 tot 777 196608 2 dest
0x0000000 1971978243 root 644 110592 2 dest
0x0000000 1975975940 root 644 110592 2 dest
0x0000000 1981513733 root 644 110592 2 dest
----- Semaphorfelder -----
Schlüssel SemID Besitzer Rechte nsems
0x0001e242 1048577 tot 666 1
----- Nachrichtenwarteschlangen -----
Schlüssel msqid Besitzer Rechte used-bytes messages
Hier sind momentan nur die Semaphorfelder von Interesse. Das Semaphor bleibt also nach Beendigung aller Prozesse erhalten. Von »Hand« können Sie dieses folgendermaßen entfernen (die ID sei 1048577):
$ ipcrm -s 1048577
Das dürfte aber bei sehr vielen angelegten Semaphoren ein Problem werden. Die folgende (auskommentierte) Zeile würde diesen Vorgang zwar für Sie übernehmen:
//semctl (semid, 0, IPC_RMID, 0);
hätte aber zur Folge, dass nach Beendigung des ersten Prozesses das Semaphor gelöscht wird, und alle anderen Prozesse hätten diesbezüglich kein Semaphor mehr zur Verfügung. Daher verwendet man auch hier in der Praxis das Server-Client-Prinzip. Der Server erzeugt ein Semaphor und kümmert sich auch wieder darum, dass dies beseitigt wird. Die Clients greifen nur auf dieses eine Semaphor zu. Aber jetzt erst zu den Beschreibungen der einzelnen Funktionen im Listing.
Zuerst benötigen Sie, um ein Semaphor anzulegen, die magische Nummer, die hier einfach mit 123458L definiert wurde. Anhand dieser Nummer werden die Semaphore eindeutig im System identifiziert.
9.5.2 Ein Semaphor öffnen oder erstellen – semget()
Die erste Funktion, die im Listing aufgerufen wurde, war init_semaphore(). Zuerst wird versucht, mit der Funktion semget() ein bereits erstelltes Semaphor zu öffnen.
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/sem.h>
int semget(key_t key, int n_sems, int flag);
Als erstes Argument bekommt semget() die magische Nummer. Das zweite Argument entspricht der Anzahl der Semaphore im Satz, und mit dem letzten Argument können die Zugriffsprivilegien vergeben werden. Bei den Flags können Sie im Prinzip drei Angaben machen, was ebenso auf die Message Queues und die Shared Memories zutrifft.
|
IPC_PRIVATE – Öffnen eines privaten Schlüssels |
|
IPC_CREAT – Damit wird ein noch nicht existierender Schlüssel erzeugt. |
|
IPC_EXCL – Dieses Flag wird in der Regel mit dem bitweisen ODER-Operator hinter IPC_CREAT angefügt. Damit gehen Sie sicher, dass wirklich ein neues Objekt (Semaphor) angelegt wird und nicht ein bereits existierendes mit derselben Kennung. Existiert bereits eine Kennung, bricht die entsprechende Funktion – hier semget() – mit einem Fehler ab (errno == EEXIST). |
Beim ersten semget()-Aufruf geben Sie als erstes Argument den magischen Schlüssel an, als zweites Argument 0, was bedeutet, dass Sie eine bereits existierende Semaphormenge öffnen wollen. Als letztes Argument geben Sie IPC_PRIVATE an – also das Öffnen eines bereits existierenden Schlüssels. Bei erfolgreichem Funktionsaufruf gibt semget() die Semaphor-ID zurück, die Sie in der Konsole mit dem Tool ipcs ermitteln können. Schlägt die Funktion hingegen fehl, was beim ersten Aufruf der Fall sein sollte, so gibt semget() -1 zurück. Somit springt das Programmbeispiel mit seiner Ausführung in die if-Verzweigung.
Darin wird nun versucht, eine neue Semaphormenge zu erstellen:
semid = semget (KEY, 1, IPC_CREAT | IPC_EXCL | PERM);
Auch hier wird als erstes Argument die magische Kennung übergeben, als zweites Argument die Anzahl der Semaphormenge (1). Und zu guter Letzt die Flags IPC_CREAT, IPC_EXCL und, sehr wichtig, die Zugriffsrechte auf die Semaphore. Vergessen Sie die Zugriffsrechte oder setzen Sie diese falsch, werden Sie keinen Zugriff auf die Semaphormenge haben. Zurückgegeben wird hier bei Erfolg die Semaphor-ID.
9.5.3 Abfragen, Ändern oder Löschen der Semaphormenge – semctl()
Im nächsten Schritt setzen Sie den Wert der Semaphorvariablen auf den Wert 1. Dieser Vorgang ist ebenfalls von Bedeutung. Würden Sie die Semaphore gleich zu Beginn auf -1 setzen, dann müssen dies die Clientprogramme bzw. das Programm, das auf die Semaphormenge zugreifen will, ebenso berücksichtigen. Gewöhnlich setzt man den Wert allerdings auf 1. Ein negatives Setzen der Semaphorvariablen zu Beginn entspricht etwa dem Sinn bei einer Bahnschranke, die nur öffnet, wenn ein Auto durchfahren will, was ja gewöhnlich umgekehrt ist.
Das Setzen, aber auch Abfragen oder Löschen einer Semaphormenge lässt sich alles mit der Funktion semctl() realisieren:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semphoren_ID, int sem_num,
int kommando, union semun arg);
Als erstes Argument legen Sie die Semaphore fest, worauf sich semctl() beziehen soll. Die ID dazu haben Sie als Rückgabewert der Funktion semget() erhalten. Mit dem zweiten Argument spezifizieren Sie einen bestimmten Semaphorwert aus der Menge. sem_num ist auch bei einigen der kommando-Angaben von Bedeutung. Bevor auf die einzelnen Kommandos und deren Bedeutung eingegangen wird, zuerst noch die union semun, die wie folgt definiert ist:
#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; /* Werte für SETVAL */
struct semid_ds *buf; /* Puffer IPC_STAT, IPC_SET */
unsigned short *array; /* Array für GETALL, SETALL */
/* Linux specific part: */
struct seminfo *__buf; /* Puffer für IPC_INFO */
};
#endif
Jetzt zu den einzelnen gängigen Kommandos, die als drittes Argument angegeben werden können, und deren Bedeutung. Für mehr Details sollten Sie allerdings die Manual Page lesen, da eine ausführlichere Erklärung weit über den Rahmen des (ohnehin schon umfangreichen) Kapitels hinausgehen würde.
Tabelle 9.1
Kommandoangaben für die Funktion semctl()
Kommando
|
Bedeutung
|
IPC_STAT
|
Hiermit kann die Struktur semid_ds angefragt werden. Der Inhalt befindet sich anschließend in der Adresse von buf.
|
IPC_SET
|
Hiermit können Eigentümer und die Zugriffsrechte gesetzt werden.
|
IPC_RMID
|
Löschen einer Semaphormenge
|
GETVAL
|
Hiermit fragen Sie den Wert der Semaphorvariablen ab.
|
SETVAL
|
Hiermit können Sie den Wert der Semaphorvariablen setzen.
|
GETPID
|
Gibt die PID des Prozesses zurück, der zuletzt auf die Semaphorvariablen zugegriffen hat.
|
GETNCNT
|
Abfragen der Prozesse, die warten, bis die Semaphorvariable größer als 0 wird
|
GETZCNT
|
Abfragen der Prozesse, die warten, bis die Semaphorvariable gleich 0 ist
|
GETALL
|
Abfragen der Werte aller Semaphorvariablen
|
SETALL
|
Setzen aller Semaphorvariablen
|
IPC_INFO
|
(nur Linux) Damit lassen sich Informationen zur entsprechenden Semaphor erfragen.
|
Der Rückgabewert der Funktion semctl() ist entweder bei Erfolg 0 oder, wurde eines der GET..-Kommandos verwendet, dann eben ein entsprechender Wert. Bei einem Fehler gibt semctl()-1 zurück.
Somit hätten Sie die Semaphore für den eigentlichen Arbeitsvorgang eingerichtet; wobei der weitere Vorgang keine Bäume mehr ausreißen dürfte.
9.5.4 Operationen auf Semaphormengen – semop()
Eine Operation können Sie mit der Funktion semop()durchführen – wobei mit durchführen das Verändern der Semaphorvariablen gemeint ist.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf sem_array[], size_t n_op);
Mit dem ersten Argument legen Sie wieder fest, auf welcher Semaphormenge die Operationen mit der Funktion semop() durchgeführt werden sollen. sem_array ist eine Adresse eines Arrays von Semaphoroperationen. Folgende Elemente sind in dieser Struktur enthalten:
struct sembuf {
unsigned short sem_num; /* Semaphornummer der Menge */
short sem_op; /* Semaphoroperation */
short sem_flg; /* Flags: IPC_NOWAIT,SEM_UNDO */
}
Mit dem dritten Argument von semop() geben Sie die Anzahl der Elemente im Array sem_array an, was durchaus mehr als, wie hier im Beispiel demonstriert, ein Element sein kann.
Für die Angabe von sem_op in der Struktur sembuf sind folgende drei Fälle zu unterscheiden:
|
sem_op > 0 – Ist sem_op größer als 0, also 1, wird die Ressource für alle anderen Prozesse freigegeben. |
|
sem_op < 0 – Ist die Semaphorvariable kleiner als 0, also -1, dann ist der kritische Bereich für alle anderen Prozesse gesperrt. Will jetzt ein weiterer Prozess auf diese Ressource zugreifen, hängt dies davon ab, ob sem_flag, das dritte Element der Struktur sembuf, auf IPC_NOWAIT gesetzt wurde. Ist dies der Fall, schlägt der Funktionsaufruf semop() fehl. Ist IPC_NOWAIT nicht gesetzt, wartet der Prozess so lange, bis die Ressource wieder freigegeben wurde. |
|
sem_op == 0 – Wird die Semaphorvariable auf 0 gesetzt, kehrt semop() sofort wieder zurück. Wenn die Semaphorvariable ungleich 0 ist, hängt dies wiederum von sem_flag und IPC_NOWAIT ab. Ist IPC_NOWAIT gesetzt, beendet sich semop() mit einem Fehler. Ansonsten wartet der Prozess so lange, bis entweder sem_op gleich 0 ist oder das Semaphor gelöscht wurde. |
Hilfreich ist auch das Flag SEM_UNDO. Wird nämlich ein Prozess vorzeitig beendet, wird damit sichergestellt, dass mithilfe eines undo-Zählers das vom Prozess gesetzte Semaphor wieder zurückgesetzt wird.
Anders als im Beispiel gesehen können Sie sembuf auch wie folgt mit Werten initialisieren:
struct sembuf semaphore_lock[1] = { 0, -1, SEM_UNDO };
struct sembuf semaphore_unlock[1] = { 0, 1, SEM_UNDO };
...
/* Ressource sperren */
semop(semid, &semaphore_lock[0], 1);
...
/* Ressource freigeben */
semop(semid, &semaphore_unlock[0], 1 );
Um in diesem Beispiel die Funktion semaphore_operation() komplett zu machen, sollten Sie noch eine weitere symbolische Konstante 0 als ZERO definieren, womit Sie alle drei Möglichkeiten der Semaphorvariablen ausgeschöpft hätten. Mit
semaphore_operation ( LOCK );
sperren Sie einen kritischen Bereich. LOCK ist als -1 definiert. Und mittels
semaphore_operation ( UNLOCK );
geben Sie die Ressource wieder frei, da UNLOCK als 1 definiert wurde.
9.5.5 Semaphore im Vergleich mit Sperren
Semaphore sind den Sperren ja nicht unähnlich, weshalb ein Vergleich hier lohnt. Auf beiden Seiten hat man einen Vorteil. Die Verwendung von Sperren (Record Locking) gestaltet sich wesentlich einfacher. Dafür ist der Geschwindigkeitsvorteil ganz klar auf der Seite der Semaphore. Somit bleibt es letztendlich dem Entwickler selbst überlassen, wofür er sich entscheidet.
|