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
toS = 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
) toS = S+n-m
. W przeciwnym wypadkuS = 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_flg |
Możliwe flagi:
|
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);
}
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...