4.7 Synchronisation

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; /* PID‘s 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.