[LinuxFocus-icon]
Strona G³ówna  |  Mapa Serwisu  |  Indeks  |  Szukaj

Nowo¶ci | Archiwum | Linki | O Nas
[an error occurred while processing this directive]
[Leonardo]
Leonardo Giordani
<leo.giordani(at)libero.it>

O Autorze:

Student in¿ynierii telekomunikacji na politechnice w Milan, pracuje jako administrator sieciowy i interesuje siê programownaiem (g³ównie Asembler i C/C++). Od 1999 pracuje prawie wy³±cznie pod systemem Linux/Unix.

Translated to English by:
Mariusz Koz³owski <sp3fxc(at)linuxfocus.org>

Zawarto¶æ:


 

Programowanie wspó³bie¿ne - komunikacja miêdzy procesami

[run in paralell]

Notka:

Ta seria artyku³ów ma na celu wprowadzenie czytelnika w zagadnienia programownaia wspó³bie¿nego i jego implementacji na systemie Linux. Poczynaj±c od teoretycznego zarysu zagadnieñ wspo³bie¿nej pracy procesów skoñczymy na napisaniu kompletnej aplikacji demonstruj±cej komunikacjê miêdzy procesami opart± na prostym lecz skutecznym protokole komunikacji.

Mimimalne wymagania aby zrozumieæ ten artyku³ to:

Powiniene¶ przeczytaæ pierwszy artyku³ z tej serii poniewa¿ jest on wprowadzeniem do tego artyku³u: November 2002, article 272.
_________________ _________________ _________________

 

Introduction

I oto znowu zmagamy siê z wspo³bie¿no¶ci± w systemie Linux. Jak ju¿ wcze¶niej widzieli¶my we wcze¶niejszym artykule u¿ywanie "fork" wymaga tylko kilku lini kodu, poniewa¿ system sam przejmuje kontrole nad inicjalizacj± i zarz±dzaniem procesów przez nas tworzonych.

Ta us³uga dostarczana przez system operacyjny ma fundamentalne znaczenie, jest to "kontroler" wykonywanych procesów; dlatego wiêc, procesy s± wykonywane w odpowiednim ¶rodowisku. Utrata kontroli nad wykonywaniem tych procesów powoduje powstanie nowego problemu - synchronizacji wykonywania procesów, co mo¿na podsumowaæ pyaniem: jak to mo¿liwe aby pozwoliæ dwum niezale¿nym procesom pracowaæ razem?

Problem jest bardziej skomplikowany niz na to wyglada: to nie jest tylko kwestia synchronizacji w wykonywaniu procesów, ale równie¿ dzielenia danych zarówno w trybie "read" jak i "read-write".

Rozwa¿my teraz kilka klasycznych probemów rownoczesnego dostêpu do danych; je¶li dwa procesy czytaj± ten sam zestaw danych to oczywi¶cie nie jest problem, i wtedy wykonywanie nastêpuje w sposób harmonijny. Niech teraz jeden z procesów zmodyfikuje dane: drugi zwróci inne wyniki zale¿nie od czasu kiedy je odczyta³, przed czy po ich modyfikacji przez pierwszy proces. Np.: mamy dwa procesy "A" i "B" i zmienna "d". Proces A zwieksza d o 1, a proces B wypisuje wartosc tej zmiennej. Zapisuj±c to w pseudo-jêzyku mo¿emy t oprzedstawiæ nastêpuj±co:

A { d->d+1 } & B { d->wynik }

gdzie "&" oznacza wspó³bie¿ne wykonywanie. Pierwsza mo¿liweo¶æ wykonania to:

(-) d = 5 (A) d = 6 (B) wynik = 6

ale je¶li proces B wykona siê pierwszy otrzymamy:

(-) d = 5 (B) wynik = 5 (A) d = 6

Teraz widzisz jak istotne jest odpowienie zarz±dzanie takimi sytuacjami: ryzyko nieodpowiedniego wykonania siê jest du¿e i nie do zaakceptowanie. Pomy¶l ¿e te dane reprezentuj± stan Twojego konta w banku i nigdy nie bedziesz w stanie stwierdziæ prawdziwego stanu konta.

