|
 
8. Fire de executie
Intr-o lucrare de laborator anterioara au fost introduse elementele de
baza referitoare la procese. Recapituland pe scurt, un proces era
vazut ca fiind format dintr-o zona de cod, o zona de date, stiva si registri
ai procesorului (Program Counter si altii). In consecinta, fiecare proces
aparea ca o entitate distincta, independenta de celelalte procese aflate
in executie la un moment dat. De asemenea, a fost remarcat faptul ca, avand
in vedere ca procesorul poate rula la un moment dat un singur proces, procesele
sunt executate pe rand, dupa un anumit algoritm de planificare, astfel
incat, la nivelul aplicatiilor, acestea par ca se executa in paralel.
Se desprind, astfel, doua idei importante referitoare la procese:
-
ruleaza independent, avand zone de cod, stiva si date distincte
-
trebuie planificate la executie, astfel incat ele sa ruleze aparent in
paralel
Executia planificata a proceselor presupune ca, la momente de timp determinate
de algoritmul folosit, procesorul sa fie "luat" de la procesul care tocmai
se executa si sa fie "dat" unui alt proces. Aceasta comutare intre procese
(process switching) este o operatie consumatoare de timp, deoarece
trebuie "comutate" toate resursele care apartin proceselor: trebuie salvati
si restaurati toti registrii procesor, trebuie (re)mapate zonele de memorie
care apartin de noul proces etc.
Un concept interesant care se regaseste in toate sistemele de operare
moderne este acela de fir de executie (thread) in interiorul
unui proces.
Firele de executie sunt uneori numite procese usoare (lightweight
processes), sugerandu-se asemanarea lor cu procesele, dar si, intr-un
anume sens, deosebirile dintre ele.
Un fir de executie trebuie vazut ca un flux de instructiuni care se
executa in interiorul unui proces. Un proces poate sa fie format
din mai multe asemenea fire, care se executa in paralel, avand, insa,
in comun toate resursele principale caracteristice procesului. Prin
urmare, in interiorul unui proces, firele de executie sunt entitati care
ruleaza in paralel, impartind intre ele zona de date si executand portiuni
distincte din acelasi cod. Deoarece zona de date este comuna, toate variabilele
procesului vor fi vazute la fel de catre toate firele de executie, orice
modificare facuta de catre un fir devenind vizibila pentru toate celelalte.
Generalizand, un proces, asa cum era el perceput in lucrarile de laborator
precedente, este de fapt un proces format dintr-un singur fir de executie.
La nivelul sistemului de operare, executia in paralel a firelor de executie
este obtinuta in mod asemanator cu cea a proceselor, realizandu-se o comutare
intre fire, conform unui algoritm de planificare. Spre deosebire de cazul
proceselor, insa, aici comutarea poate fi facuta mult mai rapid, deoarece
informatiile memorate de catre sistem pentru fiecare fir de executie sunt
mult mai putine decat in cazul proceselor, datorita faptului ca firele
de executie au foarte putine resurse proprii. Practic, un fir de executie
poate fi vazut ca un numarator de program, o stiva si un set de registri,
toate celelalte resurse (zona de date, identificatori de fisier etc) apartinand
procesului in care ruleaza si fiind exploatate in comun.
1. Implementarea firelor de executie in Linux
Linux implementeaza firele de executie oferind, la nivel scazut, apelul
sistem clone( ):
pid_t clone(void *sp, unsigned long flags)
Functia clone( ) este o interfata alternativa la functia sistem
fork( ), ea avand ca efect crearea unui proces fiu, oferind, insa,
mai multe optiuni la creare.
Daca sp este diferit de zero, procesul fiu va folosi sp ca indicator
al stivei sale, permitandu-se astfel programatorului sa aleaga stiva noului
proces.
Argumentul flags este un sir de biti continand diferite optiuni
pentru crearea procesului fiu. Octetul inferior din flags contine
semnalul care va fi trimis la parinte in momentul terminarii fiului nou
creat. Alte optiuni care pot fi introduse in cuvantul flags sunt:
COPYVM si COPYFD. Daca este setat COPYVM, paginile de memorie al fiului
vor fi copii fidele ale paginilor de memori ale parintelui, ca la functia
fork( ). Daca COPYVM nu este setat, fiul va imparti cu parintele paginile
de memorie ale acestuia. Cand COPYFD este setat, fiul va primi descriptorii
de fisier ai parintelui ca si copii distincte, iar daca nu este setat,
fiul va imparti descriptorii de fisier cu parintele.
Functia returneaza PID-ul fiului in parinte si zero in fiu.
Prin urmare, apelul sistem fork( ) este echivalent cu:
clone(0, SIGCLD | COPYVM)
Se observa ca functia clone( ) ofera suficiente facilitati pentru
a putea crea primitive de tip fire de executie.
Pentru un exemplu "clasic" de utilizare a functiei clone( ),
consultati fisierul clone.c, al carui autor
este Linus Torvalds.
2. Utilizarea firelor de executie
In programe este indicat sa nu se foloseasca direct functia clone( ),
in primul rand din cauza ca ea nu este portabila (fiind specifica Linux)
si apoi pentru ca utilizarea ei este intrucatva greoaie.
Standardul POSIX 1003.1c, adoptat de catre IEEE ca parte a standardelor
POSIX, defineste o interfata de programare pentru utilizarea firelor de
executie, numita pthread. Interfata este implementata pe multe arhitecturi;
mai mult, sistemele de operare care contineau biblioteci proprii de fire
de executie (cum este SOLARIS) introduc suport pentru acest standard.
In Linux exista o biblioteca numita LinuxThreads, care implementeaza
versiunea finala a standardului POSIX 1003.1c si utilizeaza functia clone()
ca instrument de creare a firelor de executie. In continuare, ne vom referi
la aceasta biblioteca de functii si vom trece in revista o parte din primitivele
introduse de catre ea.
2.1 Aspecte practice privind utilizarea bibliotecii LinuxThreads
Biblioteca LinuxThreads este disponibila automat in toate
distributiile moderne de Linux (presupunand ca au fost instalate
pachetele de development).
Pentru a putea utiliza fire de executie, este
necesar ca programele sa fie compilate
folosind comanda:
gcc -D_REENTRANT -lpthread <fisier.c> -o <executabil>
Se observa ca este necesara definirea constantei _REENTRANT (din considerente
legate de executia paralela a firelor) si ca trebuie inclusa explicit biblioteca
pthread (numele sub care se regaseste LinuxThreads, conform POSIX).
2.2 Crearea firelor de executie
Un fir de executie se creeaza folosind functia
int pthread_create(pthread_t *thread, pthread_attr_t
*attr, void *(*start_routine)(void *), void *arg);
Functia creeaza un thread care se va executa in paralel cu thread-ul
creator. Noul fir de executie va fi format de functia start_routine
care trebuie definita in program avand un singur argument de tip
(void *). Parametrul arg este argumentul care va fi transmis
acestei functii. Parametrul attr este un cuvant care specifica diferite
optiuni de creare a firului de executie. In mod obisnuit, acesta este dat
ca NULL, acceptand optiunile implicite. Firul de executie creat va primi
un identificator care va fi returnat in variabila indicata de parametrul
thread. Functia returneaza 0 daca crearea a avut succes si un numar
diferit de zero in caz contrar.
Thread-ul va consta in executia functiei date ca argument, iar
terminarea lui se va face ori apeland explicit functia pthread_exit(
), ori implicit, prin iesirea din functia start_routine.
2.3 Terminarea firelor de executie
Un fir de executie se poate termina apeland:
void pthread_exit(void *retval);
Valoarea retval este valoarea pe care thread-ul o returneaza
la terminare. Starea returnata de firele de executie poate fi preluata
de catre oricare din thread-urile aceluiasi proces,
folosind functia:
int pthread_join(pthread_t th, void **thread_return);
Aceasta intrerupe firul de executie care o apeleaza pana cand firul de
executie cu identificatorul th se termina, moment in care starea
lui va fi returnata la adresa data de parametrul thread_return.
Demn de observat este faptul ca nu pentru toate firele de executie
poate fi preluata starea de iesire. De fapt, conform standardului POSIX,
firele de executie se impart in doua categorii:
-
joinable - ale caror stari pot fi preluate de catre celelalte fire
din proces
-
detached - ale caror stari nu pot fi preluate
In cazul thread-urilor joinable, in momentul terminarii acestora,
resursele lor nu sunt complet dezalocate, asteptandu-se un viitor pthread_join
pentru ele. Firele de executie detached se dezaloca in intregime,
starea lor devenind nedisponibila pentru alte fire de executie.
Tipul unui fir de executie poate fi specificat la crearea acestuia,
folosind optiunile din argumentul attr (implicit este joinable).
De asemenea, un fir de executie joinable poate fi "detasat" mai
tarziu, folosind functia pthread_detach( ).
2.4 Observatii
-
Un proces, imediat ce a fost creat, este format dintr-un singur fir de
executie, numit fir de executie principal (initial).
-
Toate firele de executie din cadrul unui proces se vor executa in paralel.
-
Datorita faptului ca impart aceeasi zona de date, firele de executie ale
unui proces vor folosi in comun toate variabilele globale. De aceea, se
recomanda ca in programe firele de executie sa utilizeze numai variabilele
locale, definite in functiile care implementeaza firul, in afara de cazurile
in care se doreste partajarea explicita a unor resurse.
-
Daca un proces format din mai multe fire de executie se termina ca urmare
a primirii unui semnal, toate firele de executie ale sale se vor termina.
-
Daca un fir de executie apeleaza functia exit( ), efectul va fi
terminarea intregului proces, cu toate firele de executie din interior.
-
Orice functie sau apel sistem care lucreaza cu sau afecteaza procese, va
avea efect asupra intregului proces, indiferent de firul de executie in
care a fost apelata functia respectiva. De exemplu, functia sleep( )
va "adormi" toate firele de executie din proces, inclusiv firul de executie
principal (initial), indiferent de firul care a apelat-o.
-
Standardul POSIX defineste si cateva primitive de sincronizare intre firele
de executie pentru accesul la resursele comune, care nu fac obiectul lucrarii
de fata.
Sa se scrie un program C
format dintr-un singur proces in care ruleaza 6 fire de executie (inclusiv
cel initial). Procesul creeaza un tablou de 5 caractere, ale carui elemente
sunt initializate cu '#'. In proces vor exista 5 fire de executie "producator"
care vor completa continuu cate o locatie aleatoare din tablou cu cate
un caracter, astfel: primul fir cu caracterul 'A', al doilea cu 'B' s.a.m.d.
pana la 'E'. Fiecare fir de executie "producator" va numara cate caractere
a introdus in tablou; primul fir se va termina in momentul in care a introdus
100 000 de caractere, al doilea 200 000 s.a.m.d. Thread-ul principal va
afisa continuu continutul tabloului, pana cand toate firele "producator"
se incheie, moment in care va prelua starea acestora si va termina procesul.
Indicatie: pentru anuntarea terminarii firelor de executie "producatori",
fiecare din ele va seta o variabila globala distincta, firul principal
testand toate aceste variabile.
 
Autor: Dan Cosma
|
|