Synchronizacja procesów w linuxie - semafory

eax

Synchronizacja procesów - semafory

1 Synchronizacja procesów - semafory
     1.1 Ogólnie o semaforach
          1.1.1 Semafor ogólny
          1.1.2 Semafor binarny
          1.1.3 Semafor uogólniony
          1.1.4 Semafor dwustronnie ograniczony
     1.2 Operacje na semaforach
     1.3 Obsługa semaforów z konsoli
          1.3.5 ipcs
          1.3.6 ipcrm
     1.4 Operacje na semaforach
          1.4.7 ftok()
          1.4.8 semget()
          1.4.9 semctl()
          1.4.10 semop()
     1.5 Przykład wykorzystania semaforów
     1.6 Przykładowa biblioteka do obsługi semaforów binarnych.

Ogólnie o semaforach

Semafory służą do synchronizacji procesów. Pozwalają na czasowe zabezpieczenie jakichś zasobów przed innymi procesami. Ich przykładowe zastosowanie to np. współbieżny serwer zapisujący do pliku wiadomości odebrane od klientów wraz z adresem z którego nadeszły. Jeżeli adres i wiadomość są zapisywane do pliku oddzielnie może się zdarzyć, że pomiędzy zapisaniem adresu a komunikatu do pliku inny proces zapisze tam część swoich danych. Odpowiednie zastosowanie semaforów zabezpiecza nas przed taką sytuacją.
Dwiema podstawowymi operacjami na semaforze są jego podniesienie oznaczane V i opuszczenie oznaczana P. Opuszczony semafor oczywiście uniemozliwia dostęp do zasobów.

Semafor ogólny

Semafor taki może dopuszczać do zasobów określoną ilość procesów na raz. Jeżeli procesów bedzie zbyt dużo to nadmiarowe będą wstrzymane do czasu zwolnienia zasobów przez któryś z poprzednich.

  • P(S) - jeżeli wartość semafora S>0 to S=S-1. Jeżeli S==0 to proces zostaje wstrzymany dopóki wartość S nie bedzie większa od zera
  • V(S) - jeżeli procesy są wstrzymane to wznów jeden z nich, w przeciwnym przypadku S=S+1

Semafor binarny

Semafor taki jest albo opuszczony (S=0) albo podniesiony (S=1). Innych możliwości nie ma - przepuszcza tylko jeden proces.

Semafor uogólniony

Semafor ten zmienia wartość licznika S o dowolną liczbę naturalną:

  • P(S,n) - jeżeli S > n to S = S-n, w przeciwnym wypadku proces zostaje wstrzymany.
  • V(S,n) - jeżeli jakieś procesy są wstrzymane wskutek operacji (S,m) i istnieje możliwość wznowienia któregoś z nich (m < n) to S = S+n-m. W przeciwnym wypadku S = S+n

Semafor dwustronnie ograniczony

Semafor ten ma możliwość blokowania procesów zarówno wtedy gdy S jest zbyt małe jak i wtedy kiedy jest zbyt duże. Operacja P działa tak jak w semaforze ogólnym, natomiast operacja V powoduje wstrzymanie procesu również gdy S osiągnie ustaloną liczbę N

Operacje na semaforach

Oprócz blokujących operacji P i V na semaforach możemy wykonywać wiele innych operacji:

  • Z - "przejscie pod semaforem" Jest to odwrotność operacji P. Jeżeli wartość S>0 to proces jest wstrzymywany, natomiast jest wznawiany gdy S==0. Ta operacja nie zmienia wartości semafora
  • nieblokujące operacje P i Z. Operacje te nie wstrzymują wykonywania procesu. Jeżeli operacja spowodowałaby wstrzymanie procesu jest sygnalizowany błąd. Operacje te są używane jeżeli chcemy sami w jakiś sposób wykorzystać czas oczekiwania na podniesienie (opuszczenie) semafora.
  • zwracanie wartości semafora
  • zwracanie ilości procesów oczekujących na wykonanie operacji P lub Z
    Ponieważ unix operuje na całych zbiorach semaforów na semaforach należących do takiego zbioru można wykonywać jednoczesne operacje. Trzeba przy tym pamiętać, że jeżeli któraś z tych operacji jest nieblokująca to jeżeli któraś z operacji na zbiorze nie może być wykonana od razu cała "duża operacja" zwróci błąd. Jeżeli wszystkie operacje są blokujące to całość zosatnie wykonana dopiero gdy można będzie wykonać wszystkie operacje elementarne.

