Sincronizarea proceselor in UNIX se poate realiza in doua moduri: controlata de catre sistemul de operare sau controlata de catre utilizator. c4h14hy
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 ()
A static int first=1; if (first) /* doar prima data salvam starea */
A first=0; intvechi=signal(SIGINT,SIG_IGN); quitvechi=signal(SIGQUIT,SIG_IGN); if (intvechi==BADSIG || quitvechi==BADSIG) perror("signal");
S else if(signal(SIGINT,SIG_IGN)==BADSIG||signal(SIGQUIT,SIG_IGN)==BADSIG) perror ("signal");
S void refaint()
A if(signal(SIGINT,intvechi)==BADSIG||signal(SIGQUIT,quitvechi)==BASSIG) perror("signal");
S
void main(argc,argv) /* inlocuieste in fisierul de pe linia */
/* de comanda caracterele mici cu mari */ int argc; char **argv;
A
FILE *in, *out; char buffera256i; // buffer comand_ UNIX char *p;
// parte de cod care poate rula cu intreruperile activate if (argc!=2)
A printf("Argumente gresite !\n"); exit(1);
S in=fopen(argva1i,"r");//atribut read out=fopen("temporar","w"); if (!in || !out)
A printf("Nu se pot deschide fisierele !\n\a"); exit(1);
S
while(fgets(buffer,256,in))
A p=buffer;
while(*p) //converteste bufferul la caractere mari
A
*p=toupper(*p); p++;
S fputs(buffer,out);
S fclose(in); fclose(out); ignoraint();
/* sectiune critica */ unlink(argva1i); rename("temporar",argva1i); refaint();
/* terminare sectiune critica */
S
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)
A void (*sigvechi)(int); sigvechi=signal(semnal,SIG_IGN); if(sigvechi==BADSIG) perror("signal"); else if(sigvechi==SIG_IGN) return; else if(sigvechi!=SIG_DFL)
A printf("Semnalul era deja captat!\n"); exit(1);
S if(signal(semnal,functie)==BADSIG) perror("signal");
S
void capteaza_semnale()
A void curata(); seteaza_semnal(SIGHUP,curata); seteaza_semnal(SIGINT,curata); seteaza_semnal(SIGQUIT,curata); seteaza_semnal(SIGTERM,curata);
S
void curata(semnal) int semnal;
A if(signal(semnal,SIG_IGN)==BADSIG) perror("signal"); if(unlink("temporar")==-1) perror("unlink"); switch(semnal)
A case SIGHUP: fprintf(stderr,"Hangup \n"); break; case SIGINT: fprintf(stderr,"Interrupt \n"); break; case SIGQUIT: fprintf(stderr,"Quit \n"); break;
S exit(1);
S int main(argc,argv) int argc; char **argv;
A
FILE *file; int i; if(argc!=2)
A printf("Argumente gresite !\n\a"); exit(1);
S capteaza_semnale(); file=fopen("temporar","w"); if(!file)
A printf("Nu pot deschide fisierul temporar !\n"); exit(1);
S for(i=0;i<10000;i++) fprintf(file,"Articol: %5d\n",i); fclose(file); rename("temporar",argva1i); return 0;
S
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)
A longjmp(jmpbuf,1); printf("longjmp n-ar trebui sa revina\n"); exit(1);
S
void bucla_principala()
A int i; for(i=1; i!=0; i++) printf("%d wait\r",i);
S
main()
A int nr; char buffera256i; if(signal(SIGINT,SIG_IGN)!=SIG_IGN)
A if(setjmp(jmpbuf)!=0) printf("\n intrerupere \n"); if(signal(SIGINT,jumper)==BADSIG perror("signal");
S bucla_principala();
S
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;
A if(signal(SIGALRM,nimic)==BADSIG) perror("signal"); alarm(secunde); pause();
S void nimic(semnal); int semnal;
main()
A printf("Se asteapta 5 secunde !\n"); sleep2(5); printf("Thank you !\n");
S
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