6. Comunicarea intre procese folosind pipes

O metoda foarte des utilizata in UNIX pentru comunicarea intre procese este folosirea primitivei numita pipe (conducta). "Conducta" este o cale de legatura care poate fi stabilita intre doua procese inrudite (au un stramos comun sau sunt in relatia stramos-urmas). Ea are doua capete, unul prin care se pot scrie date si altul prin care datele pot fi citite, permitand o comunicare intr-o singura directie. In general, sistemul de operare permite conectarea a unuia sau mai multor procese la fiecare din capetele unui pipe, astfel incat, la un moment dat este posibil sa existe mai multe procese care scriu, respectiv mai multe procese care citesc din pipe. Se realizeaza, astfel, comunicarea unidirectionala intre procesele care scriu si procesele care citesc.  

1. Apelul sistem pipe( )

Crearea conductelor de date de face in UNIX folosind apelul sistem pipe( ):
int pipe(int filedes[2]);
Functia creeaza un pipe, precum si o pereche de descriptori de fisier care refera cele doua capete ale acestuia. Descriptorii sunt returnati catre programul apelant completandu-se cele doua pozitii ale tabloului filedes trimis ca parametru apelului sistem. Pe prima pozitie va fi memorat descriptorul care indica extremitatea prin care se pot citi date (capatul de citire), iar pe a doua pozitie va fi memorat descriptorul capatului de scriere in pipe.

Cei doi descriptori sunt descriptori de fisier obisnuiti, asemanatori celor returnati de apelul sistem open( ). Mai mult, pipe-ul poate fi folosit in mod similar folosirii fisierelor, adica in el pot fi scrise date folosind functia write( ) (aplicata capatului de scriere) si pot fi citite date prin functia read( ) (aplicata capatului de citire).

Fiind implicati descriptori de fisier obisnuiti, daca un pipe este creat intr-un proces parinte, fiii acestuia vor mosteni cei doi descriptori (asa cum, in general, ei mostenesc orice descriptor de fisier deschis de parinte). Prin urmare, atat parintele cat si fiii vor putea scrie sau citi din pipe. In acest mod se justifica afirmatia facuta la inceputul acestui document prin care se spunea ca pipe-urile sunt folosite la comunicarea intre procese inrudite. Pentru ca legatura dintre procese sa se faca corect, fiecare proces trebuie sa declare daca va folosi pipe-ul pentru a scrie in el (transmitand informatii altor procese) sau il va folosi doar pentru citire. In acest scop, fiecare proces trebuie sa inchida capatul pipe-ului pe care nu il foloseste: procesele care scriu in pipe vor inchide capatul de citire, iar procesele care citesc vor inchide capatul de scriere, folosind functia close( ).

Functia returneaza 0 daca operatia de creare s-a efectuat cu succes si -1 in caz de eroare.

Un posibil scenariu pentru crearea unui sistem format din doua procese care comunica prin pipe este urmatorul:

  • procesul parinte creeaza un pipe
  • parintele apeleaza fork( ) pentru a crea fiul
  • fiul inchide unul din capete (ex: capatul de citire)
  • parintele inchide celalalt capat al pipe-ului (cel de scriere)
  • fiul scrie date in pipe folosind descriptorul ramas deschis (capatul de scriere)
  • parintele citeste date din pipe prin capatul de citire.
Primitiva pipe se comporta in mod asemanator cu o structura de date coada: scrierea introduce elemente in coada, iar citirea le extrage pe la capatul opus.

Iata in continuare o portiune de program scris conform scenariului de mai sus:

void main()
{
  int pfd[2];
  int pid;

  ...
  if(pipe(pfd)<0)
    {
      printf("Eroare la crearea pipe-ului\n");
      exit(1);
    }
  ...
  if((pid=fork())<0)
    {
      printf("Eroare la fork\n");
      exit(1);
    }
  if(pid==0) /* procesul fiu */
    {
      close(pfd[0]); /* inchide capatul de citire; */
                     /* procesul va scrie in pipe  */
      ...
      write(pfd[1],buff,len); /* operatie de scriere in pipe */
      ...
      close(pfd[1]); /* la sfarsit inchide si capatul utilizat */
      exit(0);
    }
  else /* procesul parinte */
    {
      close(pfd[1]); /* inchide capatul de scriere; */
                     /* procesul va citi din pipe  */
      ...
      read(pfd[0],buff,len); /* operatie de citire din pipe */
      ...
      close(pfd[0]); /* la sfarsit inchide si capatul utilizat */
      exit(0);
    }
}