We wcze¶niejszym artykule mówili¶my ju¿ o pierwszym rodzaju synchronizacji poprzez u¿ycie funkcji waitpid(2), która pozwala procesowi czekaæ na zakoñczenie innego procesu zanim wykona siê dalej. W rzeczywisto¶ci pozwala to nam rozwi±zaæ niektóre z konfliktów powsta³ych podczas odczytu i zapisu danych: jak tylko dane, na których pracowa³ proces P1, zostana zapisane proces P2, który ma pracowaæ na tych samych danych powinien zaczekaæ na zakoñczenie procesu P1 zanim zacznie siê wykonywaæ.

Mowi±c ja¶niej ta metoda reprezentuje pierwsze rozwi±zanie, ale jest daleka od idea³u, poniewa¿ P2 musi czekaæ bezczynnie przez okres czasu, który mo¿e byæ bardzo d³ugi, czelaj±c a¿ P1 zakoñczy dzia³anie, nawet je¶li nie musi dzia³±æ na wspólnych danych. Dlatego musimy zwiêkszyæ nasz± kontrolê nad dzia³aniem procesów np.: ustaliæ zasady rz±dz±ce dostêpem do konkretnych danych. Rozwi±zaniem tego problemu jest zestaw podstawowych funkcji standardowej biblioteki znanej jako SysV IPC (System V InterProcess Communication).  

klucze SysV

Zanim zajmiemy siê rzeczami ¶ci¶le zwi±zanymi z teori± wspo³bie¿noo¶ci i jej implementacj± pozwólcie na ma³e wprowadzenie w typow± strukturê kluczy SysV IPC. Klucz IPC jest to numer u¿ywany do jednoznacznej identyfikacji struktury IPC (control structure opisanej pózniej), ale równie¿ mo¿e byæ u¿yty do generowania podstawowych identyfikatorów, np.: do organizacji struktur spoza IPC. Klucz mo¿e byæ utwo¿ony przez funkcjê ftok(3)

key_t ftok(const char *pathname, int proj_id);

która u¿ywa nazwy istniej±cego pliku (pathname) i liczby integer. Nie zapewnia nam to ze klucz jest niepowtarzalny, poniewa¿ parametry wziête z pliku(numer i-node i numer urz±dzenia) mog± wytworzyæ identyczne kombinacje. Dobrym rozwi±zaniem jest stworzenie ma³ej biblioteki, która ¶ledzi zaznaczone klucze i unika duplikatów.  

Semafory

Idea semaforu dla ruchu samochodowego mo¿e byæ u¿yta bez wiêkszych modyfikacji dla kontrolowania dostêpu do danych. Semafor to specyficzna struktura zawieraj±ca warto¶ci wiêksz lub równe zeru, która zarz±dza kolejk± procesów czekaj±cych na wyst±pienie odpowiednich warunków w semaforze. Nawet je¶li wydaje siê to prste semafory s± bardzo dobrym rozwi±zaniem. Na pocz±tek pominiemy obs³ugê b³êdów: do³o¿ymy to do naszego kodu gdy zaczniemy pisaæ bardziej skomplikowany program.

Semafory mog± byæ u¿yte do kontrolowania dostêpu do danych: warto¶æ semafora reprezentuje ilo¶æ procesów jakie mog± dzia³aæ jednocze¶nie na danym zasobie; Za ka¿dym razem jak proces korzysta z zasoby warto¶æ semafora powinna byc dekrementowana i inkrementowana po zowlnieniu zasobu przez proces. Je¶li dostêp do zasobu jest wy³±czny (np.: tylko jeden proces w danej chwili mo¿e z niego korzystaæ) warto¶æ pocz±tkowa semafora bêdzie równa 1.

Semafor mo¿e spe³niaæ równie¿ inn± rolê - licznik zasobów: warto¶æ jak± przedstawia w tym przypadku to ilo¶æ zasobów dostêpnych (np.: ilo¶æ wolnych komórek pamiêci).

