In Unix / Linux gibt es
verschiedene Arten und Methoden, um Prozesse zu synchronisieren.
Die wichtigsten Methoden werden hier genauer besprochen.
4.7.1 Synchronisation durch Semaphore
Ein Semaphor ist ein Zähler der mehreren Prozessen gleichzeitig den Zugriff auf ein gemeinsames Datenobjekt ermöglicht. Es wird geregelt, wieviele Prozesse gleichzeitig eine gemeinsame Ressource benutzen dürfen.
Ein Beispiel aus dem Leben ist z.B. die Benutzung eines Aufzuges, in dem maximal 5 Personen Platz haben. Ein Semaphor wäre hier ein Zähler, der auf 5 initialisiert wird und jedesmal, wenn eine Person den Aufzug betritt, wird er um 1 herabgezählt (dekrementiert). Verlässt eine Person den Aufzug, dann wird der Zähler um 1 erhöht (inkrementiert). Ist der Zähler auf 0, so darf keine weitere Person den Aufzug betreten.
Um eine gemeinsam genutzte Ressource zu verwenden, muß ein Prozess folgende Schritte ausführen:
Oft werden auch binäre Semaphore eingesetzt. Sie werden mit 1 initialisiert und bewirken so den wechselseitigen Ausschluß von einer Ressource.
Diesen wechselseitigen Ausschluß kann man auch mit Spinlocks lösen (siehe wechselseitiger Ausschluß mit Spinlocks). Bei der Verwendung von Semaphoren werden die Prozesse, falls ein kritischer Bereich belegt ist, angehalten. Sie warten im Zustand blockiert, bis der kritische Bereich frei ist, ohne das System zu belasten.
Eine Semaphorgruppe wird vom System nicht automatisch gelöscht, wenn sie nicht mehr benötigt wird. Sie muß durch den entsprechenden semctl()Aufruf im Programmcode gelöscht werden. Von der Kommandooberfläche aus kann man sich die Semaphorgruppen mit dem Befehl ipcs -s anzeigen lassen und eventuell nicht mehr benötigte Semaphorgruppen mit dem Befehl ipcrm sem id (id der Semaphorgruppe) löschen.
Schnittstellenfunktionen für die Arbeit mit Semaphoren:
semctl | Steuerfunktion für Semaphore |
Includes: | #include
<sys/ipc.h> #include <sys/sem.h> |
Prototyp: | int semctl ( int semid, int semnum, int befehl, union semun arg ); |
Parameter: | int semid | int- Wert für die Identifikation der Semaphorgruppe.Rückgabewert von semget(). |
int semnum | Nummer des Semaphors. |
int befehl | Gibt an,
was getan wird. SETALL Setzen aller Semaphorwerte GETALL Auslesen aller Semaphorwerte SETVAL Setzen eines bestimmten Semaphorwertes GETVAL Auslesen eines bestimmten Semaphorwertes IPC_RMID Löschen der Semaphorgruppe |
union semun arg |
Parameter
für befehl Wenn befehl== SETALL oder befehl== GETALL Ist arg ein Feld vom Typ unsigned short. In diesen Feldern stehen die Semaphorwerte, wie sie gesetzt werden sollen bzw. wie sie ausgelesen wurden. Bei befehl == SETVAL ist arg ein int-Wert für den bestimmten Semaphor. Bei befehl == GETVAL hat arg keine Bedeutung, da die Funktion den aktuellen Wert des einzelnen Semaphores als Rückgabewert zurückliefert. |
union semun { int val; struct semid_ds buf; ushort *array } |
/* für SETVAL */ /* IPC_STAT */ /* und IPC_SET */ /*für GETALL u. SETALL*/ |
Rückgabe: | Bei allen GET-Befehlen außer GETALL liefert die Funktion den entsprechenden Wert. Bei anderen Befehlen ist der Rückgabewert gleich NULL oder 1 bei einem Fehler. |
Beispiel für die Funktion semctl():
#include <sys/ipc.h> #include <sys/sem.h> int semid; unsigned short initarray[3]; /* Initialisierungsfeld */ unsigned short outarray[3]; /* Ausgabefeld */ initarray[0] = 3; /* Wert des 1. Semaphores soll 3 sein */ initarray[1] = 5; /* Wert des 2. Semaphores soll 5 sein */ initarray[2] = 1; /* Wert des 3. Semaphores soll 1 sein */ semctl ( semid, 0, SETALL, initarray ); /* Initialisiert die Semaphore mit 3, 5 und 1 */ semctl ( semid, 0, SETVAL, 6 ); /* Initialisiert den 1. Semaphor mit 6 */ semctl ( semid, 0, GETALL, outarray ); /* Semaphorwerte werden in das Feld outarray geschrieben */ semctl ( semid, 0, IPC_RMID, 0 ); /* Löscht die Semaphorgruppe */ Achtung: Manche Unix bzw. Linux Versionen akzeptieren bei SETALL und GETALL keine Felder. Sie verlangen die Übergabe einer Union.
Beispiel:
union semun para ; union semun para2 ; unsigned short initarray[3]; /* Initialisierungsfeld */ unsigned short outarray[3]; /* Ausgabefeld */ initarray[0] = 3; initarray[1] = 5; /* siehe oben */ initarray[2] = 1; para.array = initarray; /* Zeigerzuweisung für */ /* Initialisierungsfeld */ para2.array = outarray; /* Zeigerzuweisung für */ semctl ( semid, 0, SETALL, para ); /* Ausgabefeld */ semctl ( semid, 0, GETALL, para2 );
semget | Im System wird eine neue Semaphordatenstruktur angelegt. Bei einer bestehenden Gruppe wird der Zugriff ermöglicht. |
Includes: | #include
<sys/ipc.h> #include <sys/sem.h> |
Prototyp: | int semget ( long schlüssel, int n, int modus ); |
Parameter: | long schlüssel | Schlüssel, der durch den Programmierer frei definierbar ist. Wird hier IPC_PRIVATE eingegeben und im Parameter modus IPC_CREAT so wird auf jeden Fall eine neue Semaphorengruppe angelegt. |
int n | Anzahl der Semaphore in der Gruppe. |
int modus | Bitmuster: Die letzten 9 Bitstellen geben die Zugriffsrechte auf die Semaphorgruppe an. |
Rückgabe: | -1 bei einem Fehler. int-Wert für die Identifikation der Semaphorgruppe. |
Beispiele:
#include <sys/ipc.h> #include <sys/sem.h> int semid; semid = semget ( IPC_PRIVATE, 1, IPC_CREAT|0777 ) ; /* Erzeugt Semaphorgruppe mit einem Semaphor mit allen Zugriffsrechten. Der interne Identifikationswert wird an semid übergeben.*/ oder semid = semget ( 20, 2, IPC_CREAT|0777 ) ; /* Erzeugt unter dem externen Schlüssel 20 eine Semaphorgruppe mit zwei Semaphoren mit allen Zugriffsrechten. Der interne Identivikationswert wird an semid übergeben. */ oder semid = semget ( 20, 2, 0777 ) ; /* Ermittelt die unter dem externen Schlüssel 20 erzeugte Semaphorgruppe . Der interne Identifikationswert wird an semid übergeben. */
semop | Führt die in einem Array gespeicherten Semaphorbefehle in einer atomaren Operation aus. |
Prototyp: | int semop ( int semid, struct sembuf semoparray[], int anzahl ); |
Parameter: | int semid | int- Wert für die Identifikation der Semaphorgruppe. Rückgabewert von semget(). |
struct sembuf semoparray[] | Zeiger auf ein Array mit Semaphorenoperationen. |
struct sembuf { ushort sem_num; short sem_op; short sem_flg; } |
/*Nummer des Elementes Operation die ausgeführt wird negativ, 0 oder positiv Legt fest was zu tun ist falls die Operation nicht ausführbar ist */ |
4.7.2 Synchronisation durch Signale
Bei der Synchronisation / Steuerung von Prozessen durch Signale kann man die Reihenfolge festlegen, in der bestimmte Prozesse bearbeitet werden. Die Funktionen signal(), pause() und kill() werden hierfür verwendet. Durch die Funktion signal() wird ein Signal-Handler, der beim Eintreffen des Signals ausgeführt wird, an das Signal gebunden. Bei größeren Programmen, die mehrere Prozesse haben, wird es allerdings schwierig den Überblick zu behalten. Bei dieser Methode wird die Reihenfolge festgelegt in der Prozesse bzw. Teile von Prozessen ausgeführt werden. Die "parallele" Bearbeitung von Prozessen wird dadurch eingeschränkt. Ein weiteres Problem bei der Arbeit mit Signalen ist, daß Signale nicht vom System gespeichert werden. Erhält ein Prozess ein Signal bevor dieser selbst die Funktion pause() aufgerufen hat geht dieses Signal verloren und der Prozess wartet, wenn er später die Funktion pause() aufruft, vergeblich auf ein Signal.
Schnittstellenfunktionen für die Arbeit mit Signalen:
pause(), kill(), signal() siehe Kapitel 4.5 Erzeugung und Steuerung von Prozessen.
Beispiel für die Synchronisation durch Signale:
#include<stdio.h> #include<signal.h> void sighand() /* Signal Handler wird beim Eintreffen */ { /* des Signales SIGUSR1 ausgefuehrt */ signal(SIGUSR1,&sighand); /* Hier wird die Bindung des Signals */ /* SIGUSR1 an den Signal Handler */ /* sighand() erneuert. Dies muß nach */ /* jedem Eingang des Signals geschehen. */ ** Programmcode der beim Eintreffen des Signals ausgeführt wird** } main() { int vater_pid,prozess1_pid,prozess2_pid; /* PIDs der Söhne */ signal (SIGUSR1,&sighand);/* Bindung des Signals SIGUSR1 */ /* an den Signal Handler sighand()*/ if((prozess1_pid = fork())==0)/* Sohnprozess 1 wird erzeugt */ { /* und gestartet */ vater_pid=getppid(); /* Sohnprozess erfragt die */ for(hilf=0;hilf<=10;hilf++) /* PID des Vaters */ { ** Programmcode des 1. Sohnes wird durchlaufen ** } kill(vater_pid,SIGUSR1); /* Dem Vaterprozess wird das */ /* Signal SIGUSR1 gesendet */ exit(0); /* 1. Sohnprozess terminiert */ } if((prozess2_pid = fork())==0) /* Sohnprozess 2 wird */ { /* erzeugt und gestartet */ pause(); /* Sohnprozess 2 wartet */ for(hilf=0;hilf<=10;hilf++) /* auf ein Signal */ { ** Programmcode des 2. Sohnes wird durchlaufen ** } exit(0); /* 2. Sohnprozess terminiert */ } pause(); /* Vaterprozess wartet auf ein */ kill(prozess2_pid,SIGUSR1); /* Signal Vaterprozess sendet */ return 0; /* Signal an 2. Sohnprozess */ }
4.7.3 Wechselseitiger Ausschluß mit Spinlocks
Der Begriff "Spinlock" ist sehr treffend (engl. Spin = drehen, kreisen). Da bei dieser Art des wechselseitigen Ausschlußes eine while-Schleife der Hauptbestandteil ist. Diese wird sooft durchlaufen, bis der Prozess Zugriff auf die benötigte Ressource erhält.
Man unterscheidet zwei Arten.
1. Spinlocks durch Maschinenbefehle
Hier wird durch Aufruf einer Funktion innerhalb einer while-Schleife
( while(TEST_AND_SET(&lock)); ) getestet, ob ein kritischer Bereich frei ist oder nicht. Dies wird anhand einer Sperrvariablen festgestellt. Wenn der kritische Bereich besetzt ist, liefert die Funktion z.B. den Wert TRUE zurück, sodaß die while-Schleife erneut durchlaufen wird.
Ist der kritische Bereich frei, so erhält man den Wert FALSE und die Sperrvariable wird auf TRUE gesetzt. Hierbei ist wichtig, daß die Überprüfung und das Setzen der Variable ohne Unterbrechung also "atomar" erfolgt.
Beim Verlassen des kritischen Bereiches setzt der Prozess die Sperrvariable einfach auf FALSE ( lock = FALSE ).
2. Spinlocks durch Lock Files
Bei dieser Art von Spinlock wird anstelle einer Sperrvariable ein sogenannter Lock File verwendet. Dieser Lock File ist eine Datei mit einem bestimmten Namen. Dieser Name muß allen Prozessen bekannt sein die sich einen kritischen Bereich teilen. Ein Prozess versucht vor dem Eintritt in einen kritischen Bereich diese Datei anzulegen. Existiert diese Datei, ist der kritische Bereich bereits von einem anderen Prozess belegt. Verläßt dieser den kritischen Bereich, löscht er die Datei, die er beim Eintritt in diesen Bereich angelegt hat. Durch die while-Schleife versuchen die Prozesse solange diese Datei anzulegen, bis es ihnen gelingt. Das Erzeugen diese Lock Files muß "atomar" erfolgen.
Die Verwendung von Spinlocks zum
gegenseitigem Ausschluß hat aber einige Nachteile. Es wird keine
Reihenfolge festgelegt, so das es passieren kann, das einige
Prozesse sehr lange warten müssen. Nach Freigabe des kritischen
Bereiches besitzt jeder Prozess, auch der den kritischen Bereich
gerade verlassen hat, die selbe Wahrscheinlichkeit den kritischen
Bereich als nächstes zu erhalten.
Ein weiterer und auch größerer Nachteil ist, daß die Prozesse
nicht in den Zustand blockiert übergehen sondern immer wieder
die while-Schleife durchlaufen, was zu einer
Belastung des Systems beiträgt, da sie die CPU belasten.