Sincronizarea prin semafoare

Am vazut in capitolul anterior modul în care se pot crea procese în Linux folosind funcţia fork. Un proces astfel creat poate să execute un anumit set de activităţi fără a interacţiona cu alte procese. De multe ori însă, apare necesitatea ca un proces să comunice cu alte procese aflate în execuţie în cadrul aceluiaşi sistem.

Deoarece procesele sunt entităţi complet independente ce nu îşi pot accesa decât propria zonă de date şi de cod, sistemul de operare Linux pune la dispoziţia programatorului o serie de mecanisme pe care le poate utiliza pentru a realiza schimbul de dare între procese.

Cel mai simplu mecanism de comunicare este cel prin fişiere standard. Două procese deschid un fişier în \ din care datele sunt scrise sau citite. Problema sincronizării între cele două procese, pentru a asigura consistenţa datelor revine programatorului.

Un alt mecanism de comunicare bazat pe fişiere este comunicarea prin pipe-uri (cu nume sau fără nume). Pipe-urile sunt fişiere speciale gestionate de către nucleul sistemului de operare. Pipe-urile pot fi asemănate cu nişte conducte care conectează două procese comunicante. Printr-un capăt al pipe-ului datele sunt scrise de către un proces iar prin celălalt capăt datele sunt citite de un alt proces.

Pe lângă mecanismele clasice de comunicare prin fişiere amintite anterior, Linux oferă prin intermediul Inter Process Communication Package (IPC) o serie de mecanisme avansate de comunicare între procese. Aceste mecanisme sunt: semafoarele, cozile de mesaje şi zonele de memorie comună.

În cadrul acestui capitol se va detalia mecanismul de sincronizare între procese folosind semafoarele IPC.

Semafoare Linux

Semafoarele reprezintă un mecanism prin care se poate controla accesul a două sau mai multe procese la o resursă comună. De exemplu în situaţia în care două procese trebuie să acceseze acelaşi fişier pentru a scrie şi citi date, pentru a asigura consistenţa datelor, trebuie să se folosească un mecanism de excludere mutuală, astfel încât, atâta timp cât unul dintre procese se află în zona de cod în care se realizează operaţii asupra conţinutului fişierului, celălalt proces să nu poată accesa respectivul fişier.

În esenţa un semafor este un număr întreg care poate fi incrementat respectiv decrementat de către două sau mai multe procese prin intermediul unei funcţii speciale. Această funcţia asigură de asemenea şi blocarea sau deblocarea proceselor în momentul în care valoarea semaforului atinge o anumită limită.

Crearea semafoarelor

Funcţia sistem prin intermediul căreia se construieşte un set de semafoare este funcţia semget. Semnătura funcţiei este următoarea:

     #include <sys/types.h>
     #include <sys/ipc.h>
     #include <sys/sem.h>
 
     int
     semget(key_t key, int nsems, int flag);

Pagina man asociată funcţiei semget este aceasta.

Funcţia semget construieşte un set format din unul sau mai multe semafoare. Deşi pentru a sincroniza două procese între ele este suficient un singur semafor, pentru operaţii mai complexe de sincronizare (stabilirea ordinii de execuţie a unor zone critice) sau pentru cazurile în care există mai multe zone critice ce trebuie sincronizate prin semafoare diferite, se pot construi seturi cu mai multe semafoare.

Argumentul key, reprezintă o cheie pe baza căreia se generează un identificator unic pentru setul de semafoare. Argumentul nsemn specifică numărul de semafoare care vor fi create în cadrul setului. Argumentul flag specifică drepturile de acces asupra setului de semafoare şi dacă să se creeze un nou set de semafoare sau doar să se caute în sistem existenţa unui set creat anterior cu cheia key şi să returneze id-ul acelui set.

Iată o secvenţă ce construieşte un set format din 10 semafoare:

    #include <sys/ipc.h>
    #include <sys/sem.h>
    ...
    int semid;
    semid = semget(100, 10, 0666 | IPC_CREAT);
    ...

Dacă se doreşte obţinerea id-ului unui set existent deja în sistem atunci al doilea şi al treilea argument pot avea valoarea 0. Toate semafoarele din set sunt iniţializate la valoarea 0. Primul semafor din set are indexul 0.

Utilizarea semafoarelor

Odată obţinut id-ul unui set de semafoare (fie prin crearea, fie prin conectare la un set deja existent în sistem), se pot implementa mecanisme de sincronizare între procese. Cele mai cunoscute primitive de sincronizare sunt P şi V şi sunt prezentate în continuare:

P(x)
{
      e(x) = e(x) – 1;
      if(e(x) < 0) {
            blochează proces_apelant;
            adaugă prces_apenat în coada de aşteptare;
}
}

 
V(x)
{
e(x) = e(x) + 1;
if(e(x) <= 0){
      extrage proces din coada de aşteptare;
      deblocheză procesul extras;
}
}

Unde prin v este identificat un semafor, iar e(x) reprezintă valoarea semaforului v.

Pentru realizarea unor regiuni critice se setează valoarea iniţială a semaforului la 1, şi regiunile critice sunt delimitate de apelul primitivelor P şi V.