Rozwa¿my rzeczywisty przypadek, w którym semafory bêd± u¿yte: mamy bufor, w którym kilka procesów S1,...,Sn mo¿e zapisywaæ ale tylko jeden proces L mo¿e czytaæ; cowiêcej, operacje nie mog± byæ wykonywane w tym samym czasie (np.: tylko jeden proces mo¿e w danym czasie operowaæ na buforze). Oczywi¶cie S procesów mo¿e zawsze pisaæ poza przypadkiem gdy bufor jest pe³ny, a proces L mo¿e czytaæ tylko wtedy gdy bufor nie jest pusty. Dlatego potrzebujemy trzy semafory: pierwszy zarz±dzaj±cy dostêpem do zasobów, drugi i trzeci bêdz± ¶ledziæ ile elementów znajduje siê w buforze (po¿niej siê przekonamy dlaczego dwa semafory nie wystarcz±).

Bior±c pod uwagê ze dostêp do danych jest wy³±czny pierwszy semafor bêdzie binarny (jego warto¶æ bêdzie równa 0 lub 1). Drugi i trzeci przyjm± warto¶ci zale¿ne od rozmiaru bufora.

Teraz zobaczymy jak semafory s± zaimplementowane w C z u¿yciem SysV. Funkcja tworz±ca semafor to semget(2)

int semget(key_t key, int nsems, int semflg);