Obsługa semaforów z konsoli

ipcs

Polecenie ipcs informuje nas o aktualnych semaforach, kolejkach i zarezerwowanych segmentach pamięci dzielonej. Podane z parametrem -s informuje nas wyłącznie o semaforach - ich właścicielu, grupie, prawach dostępu, identyfikatorze, etc.

ipcrm

To polecenie umożliwia usunięcie zbioru semaforów. Identyfikator zbioru otrzymamy przy pomocy poprzedniego polecenia. Polecenie to ma różna składnię na różnych systemach (ipcrm -s, ipcrm sem) więc odsyłam do man'a.

Operacje na semaforach

Wszystkie wymienione finkcje wymagają włączenia plików <sys/types.h>, <sys/ipc.h>, <sys/sem.h>. Dodatkowo jeżeli chcemy korzystać z semaforów musimy na początku programu wkleić taką dyrektywę:

#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* jest zdefiniowane w sys/sem.h */
#else
union semun
{
	int val;			//value for SETVAL
	struct semid_ds *buf;	//buffer for IPC_STAT, IPC_SET
	unsigned short int *array;	//array for GETALL, SETALL
	struct seminfo *__buf;	//buffer for IPC_INFO
};
#endif

ponieważ ta unia jest zdefiniowana tylko na niektórych systemach.

ftok()

key_t ftok(char* path, int id);

Funkcja na podstawie ścieżki i nazwy istniejącego pliku oraz pojedyńczego znaku id wyznacza jednoznaczny klucz który może być użyty do odwoływania się np. do semaforów. W przypadku błędu funkcja zwraca -1.

Przykład użycia:

key_t k;
k = ftok(".", 'a');

semget()

int semget(key_t key, int ns, int flags);

Ta funkcja na podstawie klucza tworzy lub umożliwia nam dostęp do zbioru semaforów. Parametr key jest kluczem do zbioru semaforów. Jeżeli różne procesy chcą uzyskać dostęp do tego samego zbioru semaforów muszą użyć tego samego klucza. Parametr ns to liczba semaforów która ma znajdować się w tworzonym zbiorze. Parametr flags określa prawa dostępu do semaforów oraz sposób wykonania funkcji. Może przyjmować następujące wartości:

IPC_CREAT Uzyskanie dostępu do zbioru semaforów lub utworzenie nowego gdy zbiór nie istnieje.
IPC_EXCL W połączeniu z IPC_CREAT zwraca błąd gdy zbiór już istnieje
prawa dostępu tak samo jak dla plików np. 0600

Oczywiście poszczególne flagi można łączyć ze sobą przy pomocy sumy bitowej.
Funkcja zwraca identyfikator zbioru semaforów lub -1 gdy wystąpił błąd. W przypadku błędu jest ustawiana zmienna errno - znaczenia poszczególnych błędów podane są w man'ie.

Przykład użycia:

int semafor;
semafor - semget(k, 1, IPC_CREAT | 0600);

Nie należy zakładać, że semafory po stworzeniu mają jakąś określoną wartość. Zaraz po stworzeniu należy je zainicjować aby uniknąć późniejszych błędów.

semctl()

int semctl(int semid, int semnum, int cmd, union semun arg);

Funkcja służy do sterowania semaforami. Jej parametry to kolejno:

semid Numer zbioru semaforów
semnum Numer semafora w zbiorze (począwszy od 0)
cmd Polecenie jakie ma być wykonane na zbiorze semaforów
arg Parametry polecenia (definicja semnum pod tabelką)

arg - parametry polecenia, przy czym semun jest zdefiniowana następująco: //DELETEME!
Unia semnum zdefiniowana jest następująco:

