Programowanie w języku C/C++ » Artykuły

Synchronizacja procesów w linuxie - semafory

  • 2011-05-23 16:47
  • 4 komentarze
  • 14552 odsłony
  • 6/6

Synchronizacja procesów - semafory


Spis treści

     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.1 ipcs
               1.3.2 ipcrm
          1.4 Operacje na semaforach
               1.4.1 ftok()
               1.4.2 semget()
               1.4.3 semctl()
               1.4.4 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 komentarze

gacek999 2011-05-29 17:04

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

zoogolo 2011-05-23 16:50

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 :)

flabra 2007-04-30 14:26

fajnie opisane :)

Marooned 2004-07-13 02:38

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