![]() |
![]() |
||||||||||||||||||
![]() |
|
||||||||||||||||||
![]() |
|
||||||||||||||||||
|
In dieser Folge beschäftigen wir uns mit Semaphoren (Ampeln), die sowohl als Singular als auch im Plural den konkurrierenden Zugriff auf gemeinsame Betriebsmittel synchronisieren können. Leider sind, wie schon Rochkind bemerkte, die Semaphore unter Unix (und damit natürlich auch für Linux) äußerst komplex angelegt. Wir werden uns hier auf die binären Semaphore (Zustände Rot/Grün) beschränken. Zudem werde ich zeigen, wie sie effektiv implementiert werden können und wie sie den Verkehr regeln. Der Gedanke des binären Semaphors wird duch die Funktionen p() und v() realisiert, die wir uns wie folgt vorstellen können:
p(int semaphor) { while(semaphor <= 0); --semaphor; } v(int semaphor) { ++semaphor; }
Die Funktion p() wartet auf die Freigabe des Semaphors (der Wert geht auf 1) und setzt ihn auf Rot (der Wert geht auf 0), die Funktion v() setzt das Semaphor auf Grün (der Wert geht auf 1), dies impliziert für die Gestaltung zweier Prozesse, die auf ein gemeinsames Betriebsmittel zugreifen, folgende Abläufe (siehe Listing 1):
Listing 1: Prozeß-Ablauf |
process_0() { p(); // Versuch, das Semaphor zu erhalten do_some_critical_stuff(); v(); // gib das Semaphor frei } process_1() { p(); // Versuch, den Semaphor zu erhalten do_some_critical_stuff(); v(); // gib das Semaphor frei } |
Aufgrund der Prozeßgestaltung wird nur einer der beiden das Semaphor erhalten, der andere wird in der Funktion p hängen bleiben und erst seine kritische Tätigkeit, die sich auf ein gemeinsames Betriebsmittel bezieht, aufnehmen können, wenn der andere das Semaphor freigegeben hat. Natürlich muß das Semaphor von einer dritten Instanz (hier das Betriebssystem) korrekt verwaltet werden - dies leisten die entsprechenden Systemcalls, deren Prototypen ich zunächst kurz vorstelle (siehe Abb. 1):
Abb 1: Die Semaphore-Prototypen |
int semget(key_t key, int nsems, int semflg); int semop( int semid, struct sembuf *sops, int nsops); int semctl(); |
Der Systemcall semget legt unter dem Namen key eine Anzahl von Semaphoren an, die mit nsems übergeben wird, semflg regelt den Zugriff auf die Semaphore. semop gestattet das Manipulieren der Semaphore, wobei für jedes Semaphor eine Struktur sembuf, die die Befehler zur Manipulation enthält, übergeben werden muß, nsops liefert die Anzahl. semctl wird wie gewohnt zur Information bzw. zum Löschen benutzt. Ich schreibe zunächst die beiden Funktion v() und p() (siehe Listing 2 und 3):
Listing 2: v.cpp |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int v(int sid) { struct sembuf sb[1]; sb[0].sem_num = 0; sb[0].sem_op = 1; sb[0].sem_flg = SEM_UNDO; return semop(sid,sb,1); } |
Übergeben wird beim Aufruf von v() die id des Semaphors. Da wir jeweils nur ein binäres Semaphor ansprechen, benötigen wir exakt eine Struktur, die wir als Array anlegen, semop erwartet einen Zeiger auf die Strukturen. Die Nummer des Semaphors ist 0, der Befehl ist 1 - das impliziert, daß dieser Wert auf den aktuellen Wert des Semaphors addiert wird, das Flag signalisiert eine Rücknahme aller Operationen auf dieses Semaphor bei Termination. Abschließend wird semop aufgerufen und der Wert dieses Systemcalls zrückgliefert. Als nächstes schreibe ich die Funktion p() (siehe Listing 3):
Listing 3: p.cpp |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int p(int sid) { struct sembuf sb[1]; sb[0].sem_num = 0; sb[0].sem_op = -1; sb[0].sem_flg = SEM_UNDO; return semop(sid,sb,1); } |
Die Funktion p() hat in derBefehlskomponente die -1, das impliziert, daß der ausführende Prozeß blockiert, wenn der Wert des Semaphors durch eine Subtraktion negativ wird, ansonsten geht es weiter im Ablauf. Zur Initialisierung des Semaphors benutzen wir die Funktion initsem(), die ich hier kurz wiedergebe (siehe Listing 4):
Listing 4: initsem.cpp |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int initsem(int semid,int val) { semctl(semid,0,SETVAL,val); } |
Übergeben wird die id des Semaphors und der Initialisierungswert, bei einem binären Semaphor also 0 oder 1, der anschließend mit semctl eingestellt wird. Im Listing 5 ist der serverseitige Erwerb des Semaphors sowie ein entsprechender Client zu sehen:
Listing 5: msgbuf.h |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> // der Server main() { int sem_id; sem_id = semget(100,1,IPC_CREATE|0666); initsem(sem_id,1); p(sem_id); // critical_section v(sem_id); } // der client main() { int sem_id; sem_id = semget(100,1,0666); p(sem_id); // critical_section; v(sem_id); } |
Der Server legt das Semaphor an und initialisiert es mit 1, um es anschließend mit der p()-Operation zu erwerben und begibt sich in seine critical-section, um anschließend für alle weiteren das Semaphor auf Grün zu setzen. Der Client blockiert in der p()-Operation, falls das Semaphor vom Server nocht nicht via der v()-Operation auf Grün gesetzt worden ist, um anschließend in seine critical-section zu gehen und nachfolgend wieder grünes Licht zu geben.
Blockieren meint hier, daß ein Prozeß solange gestoppt wird, bis ein bestimmtes Ereignis (hier grünes Licht, oder das Semaphor kann dekrementiert werden, ohne das es negativ wird) eintritt. Natürlich ist unter diesen Voraussetzungen jederzeit auch eine totale Blockade aller Prozesse möglich - der sogenannte Deadlock. Zum Abschluß zeige ich den Server und den Client einer kleinen Anwendung, die serverseitig Files an einen Client tranportiert. Der Name der Datei wird dabei an den Server via Messages übertragen und die Datei wird im Bereich Shared Memory übergeben. Zur Synchronisation verwenden Client und Server Semaphore. Zunächst also der Server (siehe Listing 6):
Listing 6: ipcs.cpp |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <sys/msg.h> #include <stdio.h> #define SSEMKEY 101 #define KEY 100 #define RSEMKEY 102 #define KSEMKEY 103 #define MKEY 104 struct buffer { int type; int count; char buffer[1024]; }; struct proto { int count; char buffer[1024]; }; struct buffer buf; int fd,n,ssemkey,key,rsemkey,ksemkey,mkey; int transfer_file(char *); main(int argc, char **argv) { ssemkey=semget(SSEMKEY,1,IPC_CREAT|0666); rsemkey=semget(RSEMKEY,1,IPC_CREAT|0666); ksemkey=semget(KSEMKEY,1,IPC_CREAT|0666); mkey=msgget(MKEY,IPC_CREAT|0666); key = shmget(KEY,sizeof(struct proto), IPC_CREAT|0666); while(1) { initsem(ksemkey,1); msgrcv(mkey,(msgbuf *)& buf, sizeof(buf.buffer)+sizeof(int), 0,MSG_NOERROR); transfer_file(buf.buffer); } } int transfer_file(char * buffer) { struct proto *shmp; fd = open(buffer,0); initsem(ssemkey,1); initsem(rsemkey,0); shmp = (struct proto *)shmat(key,0,0); while(1) { p(ssemkey); shmp->count = read(fd,shmp->buffer,1024); if(!shmp->count) { v(rsemkey); break;rsemkey } v(rsemkey); } shmdt(key); return 1; } |
Zur Begriffsdefinition: rsemkey repräsentiert das Semaphor, das den Empfang von Datenblöcken synchronisiert, ssemkey realisiert diesen Sachverhalt für das Versenden von Datenblöcke und ksemkey stimmt das Verhalten der Clients ab. mkey ermöglicht den Zugriff auf die Message-Queue und key entspricht dem Shared-Memory-Bereich.
Der Server begibt sich in eine Endlosschleife und setzt die Semaphore für die Kunden auf 1, ein Zugriff auf Dienstleistungen des Servers ist damit möglich. Anschließend versucht er eine Nachricht zu lesen, die dem Namen der zu sendenden Datei entspricht und ruft seine Funktion transfer_file zum Übertragen der Datei auf. transfer_file öffnet die Datei, initialisiert das Sendesemaphor mit 1 und das Empfangssemaphor mit 0, realisiert den Zugriff auf den Shared-Memory Bereich und begibt sich zum Übertragen in eine Endlossschleife.
In der Schleife versucht der Server, das Sendesemaphor zu erlangen, was ja auch mühelos klappt, da er es gerade auf 1 gesetzt hat. Anschließend versucht er 1024 Bytes in den Shared-Memory Bereich zu lesen. Der Rückgabewert von read landet ebenfalls in Shared-Memory, checkt den Rückgabewert für eine korrekte EOF-Behandlung, die zum Schleifenabbruch führen muß und gibt das Empfangssemaphor frei und bleibt in der nächsten Runde beim Versuch, das Sendesemaphor zu erlangen, hängen, da es ja auf Rot = 0 steht. Betrachten wir zum Verständinis die Client-Seite (siehe Listing 7).
Listing 7: ipcc.cpp |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <stdio.h> #define SSEMKEY 101 #define KEY 100 #define RSEMKEY 102 #define KSEMKEY 103 #define MKEY 104 struct buffer { int type; int count; char buffer[1024]; }; struct proto { int count; char buffer[1024]; }; main(int argc, char **argv) { int ssemkey,rsemkey,key,mkey,ksemkey; ssemkey=semget(SSEMKEY,1,0); rsemkey=semget(RSEMKEY,1,0); ksemkey=semget(KSEMKEY,1,0); mkey=msgget(MKEY,0); key = shmget(KEY,1024,0); struct buffer buf; struct proto *shmp; gets(buf.buffer); p(ksemkey); buf.type=1; msgsnd(mkey,&buf,sizeof(buf.buffer)+sizeof(int),0); shmp = (struct proto *)shmat(key,0,0); while(1) { p(rsemkey); if( shmp->count == 0) break; write(1,shmp->buffer,shmp->count); v(ssemkey); } shmdt((char *)shmp); v(ksemkey); } |
Der Client erlangt Zugrif auf die Resourcen in Analogie zum Server, liest von der Standardeingabe den Namen einer Datei und versucht, das Semaphor für den Zugriff auf den Server zu erlangen. Ist das Semaphor auf Rot, blockiert der Prozeß, bis das Signal auf Grün geschaltet wird, anschließend sendet der Client den Dateinamen via Message an den Server und erlangt Zugriff auf den Shared-Memory Bereich. Die while-Schleife dient der Übertrgung der Datei und wird ebenfals als Endlosschleife konstruiert, in der der Client wechselseitig Zugriff auf das Empfangssemaphor zu erlangen versucht und nach Empfang das Sendesemaphor, das ja der Server via p-Operation zu erlangen versucht, über die v-Operation freigibt.
Zu beachten ist auch die Tatsache, daß über die Struktur proto eine minimale Protokollvereinbarung zwischen Client und Server vorliegt: Eine Sendung im Shared-Memory Bereich besteht aus Daten und der Information, wieviel Nutzdaten effektiv übertragen worden sind. Nach Abschluß des Transfers gibt der Klient das Semaphor für den Zugriff auf den Server wieder frei. Das Semaphor für den Zugriff auf den Server ist nicht von Bedeutung, da er ja iterativ arbeitet und somit ein anderer Klient keinen Durchgriff erlangen kann.
Das Konzept der Semaphore greift natürlich nur, wenn alle Beteiligten sich an die Spielregeln halten. Da aber bei Client/Server- Programmierung die Beteiligten in der Regel identisch sind, dürfte das kein Problem darstellen. Soweit zu Semaphoren. Die nächsten Folgen dieser Reihe beschäftigen sich mit Kommunikationsstrukturen in Netzwerken.
Infos |
[1] M. Beck, H. Böhme et al: Linux-Kernel-Programmierung, Addison-Wesley, 1997 [2] Rochkind: Unix-Programmierung für Fortgeschrittene, Hanser, 1985 [3] Bentson,R.: Inside Linux, Seattle, SSC 1996 [4] Brown, Chris: UNIX distributed programming, Prentice Hall [5] Robbins, K.A:,Robbins,Steven: Practical Unix Programming,Prentice Hall |
Der Autor |
Wolfgang Hetzler arbeitet seit 1985 als Dozent an einem privaten Institut für Fort- und Ausbildung im EDV-Bereich und als Lehrbeauftragter an der FH-Frankfurt im Bereich Ingenieur-Informatik. Linux ist ihm seit der Version 0.9xx bekannt und eine Quelle ständiger Auseinandersetzung. Zu erreichen ist er unter het@het.gg.uunet.de . |
Copyright © 1998 Linux-Magazin Verlag
Dieser Online-Artikel kann Links enthalten, die auf nicht mehr vorhandene Seiten verweisen. Wir ändern solche "broken links" nur in wenigen Ausnahmefällen. Der Online-Artikel soll möglichst unverändert der gedruckten Fassung entsprechen.
Partner-Sites:
[LinuxUser]
[EasyLinux]
[Linux-Community]
[Linux Events]
[OpenBytes]
[Linux Magazine]
[Linux Magazin Romania]
[Linux Magazine Poland]
[Linux Magazine Brasil]
[Linux Magazine Spain]