gdzie key to klucz IPC, nsems to ilo¶æ semaforów jak± chcemy stworzyæ, a semflg to kontrola dostêpu zaimplementowana na 12 bitach, pierwsze 3 s± zale¿ne od metody tworzenia, a kolejne 9 prawa zapisu i odczytu dla u¿ytkownika, grupy i innych (zauwa¿ podobieñstwo do systemu plików w Unix'ie). Szczegó³owy opis znajduje siê w manualu do ipc(5). Jak mog³e¶ ju¿ zauwa¿yæ SysV zarz±dza zestawem semaforów zamiast pojedyñczym semaforem, co pozwala uzyskaæ bardziej zwarty kod.

Stwórzmy nasz pierwszy semafor

#include <stdio.h>
#include <stdlib.h>
#include <linux/types.h>
#include <linux/ipc.h>
#include <linux/sem.h>

int main(void)
{
  key_t key;
  int semid;

  key = ftok("/etc/fstab", getpid());

  /* tworzy zestaw semaforów z³o¿ony tylko z jednego semafora */
  semid = semget(key, 1, 0666 | IPC_CREAT);

  return 0;
}
Id±c dalej musimy siê nauczyæ jak zarz±dzaæ semaforami i je usuwaæ; zarz±dzanie semaforem jest uzyskiwane poprzez funkcjê semctl(2)

int semctl(int semid, int semnum, int cmd, ...)

,która dzia³a odpowiednio do akcji zidentyfikowanej przez cmd na zestawie semid i (je¶li wymaga tego akcja) na pojedyñczym semaforze semnum. Wprowadzimy pewne opcje wraz z tym jak bêdziemy ich potrzebowaæ, ale kompletna ich lista jest zawarta w manualu. Zale¿nie od akcji cmd mo¿e nast±piæ potrzeba podania dodatkowych argumentów dla funkcji, której typ to
union semun {
 int val;                  /* warto¶æ dla SETVAL */
 struct semid_ds *buf;     /* bufor dla IPC_STAT, IPC_SET */
 unsigned short *array;    /* tablica dla GETALL, SETALL */
                           /* czê¶æ zale¿na od linuxa: */
 struct seminfo *__buf;    /* bufor dla IPC_INFO */
};
Aby ustawiæ warto¶æ semafora dyrektywa SETVAL powinna byæ u¿yta, a warto¶æ podana w unii semun; zmodyfikujmy poprzedni program ustawiaj±c warto¶æ semafora na 1
[...]

  /* tworzy zestaw semaforów z³o¿ony tylko z jednego semafora */
  semid = semget(key, 1, 0666 | IPC_CREAT);

  /* ustawia warto¶æ semafora 0 na 1 */
  arg.val = 1;
  semctl(semid, 0, SETVAL, arg);

[...]
Dalej musimy zwolniæ seamfor dealokuj±c strukturê u¿yt± do jego zarz±dzania; to zadanie zostanie wykonane przez dyrektywê IPC_RMID dla semctl. Ta dyrektywa usuwa seamfor i wysy³a wiadomo¶æ do wszystkich procesów czekaj±cych na uzyskanie dostêpu do zasobu. Ostatnia modyfikacja programu to
[...]

  /* ustawienie warto¶ci semafora o numerze 0 na 1 */
  arg.val = 1;
  semctl(semid, 0, SETVAL, arg);

  /* dealokacja semafora */
  semctl(semid, 0, IPC_RMID);

[...]
Jak widaæ tworzenie i zarz±dzanie struktur± do kontrolowania wspo³bie¿nego wykonywania siê kodu nie jest trudne; kiedy wprowadzimy obs³ugê b³êdów stanie siê to nieco bardziej skomplikowane, ale tylko z punku widzenia z³o¿ono¶ci kodu.

Semafor mo¿e teraz byæ u¿ywany przez funkcjê semop(2)

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

gdzie semid to identyfikator zestawu, sops to tablica array zawieraj±ca operacje do wykonania i nsops to ilo¶æ tych operacji. Ka¿da operacja jest reprezentowana przez strukturê sembuf.

unsigned short sem_num; short sem_op; short sem_flg;

np.: przez nr semafora w zestawie (sem_num), operacje (sem_op) i flagê okre¶laj±c± oczekiwanie; narazie niech sem_flg bêdzie równe 0. Operacje jakie mo¿emy podaæ s± dodtnimi numerami przydzielanymi wg nastepuj±cych zasad:
  1. sem_op < 0
    Je¶li ca³kowita warto¶æ semafora jest równa b±dz wiêksza od warto¶ci sem_op to operacja jest wykonywana i sem_op jest dodane do warto¶ci semafora (w³a¶ciwie jest odejmowana ujemna warto¶æ). Je¶li ca³kowita warto¶æ sem_op jest mniejsza od warto¶ci semafora proces jest wprowadzany w stan u¶pienia az taka liczba zasobów jest dostêpna.
  2. sem_op = 0
    Proces ¶pi przez czas gdy warto¶æ semafora wynosi 0.
  3. sem_op > 0
    Warto¶æ sem_op jest dodawana do warto¶ci semafora, zwalniaj±c zasoby uprzednio zajête.
Poni¿szy program pokazuje jak u¿ywaæ semaforów implementuj±c poprzedni przyk³ad z buforem: stworzymy 5 procesów zwanych W (writers) i proces R (reader). Ka¿dy proces W próbuje uzyskaæ dostêp do zasobów (bufor) blokuj±c go przez semafor i, je¶li bufor nie jest pe³ny, umieszcza element w nim i zwalnia zasób. Proces R próbuje uzyskaæ dostêp do zasobów i bierze element je¶li bufor nie jest pusty i odblokowuje zasób.

Odczyt i Zapis to tylko wirtualne dzia³ania: dzieje siê tak dlatego, ¿e jak mowili¶my w poprzednim artykule, ka¿dy proces ma w³asn± przestrzeñ w pamiêci i nie mo¿e uzyskaæ dostêpu do przestrzeni innego procesu. To powoduje niemo¿liwe odpowiednie zarz±dzanie 5 procesów, poniewa¿ ka¿dy dziala na wlasniej kopii bufora. Zmieni siê to gdy zaczniemy mówiæ o pamiêci dzielonej ale narazie uczmy siê krok po kroku.

Dlaczego potrzebujemy 3 semaforów? Pierwszy (numer 0) dzia³a jako stra¿nik dostêpu do bufora i jego maksymalna warto¶æ to 1. Drugi i trzeci kontroluj± warunek niedope³nienia i przepe³nienia bufora. Pojedyñczy semafor nie mo¿e obs³u¿yæ tych dwóch sytuacji poniewa¿ semop dzia³a tylko w jedn± stronê.

Rozja¶nijmy trochê sytuacjê z semaforem (zwanym O), którego warto¶æ reprezentuje ilo¶æ wolnych komórek w buforze. Za ka¿dym razem gdy S procesów wrzuca co¶ do bufora warto¶æ semafora zmniejsza siê o 1 a¿ wreszcie osi±gnie warto¶æ 0. Np.: bufor jest pe³ny. Ten semafor nie mo¿e obs³ugiwaæ sytuacji niedope³nienia bufora: proces R mo¿e w rzeczywisto¶ci zwiêkszaæ jego warto¶æ bez koñca. Potrzebujemy tego specjalnego semafora (zwanego U), którego warto¶æ reprezentuje ilo¶æ elementów w buforze. Za ka¿dym razem gdy proces W wrzuca element do bufora jednocze¶nie zwiêksza warto¶æ semafora U i zmniejsza warto¶æ semafora O. W przeciwieñstwie, proces R zmniejszy warto¶æ semafora U i zwiêkszy semafora O.

Przepe³nienie nastêpuje wtedy gdy nie mo¿emy zmniejszyæ warto¶ci semafora O, a niedope³nienie nastêpuje gdy niemo¿emy zmniejszyæ warto¶ci semafora U.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <linux/types.h>
#include <linux/ipc.h>
#include <linux/sem.h>

int main(int argc, char *argv[])
{
  /* IPC */
  pid_t pid;
  key_t key;
  int semid;
  union semun arg;
  struct sembuf lock_res = {0, -1, 0};
  struct sembuf rel_res = {0, 1, 0};
  struct sembuf push[2] = {1, -1, IPC_NOWAIT, 2, 1, IPC_NOWAIT};
  struct sembuf pop[2] = {1, 1, IPC_NOWAIT, 2, -1, IPC_NOWAIT};

  /* Other */
  int i;

  if(argc < 2){
    printf("U¿ycie: bufdemo [rozmiar]\n");
    exit(0);
  }

  /* Semafory */
  key = ftok("/etc/fstab", getpid());

  /* Tworzy zestaw semaforów z³o¿ony z 3 semaforów */
  semid = semget(key, 3, 0666 | IPC_CREAT);

  /* Inicjacja semafora #0 na 1 - kontrola zasobów */
  arg.val = 1;
  semctl(semid, 0, SETVAL, arg);

  /* Inicjacja semafora #1 na buf_length - kontrola przepe³nienia */
  /* Sem value is 'free space in buffer' */
  arg.val = atol(argv[1]);
  semctl(semid, 1, SETVAL, arg);

  /* Inicjacja semafora #2 na buf_length - kontrola niedope³nienia */
  /* Sem value is 'elements in buffer' */
  arg.val = 0;
  semctl(semid, 2, SETVAL, arg);

  /* Fork */
  for (i = 0; i < 5; i++){
    pid = fork();
    if (!pid){
      for (i = 0; i < 20; i++){
	sleep(rand()%6);
	/* Próba zajêcia zasobu - sem #0 */
	if (semop(semid, &lock_res, 1) == -1){
	  perror("semop:lock_res");
	}
	/* Zab³okowanie wolnej przestrzeni - sem #1 / wrzucenie elementu - sem #2*/
	if (semop(semid, &push, 2) != -1){
	  printf("---> Process:%d\n", getpid());
	}
	else{
	  printf("---> Process:%d  BUFFER FULL\n", getpid());
	}
	/* Zwolnij zasób */
	semop(semid, &rel_res, 1);
      }
      exit(0);
    }
  }

  for (i = 0;i < 100; i++){
    sleep(rand()%3);
    /*Spróbuj zablokowaæ zasób - sem #0 */
    if (semop(semid, &lock_res, 1) == -1){
      perror("semop:lock_res");
    }
    /* Odblokuj woln± przestrzeñ - sem #1 / pobierz element - sem #2 */
    if (semop(semid, &pop, 2) != -1){
      printf("<--- Process:%d\n", getpid());
    }
    else printf("<--- Process:%d  BUFFER EMPTY\n", getpid());
    /* Zwolnij zasób */
    semop(semid, &rel_res, 1);
  }

  /* Kasuj semafory */
  semctl(semid, 0, IPC_RMID);

  return 0;
}
Skomentujmy najbardziej interesuj±ce partie kodu:
struct sembuf lock_res = {0, -1, 0};
struct sembuf rel_res = {0, 1, 0};
struct sembuf push[2] = {1, -1, IPC_NOWAIT, 2, 1, IPC_NOWAIT};
struct sembuf pop[2] = {1, 1, IPC_NOWAIT, 2, -1, IPC_NOWAIT};
Te 4 linie to akcje jakie mo¿emy wykonaæ na zestawie semaforów: pierwsze dwie to pojedyñcze akcje, a kolejne s± podwójne. Pierwsza akcja, lock_res, próbuje zabokowaæ zasób: zmniejsza warto¶æ pierwszego semafora (numer 0) o 1 (je¶li jego warto¶æ nie jest równa zero) i dzia³anie procesów je¶li zasób jest zajêty jest ¿adne (np.: the procesy czekaj±). Akcja rel_res jest identyczna lock_res ale zasób jest zwalniany (warto¶æ jest dodatnia).

Akcje pobierz i po³ó¿ s± akcjami bitowymi. S± to tablice dwóch akcji, pierwsza na semaforze 1 i druga na semaforze 2; gdy pierwszy siê zwiêksza drugi maleje i na odwrót, ale procesy ju¿ nie oczekuj±: IPC_NOWAIT zmusza proces do kontynuowania wykonywania je¶li zasób jest zajêty.

/* Inicjacja semafora #0 na 1 - kontrola zasobów */
arg.val = 1;
semctl(semid, 0, SETVAL, arg);

/* Inicjacja semafora #1 na buf_length - kontrola przepe³nienia */
/* warto¶æ Sem  to 'wolna przestrzeñ w buforze' */
arg.val = atol(argv[1]);
semctl(semid, 1, SETVAL, arg);

/* Inicjacja semafora #2 na buf_length - kontrola niedope³nienia */
/* warto¶æ Sem to 'element w buforze' */
arg.val = 0;
semctl(semid, 2, SETVAL, arg);
Tutaj inicjalizujemy warto¶æ semaforów: pierwszy na 1 poniewa¿ kontroluje wy³±czny dostêp do zasobów, drugi na d³ugo¶æ bufora (podan± z lini komend) i trzeci na 0, jak wcze¶niej napisa³em apropo przpe³enienia i niedope³nienia.
/* Próba zablokowania zasobu - sem #0 */
if (semop(semid, &lock_res, 1) == -1){
  perror("semop:lock_res");
}
/* Zablokuj woln± przestrzeñ - sem #1 / wrzyæ element - sem #2*/
if (semop(semid, &push, 2) != -1){
  printf("---> Process:%d\n", getpid());
}
else{
  printf("---> Process:%d  BUFFER FULL\n", getpid());
}
/* Zwolnij zasób */
semop(semid, &rel_res, 1);
Proces W próbuje zablokowaæ zasób poprzez akcjê lock_res; kiedy ju¿ to zrobi wywo³ywane jest wrzucenie elementu do bufora i wypisuje na standardowym wyj¶ciu (monitor): je¶li operacja nie mo¿e byæ wykonana wypisuje ze bufor jest pe³ny po czym zwalnia zasób.
/* Próba zablokowania zasobu - sem #0 */
if (semop(semid, &lock_res, 1) == -1){
  perror("semop:lock_res");
}
/* Zwolnij woln± przestrzeñ - sem #1 /  odczytaj element- sem #2 */
if (semop(semid, &pop, 2) != -1){
  printf("<--- Process:%d\n", getpid());
}
else printf("<--- Process:%d  BUFFER EMPTY\n", getpid());
/* Zwolnij zasób */
semop(semid, &rel_res, 1);
Proces R dzia³a mniejwiêcej jak proces W: blokuje urz±dzenie, pobiera element i zwalnia je.

W nastêpnym artykule omówiê kolejki komunikatów, inn± strukturê InterProcess Communication i synchroznizacjê. Jak zwykle je¶li napiszecie co¶ prostego z wykorzystaniem tego czego siê dowiedzieli¶cie z teog artyku³u wy¶lijcie to do mnie wraz z imieniem i adresem email. Bêdê szczêsliwy mog±c to przeczytaæ. Powodzenia!  

Warto przeczytaæ

 

Dyskusja dotycz±ca tego artyku³u

Komentarze do dyskusji:
 Strona talkback 

Strona prowadzona przez redakcjê LinuxFocus
© Leonardo Giordani, FDL
LinuxFocus.org
t³umaczenie:
it --> -- : Leonardo Giordani <leo.giordani(at)libero.it>
it --> en: Leonardo Giordani <leo.giordani(at)libero.it>
en --> pl: Mariusz Koz³owski <sp3fxc(at)linuxfocus.org>

2002-12-23, generated by lfparser version 2.33