1. Scopul lucrării Pentru a înțelege și stăpâni Unix-ul este foarte importanta noțiunea de proces. Procesul sta la baza oricărei activități din sistemul de operare. La lansarea unei comenzi utilizator se creează și un nou proces. Controlul proceselor este un element important în programarea pe sisteme multitasking. Ea include operații de creare de procese, terminare și sincronizare. Sistemul Unix este și un sistem multi-utilizator acest lucru fiind un aspect important în dezvoltarea aplicațiilor multiutilizator. Controlul proceselor este realizat prin câteva apeluri sistem, care nu sunt altceva decât funcții, înglobate în nucleu, accesibile utilizatorului. Utilizarea corecta a apelurilor este esentiala în controlul corect al proceselor Unix. 2. Considerații teoretice Un proces este instanța execuției unui program. Procesele nu se confunda cu programul, care este fișierul executat de proces. Pe un sistem multitasking mai multe procese pot executa același program concurent și fiecare proces se poate autotransforma pentru a executa un program anume. Fiecare proces în Unix are asociat un identificator unic numit identicator de proces, prescurtat PID. Pentru identificator se utilizează în continuare prescurtarea ID. PID este un numar pozitiv atribuit de sistemul de operare fiecărui proces nou creat. Un proces poate sa determine PID-ul sau folosind apelul sistem getpid. Cum PID-ul unui proces este unic el nu poate fi schimbat, dar se poate refolosi când procesul nu mai exista.
Tab.1. Apeluri care întorc identificatori de proces. Orice proces nou în Unix este creat de un proces anterior existent, dând naștere unei relații părinte-fiu. Excepție face procesul 0, care este creat și utilizat chiar de nucleu. Un proces poate sa determine PID-ul părintelui prin apelul getppid(). PID-ul procesului părinte nu se poate modifica. Sistemul de operare Unix tine evidenta proceselor intr-o structura de date interna numita tabela de procese. Ea are o intrare pentru fiecare proces din sistem. Lista proceselor din tabela de procese poate fi obtinuta prin comanda ps. Uneori se doreste crearea unui subsistem ca un grup de procese înrudite în locul unui proces singular. De exemplu, un sistem complex de gestiune al unei baze de date poate fi împărțit în câteva procese pentru a "câștiga" operații de I/E cu discul. Pe lângă ID asociat unui proces, care permite identificarea individuala a fiecăruia, fiecare proces are și un ID de grup de procese, prescurtat (PGID), care permite identificarea unui grup de procese. PGID este moștenit de procesul fiu de la procesul părinte. Contrar PID-ului, un proces poate sa-si modifice PGID, dar numai prin crearea unui nou grup. Acest lucru se realizează prin apelul sistem setpgrp. int setpgrp(); setpgrp actualizează PGID-ul procesului apelant la valoarea PID-ului sau și întoarce noul PGID. Procesul apelant părăsește astfel vechiul grup devenind leaderul propriului grup urmând a-si crea procesele fiu, care sa formeze grupul. Deoarece procesul apelant este primul membru al grupului și numai descendenții săi pot sa aparțină grupului (prin moștenirea PGID), el este referit ca reprezentantul (leaderul) grupului. Deoarece doar descendenții leaderului pot fi membri ai grupului exista o corelație intre grupul de procese și arborele proceselor. Fiecare leader de grup este rădăcina unui subarbore, care după eliminarea rădăcinii conține doar procese ce aparțin grupului. Daca nici un proces din grup nu s-a terminat lăsând fii care au fost adoptați de procesul init, acest subarbore conține toate procesele din grup. Un proces poata determina PGID sau folosind apelul sistem: int getpgrp(); Apelul întoarce PGID procesului apelant. Deoarece PID-ul leaderului este același cu PGID-ul, getpgrp identifica leaderul. Un proces poate fi asociat unui terminal, care este numit terminalul de control asociat procesului. Acesta este moștenit de la procesul părinte la crearea unui nou proces. Un proces este deconectat (eliberat) de terminalul sau de control la apelul setpgrp, devenind astfel un leader de grup de procese ( nu se închide terminalul). Ca atare, numai leaderul poate stabili un terminal de control, devenind procesul de control pentru terminalul în cauza. Rațiunea existentei unui grup este legata de comunicarea prin semnale. în mod uzual, procesele din același grup sunt conectate logic în acest fel. De exemplu, managerul unei baze de date multiproces este constituit dintr-un proces master și câteva procese subsidiare. Pentru a face din suita proceselor bazei de date un grup de procese, procesul master apelează setpgrp înainte de a crea procesele subsidiare. Un proces care nu este asociat cu un terminal de control este numit daemon. Spoolerul de imprimanta este un exemplu de astfel de proces. Un proces daemon este identificat în rezultul afișării comenzii ps prin simbolul ? plasat în coloana TTY. 2.2. Programe și procese Un program este o colecție de instrucțiuni și date păstrate intr-un fișier ordinar pe disc. în i-node-ul sau fișierul este marcat executabil și conținutul sau este aranjat conform regulilor stabilite de nucleu. Un program în Unix este format din mai multe segmente. în segmentul de cod se găsesc instrucțiuni sub forma binara. în segmentul de date se găsesc date predefinite (de exemplu constante) și date inițializate. Al treilea segment este segmentul de stiva, care conține date alocate dinamic la execuția procesului. Aceste trei segmente sunt și parți funcționale ale unui proces Unix. Pentru a executa un program nucleul este informat pentru a crea un nou proces, care nu este altceva decât un mediu în care se executa un program. Un proces consta din trei segmente: segmentul de instrucțiuni, segmentul de date utilizator și segmentul de date sistem. Programul este folosit pentru a inițializa primele doua segmente, după care nu mai exista nici o legătura intre procesul și programul pe care-l executa. Datele sistem ale unui proces includ informații ca directorul curent, descriptori de fișiere deschise, cai implicite, tipul terminalului, timp CPU, etc. Un proces nu poate accesa sau modifica direct propriile date sistem, deoarece acestea sunt în afara spațiului de adresare. Exista insa multiple apeluri sistem pentru a accesa sau modifica aceste informații. Structura arborescenta a sistemului de fișiere implica o structura identica pentru procese. Toate procesele active la un moment dat în Unix sunt de fapt descendenți direcți sau indirecți ai unui singur proces, lansat la pornirea sistemului prin comanda /etc/init. După pornirea sistemului, printr-un dialog la consola se poate opta pentru trecerea în mod multiutilizator. în acest caz, sistemul citește din fișierul /etc/ttys numerele terminalelor utilizator. Pentru fiecare dintre aceste terminale va fi lansat un nou proces. Procesul de la un terminal precum și urmașii lui vor avea intrarea standard, ieșirea standard și ieșirea de erori fixate la terminalul respectiv. Se setează apoi viteza de lucru, se citesc parametrii de comunicație ai terminalelor, paritatea, etc. Utilizatorul introduce apoi numele și eventual parola proprie, care în cazul în care sunt corecte, se lansează un nou proces care nu este altcineva decât interpretorul de comenzi shell. Acesta are menirea de a interpreta comenzile utilizator. Daca lansarea unei comenzi externe în DOS se făcea prin încărcarea în memorie și lansarea în execuție, nu același lucru se poate spune despre o comanda Unix. Intr-un sistem multitasking și multiuser la lansarea unui program se creează de fiecare data un nou proces. Acest lucru se realizează prin apelul sistem fork. La fiecare execuție a acestui apel, se obțin doua procese concurente, identice la început, dar cu nume diferite. Apelul sistem fork realizează o copie a procesului inițial, ca atare imaginea proceselor în memorie este identica. Procesul care a inițiat apelul fork este identificat ca proces părinte sau tata, iar procesul rezultat în urma apelului este identificat ca proces fiu. De exemplu, se considera comanda: $echo Exemplu Shell-ul desparte comanda de argumente. Se executa apelul fork și rezulta procesul fiu. Procesul tata prin apelul sistem wait cedează procesorul procesului fiu. Procesul fiu cere nucleului, prin apelul sistem exec, execuția unui nou program, respectiv echo și comunica în același timp și argumentele pentru noul program. Nucleul eliberează zona alocata pentru shell și încărca un nou program pentru procesul fiu. Procesul continua cu execuția noului program. Execuția lui exit are ca efect terminarea procesului curent (fiu), ștergerea legaturilor și transmiterea unui cod de terminare procesului tata, care părăsește astfel starea de așteptare și înlătura procesul fiu din sistem. Este posibila execuția unei comenzi intr-un plan secundar (background), daca după specificarea comenzii este adăugat caracterul &. La o astfel de comanda interpretorul răspunde cu un număr după care afișează prompterul. Acest număr reprezintă ID de proces al comenzii introduse. Procesul shell care lansează comanda nu mai așteaptă încheierea procesului fiu. Acesta din urma se va executa independent de procesul care l-a creat, iar după apelul exit el rămâne în stare inactiva în sistem pana când shell-ul va executa un apel wait în urma căruia procesul este înlăturat din sistem. 2.3. Apelurile sistem FORK și EXEC Apelul sistem fork are sintaxa: #include <sys/types.h> #include <unistd.h> pid_t fork( void); Returneaza 0 în procesul fiu, PID fiului în procesul tata și -1 în caz de eroare. Figura de mai jos ilustrează cum se obține prin fork o copie identica a procesului tata.
Procesul tata Procesul fiu
Fig.1. Generarea unui nou proces prin fork In noul proces (fiu) toate vechile variabile își păstrează valorile, toți descriptorii de fișier sunt aceeași, se moștenește același UID real și GUID real, același ID de grup de procese, aceleași variabile de context. Bineințeles ca noua copie a procesului tata se găsește fizic la o alta adresa de memorie. Din momentul revenirii din apelul fork, procesele tata și fiu se executa independent, concurând unul cu celalalt pentru obținerea resurselor. Procesul fiu își începe execuția din locul unde rămăsese procesul tata. Nu se poate preciza care dintre procese va porni primul. Este posibila insa separarea execuției în cele doua procese prin testarea valorii intoarse de apelul fork. Secvența de mai jos partiționează codul programului în cele doua procese: ... if ((pid=fork())==0)/* actiuni specifice fiului */ else if ( pid!=0) /* actiuni specifice tatalui */ else /* eroare la apelul fork */ Cazul de eroare poate apare daca s-a atins limita maxima de procese pe care le poate lansa un utilizator sau daca s-a atins limita maxima de procese ce se pot executa deodata în sistem. Rațiunea a doua procese identice are sens daca se poate modifica segmentul de date și cel de cod al procesului rezultat așa încât să se poată încărca un nou program. Pentru acest lucru exista apelul exec (împreuna cu familia de astfel de apeluri execl, execlp, execv și execvp). Partea de sistem a procesului nu se modifica în nici un fel prin apelul exec. Deci nici numărul procesului nu se schimba. Practic procesul fiu executa cu totul altceva decât părintele sau. După un apel exec reușit nu se mai revine în vechiul cod. A nu se uita ca fișierele deschise ale tatălui se regăsesc deschise și la fiu după apelul exec și ca indicatorul de citire/scriere al fișierelor deschise rămâne nemodificat, ceea ce poate cauza neplăceri în cazul în care tatăl și fiul vor sa scrie în același loc. Un apel exec nereușit returneaza valoarea -1, dar cum alta valoare nu se returnează ea nu trebuie testata. Insuccesul poate fi determinat de cale eronata sau fișier neexecutabil. Diferitele variante de exec dau utilizatorului mai multa flexibilitate la transmiterea parametrilor. Sintaxele lor sunt: #include <unistd.h> (1) execl ( const char *path, const char *arg0, ..., NULL); (2) execv ( const char *path, char *argv[]); (3) execlp( const cahr *filename, const char *arg0, ..., NULL); (4) execvp( const cahr *filename, char *argv[]); Returneaza -1 în caz de eroare, nimic în caz de succes. Intre apelurile 1) și 3) respectiv intre 2) și 4) exista doua deosebiri de implementare. Prima este ca al ultimele doua apeluri căuta programul pe caile implicite setate de variabila PATH și se pot lansa și proceduri shell. Daca filename nu conține caractere slash se expandează numele fișierului cu caile găsite în variabila PATH și se încearcă găsirea unui fișier executabil care sa se potrivească cu calea obținuta. Daca nu se găsește nici un astfel de fișier se presupune ca este o comanda shell și se executa /bin/sh. Daca în cale se precizează caractere slash se presupune calea completa și nu se face nici o căutare. A doua deosebire se refera la transmiterea argumentelor ca lista sau vector. în cazul listelor fiecare argument este precizat separat și sfârșitul listei este marcat printr-un pointer NULL. în cazul vectorului se specifica doar adresa unui vector cu adresele argumentelor. Mai aproape de modul obișnuit de lucru este apelul 2). Se folosește des în momentul compilării când nu se cunoaște numărul de argumente. Se observa ca fără fork, exec este limitat ca acțiune, iar fără exec, fork nu are aplicabilitate practica. Deși efectul lor conjugat este cel dorit, rațiunea existentei a doua apeluri distincte va rezulta din parcurgerea lucrărilor următoare. 2.4. Sincronizare tata-fiu. Apelul sistem WAIT și WAITPID Procesul tata poate așteaptă terminarea (normala sau cu eroare) procesului fiu folosind apelul sistem wait sau waitpid. #include <sys/types.h> #include <sys/wait.h> pid_t wait( int *pstatus); pid_t waitpid(pid_t pid, int *pstatus, int opt); Returneaza PID în caz de succes sau 0 ( waitpid), -1 în caz de eroare. Argumentul pstatus este adresa cuvântului de stare. Un proces ce apelează wait sau waitpid poate:
a) wait blochează procesul apelant pana la terminarea unui fiu, în timp ce waitpid are o opțiune, precizata prin argumentul opt, care evita acest lucru. b) waitpid nu așteaptă terminarea primului fiu, ci poate specifica prin argumentul opt procesul fiu așteptat. c) waitpid permite controlul programelor prin argumentul opt. Daca, nu exista procese fiu, apelul wait întoarce valoarea -1 și poziționeazș variabila errno la ECHILD. În ce mod s-a terminat procesul fiu, normal sau cu eroare, se poate afla cu ajutorul parametrului pstatus. Exista cazurile:
Procesul fiu Procesul tata: pstatus
Tab.2. Valorile returnate prin parametrul pstatus. Exista trei moduri de a termina un proces: apelul exit, receptionarea unui semnal fatal, sau caderea sistemului. Codul de stare returnat prin variabila pstatus indica care din cele doua moduri a cauzat terminarea ( în al treilea mod procesul parinte și nucleul dispar, asa incat pstatus nu conteaza). Funcția waitpid exista în SRV4 și 4.3+BSD. Argumentul opt poate avea valorile:
Tab.3. Valorile argumentului opt. Argumentul opt poate fi și 0 sau rezultatul unui SAU intre constantele simbolice WNOHANG și WUNTRACED. In funcție pid, interpretarea funcției waitpid este: pid==-1 Se așteaptă orice proces fiu (echivalent wait). pid > 0 Se așteaptă procesul pid. pid==0 Se așteaptă orice proces cu ID de grup de proces egal cu cel al apelantului. pid< -1 Se așteaptă orice proces cu ID de grup de proces egal cu valoarea absoluta a pid. Apelul waitpid returnează (-1) daca nu exista proces sau grup de procese cu pid-ul specificat sau pid-ul respectiv nu este al unui fiu de al sau. Pentru a analiza starea în care s-a terminat un proces fiu exista trei macrouri excluse mutual, toate prefixate de WIF și definite în fișierul sys/wait.h. Pe lângă acestea, exista alte macrouri pentru determinarea codului de exit, număr semnal, etc. Acestea sunt ilustrate mai jos:
Tab.4. Macrouri pentru determinarea starii. Procesul fiu semnalează terminarea sa tatălui aflat în așteptare. Pentru aceasta exista apelul exit care transmite prin parametrul sau un număr care semnalează tatălui o terminare normala sau cu eroare. Prin convenție, un cod de stare 0 semnifica terminarea normala a procesului, iar un cod diferit de zero indica apariția unei erori. Sintaxa apelului este: void exit( int status); Acest apel termina procesul care-l executa cu un cod de stare egal cu octetul mai semnificativ al cuvântului de stare, status, și închide toate fișierele deschise de acesta. După aceea, procesului tata ii este transmis semnalul SIGCLD. Pentru procesele aflate intr-o relație părinte-fiu la un apel exit sunt esențiale trei cazuri:
Daca procesul fiu se termina înaintea procesului părinte, nucleul trebuie sa păstreze anumite informații ( pid, starea de terminare, timp de utilizare CPU) asupra modului în care fiul s-a terminat. Aceste informații sunt accesibile părintelui prin apelul wait sau waitpid. în terminologie Unix un proces care s-a terminat și pentru care procesul părinte nu a executat wait se numește zombie. în aceasta stare, procesul nu are resurse alocate, ci doar intrarea sa în tabela proceselor. Nucleul poate descarca toata memoria folosita de proces și inchide fișierele deschise. Un proces zombie se poate observa prin comanda Unix ps care afiseaza la starea procesului litera 'Z'. Daca un proces care are ca părinte procesul init se termina, acesta nu devine zombie iarăși deoarece, procesul init apelează una dintre funcțiile wait pentru a analiza starea în care procesul a fost terminat. Prin aceasta comportare procesul init evita încărcarea sistemului cu procese zombie. Apelul _exit nu golește tampoanele de I/E. 2.6. Gestiunea și planificarea proceselor Nucleul Unix asigura gestiunea și planificarea pentru execuție a proceselor. Starea sistemului la un moment dat reprezintă o ierarhie de procese organizate astfel: Proces 0 Proces 1 ÚÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄĆÄÄÄÄÄÄÄÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄż sh1 sh2 ... shn cron update Procesul 0 (swapper) este un proces sistem care asigura gestiunea memoriei printr-un mecanism de swapping. Procesul 1 (init) este lansat în faza de inițializare fiind un proces permanent în sistem. Acesta este procesul care inițializează procesul fiu login pe terminalele anunțate și după deschiderea unei sesiuni de lucru generează procese corespunzătoare de asistenta a utilizatorului (implicit un proces shell). Acesta la rândul sau utilizează un mecanism de tip forkÄexec pentru execuția comenzilor. Procesele cron și update sunt create de fișiere de comenzi shell etc/rc și sunt executate la trecerea din mod monoutilizator în multiutilizator. Gestiunea tuturor proceselor este asigurata de nucleu prin intermediul unei tabele interne PROC, fiecare proces fiind nominalizat la generare printr-un număr pozitiv. Planificarea acestora este realizata după un principiu de timesharing, care consta în partiționarea timpului sistem intre procesele active după prioritari. Prioritățile proceselor utilizator sunt evaluate dinamic la intervale fixe: timp în UC proces_i prioritate proces_i = ---------------------------------------- timp în memorie proces_ifiind preferate procesele cu valori mici ale prioritarilor (cele cu o activitate de I/E intensa și cele abia intrate în memorie). Prioritățile proceselor sistem sunt fixe și mai mari decât ale proceselor utilizator. Algoritmul corespunde, în mare, unui mecanism de tip round-robin ( daca se considera procesele fără I/E), care asigura eliberarea unității centrale de către procesul curent intr-un timp finit. 3. Aplicatii 3.1. Sa se scrie programul par, care creează un proces fiu ce executa programul numit fiu. Procesul părinte așteaptă terminarea fiului și afișează pid-ul procesului fiu și starea cu care s-a terminat acesta (in zecimal și hexazecimal). /* par.c */ main() { int pid, stare; printf(" Parinte: inainte de fork()\n"); if ( (pid=fork()) !=0) wait( &stare);else execl("/usr/acct/k/L10/fiu", 0);printf("Parinte: dupa fork()\n"); printf("\tId proces fiu=%d; Terminat cu valoarea %d=%x\n", pid, stare, stare); } /* fiu.c */ main() { int pid; printf("Fiul: incepe executia \n"); pid=getpid(); printf("Fiul: %d se termina\n", pid); exit( pid); } 3.2. Sa se scrie o funcție pr_exit care, folosind macrourile definite în lucrare, sa permită afișarea informațiilor de stare. #include <sys/types.h> #include <sys/wait.h> extern char *sys_siglist[]; void print_exit( int status) { if ( WIFEXITED( status)) printf("Terminare normala, starea de exit=%d\n",WEXITSTATUS( status) ); else if ( WIFSIGNALED( status)) printf("Terminare Anormala, numar semnal=%d=%s%s\n", sys_siglist[ WTERMSIG( status)], #ifdef WCOREDUMP WCOREDUMP( status) ? "( generat fișierul core":"");#else ""); #endif else if ( WIFSTOPPED( status)) printf("Proces fiu oprit, numar semnal=%d%s\n",WSTOPSIG( status) , sys_siglist[ WSTOPSIG( status)] ); } 3.3. Sa se scrie un program care preia argumente de la intrare și le executa ca și comenzi Unix. Se va folosi apelul sistem execlp. La așteptarea introducerii unei comenzi, programul afișează prompterul >. #include <sys/types.h> #include <sys/wait.h> #include "hdr.h" int main( void) { char buf[MAXLINE]; pid_t pid; int status; printf("> "); while ( fgets( buf, MAXLINE, stdin) != NULL) { buf[ strlen( buf) 1] = 0; if ( (pid=fork()) < 0) err_sys("Eroare fork"); else if ( pid == 0) { execlp( buf, buf, NULL); err_ret("Nu sa putut executa: %s", buf); exit( 127); if ( (pid=waitpid( pid, &status, 0)) < 0) err_sys("Eroare waitpid");printf("> "); } exit(0); } 4. Probleme propuse 4.1. Ce afișează programul: int main( void) { int pid, k=7; pid=fork(); printf("Returnat %d\n", pid); if ( pid) k=2; printf("k= %d\n",k); } 4.2. Să se execute programul de mai jos pe date de test și sa se explice rezultatul. #include <fcntl.h> #include "hdr.h" int fdR, fdW; char c; main( int argc, char * argv[]) { if ( argc != 3) exit(1); if (( fdR=open( argv[1], ODONLY)) == 1) err_sys( "Eroare open1\n");if (( fdW=open( argv[2], O_WRITE)) == 1) err_sys( "Eroare open2\n");fork(); rd_wr(); exit(0); } rd_wr() { for ever { if ( read( fdR, &c, 1) != 1) return; write( fdW, &c, 1);} } 4.3. Să se scrie un program care sa demonstreze ca doua procese aflate în relatia parinte-fiu sunt concurente. 4.4. Să se scrie patru funcții de sincronizare pentru a putea controla execuția proceselor aflate în relația părinte-fiu. 4.5. Să se scrie un program de test pentru funcția pr_exit. 4.6. Să se scrie un program care generează un proces zombie. 4.7. Să se scrie o funcție sistem similara ca acțiune apelului sistem system folosind apelurile sistem fork si exec. Nota: semnalele nu se vor trata. 4.8. Sa se scrie un exemplu de test pentru problema 4.7. 4.9. Sa se implementeze apelurile execvp și execlp ca subrutine ce folosesc apelurile execl și execv. |