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