| 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.
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.
Würde dabei der Wert kleiner als 0, blockiert der Prozeß,
bis der Semaphor mindestens den Wert |sem_op| angenommen hat, und
erst dann findet die Subtraktion statt!
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:
inline void sem_down(int id) { sem_up_down(id,-1); }
inline void sem_up(int id) { sem_up_down(id,1); }
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:
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
};
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:
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).
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); }
}
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.
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);
}
}
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:
Ein Client-Prozeß erhält den Zugriff auf diese Menge wie folgt
über das Lockfile:
ipcs gibt in etwa folgendes aus:
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.
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):
Die verwendete Struktur semun ist wie folgt aufgebaut:
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
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:
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:
Die Werte von sem_op haben folgende Auswirkungen (Lese- bzw.
Schreiberlaubnis des jeweiligen Prozesses vorausgesetzt):
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.
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:
Ein passender Client hängt sich dann mit folgender Definition an:
Der Rest seines Code könnte dem Server-Code entsprechen.
Beispiel: Wir implementieren die Semaphor-Lösung für das
Philosophen-Problem mit Hilfe der Klasse
semarray.
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 
|
|   |