Axel Rogat
Betriebssysteme und betriebssystemnahes Programmieren
 
9.3: Message Queues Kapitel 9 9.5: Shared Memory 
 
  9.4 Semaphore  
 

Die Werte von System-V-Semaphoren sind vom Typ unsigned short, der maximale Wert ist SEMVMX (aus sys/sem.h, z.B. 32767). Man kann allerdings gleich mit einer ganzen Menge von Semaphoren in einem Aufruf arbeiten (UND-Synchronisation). Das macht z.B. immer Sinn, wenn man mit mehreren verknüpften Ressourcen arbeitet.

Die verwendeten Funktionen und Strukturen sind in sys/sem.h und sys/ipc.h definiert.

UNIX
int semget(key_t key, int nsems, int flg);
  get semaphore, erfragt die ID einer bestehenden Semaphor-Menge oder legt eine neue an (siehe 9.2). nsems gibt die Anzahl der Elemente an. Die Semaphor-Werte sind nach dem Aufruf uninitialisiert!
int semctl(int semid, int semnum, int cmd, union semun arg);
  semaphore control, führt eine Kontroll-Operation auf der Semaphor-Menge mit der ID semid aus. semnum selektiert ggf. einen einzelnen Semaphor. cmd wählt das genaue Kommando aus. arg enthält Kommando-spezifische Argumente.
int semop(int semid, struct sembuf *sops, unsigned int nsops);
  semaphore operation, führt mehrere Semaphor-Operationen atomar aus. sops ist ein Zeiger auf ein Array mit den Operations-Beschreibungen, nsops die Länge des Arrays. Nur wenn alle Operationen erfolgreich wären, werden sie auch überhaupt durchgeführt.

9.4.1 semget

Ein Server-Prozeß könnte wie folgt eine Menge von acht Semaphoren anlegen und die erhaltene ID durch ein Lockfile bekanntgeben:
int semid=semget(IPC_PRIVATE,8,0666|IPC_CREAT); int lockfile=open("/tmp/semtest_lock",O_WRONLY|O_CREAT); if (lockfile<=0) exit(0); write(lockfile,&semid,sizeof(int)); close(lockfile);
Ein Client-Prozeß erhält den Zugriff auf diese Menge wie folgt über das Lockfile:
int semid; int lockfile=open("/tmp/semtest_lock",O_RDONLY); if (lockfile<=0) exit(0); read(lockfile,&semid,sizeof(int)); close(lockfile);
ipcs gibt in etwa folgendes aus:
------ Semaphore Arrays -------- semid owner perms nsems status 1 axel 0 8
Leider sind die neuen Semaphoren nicht direkt im semget-Aufruf mit Werten belegbar. Sie werden alle mit 0 initialisiert, was üblicherweise bedeutet, daß die Ressourcen, die sie schützen, als "belegt" markiert sind.

Ist das (wie meistens) nicht gewünscht, muß man danach einen semctl-Aufruf (s.u.) tätigen. Leider kann der Prozeß dann aber zwischen den beiden Aufrufen bereits unterbrochen werden! Das könnte zu großen Problemen führen.

In unserem Server-Beispiel läßt sich das glücklicherweise dadurch lösen, daß die ID einfach erst dann in die Datei geschrieben wird, wenn die Semaphore ihre Werte erhalten haben.

9.4.2 semctl