Conexiunea intre cele doua procese din care este format programul de mai sus este reflectata in figura urmatoare:

Observatii:

1. Cantitatea de date care poate fi scrisa la un moment dat intr-un pipe este limitata. Numarul de octeti pe care un pipe ii poate pastra fara ca ei sa fie extrasi prin citire de catre un proces este dependenta de sistem (de implementare). Standardul POSIX specifica limita minima a capacitatii unui pipe: 512 octeti. Atunci cind un pipe este "plin", operatia write() se va bloca pina cind un alt proces nu citeste suficienti octeti din pipe.

2. Un proces care citeste din pipe va primi valoarea 0 ca valoare returnata de read( ) in momentul in care toate procesele care scriau in pipe au inchis capatul de scriere si nu mai exista date in pipe.

3. Daca pentru un pipe sunt conectate procese doar la capatul de scriere (cele de la capatul opus au inchis toate conexiunea) operatiile  write efectuate de procesele ramase vor returna eroare. Intern, in aceasta situatie va fi generat semnalul SIG_PIPE care va intrerupe apelul sistem write respectiv. Codul de eroare (setat in variabila globala errno) rezultat este cel corespunzator mesajului de eroare "Broken pipe".

4. Operatia de scriere in pipe este atomica doar in cazul in care numarul de octeti scrisi este mai mic decit constanta PIPE_BUF. Altfel, in sirul de octeti scrisi pot fi intercalate datele scrise de un alt proces in pipe. Pentru detalii, consultati manualul pipe(7).

2. Redirectarea descriptorilor de fisier

Se stie ca functia open( ) returneaza un descriptor de fisier. Acest descriptor va indica fisierul deschis cu open( ) pana la terminarea programului sau pana la inchiderea fisierului. Sistemul de operare UNIX ofera, insa, posibilitatea ca un descriptor oarecare sa indice un alt fisier decat cel obisnuit. Operatia se numeste redirectare si se foloseste cel mai des in cazul descriptorilor de fisier cu valorile 0, 1 si 2 care reprezinta intrarea standard, iesirea standard si, respectiv, iesirea standard de eroare. De asemenea, este folosita si operatia de duplicare a descriptorilor de fisier, care determina existenta a mai mult de un descriptor pentru acelasi fisier. De fapt, redirectarea poate fi vazuta ca un caz particular de duplicare.

Duplicarea si redirectarea se fac, in functie de cerinte, folosind una din urmatoarele apeluri sistem:

 int dup(int oldfd);
 int dup2(int oldfd, int newfd);
Functia dup( ) realizeaza duplicarea descriptorului oldfd, returnand noul descriptor. Aceasta inseamna ca descriptorul returnat va indica acelasi fisier ca si oldfd, atat noul cat si vechiul descriptor folosind in comun pointerul de pozitie in fisier, flag-urile fisierului etc. Daca pozitia in fisier e modificata prin intermediul functiei lseek( ) folosind unul dintre descriptori, efectul va fi observat si pentru operatiile facute folosind celalalt descriptor. Descriptorul nou alocat de dup( ) este cel mai mic descriptor liber (inchis) disponibil.

Functia dup2( ) se comporta in mod asemanator cu dup( ), cu deosebirea ca poate fi indicat explicit care sa fie noul descriptor. Dupa apelul dup2( ), descriptorul newfd va indica acelasi fisier ca si oldfd. Daca inainte de operatie descriptorul newfd era deschis, fisierul indicat este mai intai inchis, dupa care se face duplicarea.

Ambele functii returneaza descriptorul nou creat (in cazul lui dup2( ), egal cu newfd) sau -1 in caz de eroare.

Urmatoarea secventa de cod realizeaza redirectarea iesirii standard spre un fisier deschis, cu descriptorul corespunzator fd:

...
fd=open("Fisier.txt", O_WRONLY);
...
if((newfd=dup2(fd,1))<0)
  {
    printf("Eroare la dup2\n");
    exit(1);
  }
...
printf("ABCD");
...
In urma redirectarii, textul "ABCD" tiparit cu printf( ) nu va fi scris pe ecran, ci in fisierul cu numele "Fisier.txt".

