Caracteristici ANSI C
Toate exemplele descrise in continuare sunt scrise in limbajul C
care respecta standardul ANSI C.
Prototipul si interfata unei functii
Fisierul antet unistd.h contine prototipul multor functii oferite de sistemul Unix. Prototipul unei functii specifica tipul parametrilor de apel si tipul valorii returnate de functie. Exemple de prototip de functii sunt:
pid_t getpid( void);
ssize_t read( int, void *, size_t);
void *malloc( size_t);
In primul caz functia getpid nu are argumente si returneaza o valoare de tipul pid_t. In al doilea caz functia read are trei argumente de tipuri diferite:int, un pointer la tipul void si size_t. Valoarea returnata de functie este de tipul ssize_t. In ultimul caz, functia malloc are un singur argument de tipul size_t si valoarea returnata este un pointer la tipul void.
Interfata functiei precizeaza pe linga prototip si fisierele care trebuie incluse in programul care apeleaza aceste functii.
In versiunile anterioare de UNIX nu era definit tipul void motiv pentru care prototipul acestor functii folosea in loc de void tipul int iar in loc de void * tipul char *. Introducerea tipului void permite eliminarea castului. De exemplu valoarea returnata de functia malloc trebuia prefixata cu un cast explicit la tipul pointerului dorit, deoarece ea returna un pointer la tipul char. Noul prototip al functiei malloc permite apelul sub forma:
int *p;
p=malloc( 77 * sizeof( int));
Compilatorul folosit, gcc sau cc, respecta standardul ANSI C.
In versiunile anterioare de UNIX nu erau definite tipurile pid_t,ssize_t si size_t folosite in exemplele anterior prezentate. Aceste tipuri de date si in general toate tipurile terminate cu _t sunt definite ca tipuri primitive de sistem in fisierul antet sys/types.h
Cele doua fisiere antet unistd.h si sys/types.h au rolul de a preveni programele sa foloseasca tipuri specifice de date. Astfel, fiecare implementare alege acele date care sunt impuse de un anumit sistem.
De exemplu, daca se doreste gasirea identificatorului de proces, se aloca o variabila de tip pid_t pentru a memora valoarea returnata de functia getpid. Definitia acestui tip poate sa difere de la o implementare la alt, dar ea se reduce la inlocuirea unui singur fisier (care se include) si aplicatia poate fi recompilata.
Timpul in UNIX
Exista doua timpuri diferite pastrate de sistemul UNIX. Primul este timpul calendar. Acesta valoare precizeaza numarul de secunde scurse de la 1 ianuarie 1970, ora 00:00:00 (Timpul Universal). Acest timp serveste la stabilirea timpului ultimei modificari efectuate asupra unui fisier. Al doilea este timpul de proces. Acestea este numit timp CPU si masoara timpul cit procesul foloseste resursa CPU. Timpul de proces este masurat in tacti (de regula 100 tacti pe secunda). Acest timp este pastrat intr-o variabila de tip clock_t, spre deosebire de primul care este pastrat intr-o variabila de tipul time_t. Standardul POSIX defineste constanta CLK_TCK pentru a specifica numarul de tacti pe secunda.
La masurarea timpului de executie a unui proces, UNIX-ul pastreaza trei valori:
- timpul real consumat;
- timpul CPU consumat in mod utilizator;
- timpul CPU consumat in mod nucleu.
Timpul real consumat este timpul cit procesul se executa. Acesta depinde de incarcarea sistemului, adica de numarul proceselor care se executa in sistem.
Timpul CPU consumat in mod utilizator este timpul scurs pentru executia instructiunilor utilizatorului.
Timpul CPU consumat in mod nucleu este timpul scurs pentru executia functiilor de sistem aferente procesului (de exemplu, timpul scurs pentru rezolvarea unui apel read de catre nucleu).
Pentru a masura acesti timpi se poate executa executa comanda time, care va avea ca argument comanda pentru care se doreste a fi efectuata masuratoarea. De exemplu, pentru a masura timpul scurs intr-o operatie de cautare a secventei "_POSIX_" in toate fisierele dintr-un director se poate folosi:
$cd /usr/include
$time grep _POSIX_ */*.h > dev/null
Iesirea acestui program depinde de interpretorul folosit.
Semnale
Semnalul este un mecanism de sincronizare important in sistem prin care un proces este informat asupra aparitiei unui eveniment. Semnalul poate proveni de la nucleu, de la un alt proces, chiar de la acelasi proces si de la utilizator 333g65d . Un exemplu de semnal provenit de la nucleu este semnalul SIGSEGV transmis procesului care incearca un acces la memorie in afara propriului spatiu de adrese. Un proces poate trimite la alt proces un semnal de terminare (SIGTERM) generat la apasarea combinatiei de taste Ctrl-C. Exista o gama variata de semnale, numarul lor depinzind de versiunea de UNIX folosita. De regula numarul lor este intre 12 si 32. La primirea unui semnal, un proces are trei alternative:
a. sa ignore semnalul, lucru nerecomandat in cazul semnalelor ce indica
erori hardware (de exemplu impartirea cu 0);
b. sa execute actiunea implicita atasata semnalului (de exemplu pentru
impartirea cu 0 actiunea implicita este terminarea);
c. sa execute o rutina (functie) de tratare a semnalului.
Nu se poate opta pentru oricare dintre alternative pentru orice semnal.
Comunicarea intre procese
Comunicarea intre procese (IPC=Inter Proces Comunication) se poate realiza in diverse moduri: semnale, fisiere, pipe, pipe cu nume, semafoare, mesaje, memorie partajata, socket si stream. Tipurile de comunicatie folosite actualmente mai des si permise de diverse versiuni de UNIX sunt ilustrate in tabelul 1. Primele cinci metode sunt folosite de procese aflate pe acelasi host, iar ultimele doua metode sunt folosite de procese aflate si pe host-uri diferite.
Tipuri de comunicare |
V7 |
SVR |
2SVR3.2 |
SVR4 |
4.3BSD |
4.3+BSD |
pipe | ||||||
FIFO | ||||||
cozi de mesaje | ||||||
semafoare | ||||||
memorie partajata | ||||||
socket | ||||||
stream-uri |
Tabelul 1 - metode de comunicare folosite in UNIX
Semnalele sunt folosite de procese pentru a informa alte procese. Din pacate semnalele nu contin suficienta informatie, deseori necesara in aplicatii. Un semnal este un eveniment asincron, care intrerupe procesul care-l receptioneaza, programarea in aceste cazuri fiind mai complexa. Semnalele sunt, de regula, folosite pentru terminarea unui proces.
Prin trasarea procesului, un proces parinte poate controla executia unui proces fiu. Doarece procesul tata poate citi si scrie datele procesului fiu, cele doua procese pot comunica liber. Trasarea proceselor este folosita numai in depanatoare, doearece este complicata si nesigura pentru uz general. In general se prefera comunicarea prin pipe intre doua procese in relatia parinte fiu.
Comunicarea printr-un fisier este o metoda simpla prin care procesele pot schimba date. De exemplu, un proces ce executa editorul ed poate scrie intr-un fisier, iar alt proces ce executa utilitarul nroff poate sa-l formateze. Folosirea fisierelor pentru comunicare are in principal doua dezavantaje:
a. daca cele doua procese lucreaza concurent, nu exista
sincronizare corecta intre procese (procesul cititor poate
conchide din detectarea unui sfirsit de fisier gresit,
terminarea comunicarii);
b. daca comunicarea este de durata, dimensiunea fisierului creste
considerabil.
Folosirea unui fisier gol pe post de semafor este o tehnica des folosita
in UNIX.
Pipe rezolva problema de sincronizare a fisierelor. Cu toate ca are un i-node atasat, nu exista referinta la el. Citirea si scrierea unui pipe este asemanatoare cu citirea si scrierea unui fisier, dar exista diferente majore. Dimensiunea maxima a unui pipe este fixa, valoarea pe care nu se poate scrie, iar dintr-un pipe gol nu se poate citi. Un octet citit nu se poate reciti, deci datele vehiculate prin pipe nu sunt pastrate. Pipe poate fi folosit si in linia de comanda (de exemplu ls | more). Comunicarea prin pipe are trei dezavantaje majore:
a. procesele care comunica trebuie sa fie in relatia parinte fiu sau cu
stramos comun (inconvenient major cind, spre exemplu, unul dintre
procese este gestionarul unei baze de date, iar celalalt o aplicatie
care gestioneaza baza);
b. versiunile mai vechi nu ofera citire si scriere atomica pentru pipe cu
mai multe procese ce scriu si citesc (acelasi proces gestionar al
bazei de date si mai multi clienti care lanseaza cereri);
c. operatii lente, cu toate ca nu se fac operatii de I/E, copierea user-
kernel si invers pot fi critice pentru unele aplicatii.
Pipe cu nume, numite si FIFO au aparut odata cu versiunea III pentru a elimina primele doua neajunsuri. Un fisier FIFO se comporta ca un fisier pipe si orice proces, daca are drepturi poate sa-l acceseze. Atomicitatea operatiilor este asigurata. Singurul dezavantaj ramine viteza, mult prea lenta pentru unele aplicatii.
Semaforul este un fanion care previne doua sau mai multe procese sa acceseze simultan aceasi resursa.
Mesajul este un pachet mic de date, ce poate fi trimis intr-o coada de masaje. Mesajele pot fi de diferite tipuri. Orice proces, daca are drepturile necesare, poate primi un mesaj din coada. Mesajul poate fi ales: fie primul, fie primul de un anumit tip, fie primul dintr-un anumit grup de tipuri.
Memoria partajata este mijlocul de comunicatie cel mai rapid. Principiul este simplu: acelasi spatiu de memorie este mapat in spatiul de adrese a doua sau mai multe procese. Cit de repede datele sunt scrise in memoria partajata, atit de repede pot fi disponibile procesului cititor. Un semafor sau mesaj este utilizat pentru a sincroniza procesul care scrie in raport cu cel care citeste.
Gestionarea erorilor
La aparitia unei erori la apelul unei functii UNIX se returneaza deseori valoarea (-1) si varianbila globala errno ofera o valoare ce contine informatii suplimentare despre eroare. Aceasta nu este insa o conventie generala pentru ca exista functii care intorc in caz de eroare un pointer NULL. Fisierul antet errno.h, ce se include in fisierele din directiva #include, defineste variabila errno si valorile constantelor simbolice pe care aceasta variabila le poate lua. De exemplu functia open, in cazul aparitiei unei erori la deschiderea unui fisier asupra caruia nu exista dreptul de scriere, atribuie variabilei errno valoarea EACCES. Valorile de eroare posibile sunt descrise in manuale sistem. Tabelul 2 prezinta diferentele dintre functii.
Functii |
strerror(errno) |
Terminare cu |
err_sys |
da |
exit(1); |
err_ret |
da |
return; |
err_quit |
nu |
exit(1); |
err_msg |
nu |
return; |
err_dump |
da |
abort(); |
tabelul 2
Standardul POSIX defineste variabila errno ca:
extern int errno;
Valoarea variabilei errno nu este stearsa de apelul unei functii fara eroare. Daca insa valoarea variabilei nu este testata in cazul unei erori, apelul urmator al unei functii poate suprascrie valoarea variabilei. Valoarea 0 nu poate fi atribuita variabilei errno si nici o constanta simbolica nu are aceasta valoare.
Standardul C defineste doua functii utile pentru afisarea mesajelor de eroare. Prima este strerror cu interfata:
#include <string.h>
char *strerror( int nrerr);
care intoarce pointerul la mesajul de eroare.
Aceasta functie realizeaza corespondenta intre nrerr (este de regula valoarea variabilei errno) si mesajul de eroare aferent erorii. A doua functie este perror si are interfata:
#include <stdio.h>
void perror( const char *msg);
Functia afiseaza la iesirea standard de erori mesajul msg urmat de caracterul ':', un caracter vid si mesajul aferent valorii din variabila errno.Functia revine fara a intoarce ceva util. Un exemplu de utilizare a acestei functii este programul:
Ex 1:
A se compila cu:cc p1.c err.o
Program de test a functiilor de eroare
#include <erno.h>
include "hdr.h"
int main(int argc,char *argv[])
Functia perror primeste ca argument numele programului(argv[0]).
Acest lucru este util pentru cazurile in care programul face parte
dintr-un lant de pipe-uri, ca in:
p1 < input | p2 | p3 > output
deoarece se poate preciza exact programul care a generat mesajul de eroare.
Rezultatul executiei acestui program este:
$a.out
EACCES: Permission denied
a.out: No such file or directory
Eroare read: Bad file number
Pentru a nu apela aceste functii direct si din ratiuni de spatiu(pentru a nu extinde fiecare exemplu cu liniile de tratare a erorilor) toate exemplele vor include la compilare fisierul err.c sau direct fisierul obiect err.o In acest mod fiecare caz de eroare este tratat de un singur apel al unei functii de tratare a erorii. Exemplul anterior foloseste functia err_sys din programul err.c.
Toate functiile de tratare a erorilor din programul err.c folosesc functii cu lista de argumente de lungime variabila din ANSI C(Kern[88]). Aceasta facilitate, oferita de ANSI C, difera de varargs oferita de versiunile mai vechi de sistem ca SVR3 si 4.3BSD. Numele de macro sunt identice dar s-au schimbat argumentele de apel.
Conceptul de proces
Procesul este un program in executie. Ca urmere, procesul este dinamic,pe cind programul este static. Pentru un program dat (fisier executabil) pot exista la un moment dat unul sau mai multe procese asociate, numite instante.
Orice proces UNIX, cu exceptia procesului 0, este creat prin apelul functiei de sistem fork. Procesul nou creat se numeste proces fiu, iar procesul care a apelat functia fork se numeste proces parinte.
Programul executabil este incarcat in memorie prin apelul functiei exec. Chiar si atunci cind fiind intr-o sesiune de lucru, se lanseaza o comanda (care este de fapt un program executabil), shell-ul foloseste functiile de sistem fork si exec pentru executia comenzii date. Lucrurile se petrec pe scurt in felul urmator:
La executia unei comenzi de catre shell aceasta citeste linia de
comanda si desparte comanda de argumentele sale. Se apeleaza
functia sustem fork in urma careia rezulta cele doua procese:
parintele si fiul. Procesul parinte printr-un apel al functiei
de sistem wait cedeaza procesorul procesului fiu. Acesta la
randul sau printr-un apel al functiei de sistem exec executa un
nou program, care este chiar comanda data. Nucleul incarca in
zona de memorie a shell-ului noul program si procesul este
continuat cu acest program, caruia ii sunt transmise
argumentele. In cadrul procesului nu se schimba decit programul,
restul ramane nemodificat. La incheierea executiei programului
se apeleaza functia sistem exit, care cauzeaza terminarea
procesului fiu si iesirea din starea de asteptare a procesului
parinte.
Executia unui program utilizator se poate face in doua moduri:in modul utilizator si in modul nucleu (sau sistem) asociate modului de functionare ale procesului.
In modul utilizator procesele au acces numai la propria zona de cod, date si stiva utilizator.
In modul nucleu un proces contine instructiuni privilegiate si poate avea acces la structurile de date ale nucleului. Nucleul, in termeni de proces, nu este un proces separat care se executa in paralel cu procesul utilizator, ci devine o parte integranta a procesului utilizator.
Spatiul virtual de adrese al unui proces este cuprins intre 0 si adresa virtuala maxima accesibila procesului. Acest spatiu este alcatuit din trei segmente:
- segmentul text (cod)
Contine codul. Daca codul este pur, atunci poate fi partajat de
alte procese. Este protejat la scriere.
- segmentul de date
Contine datele alocate static sau dinamic de catre proces.
- segmentul de stiva
Contine argumente, variabile locale si alte date pentru
executia functiilor in modul utilizator.
Un fisier executabil contine antetul, segmentul text si date (numai cele initializate). Deoarece un proces se poate executa in modul utilizator si nucleu, i se mai asociaza si o stiva nucleu, care va contine informatii necesare pentru functiile sistem. Un proces trece din modul utilizator in cel nucleu cand se apeleaza o functie de sistem sau cand este generata o intrerupere.Segmentul de date si stivele utilizator si nucleu sunt dinamice, adica dimensiunea lor variaza in timpul executiei.
Asupra segmentului de date, utilizatorul poate interveni prin urmatoarele functii sistem:
Functia malloc
Aloca o zona de memorie de dimensiune ceruta. Intefata sa este:
#include <stdlib.h>
void *malloc( dimensiune);
Intoarce un pointer diferit de 0 in caz de succes, NULL in caz de eroare.In caz de succes, functia returneaza pointerul la inceputul zonei alocate. In caz de eroare functia returneaza pointerul NULL, iar variabila errno contine codul erorii.
Exemplu de utilizare:
void *ptr;
if ((ptr = malloc (10000)) == NULL)
Functia calloc
Aloca o zona de memorie contigua de dimensiune nr-element*dim_element
(in octeti) si o initializeaza cu caracterul NULL. Interfata sa este:
#include <stdlib.h>
void *calloc( size_t nr_elem, size_t dim_element);
Intoarce un pointer diferit de 0 in caz de succes, NULL in caz de eroare.Functia returneaza pointerul in zona alocata. In caz de eroare variabila erno va contine codul erorii.
Exemplu de utilizre:
struct s;
void *ptr;
if ((ptr = calloc( 10, sizeof(s))) == NULL)
Functia realloc
Redimensioneaza o zona de memorie obtinuta prin functia maloc,
calloc sau realloc la new_dim. Functia are interfata:
#include <stdlib.h>
void *realloc( void *ptr, size_t new_dim);
Intoarce un pointer diferit de zero in caz de succes, NULL in caz de eroare.
Functia returneaza in caz de succes pointerul la zona alocata, care poate fi cel initial sau nu (in cazul alocarii la o alta adresa). In caz de eroare, variabila errno contine codul de eroare.
Exemplu de utilizare:
void *ptr;
/* alocare zona de memorie */
if ((ptr = malloc (10000)) == NULL)
/* crearea zonei de memorie alocate mai sus */
if ((ptr = realloc( ptr, 200000) == NULL)
Functia free
elibereaza o zona de memorie alocata prin malloc, calloc sau realloc.
Interfata functiei este urmatoarea:
include ,stdlib.h>
free( void *ptr)
Revenirea din functie se considera ca o tratare corecta. Dar daca ptr
era eronat, rezultatul este imprevizibil.
Exemplu de utilizare:
void *ptr;
/* alocarea unei zone de memorie */
if ((ptr = malloc (10000)) == NULL)
/* eliberarea zonei alocate */
free( ptr);
Se mentioneaza faptul ca la segmentul de date poate fi atasata si o zona de memorie comuna, lucru prezentat in cadrul comunicarii intre procese.
Cand un proces invoca un program, el primeste ca parametri argumentele liniei de comanda si mediul program. Recuperarea de catre program a parametrilor de apel (argumente si mediu) se face declarand programul principal astfel:
main( int argc, char *argv[], char *envp[])
Numarul elementelor tabloului de adrese referit de argv este argc. Adresele parametrilor de apel sunt in tabloul referit de argv. Adresele variabilelor de mediu sunt in tabloul referit de envp. Afisarea parametrilor de apel si a mediului se poate face astfel:
/* afisarea argumentelor de apel */
for ( k=0; k<argc; ++k) ;
/*afisarea variabilelor de mediu*/
for ( k=0; ; ++k)
else break;
};
Variabilele de mediu sunt citeva siruri de caractere de forma:
NUME = valoare
Unele dintre cele mai uzuale valori care se gasesc in mediul transmis
unui program sunt:
HOME - numele de cale al directorului de referinta;
PATH - lista directoarelor in care sunt cautate comenzile;
MAIL - numele fisierului care contine mesajele primite prin mail;
TERM - tipul terminalului;
SHELL - numele de cale al fisierului binar ce contine shell-ul;
LOGNAME - numele sub care este cunoscut utilizatorul in sistem.
Observatii:
a. Mediul program curent poate fi afisat prin comanda env.
b. In programe, variabilele mediului pot fi recuperate nu numai prin argumentul envp mai sus prezentat, ci si prin variabila externa environ, care este un pointer la un tablou de pointeri catre sirurile de caractere care semnifica variabilele mediului.
Exemplu:
extern char **environ;
main()
else
break;
-
-
-
}
Recuperarea variabilelor de mediu prin envp si environ sunt echivalente.
c. Recuperarea valorii unei variabile de mediu se poate face cu functia getenv care are interfata:
#include <stdlib.h>
char *getenv (const char *nume);
Intoarce un pointer la valoarea variabilei asociate cu nume, NULL in cazul absentei variabilei.
Exemplu:
-
-
-
char *terminal;
if (( terminal = getenv("TERM")) == NULL)
printf ("Terminalul este: s\n", terminal);
-
-
-
Identificatori. Identificatorul utilizatorului
Identificatorul utilizatorului, pe scurt ID utilizator, din cimpul al treilea al unei intrari din fisierul /etc/passwd este un numar intreg pozitiv prin care sistemul identifica utilizatorul. Acest numar este atribuit utilizatorului de catre administratorul de sistem, la introducerea unui nou utilizator in sistem. Acest ID este, de regula, unic si nu poate fi modificat.
Utilizatorul cu ID egal cu 0 este numit superuser sau root. Intrarea in fisierul de parole corespunzatoare superuser-ului are pe primul cimp numele root.Superuser-ul are control total asupra sistemului (din acest motiv se spune ca superuser-ul are drepturi speciale sau privilegiate). Utilizarea anumitor functii este permisa doar superuser-ului.
La intrarea in sistem a unui utilizator, programul login fixeaza acest ID ca ID utilizator primului proces creat, shell-ul. Toate procesele descendente din acest shell vor mosteni acest ID utilizator. ID-ul utilizatorului este numit si ID-ul utilizatorului real.
Identificatorul grupului
Asemeni ID-ului utilizatorului, ID-ul grupului este atribuit de catre administratorul de sistem, la introducerea unui nou utilizator in sistem. Grupul serveste la reunirea acelor utilizatori care folosesc partajat anumite resurse (de exemplu fisiere). De exemplu un fisier poate avea drepturile de acces astfel fixate incit doar membrii grupului sa poata avea acces la el. Utilizatorul lucreaza de regula cu numele sau de login si cu numele grupului. Sistemul realizeaza corespondenta de la nume la ID prin fisierul /etc/passwd pentru utilizator, respectiv prin fisierul /etc/group pentru grup. De exemplu comanda:
ls -l
afiseaza numele proprietarului unui fisier folosind fisierul de parole.
ID de grup al utilizatorului care a deschis sesiunea de lucru va fi fixat ca ID de grup pentru procesul shell. Acesta va fi mostenit de toate procesele descendente din shell. ID-ul grupului este numit si ID-ul grupului real.
Identificatori suplimentari de grup
Incepind cu versiunea 4.2BSD un utilizator poate adera la mai multe grupuri (maxim 16). ID-ul grupurilor suplimentare se obtin din fisierul /etc/group, de unde se identifica primele 16 intrari care au utilizatorul ca membru.
Pentru procese avem :
a. Identificatorul de proces
Este un numar intreg mai mare sau egal cu 0. El este in strinsa corelatie cu intrarea in tabela proceselor. Procesul cu identificatorul 0 este procesul swapper, iar procesul cu identificatorul 1 este procesul init. Intrucit cuvintul identificator apare frecvent, in continuare acesta va fi prescurtat la ID.
b. ID de proces al parintelui
Acesta este ID-ul procesului care a lansat functia fork.
c. ID grupului de procese (a nu se confunda cu ID-ul grupului de utilizatori)
In UNIX se pot grupa procese legate intre ele. De exemplu, un sistem de gestiune a unei baze de date, poate fi impartit in mai multe procese pentru a obtine o concurenta in efectuarea operatiilor de I/E. Unul din membrii grupului de procese este liderul grupului. Toate procesele din grupul respectiv vor avea ca ID al grupului de procese valoarea ID al procesului lider. Deci, daca un proces are ID egal cu cel al grupului de procese, atunci acel proces va fi lider.Oricare proces poate iesi din grupul respectiv si sa devina lider, formindu-si un nou grup.
d. Terminalul de control
Este primul terminal deschis de liderul grupului de procese. Normal,terminalul de control pentru procesele unui utilizator este terminalul de la care a fost lansata comanda login. La formarea unui nou grup, procesele membre ale noului grup nu mai au un terminal de control. Cind procesul lider de grup s-a terminat, toate procesele cu acelasi terminal de control primesc semnalul "hungup".
e. ID utilizatorului real
f. ID grupului real de utilizatori
g. ID utilizatorului efectiv
Acest atribut serveste la determinarea drepturilor de acces la fisiere. Spre deosebire de acesta, ID-ul utilizatorului real serveste la autentificarea utilizatorului si la comunicatia intre utilizatori. ID-ul utilizatorului efectiv coincide de cele mai multe ori cu ID-ul utilizatorului real, dar citeodata procesul, sau unul din stramosii sai, a setat bitul de setare identificator utilizator. Denumirea acestui bit in engleza este "set user-ID".
h. ID grupului efectiv de utilizatori
Daca bitul 5 din fig 2 este pozitionat, atunci tot ce s-a spus la
punctul g, ramine valabil si aici. Denumirea acestui bit in engleza este
"set grup-ID".
r |
w |
x |
r |
w |
x |
r |
w |
x |
fig 2
biti : 0-3 tipul fisierului
4 bit setare identificator fisier
5 bit setare identificator grup
6 bit setare swapping (sticky bit)
7-9 proprietar
10-12 grup
13-15 restul utilizatorilor
Pozitionarea bitului 4 si 5 se poate face prin apelurile de sistem setuid
si setgid. Asupra lor se va reveni.
i. Dimensiunea limita a fis
ierelor
Atributul acesta exprima dimensiunea maxima a unui fisier creat prin write. Apelul de sistem ulimit poate returna limita unui fisier sau poate sa o modifice.
j. Dimensiunea limita a segmentului de date
Atributul exprima dimensiunea maxima a segmentului de date al unui proces.
k. Valoarea nice
Aceasta valoare este folosita in calculul prioritatii unui proces. Aceasta valoare este de obicei intre 0 si 39. Cu cit valoarea sa este mai mare, cu atit prioritatea procesului este mai scazuta. Numai procesele superuser pot da valori negative. Valoarea se poate fixa cu functia de sistem nice.
De la crearea unui proces pina la terminarea sa, el trece prin mai multe stari gestionate de nucleu (vezi curs).
Starile unui proces Unix
Cu exceptia procesului 0, toate celelalte procese sunt create prin apelul functiei de sistem fork. Starea "creat" este o stare de pornire.
In urma apelului functiei fork, segmentele parintelui sunt practic dublate, fiind nevoie in acest sens de memorie interna. Daca exista spatiu de memorie interna suficienta, procesul va trece in starea "ready" (gata de executie). In caz contrar va fi trecut in starea "ready suspendat", adica se va gasi in memoria externa (pe dispozitivul swapping). Din starea "ready", planificatorul de procese il va trece in starea "run mod nucleu", unde activitatea functiei fork se va incheia. Trecerea procesului in aceasta stare se va face pe baza de prioritate. Prioritatile sunt dinamice si se calculeaza din secunda in secunda astfel:
prioritate=baza+(utilizare_recenta_CPU)/constanta+valoare_nice
Se mentioneaza faptul ca rutina ceasului, inafara calculului prioritatii,
mai are si alte functii:
- activeaza periodic procesul swapper,
- transmite semnalul SIGALRM catre procese;
- masoara timpul sistem;
- reprogrameaza ceasul de timp real al sistemului;
- lanseaza anumite functii ale nucleului in functie de valoarea unor
timeri interni;
- calculul unor valori statistice pentru procese etc.
Planificatorul de procese procedeaza conform urmatorului algoritm:
algoritm schedule_proces;
input:-
output:-
Sterge procesul selectat din coada ready;
Schimbare de context pentru procesul selectat;
}
Cind functia de sistem apelata se termina (initial fork), procesorul va fi trecut in starea "run mod utilizator". Din aceasta stare poate reveni in starea "run mod nucleu" daca apare o intrerupere sau procesul apeleaza o functie de sistem.
Cind ceasul emite o interupere catre procesor, procesul care se executa in mod utilizator este trecut in starea "run mod nucleu". Cind handler-ul de ceas (rutina de tratare a intreruperii de ceas) isi termina activitatea, nucleul poate decide sa fie trecut in starea de executie alt proces. In acest caz procesul analizat va fi trecut in starea "ready preemptat". Deci in starea "ready preemptat" ajung procesele care sunt executate in mod nucleu si trebuie sa revina in mod utilizator, dar este planificat pentru executie alt proces mai prioritar.Un proces din starea preemptat poate fi swappat. Cind un proces din starea "ready preemptat" este planificat pentru executie, el este trecut direct in starea "run mod utilizator".
Tema :
- realizati exemple de programe cu limbajul de comanda al shellului
- afisati variabilele de mediu cu program
- afisati diversi identificatori in procese tata si fiu
S.O. Laborator 6
Procese Unix
Introducere. Identificatorul de proces.
Controlul proceselor in Unix include :
-crearea proceselor;
-invocarea programelor;
-punerea in asteptare a unui proces in vederea terminarii unui proces fiu;
-terminarea proceselor;
Se reaminteste faptul ca fiecare proces are un identificator de proces unic, un numar intreg pozitiv. Acesta este denumit pe scurt pid (Process IDentifier).
Intr-un sistem Unix exista citeva procese speciale :
-Procesul cu pid 0 este planificatorul de procese, numit swapper. Acest proces
face parte din nucleu si este considerat un proces sistem.
-Procesul cu pid 1 este procesul init, invocat de nucleu la sfirsitul procedurii de
incarcare a sistemului. Fisierul program pentru acest proces era /etc/init in
versiunile vechi si /sbin/init in versiunile noi. Procesul init citeste fisierele de
initializare dependente de sistem (fisierele de resurse /etc/rc*) si aduce sistemul
intr-o stare stabila (de exemplu multiuser). Procesul init este un proces utilizator
(spre deosebire de swapper), dar se executa cu drepturi de superuser. Procesul
init nu dispare niciodata.
-In anumite implementari de Unix cu memorie virtuala, procesul cu pid 2 este
pagedaemon. Acest proces este un proces sistem si realizeaza paginarea in
sisteme cu memorie virtuala.
Crearea unui proces
Crearea unui proces se realizeaza prin apelul functiei de sistem fork. Procesul care apeleaza functia fork se numeste proces parinte, iar procesul nou creat se numeste proces fiu.
Interfata functiei fork este urmatoarea :
#include <sys/types.h>
#include <unistd.h>
pid_t fork (void);
(returneaza 0 in procesul fiu, pid fiu in procesul parinte;(-1)in caz
de eroare)
In versiunile mai vechi tipul functiei fork era int. Functia fork
returneaza:
-pid fiu in procesul parinte si 0 in procesul fiu, in caz de succes;
-(-1) in caz de eroare, iar errno indica eroarea aparuta. Cazul de eroare
poate fi cauzat de:
a.exista deja prea multe procese in sistem;
b.numarul total de procese pentru acel ID utilizator depaseste
limita stabilita de sistem.
Un proces poate crea mai multi fii. Deoarece nu exista nici o functie care sa permita a determina pid-ul proceselor fiu, functia fork returneaza procesului parinte pid-ul procesului fiu. Functia fork returneaza 0 procesului fiu deoarece un proces poate avea doar un singur proces parinte, care se poate afla prin apelul functiei getppid() (pid 0 este folosit de procesul swapper,deci nu poate fi pid-ul unui proces fiu!). Procesul parinte si procesul fiu devin doua procese care se executa in mod concurent, incepind cu prima instructiune dupa apelul fork. Exemplul cel mai simplu este :
Ex 1 :
A se compila cu: cc e1.c -oe1
*/
#include <stdio.h>
#include <sys/types.h>
int main (void)
Rezultatul executiei acestui program este :
$e1
Test de apel fork : 226
Test de apel fork : 0
Procesul fiu este o copie a procesului parinte. Cu toate acestea cele doua procese nu partajeaza memoria aferenta zonelor de date, heap si stiva (eventual, zona text). Multe din implementarile actuale nu realizeaza o copie completa a zonelor de date heap si stiva din spatiul procesului parinte in spatiul procesului fiu, deoarece deseori apelul fork este urmat de un apel exec. Se foloseste o tehnica numita copy-on-write (COW). Aceste regiuni sint partajate de cele doua procese si sint protejate de nucleu. Aceste regiuni pot fi numai citite. La terminare, procesul fiu transmite procesului parinte semnalul SIGCLD. Procesul parinte poate sa astepte terminarea procesului fiu apelind functia de sistem wait sau waitpid si sa analizeze modul cum acesta s-a terminat. Aceste functii vor fi prezentate ulterior.
Algoritmul functiei fork este urmatorul :
algoritm fork;
input: -
output: pid fiu in procesul parinte;
0 in procesul fiu;
(-1) in caz de eroare;
else/* se executa proces fiu */
Din descrierea algoritmului rezulta ca la crearea unui proces fiu au loc
urmatoarele actiuni importante:
a) Nucleul verifica daca exista resurse de memorie interna si externa suficiente
pentru crearea procesului fiu (memorie pentru segmentele de date, heap, stiva
zona u, tabele de pagini etc.). In caz contrar functia fork returneaza eroare.
b) Prin parcurgerea tabelei proceselor, incepind cu urmatoarea intare dupa ultima
acordata, se gaseste o intrare libera care este asignata procesului fiu. Astfel se
obtine si pid-ul procesului fiu. Ultima intrare din tabela proceselor nu poate fi
asignata decit pentru un proces al superuserului.
c) Se verifica numarul proceselor fiu ale parintelui. Daca acest numar nu este mai
mare decit o valoare impusa procesul fiu este marcat ca fiind creat. Altfel functia
de sistem fork reurneaza eroare.
d) Se completeaza intrarea fiului in tabela proceselor cu anumite informatii luate din
cea a parintelui: ID-ul utilizatorului real al utilizatorului parinte si ID-ul
utilizatorului efectiv, ID-ul utilizatorului efectiv, ID-ul grupului de procese si
valoarea nice a parintelui (folosita pentru calculul prioritatii). Nucleul
completeaza apoi intrarea procesului fiu cu alte informatii: ID-ul procesului
parinte, prioritatea intiala, timpul de utilizare CPU etc.
e) Nucleul incrementeaza cu 1 numarul de referiri din tabela i-node-urilor pentru
directorul curent (care este cel al parintelui) si radacina, daca a fost schimbata
cu functia chroot.
f) Procesul fiu mosteneste fisierele deschise de procesul parinte pina in momentul
apelului functiei fork. Ca urmare, vor fi incrementate cu 1 nr. de referiri din tabela
fisierelor si a i-node-urilor pentru fisierele deschise de parinte. Procesul parinte
si procesul fiu partajeaza fisiewreele deschise de parinte in momentului apelului
fork, intrucit intrarea in tabela fisierelor este comuna si folosesc acelasi
deplasament.
g) Nucleul creaza contextul procesului fiu prin duplicarea in memorie a zonei u, a
segmentului de date si stiva utilizator si partajarea segmentului text ce apartine
procesului parinte
h) Nucleul creaza stiva nucleu pentru procesul fiu, completind numaratorul de
instructiuni si celelalte registre salvate pe stiva, astfel ca procesul fiu sa poata fi
reluat din acest punct. Astfel procesul fiu ajunge in starea "ready " si conform
planificarii va obtine procesorul.
i) In procesul fiu functia fork returneaza 0, iar in procesul parinte returneaza
identificatorul de proces al fiului.
Din reprezentarea algoritmului rezulta ca procesul parinte si procesul fiu sunt diferite, avind identificatori diferiti, dar au multe atribute comune (a se vedea mai jos). Prin testarea valorii returnate de fork, se poate partitiona codul programului in cele doua procese, fiecare proces executind codul corespunzator lui:
switch(fork())
Programul urmator prezinta modul de utilizare a functiei fork:
Ex 2 :
A se compila cu : cc e2.c err.o -oe2
#include <sys/types.h>
int gvar = 4; /* variabila externa */
int main(void)
else
sleep(2);
printf("Proces (pid)=%d, gvar=%d var=%d\n",
getpid(), gvar, var);
exit(0);
In functie de algoritmul de planificare folosit de nucleu, unul dintre procese se va executa primul. In exemplul prezentat s-a intirziat cu 2 secunde procesul parinte pentru pentru a permite executia fiului prima data. Nu exista nici o garantie ca aceasta intirziere este suficienta. Doua executii ale programului au afisat liniile:
$e2
Inainte de fork
Proces (pid)=184, gvar=5, var=9
Proces (pid)=183, gvar=4, var=7
$e2
Inainte de fork
Proces (pid)=188, gvar=5, var=9
Proces (pid)=187, gvar=4, var=7
Diferenta intre rezultate este cauzata de interactiunea dintre functia fork si functiile de I/E. Functiile standard de I/E lucreaza cu tampon. Iesirea standard se face prin tampon linie daca ea este conectata la un terminal, altfel printr-un singur tampon. La executia simpla a programului acesta afiseaza o singura data argumentul functiei printf, deoarece tamponul de iesire este golit de caracterul '\n'. Daca insa iesirea este redirectata in fisierul e2.buf argumentul functiei printf ramine in tampon, fiecare proces adaugind linia proprie tamponului propriu. La terminarea proceselor continutul copiilor tamponului este golit.
Rezultatul valideaza si faptul ca redirectarea iesirii procesului parinte este mostenita de procesul fiu. Toti descriptorii deschisi in procesul parinte sint duplicati in procesul fiu. Cele doua procese partajeaza aceleasi intrari in tabela proceselor. Procesele partajeaza acelasi deplasament (cimpul offset) in fisier. Daca, spre exemplu, ambele procese scriu in iesirea standard, deplasamentul este mereu actualizat de procesul care realizeaza scrierea.
Gestiunea descriptorilor de fisier dupa un apel fork se poate face in doua moduri:
a: Procesul parinte asteapta terminarea procesului fiu. In acest caz
parintele nu trebuie sa faca nimic cu descriptorii. Cind fiul se termina,
toti descriptorii fisierelor de la care el a citit sau in care el a scris au
deplasamentul actualizat.
b: Fiecare proces continua executia independent de celalalt. Se impune
in acest caz ca fiecare proces sa inchida descriptorii nefolositi.
Pe linga fisierele deschise, procesul fiu mai mosteneste de la parinte:
- ID-ul utiliuzatorului real, ID-ul grupului real, ID-ul utilizatorului
efectiv si ID-ul grupului efectiv;
- ID-i suplimentari de grup;
- ID-ul de sesiune;
- terminal de control;
- valorile fanioanelor ID utilizator setat si ID grup setat;
- directorul curent;
- directorul radacina;
- masca de semnale si actiunile atasate semnalelor;
- masca de creare fisiere (prin functia umask);
- mediul;
- indicatorul close-on-exec pentru descriptorii de fisier deschisi;
- segmente de memorie partajate;
- limite de resurse.
Diferentele intre parinte si fiu se reduc la:
* valoarea returnata de fork;
* pid si pid-ul proceselor parinte;
* pentru fiu valorile cimpurilor tms_utime, tms_stime, tms_ustime
sint puse pe 0;
* alarmele nerezolvate sint sterse pentru fiu;
* zavoarele pe fisiere puse de parinte nu sint mostenite de fiu;
* setul de semnale nerezolvate pentru fiu este sters;
Utilizarea functiei fork:
1. Cind procesul se duplica pentru ca procesele parinte si fiu sa
execute parti de cod distincte.
Acesta situatie apare la server-ele de retea - procesul parinte
asteapta o cerere de la un client. La sosirea cererii,procesul
parinte executa fork si lasa in seama procesului fiu rezolvarea ei.
Parintele revine in asteptarea unei noi cereri.
2. Cind un proces executa un alt program.
Situatia apare la shell, cind procesul fiu apeleaza functia exec.
In acest ultim context, versiunile SVR4 si 4.3+BSD permit apelul functiei vfork. Aceasta functie, deoarece nu copiaza complet spatiul de adrese al procesului parinte (nu e nevoie ca executa exec sau exit), optimizeaza implementarea. O alta diferenta intre aceste doua functii este aceea ca vfork garanteaza ca procesul fiu se executa primul. Daca in exemplu e2.c se inlocuieste fork cu vfork, exit (din fiu) cu _exit si se elimina apelul sleep rezultatul este:
$e2
Inainte de fork
Proces (pid)=188, gvar=5 var=9
Apelul _exit nu goleste tampoanele de I/E.
Terminarea unui proces (functia de sistem exit)
Terminarea normala a unui proces de catre el insusi se realizeaza prin apelul functiei de sistem exit sau _exit. Prima este definita de ANSI C.Deoarece standardul nu trateaza descriptorii de fisier, procesele multiple si controlul programelor, definitia aceste functii este incompleta pentru Unix.Functia _exit, apelata de exit si definita de POSIX, trateaza detaliile specifice Unix-ului.
Interfata celor doua functii de sistem este:
void exit(int cod_exit);
void _exit(int cod_exit);
unde:
cod_exit - Este o valoare intreaga ce se returneazza parintelui in
vederea analizei.
Un proces se poate termina anormal prin apelul functiei abort sau la
primirea unui semnal.
In urma executiei functiei de sistem exit, procesul respectiv intra in
starea terminat.
Daca apelul functiei de sistem exit lipseste din program, apelul este totusi executat implicit cind se revine din functia main.
Functia _exit este apelata intern de catre nucleu pentru terminarea unui proces cind receptioneaza semnale ce nu sint tratate. In acest caz, _exit returneaza un cuvint de stare ce include si numarul de identificare a semnalului
Algoritmul functiei exit este urmatorul:
algoritm exit;
input codul de retur pentru parintele procesului;
output -
Inchide toate fisierele deschise (apel close);
Elibereaza directorul curent (apel input);
Elibereaza radacina schimbata, daca exista (apel input);
Elibereaza regiunile asociate procesului (apel freereg);
Scrie inregistrarea curenta pe disc;
Stare proces:=terminat("zombie");
Identificatorul procesului parinte pentru toti fiii procesului
apelant al functiei exit:=1;
if(exista procese fiu in starea "zombie")
Trimite semnalul SIGCLD procesului 1;
Trimite SIGCLD parintelui procesului al functiei exit;
Switch context;
}
Din scrierea algoritmului rezulta ca la executia de catre un proces a functiei de sistem exit, au loc urmatoarele actiuni importante:
a: Ignorarea tuturor semnalelor care se transmit procesului;
b: Daca procesul este lider al grupului, nucleul trimite tuturor proceselor
din grup semnalul HANGUP si seteaza numarul lor de grup la valoarea 0.
Ultima actiune se face intrucit daca alt proces ar obtine identificatorul
de proces al celui care a executat exit, atunci acesta ar deveni liderul
de grup al vechiului grup, desi noul proces nu ar avea nimic de a face cu
vechiul grup. Se reaminteste faptul ca un proces care are identificatorul
egal cu cel al grupului sau de proces este socotit lider al grupului.
c: Inchiderea tuturor fiserelor deschise.
d: Eliberarea directorului curent, a radacinii schimbate prin chroot, a
regiunilor asociate procesului (stiva, date, text).
e: Starea procesului apelant devine "terminat"("zombie"), ocupind numai
intrarea din tabela proceselor.
f: Tuturor proceselor fiu ale procesului care a executat exit li se schimba
parintele, acesta devenind procesul init(1).
g: Trimiterea semnalului SIGCLD procesului parinte, pentru ca acesta sa poata
analiza felul cum fiul s-a terminat (mai precis, parintele primeste codul
de retur, daca procesul fiu se termina prin exit).
h: In final procesorul va fi atribuit unui alt proces.
Se observa ca sint esentiale trei cazuri:
* procesul parinte se termina inaintea procesului fiu;
* procesul fiu se termina inaintea procesului parinte;
* procesul fiu, mostenit de procesul init, se termina;
Procesul init devine parintele oricarui proces pentru care procesul parinte s-a terminat. Cind un proces se termina, nucleul parcurge toate procesele active pentru a vedea daca printre ele exista un proces care are ca parinte procesul terminat. Daca exista un astfel de proces, pid-ul procesului parinte devine 1 (pid-ul lui init). Nucleul garanteaza asfel ca fiecare proces are un parinte.
Daca procesul fiu se termina inaintea procesului parinte, nucleul trebuie sa pastreze anumite informatii (pid, stare de terminare, timp de utilizare CPU) asupra modului in care fiul s-a terminat. Aceste informatii sint accesibile parintelui prin apelul wait sau waitpid. In terminologie Unix un proces care s-a terminat si pentru care procesul parinte nu a executat wait se numeste "zombie". In aceasta stare, procesul nu are nici un fel de resurse alocate, ci doar intrarea sa in tabela proceselor. Nucleul poate descarca toata memoria folosita de proces si inchide fisierele deschise. Un proces "zombie" se poate observa prin comanda Unix, ps, care afiseaza la starea procesului litera 'Z'.
Ex 3 :
a se compila cu: cc e3.c err.o -oe3
*/
#include "hdr.h"
int main(void)
Rezultatul afisat de acest program;
PID TTY STAT TIME COMMAND
54 v01 S 0:00 -bash
90 v01 S 0:00 e3
91 v01 Z 0:00 (e3)<zombie>
92 v01 R 0:00 ps
Daca un sistem care are ca parinte procesul init se termina, acesta nu devine "zombie", deoarece procesul init apeleaza una dintre functiile wait pentru a analiza starea in care procesul a fost terminat. Prin aceasta comportare procesul init evita incarcarea sistemului cu procese "zombie".
Asteptarea unui proces (functia de sistem wait, waitpid)
Cind un proces se termina, normal sau anormal, procesul parinte este atentionat de nucleu prin transmiterea semnalului SIGCLD. Actiunea implicita atasata acestui semnal este ignorarea sa. Un proces ce apeleaza wait sau waitpid poate:
* sa se blocheze (daca toti fiii sai sint in executie);
* sa primeasca starea de terminare a fiului (daca unul dintre fii s-a
terminat);
* sa primeasca eroare (daca nu are procese fiu).
Interfata celor doua functii wait si waitpid este urmatoarea:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int opt);
(ambele intorc pid, 0 (vezi waitpid) sau -1 in caz de eroare)
unde:
status - este un pointer spre locatia din spatiul de adrese al
procesului unde sistemul depune informatia de stare la
terminarea unui proces fiu.
Analizind informatia de stare redata pe 16 biti, se poate sti si cum
s-a terminat procesul fiu. In acest sens exista urmatoarele conventii:
* daca procesul fiu s-a terminat prin exit, continutul informatiei
de stare este urmatorul:
15 8 7 0
cod_exit 0 0 ....... 0 |
unde:
cod_exit - Este valoarea de apel a functiei exit.
* daca procesul fiu s-a terminat prin receptia unui semnal,continutul
informatiei de stare este urmatorul:
unde:
x - Este 1 daca semnalul a produs vidaj de memorie
sau 0 in caz contrar.
nr_demnal - Este numarul de identificare al semnalului care
a cauzat terminarea procesului fiu.
* daca procesul fiu este stopat, continutul informatiei de stare este
urmatorul:
15 8 7 6 0
0 .... 0 x nr_semnal |
unde:
nr-semnal - Este identificatorul semnalului care a oprit
procesul.
Diferentele intre cele doua functii constau in:
a. wait blocheaza procesul apelant pana la terminarea unui fiu, in timp
ce waitpid are o optiune, precizata prin argumentul opt, care evita
acest lucru.
b. waitpid nu asteapta terminarea primului fiu, ci poate specifica prin
argumentul opt procesul fiu asteptat.
c. waitpid permite controlul programelor prin argumentul opt.
Valoarea returnata de functia wait este numarul de identificare al
procesului fiu care s-a terminat. Intotdeauna se poate preciza care fiu s-a
terminat deorece functia ii returneaza pid-ul. Asteptarea unui anumit fiu se
poate realiza astfel:
while (wait (stare)!=pid);
unde:
pid - Este identificatorul procesului fiu asteptat sa se termine
(obtinut prin fork). Nu se poate insa preciza asteptarea
terminarii unui anumit fiu. A se vedea mai jos waitpid.
In caz de eroare, wait returneaza (-1), iar variabila errno indica
eroarea aparuta.
Algoritmul functiei wait este urmatorul:
algoritm wait;
input: pointer spre locatia unde se depune informatia de
stare a procesului fiu;
output: pid fiu si informatia sa de stare;
(-1) in caz de eroare;
if(procesul in wait nu are procese fiu)
return(eroare);
sleep la o prioritate intreruptibila pana ce un proces fiu
se termina;
}
}
Din descrierea algoritmului, rezulta ca la punerea unui proces in
asteptarea terminarii unui fiu al sau, au loc urmatoarele actiuni importante:
a. Daca in momentul apelului functiei wait, procesul nu are procese fiu,
functia wait returneaza eroare, asteptarea neavand sens.
b. Daca procesul are fii in starea terminat, atunci selecteaza unul din
ei, ii contabilizeaza timpul CPU, ii elibereaza intrarea in tabela
proceselor si apoi returneaza procesului apelant identificatorul
procesului fiu selectat si codul de retur exit al acestuia.
c. Daca procesul are fii, dar nu in starea terminat ("zombie"), el va fi
trecut in starea wait cu o prioritate intreruptibila prin executarea
functiei sleep. In momentul in care un fiu se termina, procesul
parinte primeste semnalul SIGCLD. Fiind in stare wait si asteptind
acest eveniment, bucla for va fi reluata cu primul if, gasindu-se de
data acesta un proces fiu terminat.
Observatie:
In bucla for a fost necesara reluarea verificarii daca procesul
apelant al functiei wait are fii, lucru care s-ar parea la prima vedere
inutil, intrucat el a intrat in bucla numai daca avea fii. Verificarea a fost
necesara, intrucat semnalul SIGCLD la terminarea unui fiu putea fi ignorat
prin functia signal (SIGCLD, SIGIGN). Ignorarea semnalului SIGCLD nu duce la
scoaterea din starea wait a procesului parinte.
Functia waitpid exista in SRV4 si 4.3+BSD. Argumentul, opt poate avea
valorile:
WNOHANG - apelul nu se blocheaza daca fiul specificaqt prin pid nu
este disponibil. In acest caz valoarea de retur este 0
WUNTRACED - daca implementarea permite controlul lucrarilor, starea
fiecarui proces fiu oprit si neraportata este intoarsa.
Argumentul opt poate fi si 0 sau rezultatul unui SAU logic intre
constantele simbolice WNOHANG si WUNTRACED.
In functie de argumentul pid, interpretarea functiei waitpid este:
- pid==-1
Se asteapta orice proces fiu (echivalent wait).
- pid>0
Se asteapta procesul pid.
- pid==0
Se asteapta orice proces cu ID-ul de grup de proces egal cu
cel al apelantului.
- pid<-1
Se asteapta orice proces cu ID-ul de grup de proces egal cu
valoarea absoluta a pid.
Apelul waitpid returneaza (-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.
Conform POSIX.1, pentru a analiza starea in care s-a terminat un
proces fiu exista trei macrouri excluse mutual, toate prefixe de WIF si
definite in fisierul antet sys/wait.h. Pe langa aceasta, exista alte macrouri
pentru determinarea codului de exit, numarului semnalului, etc.
Un exemplu util de folosire a acestor macrouri este functia
print_exit,care permite afisarea informatiilor de stare.
Ex 4 :
A se compila cu: cc e4.c -oe4
#include <sys/types.h>
#include <sys/wait.h>
extern char *sys_siglist[];
void
print_exit( int status)
Functia print_exit foloseste macroul WCOREDUMP daca acesta este
definit. Folosirea variabilei sys_siglist, in versiunile noi, permite maparea
numarului semnalului la denumirea sa. Daca aceasta variabila nu exista,
trebuie inspectat fisierul antet signal.h pentru a afla denumirea semnalului.
Un program test pentru aceasta functie ar putea fi urmatorul:
Ex 5 :
A se compila cu: cc e5.c err.o pr.o -oe5
#include <sys/types.h>
#include <sys/wait.h>
int main( void)
Programul creeaza pe rand trei procese fiu care sunt terminate
diferit, procesul parinte afisind informatiile de stare. Rezultatul
executiei este:
$e5
Terminare normala,starea de exit=7
Terminare anormala, numar semnal=8=Floating point exception
Terminare anormala, numar semnal=6=IOT trap/Abort
Uneori se doreste ca procesul parinte sa nu astepte terminarea unui
proces fiu creat prin functia fork. In aceste cazuri, pentru ca procesul fiu
sa nu devina "zombie", este necesar ca apelul fork sa fie dublat. Programul
urmator ilustreaza acest lucru.
Ex 6 :
A se compila cu: cc e6.c err.o pr.o -oe6
#include <sys/types.h>
#include <sys/wait.h>
int main( void)
/* se asteapta primul fiu */
if(waitpid( pid, NULL, 0) !=pid)
err_sys("Eroare waitpid");
/*
Procesul parinte initial. Executia continua fara ca el sa fie parintele
celui de-al doilea proces fiu.
*/
exit(0);
Al doilea proces fiu a fost pus in starea de asteptare pentru 3
secunde pentru a fi siguri ca primul fiu se termina inainte de afisarea
identificatorului de proces al parintelui.
Executia acestui program afiseaza:
$e6
$Al diolea fiu, pid parinte=1
Shell-ul afiseaza prompter-ul la terminarea procesului,parinte
initial, adica inainte ca ce de-al doilea fiu sa afiseze pid-ul parintelui
sau.
In general nu se poate spune care din doua procese aflate in relatia
parinte-fiu se executa primul. Chiar daca am sti care proces se executa
primul, nu se poate preciza nimic despre succesinea la executie a celor doua
procese. Acest lucru depinde de incarcarea curenta a sistemului si de
algoritmul de planificare folosit de nucleu. Pentru exemplul anterior, cu
toate ca s-au prevazut cele 3 secunde intarziere, exista posibilitatea, daca
sistemul era puternic incarcat, ca al doilea proces fiu sa se termine inaintea
primului.
In situatii limita se impune o sincronizare intre procese. Daca un
proces asteapta terminarea unui fiu acesta apeleaza wait. Daca un fiu asteapta
terminarea parintelui, o bucla de forma:
while (getppid() !=1)
sleep(1);
este suficienta. Inconvenientul acesteia ramane irosirea timpului CPU,
deoarece apelantul verifica din secunda in secunda pid-ul. Pentru a
sincroniza doua procese se considera un program de test care dupa ce a
executat apelul fork afiseaza cate un sir in fiecare proces.
Ex 7:
A se compila cu: cc e7.c err.o pr.o rut.o -oe7
#include <sys/types.h>
#include "hdr.h"
static void test(char *);
int main(void)
exit(0);
static void test(char *s)
Scrierea in fisierul standard de iesire este fara tampon astfel incat
fiecare caracter este imediat scris la iesire. Nucleul realizeaza executia
intermixata a celor doua procese, rezultatul fiind:
PFrioucle sau ls cprairsi
nte a scris
Codul prezentat contine cateva linii de comentariu esentiale daca se
doreste sincronizarea celor doua procese. Considerand ca procesele se executa
intr-o sesiune utilizator (nu superuser) si ca sincronizarea se face prin
crearea unui fisier (cea mai simpla modalitate de sincronizare ) continutul
acestor rutine ar putea fi:
A se compila cu: cc rut.c -orut.o
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
config()
void
Tell_F(pid_t pid)
void
Wait_P(void)
Functia config creeaza fisierul @@sincro@@.Procesul fiu este pus in
asteptare pana la terminarea procesului parinte prin apelul Wait_P. Prin
acest apel fiul incearca sa creeze din nou acelasi fisier, lucru imposibil
atata timp cat el exista (se reaminteste faptul ca procesele sunt obisnuite).
In momentul in care procesul parinte s-a terminat, acesta apeleaza Tell_F.
Prin acest apel fisierul @@sincro@@ este sters si procesul fiu termina apelul
Wait_P si afiseaza propriul mesaj. Rezultatul programului ,in conditiile
eliminarii simbolurilor de delimitare comentariu, din exemplul 7, este:
S-a creat fisierul @@sincro@@
Procesul parinte are pid-ul:437
Procesul parinte a scris
Poate sa se execute acum procesul fiu: 438
Fiul a scris
In mod analog se poate aranja sincronizarea inversa.
Tema :
- exersati si imaginati sincronizari de programe cu fork - wait
- dindu-se programul planific.c, pentru simularea algoritmului de
planificare RR, analizati si dezvoltati programul pentru toti
algoritmii de planificare prezentati la curs
|