|
Sincronizarea in Unix
Sincronizarea proceselor in UNIX se poate realiza in doua moduri:
controlata de catre sistemul de operare sau controlata de catre utilizator.
In primul caz, mecanismul clasic utilizat este cel de conducta de comunicatie
(pipe). Sincronizarea controlata de utilizator se realizeaza in principal prin
intermediul evenimentelor.
Evenimente
Evenimentul este conceptul de baza in sincronizarea si planificarea
proceselor UNIX. El reprezinta modalitatea de precizare a momentului in care
un proces, anterior blocat (in asteptarea terminarii unei operatii de intrare/
iesire cu terminalul, a eliberarii unei zone tampon sau a unui i-nod), poate
trece in starea gata de executie (conditia pe care o asteapta s-a indeplinit).
Blocarea proceselor se face prin intermediul unei functii interne, numita sleep
(a nu se confunda cu functia de biblioteca cu acelasi nume), apelata cu un
parametru care reprezinta parametrul principal. In momentul indeplinirii conditiei
de deblocare, nucleul, prin intermediul functiei wakeup, trece toate procesele,
care asteptau acea conditie, in starea gata de executie. Evident, numai unul dintre
acestea se va executa efectiv, celelalte trecand din nou in starea blocat.
Evenimentele sunt reprezentate prin numere intregi, alese prin conventie,
astfel incat sa fie chiar adrese virtuale, cunoscute de nucleul UNIX-ului;
semnificatia lor este ca sistemul de operare se asteapta ca anumite evenimente
sa se mapeze pe anumite adrese ( de exemplu : evenimentul de terminare a unui
proces fiu este reprezentat de adresa intrarii corespunzatoare tatalui sau din
tabela de procese).
In afara de producerea propriu-zisa a unui eveniment, acest mecanism nu
permite si transmiterea altor informatii, cu evenimentul nefiind asociata memorie;
el exista doar in momentul in care este folosit.
Dezavantajul acestei abordari consta in faptul ca sincronizarea nu se poate
face in functie de o anumita cantitate de informatie. De exemplu, toate procesele
care au facut cerere de memorie vor fi planificate pentru executie la eliberarea
unei zone, indiferent de dimensiunea acesteia, desi zona eliberata s-ar putea sa
nu fie suficienta pentru multe dintre ele si deci sa fie nevoite sa treaca din
nou in asteptare (in realitate, exista un singur proces care cere memorie,
swapper-ul, iar el va fi activat de catre nucleu la eliberarea unei zone de memorie
chiar daca aceasta nu este suficienta). De asemenea, daca un proces se blocheaza
in asteptarea unui eveniment care s-a produs deja, nu exista nici o posibilitate
de a specifica acest lucru prin intermediul evenimentelor.
In cadrul sincronizarii intre procese prin intermediul evenimentelor, se
pot identifica mai multe situatii : sincronizarea prin semnale, sincronizarea
intre un proces tata si fii sai, sincronizarea prin intermediul unor functii
de sistem.
Semnale
Aparitia unor evenimente in sistem este semnalata in UNIX fie de catre
nucleu, prin intermediul functiei wakeup, fie prin intermediul semnalelor.
Acestea din urma sunt implementate cu ajutorul unor biti, memorati in tabele de
procese si care pot fi setati fie de catre nucleu (in cazul producerii unor
evenimente legate de hardware), fie de catre utilizator (prin apelul directivei
kill).
Nucleul verifica primirea unui semnal (setarea bitului corespunzator
acestuia) la trecerea din mod sistem in mod utilizator, precum si inaintea si
dupa blocarea unui proces. Tratarea semnalelor se face in contextul procesului
care le primeste.
Semnalele nu transfera cantitate de informatie proceselor, ci sunt
forme de sincronizare (functie de tipul semnalului, 19 standard in Unix V).
Cind un semnal ajunge la un proces, el este intrerupt din activitatea curenta
si obligat sa raspunda. Avem :
a)procesul poate ignora semnalul, continuindu-si activitatea (SIGKILL nu
poate fi ignorat). Sistemul isi pastreaza posibilitatea de a termina procesul.
b)procesul poate lasa sistemul sa execute actiunea implicita (valabil pentru
toate semnalele de terminare a proceselor, exceptie facind doar SIGCLD si
SIGPWR care sunt explicit ignorate).
c)procesul isi poate defini o procedura proprie de tratare a semnalului, care
va fi automat lansata la sosirea acestuia (handler).
Indiferent de modul cum reactioneaza programul la un anumit semnal,
dupa terminarea actiunii, daca nu era ignorat, semnalul este resetat pentru
viitor la actiunea implicita, excluzind semnalele SIGKILL si SIGTRAP care sunt
generate foarte des si ar fi ineficienta resetarea lor de fiecare data. Apelul
signal() comunica sistemului care este actiunea dorita de proces pentru un
anumit semnal.
#include <signal.h>
void (* signal(semnal,functie))(int);
int semnal;
void (* functie)(int);
Parametrul semnal reprezinta semnalul referit si 'functie' functia de
tratare a lui. Functiile de tratare nu intorc valori si au ca parametru unic
numarul semnalului sosit. Exista 2 valori implicite pentru functia de tratare,
SIG_IGN (ignorarea semnalului de catre proces) si SIG_DFL (resetarea functiei
la valoarea implicita). Apelul signal intoarce vechea functie de tratare (poate
fi SIG_IGN sau SIG_DFL) sau -1 in cazul cind ceva nu este corect (numar semnal
incorect, se incearca ignorarea lui SIGKILL, etc.). Aceasta valoare se defineste:
#define BADSIG (void (*)(int))-1
Valoarea returnata de signal poate fi folosita pentru a restaura starea
anterioara apelului, dupa ce se iese din zona critica. Programul de mai jos
arata cum putem ignora semnalele SIGINT si SIGQUIT intr-o regiune a programului
in care este periculos sa se termine anormal (functia ignoraint() si refaint()).
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <ctype.h>
static void (* intvechi)(int),(* quitvechi)(int);
#define BADSIG (void (*)(int))-1
void ignoraint ()
else
if(signal(SIGINT,SIG_IGN)==BADSIG||signal(SIGQUIT,SIG_IGN)==BADSIG)
perror ('signal');
}
void refaint()
void main(argc,argv) /* inlocuieste in fisierul de pe linia */
/* de comanda caracterele mici cu mari */
int argc;
char **argv;
in=fopen(argv[1],'r');//atribut read
out=fopen('temporar','w');
if (!in || !out)
while(fgets(buffer,256,in))
fputs(buffer,out);
}
fclose(in);
fclose(out);
ignoraint();
/* sectiune critica */
unlink(argv[1]);
rename('temporar',argv[1]);
refaint();
/* terminare sectiune critica */
}
Apelurile definite intr-un proces se pastreaza si in fiu in urma
apelului fork(). In cazul apelului exec() se pastreaza doar semnalele setate pe
SIG_IGN si SIG_DFL. Cele care au atasata o functie a utilizatorului se reseteaza
la valoarea SIG_DFL (in cazul exec se incarca un nou program si segmentul de
cod al procesului este modificat si evident vechile rutine de tratare a semnalelor
ori nu se regasesc ori sunt la alte adrese).
Semnalele sosite catre un proces nu sunt introduse in nici o coada de
asteptare (daca au fost ignorate s-au pierdut). Singura exceptie este SIGCLD
care asteapta pina procesul parinte executa apelul wait() pentru a lua cunostinta
de terminarea procesului fiu (uneori fiul se termina inainte ca parintele sa
execute wait()). Daca semnalul n-ar fi memorat, procesul parinte ar fi blocat
pina la terminarea unui alt fiu! SIGCLD nu este memorat in cazul in care parintele
a setat explicit pe valoarea SIG_IGN rutina de tratare a semnalului. Datorita
faptului ca semnalele ignorate se pierd, aceasta forma de sincronizare
comunicare nu este prea folosita. In cazul cind procesul are de executat mai
multe operatii la terminare (stergerea fisierelor temporare, restaurarea unui
fisier incomplet prelucrat) procesul trebuie sa intercepteze semnalele SIGHUP,
SIGINT si SIGTERM si pe ele sa execute operatiile de curatire. De asemenea, pe
parcursul dezvoltarii unei aplicatii, semnalul SIGQUIT nu trebuie interceptat,
pentru a putea termina procesul cu CTRL insotit de core dump. Dar in cazul
unui proces care lucreaza in background (lansat cu &) acesta ruleaza cu
semnalele SIGINT si SIGQUIT implicit ignorate, pentru a nu fi intrerupt
accidental de apasarea tastelor de intrerupere. In acest caz rutina de tratare
a acestor semnale trebuie sa ramina SIG_IGN. Rutina setsig din exemplul de mai
jos seteaza semnalul doar in situatia cind nu a fost anterior ignorat. Exista
si o rutina de terminare anormala a unui proces.
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#define BADSIG (void (*)(int))-1
void seteaza_semnal(semnal,functie)
int semnal;
void (*functie)(int)
if(signal(semnal,functie)==BADSIG)
perror('signal');
}
void capteaza_semnale()
void curata(semnal)
int semnal;
exit(1);
}
int main(argc,argv)
int argc;
char **argv;
capteaza_semnale();
file=fopen('temporar','w');
if(!file)
for(i=0;i<10000;i++)
fprintf(file,'Articol: %5dn',i);
fclose(file);
rename('temporar',argv[1]);
return 0;
}
La sosirea unui semnal este terminat cu eroare orice apel sistem care
asteapta, lansat anterior de proces. De exemplu, daca procesul a lansat o citire
read, la venirea unui semnal acesta se intoarce cu -1 (in acest caz variabila
errno primeste valoarea EINTR). Controlul acestor situatii nu este simplu si se
apeleaza la solutii globale: folosirea rutinelor setjmp, longjmp.
#include <setjmp.h>
int setjmp(jmpenv)
jmp_buf jmpenv;
void longjmp(jmpenv,valoare)
jmp_buf jmpenv;
int valoare;
Rutina setjmp creeaza un punct de intoarcere salvind in bufferul jmpenv
starea curenta a procesului, in asa fel ca la apelul rutinei longjmp sa para ca
tocmai setjmp s-a intors din apel. Rutina longjmp restaureaza starea salvata de
setjmp si defineste valoarea cu care setjmp se va intoarce. La prima revenire
din setjmp, atunci cind se salveaza starea, functia intoarce valoarea 0, fata
de celelalte reveniri cauzate de apelul lui longjmp care au valoarea diferita 0.
In exemplul de mai jos este o tehnica folosita de editorul vi care sta in
permanenta in asteptarea unei taste. Daca apasam in modul introducere al
editorului tasta DEL aceasta activeaza ca si ESC, pentru ca un apel de longjmp
ne trimite in bucla principala de citire, in modul comanda al editorului.
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
static jmp_buf jmpbuf;
#define BADSIG (void(*)(int))-1
void jumper(int c)
void bucla_principala()
main()
bucla_principala();
}
Se poate trimite un semnal catre un proces cu ajutorul apelului kill.
int kill(pid,semnal);
int pid;
int semnal;
Parametrul pid este numarul de identificare al procesului, iar semnal
reprezinta numarul semnalului pe care vrem sa-l trimitem. Daca pid=0, semnalul
va fi trimis tuturor proceselor din acelasi grup cu procesul care lanseaza
apelul. Aceasta proprietate poate fi folosita pentru a termina toate procesele
care ruleaza in background lansate de la un terminal. Daca pid=-1, semnalul este
trimis catre toate procesele care au uid-ul real (userid) egal cu cel al
pprprietarului procesului care a lansat apelul. Aceasta foloseste la terminarea
tuturor proceselor care apartin unui user indiferent de grupul din care fac
parte (cite terminale). Daca supervisorul executa kill cu pid=-1, toate procesele
din sistem vor fi terminate cu exceptia lui 0 si 1 (init, swap). Daca pid < -1,
semnalul este trimis tuturor proceselor care au numarul de grup egal cu valoarea
absoluta a lui pid. In practica nu se foloseste kill ca apel sistem, ci ca comanda.
Un proces poate sa-si intrerupa activitatea cu pause() in asteptarea
unui semnal: void pause(). La iesirea din pause() procesul nu poate sti ce
semnal a cauzat intreruperea, iar errno este setata pe valoarea EINTR. Cea mai
utila folosire a acestui apel sistem este asteptarea unui semnal de alarma setat
de apelul alarm.
unsigned alarm(secunde);
unsigned secunde;
Parametrul reprezinta numarul de secunde dupa care procesul porneste
semnalul SIGALRM. Valoarea rezultata este vechea valoare a ceasului (pentru
fiecare proces este un ceas, un nou apel al functiei alarm distruge vechea
valoare). Daca procesul se razgindeste intre timp el poate inhiba semnalul prin
alarm(0).
#include<stdio.h>
#include<signal.h>
#define BADSIG (void(*)(int)) -1
void sleep2(secunde)
int secunde;
void nimic(semnal);
int semnal;
main()
Tabelul semnalelor definite in UNIX V (asa cum este definit in
/usr/include/signal.h).
SIGHUP(1) Hangup. Acest semnal este trimis, atunci cind un terminal este oprit
(conexiunea este intrerupta), catre fiecare proces care apartine terminalului
respectiv. El este trimis si atunci cind procesul parinte al unui grup de
procese este terminat, oricare ar fi motivul. Acest proces ne da posibilitatea
sa simulam intreruperea conexiunii chiar daca terminalul nu este conectat la
distanta.
SIGINT(2) Interrupt. Acest semnal este trimis unui proces atunci cind la
terminalul asociat procesului s-a apasat tasta de intrerupere (de obicei DEL).
Tasta de intrerupere poate fi dezactivata sau poate fi modificata prin apelul
ioctl. Atentie, aceasta situatie nu este echivalenta cu ignorarea semnalului.
SIGQUIT(3) Quit. Semnalul este similar cu SIGINT, dar este generat la apasarea
tastei CTRL . La terminarea procesului se creeaza o imagine a starii procesului
pe disc pentru verificari ulterioare.
SIGILL(4) Illegal instruction. Acest semnal este trimis procesului cind hw
detecteaza o instructiune ilegala. Este generat de obicei pe calculatoare fara
coprocesor de VM o rutina soft interceptand semnalul si executia instructiunii.
SIGTRAP(5) Trace trap. Semnalul se trimite dupa fiecare instructiune daca
procesul are activata optiunea de trasare. Este folosit de debuggere sistem.
SIGIOT(6) I/O trap instruction. Acest semnal este trimis cind se semnaleaza o
problema hw (semnificatia este dependenta de tipul masinii). Semnalul este
folosit de functia abort pentru a provoca terminarea procesului cu salvarea
starii pe disc.
SIGEMT(7) Emulator trap instruction. Semnalul este trimis cind apar unele
probleme hard (rar).
SIGFPE(8) Floating point exception. Trimis atunci cind hw detecteaza o problema
de lucru cu sistemul de VM, de exemplu cind se incearca folosirea unui numar
care are un format incorect de reprezentare.
SIGKILL(9) Kill. Acest semnal este singurul mod sigur de a termina un proces
(nu poate fi ignorat). Se foloseste numai in caz de urgenta (de obicei este
preferat lui SIGTERM(15)).
SIGBUS(10) Bus error. Semnal dependent de masina (cind se adreseaza o adresa
impara a unei structuri de date ce trebuie sa se afle la o adresa de cuvint).
SIGSENV(11) Segmentation violation. Dependent de masina (cind se incearca
adresarea datelor din afara spatiului de adrese).
SIGSYS(12) Bad argument to DSystem Call. Nu se utilizeaza acum.
SIGPIPE(13) Write on pipe not open for reading. Semnalul este trimis procesului
atunci cind acesta incearca sa scrie intr-un canal de comunicatie din care nu
citeste nimeni (se poate folosi pentru terminarea unei intregi inlantuiri de
pipe). Daca un proces se termina anormal toate celelalte primesc acest semnal
in cascada.
SIGALRM(14) Alarm clock. Semnalul este trimis procesului cind ceasul sau a
ajuns intr-un moment fixat (fixarea se face cu apelul alarm).
SIGTERM(15) Software termination. Se opreste un proces. Comanda kill trimite
implicit acest semnal. Un proces care intercepteaza acest semnal trebuie sa
execute la primirea lui operatiile de salvare si curatire necesare, dupa care
se apeleaza exit.
SIGUSR1(16) User defined signal 1. Acest semnal poate fi folosit de procese
pemtru a comunica intre ele (nu prea este utilizat).
SIGUSR2(17) User defined signal 2. Idem.
SIGCLD(18) Death of a child. Este primit de parinte cind unul din fii s-a
terminat (actioneaza diferit fata de celelalte deoarece este pus intr-o coada
de asteptare).
SIGPWR(19) Power fail restart. Depinde de implementare (apare cind scade
tensiunea de alimentare). Procesul poate sa-si salveze starea si sa apeleze
exit sau isi salveaza starea si apeleaza sleep (daca se mai trezeste).
Tema :
-sa se exerseze procese ce se sincronizeaza prin semnale
Comunicatia prin PIPE
Pipe-urile sunt canale de comunicatie intre procese, informatia trecind
de la un proces la altul printr-un mecanism FIFO (COMMAND.COM implementeaza o
astfel de tehnica). In DOS executia are loc secvential, in timp ce in UNIX
executia are loc concurent, comunicatia fiind directa. Pipe-uri pot fi create
si in shell-ul Unix, ca si in DOS, prin concatenarea mai multor comenzi pe
aceeasi linie separate de '|'. Prin program se pot crea legaturi circulare
intre procese (bidirectional). Apelul SC(System Call) cu care se creeaza un
pipe este
int pipe(pdescr); Valoarea returnata este 0 in caz de succes
int pdescr[2]; si -1 in caz de eroare.
Fiind un canal bidirectional de comunicatie in care se pot scrie date si se pot
citi date, apelul trebuie sa intoarca 2 descriptori de fisier, astfel ca avem
tabloul cu 2 elemente intregi care va contine la intoarcerea din SC cei 2
descriptori, primul pentru citire si al doilea pentru scriere. Programul poate
folosi cei 2 descriptori ca cei pentru fisiere, asupra carora se pot aplica
apelurile read, write, close, fcntl, fstat. Apelurile open si creat nu se
folosesc pentru pipe, la fel lseek (datele se citesc in ordinea in care au
fost scrise).
ÚAAAAAAAAAAAs ÚAAAAAAAAAAAs
³ Proces_1 ³ ³ Proces_2 ³
ÀAAAAAAAAAAAÙ ÀAAAAAAAAAAAÙ
³ write(pdescr[1],) ³ read(pdescr[0],)
-----------ÀAAAAs----- ----- -----------ÚAAAAÙ----- ----- --------------
Kernel ³ ³
ÚAAÁAAAAAAAAAAAAAAAAAAAAAÁAAs
³ ³ ³ ³ ³pipe ³ ³ ³ ³
ÀAÁAÁAÁAÁAAAAAAAAAAAAAÁAÁAÁAÙ
Descriptorii de pipe au o dimensiune redusa de memorie (cca. 4 Ko).
Astfel, daca se scriu mai multi octeti decit este liber, write se blocheaza
pina cind cineva goleste pipe prin citire, write reluindu-se pina la terminare
(afara de setarea prin fcntl a comutatorului O_NDELAY). Un apel read se
termina chiar daca nu a gasit toti octetii de care avea nevoie (valoare de
retur=numarul de octeti cititi). In singurul caz ca pipe-ul este gol, read se
blocheaza pina la sosirea unor date (exceptie tot cu O_NDELAY setat). Daca
dorim sa semnalam procesului cu care comunicam ca s-a atins sfirsitul de fisier,
trebuie sa inchidem canalul cu SC close. SC fstat returneaza numarul de octeti
disponibili in pipe la un moment dat (este foarte dinamic). Fstat este util la
testarea daca un descriptor de fisier corespunde sau nu unui pipe, testind
daca numarul de legaturi este 0. Pipe foloseste acelasi mecanism de cache care
se foloseste si pentru fisierele de pe discuri. Scrierea si citirea sint
operatii atomice (scrierea este cu 512 octeti in general, citirea cu <=512).
In primul program acelasi proces scrie si citeste mai tirziu din pipe.
/*p1.c*/
#include <stdio.h>
void main()
strcpy(buffer,mesaj);
if(write(pdescr[1],buffer,strlen(mesaj)+1)==-1)
switch(nr=read(pdescr[0],buffer,sizeof(buffer)))
}
Daca dimensiunea blocului de scris este mai mare decit dimensiunea pipe-ului,
se poate produce deadlock (se poate totusi rezolva prin citirea unor octeti
din pipe). Un pipe unidirectional nu poate duce niciodata la deadlock total.
Cind avem un pipe intre doua procese, cel care citeste trebuie sa fie un fiu
al procesului care a deschis pipe-ul pentru a mosteni descriptorii de fisiere
(inclusiv al pipe-ului), sau ambele procese fiu al aceluiasi tata. Daca unul
din procese doar scrie iar celalat doar citeste, atunci avem pipe unidirectional.
/*p2.c*/
#include <stdio.h>
void main ()
switch(fork())
sprintf(buffer,'%d',pdescr[0]);
execlp('./p3','p3',buffer,NULL);
perror('eroare execlpn');
exit(1);
}
if(close(pdescr[0]==-1)
if(write(pdescr[1],'Hello !',7)==-1)
}
}
/*p3.c*/
#include <stdio.h>
void main(argc,argv)
inr argc;
char **argv;
}
In primul proces dupa fork, in cazul ca ne gasim in fiu, canalul de scriere in
pipe este inchis. Aceasta pentru ca cel de-al doilea proces nu va scrie
niciodata in pipe si in acest caz este mai bine sa eliberam descriptorul de
fisiere, care face parte dintr-o resursa limitata (20 de descriptori de proces).
Aceasta situatie este inainte de exec pentru ca dupa aceasta, desi fisierul
ramine deschis, p3 nu stie care este acel descriptor. Programul p3 nu stie
nici unde se gaseste descriptorul de citire, de acea acesta si este transmis
ca parametru in linia de comanda (nu este cea mai buna metoda !). In procesul
tata, la revenirea din fork, se inchide canalul de citire, pentru ca tatal nu
va citi niciodata din pipe. De aceea, intre SC fork si exec se mai pot face
unele prelucrari, care facute in alta parte ar insemna mult mai mult efort.
Pentru a opri blocarea lui write in cazul ca pipe este plin, putem folosi
apelul fcntl:
#include <fcntl.h>
if(fcntl(fd,F_SETFL,O_NDELAY)<0)
perror('eroare fcntln');
Astfel oprim blocarea daca fd este descriptorul fisierului de scrire din pipe.
Daca descriptorul de apel este cel de citire, read nu se va mai bloca cind
canalul este gol, intorcind valoarea zero. (Atentie! read intoarce zero si la
sfirsitul fisierului). Problema din programul p3 era ca descriptorul de fisier
se transmite ca parametru in linia de comanda, ceea ce limiteaza foarte mult
aplicabilitatea sistemului. Solutia mai buna ar reprezenta-o fisierele standard
de intrare/iesire: trebuie aranjate in asa fel lucrurile incit programul p3
sa stie exact locul descriptorului de citire, fara ca acesta sa-i vina pe
linia de comanda. Programele care respecta aceste reguli isi iau datele din
stdin(0) si se scriu in stdout(1). Acest tip de programe sunt asa numitele
filtru (more,pg,tee,sort). Din pacate la apelul pipe nu vom sti exact unde vor
fi deschisi cei doi descriptori de fisier. Pentru rezolvarea acestei probleme
s-a introdus SC dup, avind urmatoarea sintaxa:
int dup(fd);
int fd;
Apelul face duplicarea unui descriptor de fisier, dupa apel putindu-se
accesa fisierul deschis in fd, si prin descriptorul intors de dup. Particularitatea
acestui apel, care este folositoare in situatia prezentata, este ca dup asigura
ca descriptorul intors este cel cu numarul minim dintre cele neutilizate. Astfel
daca inainte de dup am inchis descriptorul 0 (stdin), dup va intoarce cu
siguranta 0, daca am inchis 1 si 0 este utilizat (in majoritatea cazurilor),
dup va intoarce 1. Dup intoarce -1 in caz de eroare. In programul urmator se
lanseaza doua procese interconectate printr-un pipe unidirectional (unul scrie
altul citeste - asemanator cu mecanismul utilizat de shell pentru a lansa doua
comenzi legate printr-un pipe).
/*p4.c*/
#include <stdio.h>
void main(argc,argv)
int argc;
char **argv;
switch(fork(0)) :
close(pdescr[1]);
execlp(argv[1],argv[1],NULL);
perror('eroare execlp 1n');
exit(1);
}
switch(fork(0)) :
close(pdescr[0]);
execlp(argv[2],argv[2],NULL);
perror('eroare execlp 2n');
exit(1);
}
/* in tata */
close(pdescr[0]);
close(pdescr[1]);
wait(&status);
/* asteapta terminarea primului fiu */
wait(&status);
/* asteapta terminarea celui de al doilea fiu */
}
In cazul cind dorim crearea unui pipe bidirectional (poate duce la
deadlock total), trebuie create doua pipe-uri (unul pentru citire/scrire,
altul scriere/citire). Descriptorii de fisier blocati sunt tot doi pentru
fiecare proces, ceilalti doi putind fi inclusi ca in exemplele anterioare.
FIFO
Un FIFO combina trasaturile unui pipe cu acelea ale unui fisier. Ca si
fisierul, FIFO-ul are un nume, o pozitie in sistemul de fisiere si poate fi
accesat de orice proces care are drepturi asupra lui. Spre deosebire de pipe-urile
clasice, cu ajutorul unui FIFO pot comunica oricare doua procese indiferent de
relatia lor de rudenie. Din momentul in care a fost deschis insa, FIFO se
comporta ca pipe. Datele se pot citi in ordinea FIFO, apelurile de tip
read/write fiind atomice, cu conditia sa nu depaseasca capacitatea FIFO-ului
(>=4 ko). Lseek nu are are efect iar datele nu mai pot fi scrise inapoi. Atunci
cind un FIFO este deschis pentru citire, kernelul asteapta pina cind un alt
proces deschide acelasi FIFO pentru scriere (se asteapta unul pe altul la
deschiderea canalului de comunicatie, rendez-vous, sincronizat inaintea
comunicatiei propiu-zise). La fel ca la pipe se poate folosi apelul fcntl
pentru a seta flagul O_NDELAY. In acest caz deschiderea pentru citire
returneaza imediat, fara sa astepte ca FIFO-ul sa fie deschis pentru scriere,
in timp ce deschiderea pentru scriere returneaza eroare (kernelul nu poate
garanta pastrarea permanenta a datelor care se inscriu in FIFO-ul care nu este
citit imediat). In plus, la inchiderea canalului de comunicatie fara
comunicarea tuturor datelor scrise, acestea se pierd fara a se indica eroare.
Flagul O_NDELAY afecteaza apelurile de citire/scriere ca la pipe-urile clasice.
Crearea unui FIFO se face cu mknod:
#include <sys/types.h>
#include <sys/stat.h>
init res;
char *path;
res=mknod(path,S_IFIFO|0666,0);
res = 0 - in caz de succes
res = 1 - in caz de eroare
Path reprezinta numele FIFO (la fel ca la fisiere) si 0666 drepturi
de acces.
Mknod foloseste pentru crearea unor fisiere normale, a unor subdirectoare,
sau a unor fisiere speciale, dar aceste facilitati sunt accesibile numai
supervizorului. Parametrul S_IFIFO este accesibil oricarui user. Cu fstat putem
prelua starea unui FIFO deschis anterior, iar cu stat starea unui FIFO
nedeschis inca.
#include <sys/types.h>
#include <sys/stat.h>
int res,fd;
char *path;
struct stat *sbuf ;
res = fstat(fd,sbuf); // res=0 succes; res=1 eroare
res = stat(path,sbuf);
Informatiile obtinute prin aceste apeluri : lungime (cite caractere
sunt in FIFO), timpul/data de creare, actualizare, numarul de Inode, numarul
de legaturi (links=0 pentru pipe clasic, acesta neexistind pe disc), uid, gid,
etc. O prima aplicare a FIFO este de a implementa un pipe clasic. In locul SC
vom deschide FIFO de doua ori, odata pentru scriere si odata pentru citire si
apoi putem trata cei doi descriptori ca la pipe clasic. De fapt FIFO s-au
introdus nu pentru a inlocui pipe ci mesajele.
/*p4*/
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define MAXOPEN 7
#define MAXTRIES 3
#define NAPTIME 5
#define FALSE0
#define TRUE 1
static char *fifoname(key) /*creeaza un nume de fisier temporar */
long key;
static int mkfifos(path) /* creeaza un FIFO */
char *path;
static int openfifo(key,flags)
long key;
int flags;
fifos[MAXOPEN];
static int clock ;
int i,avail,oldest,fd,tries;
char *fifo;
extern int errno;
avail=-1; /* caut_ un loc liber */
for(i=0;i<MAXOPEN;i++)
if(fifos[i].key==0 && avail==-1)
avail=i;
}
if(avail==-1) /* daca nu foloseste cel mai vechi */
fifo=fifoname(key);
if(mkfifos(fifo)== -1 && errno != EEXIT)
return -1;
for(tries=1;tries < MAXTRIES;tries++)
if(fd == -1)
if(fcntl(fd,F_SETFL,flags)== -1) /* reseteaza O_NDELAY */
return -1;
fifos[avail].key=key;
fifos[avail].fd=fd;
fifos[avail].time=clock;
return fd;
}
int send(dstkey,buf,nbytes) /* trimite un mesaj */
long dstkey;
char* buf;
int nbytes;
int receive(srckey,buf,nbytes) /* primeste un mesaj */
long srckey;
char* buf;
int nbytes;
void rmqueue(key)
long key;
/* Receive.c */
#include'mesaje.h'
void main()
/* Send.c */
#include'mesaje.h'
void main()
}
/* Mesaje.h */
typedef struct
MESSAGE;
Mesaje
Sub UNIX V exista 3 metode de comunicatie intre procese : mesaje,
semafoare, memorie partajata. Implementarea acestor mecanisme este optionala,
de aceea ele lipsesc din unele nuclee. Dar daca kernelul Unix le implementeaza,
ele trebuie sa respecte o interfata cu aplicatiile, care este impusa.
Un rol cheie in comunicatii o au cheile. Ele ajuta la recunoasterea
unui obiect de comunicatie interproces (coada de mesaje, semafor sau segment
de memorie partajata). Pentru reprezentarea acestor chei se folosesc
identificatori (asemenea descriptorilor de fisiere). Cu ajutorul identificatorilor,
obiectele de comunicatie se pot folosi din mai multe aplicatii diferite. Tipul
de date al identificatorului atasat acestor chei este dependent de
implementare, dar el este evitat prin declararea lui de tipul key_t definit in
<sys/types.h> (pentru programe). Nu trebuie confundate cheile cu descriptorii
de fisiere. Aceste chei trebuie alese cu grija pentru a evita efecte secundare
nedorite (daca 2 procese independente folosesc aceeasi cheie pentru o coada de
mesaje, se pierde informatia foarte greu de depistat). Comunicatia prin mesaje
are loc dupa schema :
ÚAAAAAAAAs mesaj
³client 1³<AAs ÚAAAAAAs ÚAAAAAAAs
ÀAAAAAAAAÙ ³ ÚAAAAAAsÀAAAAAAÙ AAAAAAAAAAAAAAA ÀAAAAAAAÙ
ÀAA>³ ³ transmitatorÚAAs ÚAAs receptor
³server³ AAAAAAAAA> ³ ³ . . . ³ ³ AAAAAAA>
ÚAA>³ ³ ÀAAÙ ÀAAÙ
ÚAAAAAAAAs ³ ÀAAAAAAÙ AAAAAAAAAAAAAAA
³client 2³<AAÙ coada de mesaje
ÀAAAAAAAAÙ
Rolul principal ii revine cozii de mesaje. Unul sau mai multe procese
transmit, introduc mesaje intr-o coada de mesaje, iar altele le extrag. Daca
un proces extrage un mesaj din coada de mesaje, aceasta informatie este pierduta
pentru ceilalti. Nu exista deocamdata un mecanism prin care un proces sa poata
trimite un mesaj catre mai multe procese deodata (broadcast). Ordinea mesajelor
in coada este stricta, mesajele putindu-se scoate doar in ordinea in care au
fost trimise (o aplicatie poate folosi mai multe cozi de mesaje).
Mecanismul de comunicatie prin mesaje este implementat in nucleu.
Pentru aceasta, kernelul isi rezerva o zona tampon de memorie. Fiecare mesaj
trimis trece prin kernel. Procesul care trimite mesajul stabileste pentru
acesta o cheie de recunoastere. Aici apare o problema : daca un alt proces
cunoaste cheia, el poate sa preia informatia din coada, chiar daca informatia
nu-i fusese destinata, ea pierzindu-se (o solutie ar fi codificarea mesajelor).
Un mesaj se bazeaza pe o structura C care cuprinde un identificator si
textul propriu-zis.
ÚAAAAAAAAAAs Identificatorul se foloseste pentru specificarea
mesaj ³mtip³mtext³ tipului mesajului necesar la selectarea lui din
ÀAAAAÁAAAAAÙ coada de mesaje si se reprezinta cu un long.
Informatia propriu-zisa este in cimpul text, are o lungime variabila
si se declara de exemplu prin char[] (structura unui mesaj trebuie sa inceapa
cu un long, restul dupa necesitati = regula). Pentru a programa cu mesaje,
trebuie incluse urmatoarele fisiere standard : <sys/types.h>, <sys/ipc.h>,
<sys/msg.h>. Apelul sistem folosit in lucrul cu mesagele la crearea si
deschiderea unei cozi este msgget.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int id_coada,permisii;
key_t cheie;
id_coada=msgget(cheie,permisii);
A fost creata si deschisa o coada de mesaje in functie de parametrii
specificati prin variabila permisii. Drept cheie se foloseste un identificator
global de tipul key_t. Apelul returneaza un identificator al cozii de mesaje
sub forma de intreg pozitiv in variabila id_coada (daca actiunea a avut succes).
Pentru cozile de mesaje se definesc drepturi de acces exact ca la lucrul cu
fisiere, specificate in variabila permisii. Pentru aceasta sunt definite in
<sys/ipc.h> urmatoarele constante:
IPC_CREAT : cu aceasta se deschide o coada corespunzatoare cheii date,
iar daca aceasta nu exista va fi creata. Daca aceasta constanta
lipseste din apel, coada nu va fi creata si va fi deschisa
doar daca exista.
IPC_EXCL: se foloseste impreuna cu IPC_CREAT, coada va fi creata si
deschisa doar daca nu exista deja. Daca ea exista, se intoarce
valoarea -1 de eroare (variabila globala errno va contine
EEXIST).
In afara de aceste constante, se dau valori normale de permisii
corespunzatoare proprietarului, grupului si celorlalti. De exemplu, daca
permisii=0660|IPC_CREAT, se deschide o coada de mesaje sau se creeaza daca nu
exista, proprietarul si grupul sau avind drept de r,w. Dupa ce o coada de
mesaje a fost creata se pot introduce mesaje in ea. Pentru aceasta exista
apelul msgsnd.
#include <sys/types.h>
int id_coada,marime,permisii,retur;
struct min_sg mesaj;
retur=msgsnd(id_coada,&mesaj,marime,permisii);
Id_coada este coada de mesaje cu care se lucreaza, marime specifica
lungimea cimpului mtext al structurii. Prin permisii se poate specifica modul
in care se face apelul. Daca permisii=0, apelul va astepta pina cind in coada
de mesaje va fi loc pentru mesajul transmis. Daca se introduce constanta
NOWAIT, apelul va returna imediat, iar daca nu este loc pentru mesaje se va
intoarce -1, iar in errno va fi depusa valoarea EAGAIN.
Pentru ca o comunicatie sa functioneze este nevoie de 2 procese. Cel
de-al 2-lea proces trebuie sa extraga mesagele din coada, pentru aceasta
existind apelul msgrcv.
#include <sys/types.h>
int id_coada,marime,permisii,retur;
struct min_sg mesaj;
long mtip;
retur=msgrcv(id_coada,&mesaj,marime,mtip,permi_i);
Va fi preluat din coada urmatorul mesaj de tip mtip si va fi depus in
structura de date mesaj. Pentru mtip putem avea:
mtip= 0 primul mesaj din coada indiferent de tipul sau;
mtip= n(pozitiv) primul mesaj de tipul n;
mtip= -n(negativ) primul mesaj de tipul 1,2,,n.
Prin variabila marime se transmite dimensiunea maxima a mesajului. Daca
marimea reala a mesajului este mai mare decit marime, apelul va returna -1.
Daca mesajul este mai mic decit marime, apelul va returna valoarea lui exacta.
Daca permisii=0 (modul in care se face apelul), apelul va astepta pina la
sosirea unui mesaj de tipul solicitat. Daca se introduce IPC_NOWAIT, apelul va
intoarce valoarea de eroare -1, daca nu exista mesaj de tipul solicitat.
Variabila globala errno va contine valoarea EAGAIN. Daca permisii=MSG_NOERROR,
un mesaj de lungime mai mare decit cel solicitat va fi trunchiat fara a
solicita eroare. Pentru stergerea unei cozi de mesaje se foloseste msgctl.
retur=msgctl(id_coada,IPC_RMD,0);
Singurii care pot sterge o coada sunt proprietarii cozii & supervizorul.
Daca nu s-a putut sterge coada, valoarea de retur este -1.
In exemplul de mai jos este un exemplu de aplicatie client-server.
Programul client trimite serverului identificatorul sau de proces si apoi
asteapta de la acesta un raspuns. Cind soseste raspunsul, clientul il tipareste
si se opreste. Serverul asteapta mesaje de la clienti si le raspunde,
transmitind propriul sau identificator de proces. Serverul se opreste la
aparitia oricarui semnal.
/* Mesaje.h */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define CHEIE 0100 /* identificatorul cozii */
struct mesaj ;
/* Client */
#include 'mesaje.h'
void main()
/* Server */
#include 'mesaj.h'
int id_coada;
void main()
}
/* rutina de stergere a cozii la primirea unui mesaj */
cleanup()