union semun
{
	int val;
	struct semid_ds *buf;
	ushotr *array;
};

Najważniejsze polecenia do wykonania na semaforach

SETVAL nadanie semaforowi o numerze semnum wartości podanej w `arg.val`
GETVAL odczytanie wartości semafora o numerze `semnum`
SETALL nadanie wartości wszystkim semaforom w zbiorze. Wartości do nadania podaje się przy pomocy tablicy `arg.array`
GETALL pobranie wartości wszystkich semaforów do tablicy wskazywanej przez `arg.array`
GETNCNT odczytanie liczby procesów czekających wskutek wywołania operacji P(S)
GETZCNT odczytanie liczby procesów wstrzymanych wskutek wywołania operacji Z(S)
IPC_RMID usunięcie podanego zbioru semaforów
GETPID pobranie PID'u procesu który wykonywał operację na semaforze jako ostatni.

Pozostałe operacje korzystając z arg.buf umożliwiają pobranie i ustawienie niektórych ogólnych informacji o danym zbiorze semaforów.
W przypadku błędu funkcja zwraca -1 i ustawia zmienną errno, w przypadku sukcesu funkcja zwraca 0 lub - w zależności od polecenia cmd - żądaną wartość. Poleceniami dla których funkcja zwraca wartość są GETNCNT, GETZCNT, GETPID oraz GETVAL.

semop()

int semop(int semid, struct sembuf *sops, size_t nsops);

Funkcja służy do wykonywania operacji na semaforach. Jej parametry to:

semid Identyfikator zbioru semaforów
nsops Liczba semaforów (w tym wypadku elementów tablicy sops) na których ma być wykonana operacja
sops Wskaźnik do tablicy struktur określających operacje na semaforach

Każa ze struktur zadeklarowana jest następująco:

struct sembuf
{
	ushort semnum;
	short sem_op;
	ushort sem_flg;
};

gdzie:

semnum Numer semafora w zbiorze
sem_op Operacja na semaforze. Możliwe operacje:
sem_op > 0operacja V - powoduje zwiększenie wartości semafora o sem_op
sem_op < 0operacja P - wstrzymuje proces lub powoduje zmniejszenie wartości semafora o sem_op
sem_op = 0operacja Z
sem_flg Możliwe flagi:
0Operacja blokująca
IPC_NOWAITOperacja nieblokująca

Funkcja zwraca 0 w przypadku sukcesu a -1 w przypadku porażki. Należy pamiętać, że porażka może nastąpić, jeżeli któraś z operacji na zbiorze semaforów jest nieblokująca a nie można jej wykonać natychmiast.

Przykład wykorzystania semaforów

Zaimplemetujemy prosty serwer współbierzny korzystający z fork'a. Dla niewiedzących - fork dzieli procesy - w jednym z procesów (dziecku) fork zwraca 0, a w drugim (rodzicu) zwraca PID dziecka. Dziecko dosatje kopie wszystkich danych rodzica. Serwer bedzie zapisywał do pliku komunikat który nadszedł przez sieć (przez połączenie TCP) oraz IP nadawcy.

[server.c]

#define PORT 39998
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#include <sys/ipc.h>
#include <sys/sem.h>