Diese Funktion ist für diverse Kontroll-Operationen auf einer Semaphor-Menge gedacht. Es gibt u.a. folgende Kommandos (Parameter cmd):
IPC_SET Setzen von Besitzern und Zugriffsrechten
IPC_RMID Löschen der ganzen Semaphor-Menge
GETVAL Lesen eines Semaphor-Werts
GETALL Lesen aller Semaphor-Werte
SETVAL Setzen eines Semaphor-Werts
SETALL Setzen aller Semaphor-Werte
GETPID Lesen der PID des Prozesses, der zuletzt auf den Semaphor Nummer semnum zugegriffen hat
Die verwendete Struktur semun ist wie folgt aufgebaut:
union semun { int val; // Wert beim Kommando SETVAL struct semid_ds *buf; // Werte bei IPC_STAT und IPC_SET ushort *array; // Puffer bei GETALL und SETALL };
Wir schauen uns hier nur die einfachste Operation an, nämlich das Löschen einer Semaphor-Menge. Der Parameter semnum wird ignoriert, die ganze Menge wird gelöscht. arg wird auch nicht benötigt, ein entsprechender Parameter muß leider dennoch übergeben werden:
union semun dummy; semctl(semid,0,IPC_RMID,dummy);

9.4.3 semop

Hiermit führt man up- und down-ähnliche Operationen auf einer Semaphor-Menge aus. Es wird ein ganzes Array von Operationen angegeben, und es ist garantiert, daß sie insgesamt atomar ausgeführt werden.
Die Funktion semop erhält als Parameter ein Array sops (der Länge nsops) aus Objekten der folgenden Art:
struct sembuf { short sem_num; // Nummer des Semaphors im Array, ab 0 short sem_op; // Art der Operation short sem_flg; // Flags: IPC_NOWAIT oder SEM_UNDO };
Die Werte von sem_op haben folgende Auswirkungen (Lese- bzw. Schreiberlaubnis des jeweiligen Prozesses vorausgesetzt):

Blockaden werden auch aufgehoben, wenn der Prozeß ein Signal erhält, oder wenn der Semaphor zwischendurch zerstört wird (semop liefert dann -1 zurück)!

Die Flags haben folgende Bedeutung:

up und down sind also nicht direkt implementiert, sondern lassen sich mit semop folgendermaßen nachbilden:

static void sem_up_down(int id, int value) { static struct sembuf semaphor; semaphor.sem_op=value; semaphor.sem_flg=SEM_UNDO; if (semop(id,&semaphor,1)) { perror("down"); exit(10); } }

inline void sem_down(int id) { sem_up_down(id,-1); } inline void sem_up(int id) { sem_up_down(id,1); }

9.4.4 Eine Klasse für Semaphore

Sehr oft braucht man nur ganz grundlegende Semaphor-Operationen und ist gezwungen, die sehr allgemein gehaltenen Systemaufrufe mit diversen Parametern zu versehen. Als Beispiel stellen wir daher hier eine einfache C++-Klasse vor, die die wichtigsten Mechanismen zur Verfügung stellt.

Wir implementieren dabei die einfachen up und down, wie auch die simultanen Operationen sup und sdown. Letztere nehmen beliebig viele Semaphor-Nummern als Argumente, die Anzahl ist das erste Argument, also beispielsweise sdown(3, 0,3,7);

Zunächst folgt die Header-Datei semarray.h:

class semarray { private: bool creator; // neu angelegt oder nicht? int semid,numsem; // System-ID, Anzahl const char *lockfile; // Name des Lockfiles struct sembuf *sembuffer; // Puffer fur semop void init_creator(const char *, int); void sem_up_down(int semnum, int value); void sdown_sup(va_list &va, int size, int value);

public: // Konstruktoren/Destruktor semarray(const char *lockfile, int size, int value); semarray(const char *lockfile, int size, const int *valarray); semarray(const char *lockfile); ~semarray();

int id() const { return semid; } // ID-Abfrage void down(int num=0) { sem_up_down(num,-1); } // down für einen Sem. void up(int num=0) { sem_up_down(num,1); } // up für einen Sem. void sdown(int size, ...); // simultanes down void sup(int size, ...); // simultanes up };

Verschiedene Prozesse können sich natürlich nicht solche Objekte teilen, auch wenn zwei Objekte intern auf dieselben Semaphore verweisen können. Es gibt daher zwei verschiedene Möglichkeiten, ein semarray-Objekt anzulegen:

Ob die Menge neu erzeugt oder mitverwendet wurde, wird in creator gespeichert. Nur der Erzeuger löscht sie im Destruktor auch automatisch wieder.

Die Funktionen down und up sind wieder wie oben implementiert, wobei jetzt zusätzlich die Nummer des Semaphors in der Menge angegeben werden muß. Für sdown und sup wird ein Array von struct sembuf benötigt, das einmal im Konstruktor angelegt wird.

Ein Server-ähnlicher Prozeß, der nur einen Semaphor z.B. für einen kritischen Bereich benötigt, arbeitet dann in etwa wie folgt mit der Klasse:

int main() { semarray sema("/tmp/semarraylock",1,1); for (;;) { sema.down(); puts("critical section"); sema.up(); puts("non-critical section"); } }
Ein passender Client hängt sich dann mit folgender Definition an:
semarray sema2("/tmp/semarraylock");
Der Rest seines Code könnte dem Server-Code entsprechen.

Es folgt die Implementationsdatei semarray.cpp für die restlichen Funktionen (die Include-Angaben sind aus Platzgründen in einen Kommentar gewandert). Bei irgendwelchen Fehlern wird hier der ganze Prozeß abgebrochen (das könnte man mit Exceptions natürlich eleganter gestalten).

// include sys/ipc.h sys/sem.h stdio.h stdlib.h unistd.h fcntl.h string.h semarray.h

void semarray::init_creator(const char *lockname, int size) { int fd=open(lockname,O_RDONLY); close(fd); if (fd>=0) { fprintf(stderr,"lock %s exists\n",lockname); exit(1); }

semid=semget(IPC_PRIVATE,size,0666|IPC_CREAT); if (semid<0) { perror("semget"); exit(1); }

fd=open(lockname,O_CREAT|O_TRUNC|O_WRONLY,0644); if (fd<0) { perror(lockname); exit(1); } write(fd,&semid,sizeof(semid)); close(fd); lockfile=strdup(lockname); }

semarray::semarray(const char *lockname, int size, int value) : creator(true), lockfile(0), numsem(size), sembuffer(new(struct sembuf)[size]) { init_creator(lockname,size); union semun sem_union; sem_union.val=value; for (int i=0;i<size;++i) semctl(semid,i,SETVAL,sem_union); }

semarray::semarray(const char *lockname, int size, const int *valarray) : creator(true), lockfile(0), numsem(size), sembuffer(new(struct sembuf)[size]) { init_creator(lockname,size); union semun sem_union; for (int i=0;i<size;++i) { sem_union.val=*valarray++; semctl(semid,i,SETVAL,sem_union); } }

semarray::semarray(const char *lockname) : creator(false), lockfile(0) { int fd=open(lockname,O_RDONLY); if (fd<0) { perror(lockname); exit(1); } int id; read(fd,&id,sizeof(id)); close(fd);

union semun sem_union; if (semctl(id,0,GETPID,sem_union)<0) { perror("semctl"); exit(1); } semid=id; struct semid_ds buffer; sem_union.buf=&buffer; semctl(id,0,IPC_STAT,sem_union); numsem=sem_union.buf->sem_nsems; sembuffer=new(struct sembuf)[numsem]; }

semarray::~semarray() { if (creator) { union semun dummy; semctl(semid,0,IPC_RMID,dummy); if (lockfile) { unlink(lockfile); free((char *)lockfile); } } delete[] sembuffer; }

void semarray::sdown_sup(va_list &ap, int size, int value) { struct sembuf *P=sembuffer; for (int i=0;i<size;++i,++P) { int semnum=va_arg(ap,int); if (semnum>=numsem) { fprintf(stderr,"illegal semaphore\n"); exit(1); } P->sem_num=semnum; P->sem_op=value; P->sem_flg=0; } va_end(ap); if (semop(semid,sembuffer,size)) { perror("semop2"); exit(1); } }

void semarray::sdown(int size, ...) { va_list ap; va_start(ap,size); sdown_sup(ap,size,-1); }[COMMAND:goodbreak]void semarray::sup(int size, ...) { va_list ap; va_start(ap,size); sdown_sup(ap,size,1); }

void semarray::sem_up_down(int semnum, int value) { struct sembuf sbuf={ semnum, value, 0 }; if (semnum>=numsem) { fprintf(stderr,"illegal semaphore\n"); exit(1); } if (semop(semid,&sbuf,1)) { perror("semop1"); exit(1); } }

Beispiel: Wir implementieren die Semaphor-Lösung für das Philosophen-Problem mit Hilfe der Klasse semarray.

Der erste Prozeß legt das Semaphor-Array (fünf Semaphore, einer pro Stäbchen) neu an. Dann erzeugt er vier Kinder, die sich mit dem dritten Konstruktor an das erzeugte Array anhängen.

Die fünf Prozesse bleiben hier nach dem fork im selben Code. Sie könnten also über eine globale Variable an die ID der Semaphore kommen. Da üblicherweise aber fremde Prozesse kommunizieren, arbeiten wir hier zur Demonstration dennoch mit der Lösung über das Lockfile.

semarray *chopsticks; void sigint_handler(int) { delete chopsticks; }

int main() { chopsticks=new semarray("/tmp/philolock",5,1); signal(SIGINT,sigint_handler); srandom(time(0)+i);

int i; for (i=0;i<4;++i) if (fork()==0) break; if (i!=4) chopsticks=new semarray("/tmp/philolock");

for (;;) { cout << i << " thinking" << endl; sleep(random()&7); cout << i << " hungry" << endl; chopsticks->sdown(2,i,(i+1)%5); cout << i << " eating" << endl; sleep(random()&7); chopsticks->sup(2,i,(i+1)%5); } }

Wenn das Programm mit CTRL-C abgebrochen wird, sorgt der Handler dafür, daß Lockfile und Semaphor-Array freigegeben werden. (In dieser einfachen Version beschweren sich dann evtl. die Kinder, wenn ihnen die Semaphore weggelöscht werden.)

 
9.3: Message Queues Startseite 9.5: Shared Memory