5. Procese

1. Concepte de baza

Orice sistem de calcul modern este capabil sa execute mai multe programe in acelasi timp. Cu toate acestea, in cele mai multe cazuri, unitatea centrala de prelucrare (CPU) nu poate executa la un moment dat decat un singur program. De aceea, sarcina de a rula mai multe programe in acelasi timp revine sistemului de operare, care trebuie sa introduca un model prin intermediul caruia executia programelor, privita din perspectiva utilizatorului, sa se desfasoare in paralel. Se realizeaza, de fapt, un pseudoparalelism, prin care procesorul este alocat pe rand programelor care trebuie rulate, cate o cuanta de timp pentru fiecare, astfel incat din exterior ele par ca ruleaza efectiv in acelasi timp.

Cel mai raspandit model care introduce paralelismul in executia programelor este modelul bazat pe procese. Acest model este cel adoptat de sistemul de operare Unix si va face obiectul acestei lucrari.

Un proces este un program secvential in executie, impreuna cu zona sa de date, stiva si numaratorul de instructiuni (program counter). Trebuie facuta inca de la inceput distinctia dintre proces si program. Un program este, in fond, un sir de instructiuni care trebuie executate de catre calculator, in vreme ce un proces este o abstractizare a programului, specifica sistemelor de operare. Se poate spune ca un proces executa un program si ca sistemul de operare lucreaza cu procese, iar nu cu programe. Procesul include in plus fata de program informatiile de stare legate de executia programului respectiv (stiva, valorile registrilor CPU etc.). De asemenea, este important de subliniat faptul ca un program (ca aplicatie software) poate fi format din mai multe procese care sa ruleze sau nu in paralel.

Orice proces este executat secvential, iar mai multe procese pot sa ruleze in paralel (intre ele). De cele mai multe ori, executia in paralel se realizeaza alocand pe rand procesorul cate unui proces. Desi la un moment dat se executa un singur proces, in decurs de o secunda, de exemplu,  pot fi executate portiuni din mai multe procese. Din aceasta schema rezulta ca un proces se poate gasi, la un moment dat, in una din urmatoarele trei stari [Tanenbaum]:

  • In executie
  • Pregatit pentru executie
  • Blocat
Procesul se gaseste in executie atunci cand procesorul ii executa instructiunile. Pregatit de executie este un proces care, desi ar fi gata sa isi continue executia, este lasat in asteptare din cauza ca un alt proces este in executie la momentul respectiv. De asemenea, un proces poate fi blocat din doua motive: el isi suspenda executia in mod voit sau procesul efectueaza o operatie in afara procesorului, mare consumatoare de timp (cum e cazul operatiilor de intrare-iesire - acestea sunt mai lente si intre timp procesorul ar putea executa parti din alte procese).

2. Utilizarea proceselor in UNIX

2.1 Apelul sistem fork()

Din perspectiva programatorului, sistemul de operare UNIX pune la dispozitie un mecanism elegant si simplu pentru crearea si utilizarea proceselor.

Orice proces trebuie creat de catre un alt proces.Procesul creator este numit proces parinte, iar procesul creat proces fiu. Exista o singura exceptie de la aceasta regula, si anume procesul init, care este procesul initial, creat la pornirea sistemului de operare si care este responsabil pentru crearea urmatoarelor procese. Interpretorul de comenzi, de exemplu, ruleaza si el in interiorul unui proces.

Fiecare proces are un identificator numeric, numit identificator de proces (process identifier - PID). Acest identificator este folosit atunci cand se face referire la procesul respectiv, din interiorul programelor sau prin intermediul interpretorului de comenzi.

Un proces trebuie creat folosind apelul sistem

pid_t fork()
Prin aceasta functie sistem, procesul apelant (parintele) creeaza un nou proces (fiul) care va fi o copie fidela a parintelui. Noul proces va avea propria lui zona de date, propria lui stiva, propriul lui cod executabil, toate fiind copiate de la parinte in cele mai mici detalii. Rezulta ca variabilele fiului vor avea valorile variabilelor parintelui in momentul apelului functie fork( ), iar executia fiului va continua cu instructiunile care urmeaza imediat acestui apel, codul fiului fiind identic cu cel al parintelui. Cu toate acestea, in sistem vor exista din acest moment doua procese independente, (desi identice), cu zone de date si stiva distincte. Orice modificare facuta, prin urmare, asupra unei variabile din procesul fiu, va ramane invizibila procesului parinte si invers.

Procesul fiu va mosteni de la parinte toti descriptorii de fisier deschisi de catre acesta, asa ca orice prelucrari ulterioare in fisiere vor fi efectuate in punctul in care le-a lasat parintele.

Deoarece codul parintelui si codul fiului sunt identice si pentru ca aceste procese vor rula in continuare in paralel, trebuie facuta clar distinctia, in interiorul programului, intre actiunile ce vor fi executate de fiu si cele ale parintelui. Cu alte cuvinte, este nevoie de o metoda care sa indice care este portiunea de cod a parintelui si care a fiului.
Acest lucru se poate face simplu, folosind valoarea returnata de functia fork( ). Ea returneaza:

  • -1, daca operatia nu s-a putut efectua (eroare)
  • 0, in codul fiului
  • pid, in codul parintelui, unde pid este identificatorul de proces al fiului nou-creat.