int main()
{
int s;
int serv_s, clnt_len;
struct sockaddr_in serv_addr, clnt_addr;
int pid;
char buf[255];
char kto[25];
int plik;
int pliktmp;

key_t klucz;		//klucz do stworzenia semafora
int semafor;		//identyfikator semafora
union semun{		//powinno być w define
  int val;
  struct semid_ds *buf;
  unsigned short int* array;
  struct seminfo *__buf;
} ustaw;

struct sembuf operacja;	//operacja dla semop()

//stworzenie jednoznacznego klucza dla poźniejszego stworzenia zbioru semaforów
    if ((klucz = ftok(".", 'A')) == -1)
    {
	fprintf(stderr, "blad tworzenia klucza\\n");
	exit(1);
    }  

//stworzenie zbioru zawierającego tylko 1 semafor i dostępnego tylko dla właściciela pliku, lub zwrócenie błędu gdy semafor dla danego klucza już istnieje
    if ((semafor = semget(klucz, 1, IPC_CREAT | IPC_EXCL | 0600)) == -1)
    {
	fprintf(stderr, "blad tworzenia semaforow\\n");
	exit(1);
    }
    
//zainicjowanie semafora jako podniesiony (S=1)
    ustaw.val = 1;
    if (semctl(semafor, 0, SETVAL, ustaw) == -1)
    {
	fprintf(stderr, "blad ustawienia semafora\\n");
	exit(1);
    }
    
//część sieciowa i forkowanie
    signal(SIGCHLD, SIG_IGN);    

    serv_s = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(PORT);
    
    bind(serv_s, (struct sockaddr *) &serv_addr, sizeof(serv_addr));    
    listen(serv_s, 1);
    
    while(1)
    {    
//przyjmij połączenie    
	clnt_len = sizeof(clnt_addr);
	s = accept(serv_s, (struct sockaddr *) &clnt_addr, &clnt_len);
    
//wywołaj fork
	pid = fork();
	if (pid>0)
	{
//rodzic
	    close(s);
	}
	else if (pid == 0)
	{
//czytaj dane
            read(s, buf, 255);
//zablokuj dostęp do pliku - operacja P	    
	    operacja.sem_num = 0;
	    operacja.sem_op = -1;	//zablokuj
	    operacja.sem_flg = 0;	//operacja blokujaca
	    
	    if (semop(semafor, &operacja, 1) == -1)
	    {
		fprintf(stderr, "blad blokowania semafora\\n");
		exit(1);
	    }
//zapisz dane do pliku	    
	    if ((plik = open("plik.kom", O_APPEND | O_CREAT | O_WRONLY, 0744)) == -1)
	    {
		perror("blad podczas tworzenia pliku\\n\\n");
	    }

	    strcat(buf, "\\n");
	    write(plik, buf, strlen(buf));
//zapisz IP nadawcy
	    sprintf(buf, "%s\\n", inet_ntoa(clnt_addr.sin_addr));
	    write(plik, buf, strlen(buf));
	    close(plik);
//to żeby sprawdzić że działa	    
	    system("sleep 60");
	    
//odblokuj plik - operacja V
	    operacja.sem_num = 0;
	    operacja.sem_op = 1;
	    operacja.sem_flg = 0;
	    
	    if (semop(semafor, &operacja, 1) == -1)
	    {
		fprintf(stderr, "blad odblokowania semafora\\n");
		exit(1);
	    }
	    
	    close(s);
	    exit(0);
	}
    }     
    return 0;        
}

Proces dziecka odbiera dane, blokuje plik przed dostępem innych procesów (lub czeka jeżeli inny proces zrobił to piewrszy), zapisuje dane do pliku, zapisuje adres IP nadawcy a następnie odblokowuje plik. Zablokowanie pliku zabezpiecza przed dobraniem się do niego przez inny proces pomiędzy zapisaniem komunikatu a adresu IP nadawcy.

Napisanie klienta zostawiam na zadanie domowe.

Przykładowa biblioteka do obsługi semaforów binarnych.

Sama biblioteka do obsługi semaforów nie daje łatwego dostępu do pojedyńczych semaforów. Taki dostęp może dawać np. taka biblioteka:

[binarny.h]

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#ifndef _BINARY_H
#define _BINARY_H

#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* jest zdefiniowane w sys/sem.h */
#else
union semun{
    int val;			//value for SETVAL
    struct semid_ds *buf;	//buffer for IPC_STAT, IPC_SET
    unsigned short int *array;	//array for GETALL, SETALL
    struct seminfo *__buf;	//buffer for IPC_INFO
};
#endif

//stworzenie semafora binarnego o prawach tylko dla wlasciciela pliku
//lub uzyskanie dostepu do istniejacego
//zwraca identyfikator semafora
//inicjuje go jako podniesiony
//zwraca -1 w przypadku błędu
//na wejściu wymaga ścieżki do istniejącego pliku i identyfikatora zbioru
int bsemcreate(char*, int);

