Systemprogrammierung - Teil 9Die Ampel regelt allesvon Wolfgang Hetzler Systemprogrammierung gilt als schwierig, da ihre Inhalte dem Beutzer sehr oft abstrakt gegenüber treten im Gegensatz zu praktischen Aufgabenstellungen, deren Sinn plastisch vor Augen steht. Daß diese Abstraktheit nicht sein muß und die Systemcalls eine wohldefinierte Schnittstelle zum Kernel darstellen, will diese Serie zeigen. Diesmal geht es um Semaphore. 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):
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):
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):
Ü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):
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):
Ü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:
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):
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).
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.
Copyright © 1998 Linux-Magazin Verlag |