Redirectarile de fisiere se pastreaza chiar si dupa apelarea unei functii de tip exec( ) (care suprascrie procesul curent cu programul luat de pe disc). Folosind aceasta facilitate, este posibila, de exemplu, conectarea prin pipe a doua procese, unul din ele ruland un program executabil citit de pe disc. Secventa de cod care realizeaza acest lucru este data mai jos. Se considera ca parintele deschide un pipe din care va citi date, iar fiul este un proces care executa un program de pe disc. Tot ce afiseaza la iesirea standard procesul luat de pe disc, va fi redirectat spre capatul de scriere al pipe-ului, astfel incat parintele poate citi datele produse de acesta.
 

void main()
{
  int pfd[2];
  int pid;
  FILE *stream;

  ...
  if(pipe(pfd)<0)
    {
      printf("Eroare la crearea pipe-ului\n");
      exit(1);
    }
  ...
  if((pid=fork())<0)
    {
      printf("Eroare la fork\n");
      exit(1);
    }
  if(pid==0) /* procesul fiu */
    {
      close(pfd[0]); /* inchide capatul de citire; */
                     /* procesul va scrie in pipe  */
      ...
      dup2(pfd[1],1); /* redirecteaza iesirea standard spre pipe */
      ...
      execlp("ls","ls","-l",NULL); /* procesul va rula comanda ls */
      printf("Eroare la exec\n);
        /* Daca execlp s-a intors, inseamna ca programul nu a putut fi lansat in executie */
    }
  else /* procesul parinte */
    {
      close(pfd[1]); /* inchide capatul de scriere; */
                     /* procesul va citi din pipe  */
      ...
      stream=fdopen(pfd[0],"r");
        /* deschide un stream (FILE *) pentru capatul de citire */
      fscanf(stream,"%s",string);
        /* citire din pipe, folosind stream-ul asociat */
      ...
      close(pfd[0]); /* la sfarsit inchide si capatul utilizat */
      exit(0);
    }
}

Functia fdopen( ) a fost folosita pentru a putea folosi avantajele functiilor de biblioteca pentru lucrul cu fisiere in cazul unui fisier (capatul de citire din pipe) indicat de un descriptor intreg (in speta, s-a dorit sa se efectueze o citire formatata, cu fscanf( ), din pipe).
 


1. Sa se scrie un program care creeaza trei procese, astfel:

  • primul proces (parinte) citeste dintr-un fisier cu numele date.txt un sir de caractere, pana la sfarsitul fisierului si le trimite printr-un pipe primului fiu
  • primul fiu primeste caracterele de la parinte si selecteaza din ele literele mici, pe care le trimite printr-un pipe catre cel de-al doilea fiu.
  • al doilea fiu creeaza un fisier numit statistica.txt, in care va memora, pe cate o linie, fiecare litera distincta intalnita si numarul de aparitii a acesteia in fluxul de date primit. In final, va trimite catre parinte, printr-un pipe suplimentar, numarul de litere distincte intalnite.
  • parintele afiseaza pe ecran rezultatul primit de la al doilea fiu.
2. Sa se realizeze un program de tip producatori-consumatori astfel:
  • programul primeste ca parametri in linia de comanda doua numere: numarul de procese producator si numarul de procese consumator;
  • resursa comuna tutror proceselor este un pipe creat de procesul parinte;
  • parintele este la randul lui producator;
  • in general, producatorii scriu in pipe un numar oarecare de caractere, iar consumatorii citesc din pipe, caracter cu caracter, cat timp acest lucru este posibil.
Producatorii vor avea urmatoarea forma:

a) Parintele produce un numar de caractere '*';
b) Ceilalti producatori sunt procese independente (existente pe disc ca programe de sine statatoare) care genereaza la iesirea standard un numar oarecare de caractere '#'. Pentru realizarea scopului propus, iesirea standard a acestor procese va fi conectata la capatul de scriere al pipe-ului.
c) Cel putin unul dintre producatori este comanda 'ls'.

Consumatorii citesc caracterele din pipe si isi afiseaza identificatorul de proces urmat de caracterul citit (la un moment dat).

In sistem mai exista un pipe prin care consumatorii vor raporta in final rezultatele catre parintele tuturor, scriind in el, cu ajutorul functiei fprintf( ), linii de forma:

numar_proces : numar_octeti
unde numar_octeti este numarul total de octeti cititi de procesul respectiv din pipe. Parintele va prelua aceste informatii si le va afisa pe ecran.

Nota: Aceasta lucrare de laborator se va desfasura pe parcursul a doua saptamani. In prima saptamana vor fi prezentate consideratiile teoretice si se va rezolva tema 1, iar in a doua saptamana se va rezolva tema2.



Autor: Dan Cosma