//operacja V
//zwraca 0 w przypadku sukcesu lub -1 w przypadku błędu
int VBsem(int);

//operacja P
//0 w przypadku sukcesu, -1 błąd
int PBsem(int);

//operacja Z
//0 sukces, -1 błąd
int ZBsem(int);

//operacje nieblokujace P
//zwraca 0 gdy można wykonać operację, a -1 gdy nie można jej wykonać natychmiast
int nPBsem(int);

//nieblokujace Z
//0 gdy można wykonać operacje, -1 gdy nie można wykoanć jej od razu
int nZBsem(int);

//odczytanie wartosci
int Bsemvalue(int);

//odczytanie liczby procesow czekajacych pod P
int BPwait(int);

//odczytanie liczby procesow czekajacych pod Z
int BZwait(int);

//usuniecie semafora
int bsemdelete(int);

#endif 

[binarny.c]

#include "binarny.h"

//stworzenie semafora binarnego
//wywołuje ftok() aby otrzymać klucz, a następnie tworzy zbiór
//zawierający pojedyńczy semafor 
int bsemcreate(char* path, int k)
{
key_t klucz;
int semafor;
union semun ustaw;
    if ((klucz = ftok(path, k)) == -1)
	return -1;
    if ((semafor = semget(klucz, 1, IPC_CREAT | 0600)) == -1)
	return -1;
    ustaw.val = 1;
    semctl(semafor, 0, SETVAL, ustaw);
    return semafor;
}

//operacja V
//sprawdza wartość semafora i jeżeli trzeba wykonuje operacje V
int VBsem(int s){
struct sembuf operacja;
int info;

    if ((info = semctl(s, 0, GETVAL)) == -1)
	return -1;
    if (info == 1)
	return 0;
    operacja.sem_num = 0;
    operacja.sem_op = 1;
    operacja.sem_flg = 0;
    return semop(s, &operacja, 1);
}

//operacja Z
int ZBsem(int s){
struct sembuf operacja;

    operacja.sem_num = 0;
    operacja.sem_op = 0;
    operacja.sem_flg = 0;
    return semop(s, &operacja, 1);
}

//operacja P
int PBsem(int s){
struct sembuf operacja;

    operacja.sem_num = 0;
    operacja.sem_op = -1;
    operacja.sem_flg = 0;
    
    return semop(s, &operacja, 1);
}

//nieblokujace P
int nPBsem(int s){
struct sembuf operacja;

    operacja.sem_num = 0;
    operacja.sem_op = -1;
    operacja.sem_flg = IPC_NOWAIT;
    
    return semop(s, &operacja, 1);
}


//nieblokujace Z
int nZBsem(int s){
struct sembuf operacja;

    operacja.sem_num = 0;
    operacja.sem_op = 0;
    operacja.sem_flg = IPC_NOWAIT;
    return semop(s, &operacja, 1);
}


//odczytanie wartosci
int Bsemvalue(int s){

    return semctl(s, 0, GETVAL);
}

//odczytanie liczby procesow czekajacych pod P
int BPwait(int s){

    return semctl(s, 0, GETNCNT);
}

//odczytanie liczby procesow czekajacych pod Z
int BZwait(int s){

    return semctl(s, 0, GETZCNT);
}

//usuniecie semafora
int bsemdelete(int s){

    return semctl(s, 0, IPC_RMID);
}

4 komentarzy

Warto wspomnieć, że semafor binarny to inaczej mutex.

Mały update:
Zmiana treści w sekcji :fork dzieli procesy - w jednym z procesów (rodzicu) fork zwraca 0, a w drugim (dziecku) zwraca PID rodzica." --------- błędny opis funkcji fork() - polecam man fork gdzie widnieje:
RETURN VALUE
On success, the PID of the child process is returned in the parent, and
0 is returned in the child.

Juz poprawione :)

fajnie opisane :)

Wcięło wszystkie #include <x.h> :(( To błąd Coyote - do poprawy...