Primitivele de sincronizare P şi V pot fi implementate folosind funcţia semop. Prin intermediul funcţie semop un proces poate incrementa (marchează o resursă ca fiind folosită) sau poate decrementa (marchează o resursă ca fiind eliberată) valoarea unui semafor din set.

Semnătura funcţiei semop este:

    #include <sys/types.h>
     #include <sys/ipc.h>
     #include <sys/sem.h>
 
     int
     semop(int semid, struct sembuf *array, size_t nops);

Pagina man asociată funcţiei este aceasta.

Argumentul semid reprezintă id-ul setului de semafoare (returnat de funcţia semget) asupra căruia se aplică operaţia. Argumentul array este un pointer către un vector de elemente de tip sembuf (structura sembuf va fi descrisă în pargraful următor), argumnetul noops specifică câte elmenete de tip sembuf se găsesc în vectorul array (dacă de exemplu se aplică o singură operaţie asupra structurii de semafoare atunci noops va avea valoarea 1).

Revenind la cel de-al doilea argument al funcţiei semop, acesta specifcă ce operaţii se aplică asupra semafoarelor din set. Structura sembuf are forma:

 struct sembuf {
                ushort  sem_num;        /* semaphore index in array */
                short   sem_op;         /* semaphore operation */
                short   sem_flg;        /* operation flags */
        };

Unde sem_num specifică numărul semaforului din set asupra căruia se va aplica operaţia, sem_op specifică operaţia care se va aplica semaforului (la valoarea curentă a semaforului va fi adăugată valoarea specificată de sem_op) iar argumentul sem_flg specifică modul de comportament al funcţiei semop în momentul în care semaforul este incrementat sau decrementat (vezi detalii în cadrul paginii man asociate funcţiei semop).

O proprietate foarte importantă a funcţiei semop este că aceasta se execută în mod atomic, adică sau sunt efectuate simultan toate operaţiile coprinse în vectorul de operaţii array, dacă acest lucru este posibil sau nu se efectuează nici una dintre ele şi procesul este blocat.

Modul de implementare şi de folosire a celor două primitive de sincronizare P şi V este ilustrat în programul de mai jos.

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

void semcall(int sid,int op){
   struct sembuf buf;
   buf.sem_num=0;
   buf.sem_op=op;
   buf.sem_flg=0;
   if(semop(sid,&buf,1)<0) printf(“eroare”);
}

void p(int sid)
      {semcall(sid,-1);}

void v(int sid)
      {semcall(sid,1);}

void main(){

   int semid;
   semid = semget(1999,1,0660|IPC_CREAT);

   v(semid); //linia de sters

   if(fork()==0){
        printf(“inainte de zona critica process 1\n”);              
        p(semid);
        printf("in zona critica proces 1\n");
        sleep(3);
        printf("gata zona critia proces 1\n");
        v(semid);
        printf(“dupa de zona critica process 1\n”);   
        exit(0);
   }

   if(fork()==0){
        printf(“inainte de zona critica process 2\n”);      
        p(semid);
        printf("in zona critica proces 2\n");
        sleep(3);
        printf("gata zona critia proces 2\n");
        v(semid);
        printf(“dupa de zona critica process 2\n”);
        exit(0);
   }

   while(wait()!=-1);
      semctl(semid, 0, IPC_RMID, 0);

}

Funcţiile p şi v sunt folosite în cadrul celor două procese fiu pentru delimitarea unor secvenţe de cod critice. Ca urmare a acestei delimitări, la un moment dat doar unul dintre cele două procese se poate afla în execuţia zonei delimitate de apelul funcţiilor p şi v, celălalt proces fiind blocat şi pus în coada de aşteptare.

Pentru ca mecanismul de sincronizare să funcţioneze corect este foarte important ca valoarea semaforului să fie iniţializată la 1 înainte de a lansa în execuţie cele 2 procese, în caz contrar nici unul dintre procese nu va primi permisiunea de a intra în zona critică (în cazul programul de mai sus iniţializarea semaforului la valoarea 1 se face prin apelul funcţiei v înainte de a construi cele două procese fiu).

Nu există nici o garanţie cu privire la ordinea în care cele două procese vor executa zonele lor critice. Primul dintre procese care reuşeşte să execute funcţia p de decrementare a semaforului va intra în execuţia zonei critice.

Alte opreatii cu semafoare

Funcţia semctl are rolul de a efectua o serie de operaţii asupra structurii asociate unui set de semafoare. Semnătura funcţiei este:

     #include <sys/types.h>
     #include <sys/ipc.h>
     #include <sys/sem.h>
 
     int
     semctl(int semid, int semnum, int cmd, ...);

Pagina man asociată funcţiei semctl este aceasta.

Funcţia semctl este utilizată de cele mai multe ori pentru setarea valorii iniţiale a unui semafor şi pentru ştergerea unei structuri de semafoare.

Pentru a seta valoarea iniţială a unui semafor se foloseşte funcţia semctl sub forma:

    semctl(semID,0, SETVAL,1);

Pentru a şterge o structură de semafoare se foloseşte funcţia semctl sub forma:

    semctl(semID,0,IPC_RMID,0);

Deoarece semafoarele sunt resurse de nucleu externe proceselor este important ca după terminarea utilizării unei structuri de semafoare aceasta să fie ştearsă, în caz contrar ea rămânând rezidentă în memorie chiar şi după terminarea procesului care a creato.