Documente noi - cercetari, esee, comentariu, compunere, document
Documente categorii

Sincronizarea in Unix

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