Prin urmare, o posibila schema de apelare a functiei fork( ) ar fi:
...
if( ( pid=fork() ) < 0)
  {
    perror("Eroare");
    exit(1);
  }
if(pid==0)
  {
    /* codul fiului */
    ...
    exit(0)
  }
/* codul parintelui */
...
wait(&status)

2.2 Functiile wait() si waitpid()

pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int flags)
Functia wait( ) este folosita pentru asteptarea terminarii fiului si preluarea valorii returnate de acesta. Parametrul status este folosit pentru evaluarea valorii returnate, folosind cateva macro-uri definite special (vezi paginile de manual corespunzatoare functiilor wait( ) si waitpid( ) ). Functia waitpid( ) este asemanatoare cu wait( ), dar asteapta terminarea unui anumit proces dat, in vreme ce wait( ) asteapta terminarea oricarui fiu al procesului curent. Este obligatoriu ca starea proceselor sa fie preluata dupa terminarea acestora, astfel ca functiile din aceasta categorie nu sunt optionale.  

2.3 Functiile de tipul exec()

Functia fork( ) creeaza un proces identic cu procesul parinte. Pentru a crea un nou proces care sa ruleze un program diferit de cel al parintelui, aceasta functie se va folosi impreuna cu unul din apelurile sistem de tipul exec( ): execl( ), execlp( ), execv( ), execvp( ), execle( ), execve( ).

Toate aceste functii primesc ca parametru un nume de fisier care reprezinta un program executabil si realizeaza lansarea in executie a programului. Programul va fi lansat atfel incat se va suprascrie codul, datele si stiva procesului care apeleaza exec( ), astfel incat, imediat dupa acest apel programul initial nu va mai exista in memorie. Procesul va ramane, insa, identificat prin acelasi numar (PID) si va mosteni toate eventualele redirectari facute in prealabil asupra descriptorilor de fisiere (de exemplu intrarea si iesirea standard). De asemenea, el va pastra relatia parinte-fiu cu procesul care a apelat fork( ).

Singura situatie in care procesul apelant revine din apelul functiei exec( ) este acela in care operatia nu a putut fi efectuata, caz in care functia returneaza un cod de eroare (-1).

In consecinta, lansarea intr-un proces separat a unui program de pe disc se face apeland fork( ) pentru crearea noului proces, dupa care in portiunea de cod executata de fiu se va apela una din functiile exec( ).

Observatie: consultati paginile de manual corespunzatoare acestor functii.

2.4 Functiile system() si vfork()

int system(const char *cmd)
Lanseaza in executie un program de pe disc, folosind in acest scop un apel fork( ), urmat de exec( ), impreuna cu waitpid( ) in parinte.  
pid_t vfork()
Creeaza un nou proces, la fel ca fork( ), dar nu copiaza in intregime spatiul de adrese al parintelui in fiu. Este folosit in conjunctie cu exec( ), si are avantajul ca nu se mai consuma timpul necesar operatiilor de copiere care oricum ar fi inutile daca imediat dupa aceea se apeleaza exec( ) (oricum, procesul fiu va fi supascris cu programul luat de pe disc).  

2.5 Alte functii pentru lucrul cu procese

pid_t getpid() - returneaza PID-ul procesului curent
pid_t getppid() - returneaza PID-ul parintelui procesului curent
uid_t getuid() - returneaza identificatorul utilizatorului care a lansat procesul curent
gid_t getgid() - returneaza identificatorul grupului utilizatorului care a lansat procesul curent

2.6 Gestionarea proceselor din linia de comanda

Sistemul de operare UNIX are cateva comenzi foarte utile care se refera la procese:
  • ps - afiseaza informatii despre procesele care ruleaza in mod curent pe sistem
  • kill -semnal proces - trimite un semnal unui proces. De exemplu, comanda kill -9 123 va termina
    procesul cu numarul 123
  • killall -semnal nume - trimite semnal catre toate procesele cu numele nume
Exista si alte comenzi utile; pentru folosirea lor, este recomandat sa se consulte paginile de manual UNIX.  

1. Explicati efectul urmatoarei secvente de cod:

int i;
for (i = 1; i <= 10; i++)
   fork();
2. Realizati un program  C pentru UNIX care creaza 20 de procese (inclusiv parintele). Fiecare proces afiseaza pe ecran cate 10 linii continand tipul sau (parinte, fiu1, fiu2, ... , fiu19) si PID-ul propriu. Dupa aceea, procesele fiu se vor termina returnand valori diferite, iar parintele va afisa valorile returnate de catre fii.

Bibliografie:

[Tanenbaum] Andrew S. Tanenbaum: Modern Operating Systems, Prentice Hall, 1992, pag. 27-31, 279-284



Autor: Dan Cosma