Sunteți pe pagina 1din 918

USER SPACE – KERNEL SPACE

1. De ce nucleul sistemului de operare rulează, în general, într-un spațiu dedicat,


numit kernel space?
▪ Răspuns: Pentru că în kernel space au loc operații privilegiate. Spațiul kernel
este un spațiu privilegiat la care doar nucleul sistemului de operare are acces.
În felul acesta se păstrează securitatea sistemului, orice operație privilegiată
necesitând trecerea în spațiul kernel și acordul nucleului sistemului de operare
pentru execuție.
2. Ce este un apel de sistem?
▪ Răspuns: Mecanism care asigură trecerea din user space în kernel space la
solicitarea user space. Este folosit atunci când user space-ul nu are privilegiile
de a realiza o operație și apelează la kernel space pentru acest lucru.
3. De ce este utilă separația user space / kernel space?
▪ Răspuns: Spațiul kernel are privilegii complete la nivelul sistemului.
Operațiile privilegiate nu pot fi realizate în user space din motive de securitate
a sistemului. În aceste situații user space-ul apelează la kernel space prin
intermediul unui apel de sistem.
4. De ce apelul de bibliotecă strcpy nu generează apeluri de sistem?
▪ Răspuns: Un apel de sistem are loc în momentul în care este nevoie ca o
operație privilegiată să fie realizată de kernel. Întrucât strcpy copiază octeți
dintr-o zonă de memorie în altă zonă de memorie nu realizează operație
privilegiată și, deci, nu necesită apel de sistem.
5. Precizați un rol al nucleului sistemului de operare.
▪ Răspuns: Nucleul de operare gestionează memoria sistemului. Asigură
separația între procesele sistemului la nivel de memorie pentru a preveni unul
să scrie în spațiul de adresă al altuia.
6. De ce aduce un apel de sistem mai mult overhead decât un apel de funcție obișnuit?
▪ Răspuns: Un apel de sistem aduce mai mult overhead datorită comutării în
kernel-space, în timp ce un apel de funcție se execută în user-space.
7. Care dintre următoarele apeluri durează cel mai mult: strcpy, strdup, strchr?
▪ Răspuns: strcpy copiază șirul iar strchr caută un caracter în șir; strdup
realizează operație similară strcpy dar, în plus, alocă spațiu pentru noul șir,
operație costisitoare ce poate însemna și efectuarea unui apel de sistem. În
concluzie, strdup durează, în general, cel mai mult.
8. Un apel de bibliotecă (libc) poate invoca între X și Y apeluri de sistem. Ce valori
au X și Y?
▪ Răspuns: (X, Y) = (0, inf) poate să nu invoce nici un apel de sistem (vezi
strcpy) sau mai multe apeluri de sistem (teoretic infinite); nu există o limitare
pentru ca un apel de bibliotecă să apeleze mai multe apeluri de sistem (sau
foarte multe), doar că nu este ceva comun.

SISTEMUL DE FISIERE
1. În ce situație practică este folosit apelul dup()?
▪ Răspuns: Apelul dup() este folosit practic pentru redirectarea ieșirii, intrării sau
erorii standard în fișier. Altă situație practică este pentru operatorul | (pipe) de
comunicare între procese.
2. Ce conține tabela de descriptori de fișier a unui proces?
▪ Răspuns: Tabela de descriptori de fișier a unui proces conține pointeri; ca
structură de date este un vector de pointeri. Acești pointeri referă structuri de
fișier deschis de proces. Când un proces deschide un fișier, se alocă o structură
de fișier deschis, iar adresa acestei structuri este stocată într-un loc liber
(indicat de descriptorul de fișier) din tabela de descriptori de fișier.
3. Care este un avantaj al apelurilor de tipul buffered I/O (precum fread, fwrite) și
care este un avantaj al celor de tipul system I/O (precum read, write)?
▪ Răspuns: Apelurile de tipul buffered I/O fac mai puține apeluri de sistem,
deci overhead mai redus, întrucât informația este ținută în buffere până la
nevoia de flush. Sunt, de asemenea, portabile. Apelurile de tipul system I/
O au o latența mai redusă, informațiile ajung repede pe dispozitiv. De
asemenea, apelurile de tipul system I/O nu alocă memorie suplimentară
pentru buffering, sunt mai economice din acest punct de vedere.
4. Un descriptor de fișier gestionează/referă, în general, un fișier obișnuit (regular file).
Ce altceva mai poate referi?
▪ Răspuns: Un descriptor de fișier mai poate referi un director, un link
simbolic, un pipe, un socket, un dispozitiv bloc sau caracter. Toate aceste
entități sunt gestionate de un proces prin intermediul unui descriptor de fișier.
5. Dați exemplu de apel care modifică dimensiunea unui fișier.
▪ Răspuns: Apeluri care pot modifica dimensiunea unui fișier
sunt write (poate scrie dincolo de limita unui fișier), ftruncate (modifică
chiar câmpul dimensiune) sau open cu argumentulO_TRUNC care reduce
dimensiunea fișierului la 0.
6. Câte tabele de descriptori de fișier există la nivelul sistemului de operare?
▪ Răspuns: Fiecare proces are o tabelă de descriptori de fișier, deci vor exista,
la nivelul sistemului de operare, atâtea tabele de descriptori de fișier câte
procese există în acel moment în sistem.
7. Dați un exemplu de informație care se găsește în structura de fișier deschis și un
exemplu de informație care se găsește în structura de fișier pe disc (inode).
▪ Răspuns: În structura de fișier deschis se găsesc cursorul de fișier,
permisiunile de deschidere a fișierului, pointer către structura de fișier pe
disc. În structura de fișier pe disc se găsesc permisiuni de acces, informații
despre utilizatorul deținător, grupul deținător, dimensiunea fișierului, timpi de
acces, tipul fișierului, pointeri către blocurile de date.
8. Ce conține și când este populată o intrare din tabela de descriptori de fișier a unui
proces?
▪ Răspuns: Este un pointer la o structură de fișier deschis. Când se deschide
un fișier (folosind fopen, open, CreateFile) se creează o nouă structură de
fișier deschis iar adresa acesteia este reținută în cadrul intrării din tabela de
descriptori de fișier.
9. Ce este un descriptor de fișier? Ce fel de operații folosesc descriptori de fișier?
▪ Răspuns: Este un număr (întreg) ce referă o intrare în tabela de descriptori
de fișier. Este folosit în operații de lucru cu fișiere, pentru a identifica un fișier
deschis.
10. Ce rol are cursorul de fișier al unui fișier deschis? Când se modifică?
▪ Răspuns: Stabilește care este poziția curentă de la care vor avea loc operații
la nivelul fișierului. Dacă valoarea sa este 100 și un apel read citește 30 de
octeți, valoarea sa va ajunge la 130 de octeți. Se modifică și la apeluri de
scriere sau la apeluri specifice de poziționare (seek).
11. Care intrare din tabela de descriptori de fișier este modificată în cazul apelului cu
redirectare ”./run > out.txt” față de cazul rulării simple ”./run”?
▪ Răspuns: Se modifică intrarea aferentă ieșirii standard a procesului
(standard output), în general cea cu indexul 1. Aceasta întrucât operatorul >
este redirectarea ieșirii standard. Acum intrarea de la indexul 1 din tabela de
descriptori de fișier va referi fișierul out.txt, nu ieșirea standard a sistemului.
12. Știind că apelul write(42, “X”, 1), executat în procesul P, se întoarce cu succes, care
este numărul minim de fișiere deschise de procesul P? De ce? Antetul apelului write
este write(fd, *buf, count).
▪ Răspuns Numărul minim de fișiere deschise de procesul P este 0, deoarece
este posibil ca toate fișierele să fi fost deschise de părintele lui P. Numărul
minim de fișiere deschise înprocesul P este 1, și anume fișierul cu descriptorul
42, deoarece este posibil ca toți ceilalți descriptori de fișier să fie închiși.
13. Fie secvența de pseudocod:
for (i = 0; i < 42; i++)

printf(...);

Care este numărul minim, respectiv numărul maxim de apeluri de sistem din
secvența de mai sus?
▪ Răspuns Numărul minim de apeluri de sistem din secvența de mai sus este
0. Dacă printf scrie la terminal, este line buffered și nu se va executa apel de
sistem dacă nu se umple buffer-ul sau nu a fost primit caracterul '\n'.
Numărul maxim de apeluri de sistem este 42, dacă în fiecare iterație a for-ului
se umple buffer-ul sau a fost primit caracterul '\n'.
14. De ce apelul fclose realizează în spate apel de sistem, dar apelul printf nu
întotdeauna?
▪ Răspuns Apelul fclose realizează în spate apel de sistem, deoarece închide un
fișier, modificând tabela de descriptori din proces. Apelul fclose se mapează
pe apelul de sistem close. Apelul printf scrie într-un buffer, iar apelul de
sistem write se realizează dacă se umple buffer-ul sau a fost primit caracterul
'\n'.
15. Fie P1 și P2 două procese diferite. Când este posibil ca modificarea cursorului de
fișier pentru un descriptor din P1 să conducă la modificarea cursorului de fișier
pentru un descriptor din P2?
▪ Răspuns Această situație este posibilă dacă cele două procese au un proces
“strămoș” comun și descriptorul de fișier nu a fost închis de niciunul dintre
procese. Atunci, modificarea cursorului de fișier pentru un descriptor din P1
poate conduce la modificarea cursorului de fișier pentru același descriptor din
P2.
16. Care este numărul minim de descriptori de fișier valizi în cadrul unui proces? În ce
situație este posibilă această valoare?
▪ Răspuns Numărul minim de descriptori de fișier valizi în cadrul unui proces
este 0, în cazul în care un proces închide toți descriptori de fișier, inclusiv
stdin, stdout, stderr. Un astfel de proces este numit daemon.
17. Unde este poziționat cursorul de fișier fd1 în urma secvenței de mai jos? Presupuneți
că toate apelurile se întorc cu succes.
18.fd1 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
19.fd2 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
20.write(fd2, "1", 1);
dup2(fd2, fd1);
▪ Răspuns: În urma apelului open, cursorul de fișier fd2 va poziționat la
început. După write, acesta va poziționat la 1 octet după începutul fișierului,
iar după dup2, și cursorul de fișier fd1 va poziționat la 1 octet după începutul
fișierului.
21. Fie secvența de pseudocod de mai jos:
22.fd1 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
23.pid = fork();
24.switch (pid) {
25. case 0:
26. break;
27. default:
28. dup(fd1);
}
Presupunând că toate apelurile se întorc cu succes, câți descriptori din fiecare
proces vor referi fișierului a.txt?
▪ Răspuns: 3 descriptori; 2 descriptori în părinte (fd1 și descriptorul rezultat în
urma dup) și 1 descriptor în copil (fd1, moștenit de copil în urma fork).
29. Care este numărul minim de descriptori de fișier ai unui proces pot referi, la un
moment dat, stderr(standard error)? De ce?
▪ Răspuns: Numărul minim este 0, deoarece stderr poate fi închis prin
apel close.
30. Fie secvența de pseudocod de mai jos:
31.fd1 = open("a.txt", O_RDONLY);
32.fd2 = open("b.txt", O_RDWR);
33.dup2(fd1, fd2);
write(fd2, "X", 1);
Care sunt valorile posibile ce pot fi intoarse de apelul write?
▪ Răspuns:
1. Dacă toate apelurile se întorc cu succes, în urma apelului dup2, fd2 va
puncta către a.txtdeschis O_RDONLY, iar apelul write va întoarce -1 și
va seta errno la valoarea EBADF, pentru a semnala eroarea.
2. Dacă apelul dup2 eșuează, fd2 va puncta către b.txt deschis O_RDWR,
iar apelul write va întoarce 1, dacă a scris caracterul, sau 0, dacă nu a
scris caracterul.
34. De ce apelul fopen realizează în spate apel de sistem, dar apelul memcpy nu?
▪ Răspuns:
▪ fopen realizează apelul de sistem open pentru a putea deschide/crea un fișier
sau dispozitiv, pentru acest lucru fiind necesară trecerea în kernel-space.
▪ memcpy nu realizează apel de sistem deoarece scrie și citește memorie deja
alocată în spațiul de adresă al procesului fără a trece în kernel-space.
35. Fie un fișier a.txt având dimensiunea de 1024 octeți și secvența de pseudocod de
mai jos:
36.fd1 = open("a.txt", O_RDWR | O_TRUNC);
37.close(fd1);
fd1 = open("a.txt", O_RDWR | O_APPEND);
Unde va fi poziționat cursorul de fișier ale descriptorului fd1? De ce?
▪ Răspuns: În urma primului apel open, flag-ul O_TRUNC reduce dimensiunea
fișierului la 0. Al doilea apel open poziționează cursorul la sfârșitul unui fișier
gol, adică pe poziția 0. Dacă fișierul a.txt nu există, toate apelurile vor
întoarce -1.
38. Dați exemplu de două apeluri care modifică valoarea cursorului de fișier (file
pointer).
▪ Răspuns:
1. lseek/fseek, apeluri al căror rol este de modificare a cursorului de
fișier;
2. read/fread/fgets – la fiecare citire cursorul de fișier este incrementat
cu numărul de octeți citiți;
3. write/fwrite/fputs/fprints – la fiecare scriere cursorul de fișier este
incrementat cu numărul de octeți scriși;
4. ftruncate – trunchiază fișierul (cursorul este plasat pe 0);
5. apelurile echivalante Windows.
39. Unde este poziționat cursorul de fișier în urma apelului:
open("a.txt", O_CREAT | O_RDWR, 0644);
Dar în urma apelului:

open("a.txt", O_RDWR | O_TRUNC);

Se presupune că apelurile se întorc cu succes.

▪ Răspuns: De fiecare dată cursorul este plasat la începutul fișierului: în prima


situație se va începe citirea/scrierea de la începutul fișierului; în a doua
situație fișierul este trunchiat și cursorul se află la început. Singura situație în
care cursorul este plasat altundeva în momentul deschiderii acestuia este
aceea în care se folosește flag-ul O_APPEND.
40. Câți descriptori de fișier ai unui proces pot referi, la un moment dat, ieșirea standard
(standard output)?
▪ Răspuns: Oricâți, în limita dimensiunii tabelei de descriptori de fișier a
procesului, prin intermediul folosirii apelului dup/dup2:
▪ for (i = 3; i < getdtablesize(); i++)

dup2(1, i);

1. Care dintre următoarele apeluri întoarce un întreg: open, read, malloc, fopen?
▪ Răspuns:
▪ open întoarce un file descriptor (întreg) – DA
▪ read întoarce numărul de octeți citiți (întreg) – DA
▪ malloc întoarce adresa de memorie alocată (pointer) – NU
▪ fopen întoarce FILE * (un pointer) – NU
2. În ce situație modificarea cursorului de fișier pentru un descriptor conduce la
modificarea cursorului de fișier pentru alt descriptor?
▪ Răspuns: în cazul în care unul dintre descriptori este un duplicat al altui
descriptor; amândoi vor partaja descriptorul de fișier
3. Un descriptor de fișier pentru un proces dat poate referi între X și Y fișiere. Ce valori
au X și Y?
▪ Răspuns: X = 0 în cazul în care descriptorul este nevalid/nealocat; Y = 1 – un
descriptor de fișier referă un singur fișier; nu poate să refere mai multe fișiere
4. Listați secvența de pseudocod prin care scrierea la descriptorul 1 al unui proces să
realizeze afișarea la stderr iar scrierea la descriptorul 2 să realizeze afișarea la
stdout.
▪ Răspuns:
▪ dup2(1, 3); /* descriptorul 3 indica stdout (salvare descriptor)*/

▪ dup2(2, 1); /* descriptorul 1 indica stderr */

dup2(3, 2); /* descriptorul 2 indica stdout */

PROCESE & PLANIFICAREA EXECUTIEI


1. Apelul wait() este un apel blocant. Când are loc deblocarea procesului blocat
în wait()?
▪ Răspuns: Un proces este deblocat din apelul wait() atunci când unul dintre
procesele sale copil își încheie execuția. În acel moment, apelul wait() se
deblochează și întoarce informații despre modul în care și-a încheiat procesul
copil execuția.
2. De ce spunem despre apelul fork() că este invocat o dată dar se întoarce de două
ori?
▪ Răspuns: Apelul fork() este invocat o dată de procesul părinte și se întoarce
de două ori: o dată în procesul părinte pentru continuarea execuției acestuia
și altă dată în procesul copil de unde va rula acesta.
3. Ce este un proces zombie?
▪ Răspuns: Un proces zombie este un proces care și-a încheiat execuția dar
care nu a fost încă așteptat de procesul său părinte.
4. Dați exemplu de situație care duce la trecerea unui proces din starea RUNNING în
starea READY.
▪ Răspuns: Un proces trece din starea RUNNING în starea READY atunci când îi
expiră cuanta de rulare sau când există un proces cu prioritate mai mare în
coada READY (care să îi ia locul).
5. Numiți o sursă de overhead care apare atunci când sistemul de operare schimbă
contextul de execuție între două procese.
▪ Răspuns: Surse de overhead pentru schimbarea de context între procese
sunt schimbarea tabelei de pagini, care conduce la flush la TLB, algoritmul de
alegere a următorului proces și schimbarea efectivă de context, cu salvarea
registrelor procesului curent și restaurarea procesului ales.
6. Descrieți o problemă posibilă care poate apărea dacă un sistem de operare
implementează un algoritm de planificare de tipul Shortest Job First.
▪ Răspuns: În cazul unei planificări Shortest Job First, dacă sunt adăugate în
sistem, în mod constant, procese noi și de durată scurtă, procesele de durată
mai lungă nu vor apuca să ruleze. Va rezulta într-un timp de așteptare foarte
mare pentru procesele de lungă durată sau chiar în starvation (așteptare
nedefinită pentru ca un proces să poată rula pe procesor).
7. La ce se referă noțiunea de timp de așteptare (waiting time) în contextul planificării
proceselor (process scheduling)?
▪ Răspuns: Noțiunea de waiting time se referă la timpul de așteptare al unui
proces în coadaREADY a planificatorului. Pentru un sistem interactiv/responsiv
este de dorit ca timpul de așteptare să fie cât mai scurt.
8. Care este un avantaj și un dezavantaj al folosirii unei cuante de timp scurte în
planificarea proceselor (process scheduling)?
▪ Răspuns: Folosirea unei cuante de timp scurte înseamnă un sistem interactiv
și responsiv. Dar înseamnă și schimbări dese de context adică un randament
mai scăzut al sistemului în a rula procese, deci o productivitate (throughput)
redusă.
9. În ce situație are loc tranziția din starea WAITING în starea READY a unui proces?
▪ Răspuns: În cazul în care operația care a cauzat așteptarea (de exemplu,
citirea de pe disc) s-a încheiat și acum procesul poate rula.
10. În ce situație are loc tranziția din starea RUNNING în starea WAITING a unui proces?
▪ Răspuns: În momentul în care procesul execută o operație blocantă (operație
de I/O, sleep), acesta trece din starea RUNNING în starea WAITING.
11. Ce este o schimbare de context? De ce este necesară?
▪ Răspuns: Se referă la schimbarea unui proces care rulează pe un procesor
(este în starea RUNNING) cu un alt proces (aflat în starea READY). Este
necesară pentru a asigura folosirea optimă a procesorului (dacă un proces se
blochează îi ia altul locul) și pentru asigurarea echității (fairness) a sistemului
(procesele se schimbă cu altele pentru a permite câtor mai multe să ruleze în
sistem).
12. Ce reprezintă spațiul de adrese al unui proces? De ce este util?
▪ Răspuns: Spațiul de adrese al unui proces este spațiul de lucru cu memoria a
unui proces. Procesul lucrează cu adrese de memorie iar spațiul de adresă îi
definește zonele accesibile. Spațiul de adresă asigură separația, la nivelul
memoriei, între un proces și alt proces
13. De ce sistemele cu planificare preemptivă au un nivel de interactivitate mai bun
decât sistemele cu planificare cooperativă?
▪ Răspuns: Planificarea preemptivă introduce noțiunea de cuantă de timp
alocată unui proces. Când acestuia îi expiră cuanta, este preemptat și înlocuit
pe procesor. În acest fel, fiecare proces va ajunge mai repede pe procesor; nu
apare riscul ca un proces să ruleze mult timp pe procesor. Fiecare proces
rulând destul de rapid pe procesor, vom avea un sistem mai responsiv și mai
interactiv.
14. Dați exemplu de situație în care un proces este scos de pe procesor deși NU a
efectuat o operație blocantă.
▪ Răspuns: În momentul în care unui proces îi expiră cuanta, aceasta este
scos de pe procesor și un alt proces aflat în starea READY este planificat.
Același lucru se întâmplă dacă există un proces în coada READY cu prioriate
mai bună decât cel ce rulează pe procesor.
15. De ce, în general, procesele I/O bound au prioritate mai bună decât procesele CPU
bound?
▪ Răspuns: În general, procesele I/O bound vor executa o operație de I/O
rapid, adică se vor bloca. În acest caz, aceste procese vor trece în starea
WAITING și vor elibera procesorul unui alt proces. Acordându-le prioritate mai
bună, acestea vor rula mai repede dar vor elibera rapid procesorul lăsând loc
altor procese. Un proces CPU bound va elibera mai târziu procesorul, motiv
pentru care va avea o prioritate mai puțin bună. Preferăm să planificăm
procesele I/O bound.
16. De ce sistemele care doresc productivitate ridicată au alocată o cuantă de timp mai
mare alocată fiecărui proces?
▪ Răspuns: Un sistem este productiv dacă cea mai mare parte din timp acesta
execută acțiune utilă. Pentru aceasta trebuie ca procesele să ruleze cât mai
mult timp și să existe cât mai puține schimbări de context. Prea multe
schimbări de context înseamnă un overhead semnificativ asupra timpului util
de lucru. De aceea, pentru a diminua numărul de schimbări de context un
sistem productiv va aloca o cuantă de timp mare proceselor sale, procesele
petrecând cât mai mult timp rulând.
17. Fie P0 procesul părinte al procesului P1, T0 momentul de timp la care P0 execută
apelul wait() și T1 momentul de timp la care P1 execută apelul exit(). În ce stare vor
fi cele două procese în intervalul (T0, T1) dacă T0<T1?
▪ Răspuns Procesul P0 este în starea WAITING, în așteptarea semnalului de la
copil. Procesul P1 poate fi în orice stare, în funcție de codul său, dar va trece,
cu siguranță, prin starea RUNNING pentru a putea executa apelul exit().
18. Prezentați un avantaj al mapării spațiului de memorie al kernel-ului în spațiul de
adresă al fiecărui proces.
▪ Răspuns Prin maparea spațiului de memorie al kernel-ului în spațiul de
adresă al fiecărui proces se evită schimbarea de context la fiecare apel de
sistem, inclusiv apelul schedule().
19. Fie P0 procesul părinte al procesului P1, T0 momentul de timp la care P0 execută
apelul wait() și T1 momentul de timp la care P1 execută apelul exit(). În ce stare vor
fi cele două procese în intervalul (T1, T0) dacă T1<T0?
▪ Răspuns Procesul P0 poate fi în orice stare, în funcție de codul său, dar va
trece, cu siguranță, prin starea RUNNING pentru a putea executa apelul
wait(). Procesul P1 este în starea TERMINATED (zombie), deoarece și-a
încheiat execuția și așteaptă să îi fie citită valoarea de ieșire de către părinte.
20. De ce un proces orfan nu poate deveni zombie?
▪ Răspuns Deoarece un proces orfan este adoptat imediat de init, este
imposibil ca el să devină zombie. Acesta execută wait pentru fiecare proces
copil al său, care și-a încheiat execuția, împiedicând ca acesta să devină
zombie.
21. Fie P un proces zombie. Ce procese îl pot elimina din sistem prin apelul wait()?
▪ Răspuns Procesele care pot elimina din sistem un proces zombie prin apelul
wait() sunt: părintele său (dacă nu și-a încheiat execuția) și procesul init
(care adoptă procesele orfane și execută wait pentru fiecare proces copil al
său, care și-a încheiat execuția.
22. În urma unui apel fork() pot rezulta între X și Y procese noi. Ce valori au X și Y?
▪ Răspuns Dacă apelul fork() eșuează nu va fi creat niciun proces nou. Dacă
apelul se execută cu succes, va fi creat un proces nou, copil al procesului care
a executat fork(). Astfel, pot rezulta între 0 și 1 procese noi. X=0. Y=1.
23. Dați două exemple de resurse care pot aparține unui proces, dar nu pot aparține
unui program.
▪ Răspuns Procesul reprezintă o instanță activă activă a unui program.
Resursele care pot aparține unui proces, dar nu pot aparține unui program
sunt: memoria, CPU-ul, PCB-ul (PID, spațiul de adresă - zonele de date, cod,
heap, stivă, tabela de descriptori, masca de semnale, etc.).
24. De ce un planificator echitabil (fair) nu este, în general, productiv (nu oferă un
throughput mare)?
▪ Răspuns Un planificator echitabil implică schimbări de context dese, astfel că
este petrecut un timp relativ mare cu schimbările de context, scăzând
productivitatea.
25. În ce situație este posibil ca un proces să treacă direct din starea WAITING în starea
TERMINATED?
▪ Răspuns Un proces va trece direct din starea WAITING în starea
TERMINATED dacă primește un semnal care nu poate fi ignorat sau
suprascris, precum SIGKILL sau SIGQUIT, care conduce la terminarea
procesului, indiferent de context.
26. De ce nu mai este folosită planificarea cooperativă în sistemele desktop moderne?
▪ Răspuns În sistemele desktop moderne accentul este pus pe interactivitate.
Planificarea cooperativă se bazează pe cedarea voluntară a procesorului și nu
poate oferi interactivitate.
27. De ce, pe un sistem desktop, de obicei, sunt mai multe procese în starea WAITING
decât în starea READY?
▪ Răspuns Pe un sistem desktop, majoritatea proceselor așteaptă inputul
utilizatorului, deci sunt procese I/O bound.
28. Fie procesul P1. Câte procese copil va avea P1 în urma rulării secvenței de pseudocod
de mai jos? De ce?
29.while(fork() == 0)
;
▪ Răspuns: Apelul fork întoarce 0 în procesul copil și PID-ul procesului copil,
diferit de 0, în părinte. De aceea, părintele va executa o singură dată fork,
ieșind apoi din buclă. Copilul va mai executa o dată bucla, devenind la rândul
lui părinte și ieșind din buclă. Fiecare proces va fi părinte pentru un singur
copil. Astfel, P1 va avea un singur proces copil.
30. În ce zonă de memorie este plasată adresa de retur a unei funcții? De ce?
▪ Răspuns: Adresa de retur a unei funcții este plasată pe stivă. La fiecare apel
de funcție, un nou stack frame este creat pe stivă, care conține parametrii
funcției, adresa de retur și variabile locale. La ieșirea din funcție, adresa de
retur este preluată de pe stivă.
31. Dați două exemple în care:
▪ a) un proces are mai multe procese părinte decât procese copil;
▪ b) un proces are mai multe procese copil decât procese părinte.
▪ Răspuns:
▪ toate procesele, cu excepția procesului init, au un proces părinte;
1. a) un proces care nu are procese copil (nu a executat fork niciodată);
2. b) un proces care are cel puțin două procese copil (a executat fork de
cel puțin două ori, cu succes) sau procesul init care nu are proces
părinte.
32. De ce apelul fork întoarce 0 pentru succes în procesul copil și o valoare diferită de 0
pentru succes în procesul părinte?
▪ Răspuns: În procesul copil întoarce 0 pentru că procesul poate folosi
apelul getppid pentru a afla PID-ul procesul părinte. În procesul
părinte, fork întoarce PID-ul procesului copil. Un proces poate avea mai
multe procese copil și este comod ca fork să întoarcă PID-ul procesului copil
proaspăt creat.
33. Care dintre următoarele apeluri pot modifica numărul de procese dintr-un sistem
UNIX: open, fork,dup2, wait, exec?
▪ Răspuns:
1. open deschide și, posibil, creează un nou fișier sau dispozitiv - NU
2. fork creează un nou proces - DA
3. dup2 duplică un descriptor de fișier - NU
4. wait așteaptă schimbarea stării unui proces, în cazul unui proces copil
care și-a încheiat execuția, permite sistemului să elibereze resursele
asociate cu copilul, ducând la scăderea numărului de procese din
sistem - DA
5. exec înlocuiește imaginea procesului curent cu o nouă imagine de
proces - NU
34. D a ț i e x e m p l u d e a c ț i u n e c e c o n d u c e l a t r e c e r e a u n u i p r o c e s d i n
starea RUNNING în WAITING și un exemplu de acțiune care conduce la o trecere
inversă.
▪ Răspuns
1. Un proces trece din starea RUNNING în WAITING atunci când execută o
acțiune blocantă.
2. Un proces nu poate trece direct din starea WAITING în RUNNING.
Procesul trece în starea READYatunci când a dispărut cauza blocării
sale.
35. Cum ar trebui aleasă cuanta de timp pentru procese pentru un sistem în care se
dorește interactivitate mare? De ce?
▪ Răspuns: Cuanta de timp trebuie să fie mică pentru ca procesele să fie
preemptate mai des și să crească timpul de răspuns al sistemului.
36. În ce mod este determinat numărul de procese care se pot afla, la un moment dat, în
starea RUNNINGde următoarele componente fizice ale sistemului: număr de
procesoare, arhitectură pe 32/64 biți, memorie fizică, capacitate de stocare?
▪ Răspuns: Numărul de procese care se pot afla, la un moment dat, în
starea RUNNING depinde doar de numărul de procesoare, deoarece pot exista
maxim n procese în starea RUNNING pe un sistem cu n procesoare. Celelalte
componente nu influențează acest număr.
37. În ce mod este determinat numărul de procese care se pot afla, la un moment dat, în
starea READYde următoarele componente fizice ale sistemului: număr de procesoare,
arhitectură pe 32/64 biți, memorie fizică, capacitate de stocare?
▪ Răspuns: Numărul de procese care se pot afla, la un moment dat, în
starea READY depinde de memoria fizică, deoarece coada/cozile READY sunt
reținute în liste în memoria fizică, iar dimensiunea acesteia poate limita
dimensiunile acestora. Celelalte componente nu influențează, în general, acest
număr.
1. Observație: Au fost considerate valide următoarele argumente:
1. pentru dimensiuni mari ale memoriei fizice (peste 4 GB) este
relevantă arhitectura, pentru a putea adresa memoria
disponibilă;
2. existența mai multor procesoare duce la creșterea numărului de
procese aflate simultan în starea RUNNING, astfel că scade
numărul de procese care ar fi fost în starea READY, diferența
fiind egală cu diferența dintre numărul de procesoare;
38. C a r e d i n t r e u r m ă t o a r e l e a p e l u r i a u l e g ă t u r ă d i r e c t ă c u
procesele zombie: fork, dup, wait, exec,malloc?
▪ Răspuns: Un proces zombie este un proces care a murit dar care nu a fost
așteptat (wait) de procesul părinte; informațiile legate de încheiere ocupă
spațiu în memorie până la un apelwait/waitpid/WaitForSingleObject al
procesului părinte. Apelul wait are, așadar, legătură directă cu procesele
zombie.
39. Ce valoare întoarce fork în procesul părinte? Dar în procesul copil? De ce?
▪ Răspuns: În procesul copil întoarce 0 pentru că procesul poate folosi
apelul getppid pentru a afla PID-ul procesul părinte. În procesul
părinte, fork întoarce PID-ul procesului copil. Un proces poate avea mai
multe procese copil și este comod ca fork să întoarcă PID-ul procesului copil
proaspăt creat.
40. Un proces poate avea între A și B procese copil și între C și D procese părinte. Ce
valori au (A, B) respectiv (C, D)?
▪ Răspuns:
1. (A, B) = (0, inf) - un proces poate avea oricâte procese copil, în limita
resurselor disponibile
2. (C, D) = (1, 1) - orice proces copil are un proces părinte, se poate
considera init ca proces fără proces părinte; se permite soluția (C, D)
= (0, 1) (dacă se precizează init)
41. Câte procese se pot găsi, la un moment dat, într-un sistem de operare, în
stările RUNNING, READY șiWAITING?
▪ Răspuns:
1. În starea RUNNING se găsesc procesele care execută cod pe procesor.
Numărul maxim de procese în acea stare este dat de numărul de
procesoare.
2. În starea READY, respectiv WAITING se pot găsi oricâte procese în limita
resurselor sistemului. ÎnREADY se vor găsi procese gata de rulare,
neblocate, care așteaptă acordarea de timp pe procesor; în
starea WAITING se vor găsi procese blocate în așteptarea unei acțiuni
de blocare (dispozitiv de I/O, semafoare cozi de mesaje). Nu există o
limitare dată pentru procesele din starea READY sau WAITING.
42. Pentru un sistem se dorește productivitate (throughput) mare. De ce se preferă
alegerea unei cuante de timp (timeslice) mare pentru procese?
▪ Răspuns:
1. Productivitate mare înseamnă un timp mare consumat pe lucru efectiv
(rulare pe procesor) pe lângă timpi consumați pentru alte activități,
cea mai importantă fiind schimabrea de context.
2. În cazul unei cuante de timp mari, proceselor vor lucra timp îndelungat
iar timpul consumat pe schimbarea de context (apărută la expirarea
cuantei) va fi mai mic, relativ.
43. D a ț i e x e m p l u d e a c ț i u n e c e c o n d u c e l a t r e c e r e a u n u i p r o c e s d i n
starea READY în RUNNING și un exemplu care conduce la o trecere inversă.
▪ Răspuns:
1. Un proces trece din starea RUNNING în READY în momentul în care i-a
expirat cuanta de timp sau când există un alt proces prioritar lui în
coada READY.
1. Observație: Blocarea procesului conduce la trecerea acestuia în
coada WAITING, iar omorârea procesului înseamnă că acesta nu
trece în nici o altă coadă.
2. Un proces trece din READY în RUNNING în momentul în care se găsește în
vârful cozii READY și procesului care rulează pe procesor (cel din
starea RUNNING) îi expiră cuanta, sau are prioritate mai mică, sau acel
proces se blochează sau acel proces moare.
44. D a ț i e x e m p l u d e a c ț i u n e c e c o n d u c e l a t r e c e r e a u n u i p r o c e s d i n
starea READY în WAITING și un exemplu care conduce la o trecere inversă.
▪ Răspuns:
1. Un proces nu poate trece direct din starea READY în WAITING. Pentru a
ajunge în WAITING trebuie să execute o acțiune blocantă, adică trebuie
să ruleze, adică trebuie să se găsească în stareaRUNNING.
2. Un proces trece din starea WAITING în READY atunci când a dispărut
cauza blocării sale: a apărut un eveniment de I/O pe care îl aștepta, a
fost notificat, a fost făcut unlock pe mutex-ul la care aștepta etc.
Procesul este, atunci, pregătit pentru execuție.
MEMORIA VIRTUALA
1. De ce este uzual și avantajos ca spațiul virtual de adrese al proceselor să cuprindă o
zonă dedicată pentru kernel?
▪ Răspuns: Prezența zonei dedicate pentru kernel în spațiul de adresă al fiecărui
proces înseamnă că la fiecare apel de sistem, adică la trecerea din user space în
kernel space, tabela de pagini rămâne aceeași și nu se face flush la TLB. În
cazul în care kernel-ul ar avea o zonă dedicată, atunci ar avea și o tabelă de
pagini dedicată și ar trebui schimbată tabela de pagini la fiecare apel de sistem
și la fiecare revenire din apel de sistem.
2. Procesul P1 folosește 100MB de memorie fizică (RAM) rezidentă. P1 execută fork() și
rezultă procesul P2. Câtă memorie fizică (RAM) rezidentă folosesc împreună P1 și P2
imediat dupafork()? De ce?
▪ Răspuns: Apelul fork() folosește copy-on-write ceea ce înseamnă că nu se
alocă memorie rezidentă nouă pentru noul proces. Se alocă, într-adevăr, o nouă
tabelă de pagini, dar spațiul rezident al procesului P1 este acum partajat cu
procesul P2 până la prima operație de scriere, când pagina aferentă va fi
duplicată.
3. Care este utilitatea conceptului de demand paging?
▪ Răspuns: Atunci când sistemul de operare folosește demand paging alocarea
de memorie fizică este amânată până în momentul în care nevoie (adică la
primul acces). Sistemul de operare doar rezervă memorie virtuală și nu alocă
memorie fizică în spate, economisind memorie fizică. La primul acces se alocă
și memorie fizică, la cerere (adică on demand) și se face maparea acesteia la
spațiul virtual (paging).
4. De ce zonele .text și .rodata din cadrul bibiliotecilor partajate (shared libraries)
pot fi partajate între mai multe procese?
▪ Răspuns: Zonele .text și .rodata sunt zone read only. Acest lucru
înseamna că pot fi partajate în siguranță pentru că nici un proces care
accesează zona nu o va putea modifica. Zonele conțin permanent aceleași
informații indiferent de numărul de procese care le folosesc și pot fi, deci,
partajate.
5. De ce este utilă paginarea ierarhică?
▪ Răspuns: Dacă nu am folosi paginare ierarhică tabelele de pagini ar ocupa
foarte mult spațiu; ar fi neovie de o intrare pentru fiecare pagină virtuală a
unui proces. Paginarea ierarhică conduce la reducerea spațiului ocupat de
tabela de pagini, profitând de faptul că o bună parte din spațiul virtual de
adrese al procesului nu este folosit.
6. Care este o cauză sursă pentru evacuarea unei pagini din memoria fizică (RAM) pe
disc (swap out)?
▪ Răspuns: Cauze sursă pentru evacuarea unei pagini din RAM sunt:
1. operația de swap in, care necesită o pagină liberă în RAM, conducând
la o operația de swap out
2. alocarea unei pagini fizice noi; nu există pagini libere, se execută swap
out
3. demand paging, la fel ca mai sus
4. copy on write care necesită alocarea unei noi pagini fizice, posibil
inexistente
7. Ce conține tabela de pagini a unui proces?
▪ Răspuns: Tabela de pagini a unui proces conține pointeri de pagini fizice.
Indexul în tabelă este pagina virtuală. În general tabela de pagini mai conține
și informații legate de permisiuni, validatate, dacă pagina a fost sau nu
modificată.
8. Dați exemplu de situație care cauzează page fault fără a rezulta în trimiterea unei
excepții de acces la memorie (de tipul SIGSEGV, Segmentation fault) către procesul
care a generat page fault-ul.
▪ Răspuns: Dacă un proces are rezervat un spațiu virtual în modul demand
paging atunci accesarea unei pagini virtuale din acel spațiu va conduce
la page fault. În urma page fault-ului, se va aloca și mapa o pagină fizică, iar
procesul își va continua execuția. Nu va fi generată excepție de acces la
memorie. La fel se întâmplă și în cazul copy-on-write.
9. De ce NU avem fragmentare externă în cazul folosirii paginării?
▪ Răspuns: Întreg spațiul fizic este împărțit în pagini de dimensiune fixă.
Atunci când este nevoie de spațiu nou se alocă pagini noi indiferent de poziția
lor în spațiul inițial. Dacă o pagină este liberă, este eligibilă pentru alocare. Nu
ajungem să avem fragmentare externă, adică spațiu liber nealocabil între
spații deja alocate.
10. Ce reprezintă “demand paging”? Ce rol are?
▪ Răspuns: Demand paging este o formă de amânare a alocării de pagini fizice
până în momentul în care este nevoie. Prin demand paging se alocă doar
pagini virtuale iar paginile fizice aferente se vor aloca în momentul accesului
la acele pagini. Rolul său este de a eficientiza consumul de memorie și de
timpul de alocare. În momentul alocării se alocă doar memorie virtuală, nu și
fizică, lucru care durează mai puțin. De asemenea, consumul de memorie
fizică (RAM) la un moment dat este redus la strictul necesar în acel moment.
11. Precizați un avantaj al folosirii mecanismului de memorie virtuală.
▪ Răspuns: Un avantaj este faptul că spațiul (virtual) de adrese al unui proces
este continuu, independent de forma în care este realizată maparea pe spațiul
fizic. Orice alocare se face în continuare spațiului virtual existent, iar
mecanismul de memorie virtuală face maparea cu spațiul fizic. Un alt avantaj
este posibilitatea folosirea spațiului de swap: mod prin care putem folosi
discul pentru a reține pagini care nu încap în spațiul fizic (memoria RAM). Un
alt avantaj este posibilitatea partajării memoriei, pagini virtuale din procese
diferite (sau chiar din același proces) putând fi mapate peste aceleași pagini
fizice.
12. Ce este spațiul de swap? Ce rol are?
▪ Răspuns: Spațiul de swap este spațiul localizat pe disc folosit ca depozitar
temporar al informațiilor din memorie RAM. În momentul în care spațiul fizic
(memoria RAM) devine insuficient, se evacuează (swap out) anumite pagini
fizice. În momentul în care aceste pagini sunt necesare sunt readuse în
memoria RAM (swap in).
13. Care este avantajul folosirii paginării ierarhice?
▪ Răspuns: Prin folosirea paginării ierarhice, spațiul ocupat de tabelele de
pagini ale proceselor este diminuat. În loc să existe o intrare pentru fiecare
pagină, vor exista intrări doar pentru paginile valide din spațiul virtual de
adrese al proceselor.
14. Corespondența între pagini virtuale și pagini fizice este “mai multe la una”. De ce se
întâmplă și la ce este util acest lucru?
▪ Răspuns: Tabela de pagini conține o mapare între pagini virtuale și pagini
fizice. În acest fel mai multe pagini virtuale pot avea corespondent aceeași
pagină fizică. Acest lucru este util pentru mecanismul de memorie partajată în
care procese diferite au pagini virtuale din propriu spațiu de adresă mapate
peste aceleași pagini fizice.
15. Ce este TLB? Ce rol are?
▪ Răspuns: TLB (Translation Lookaside Buffer) este o memorie cache la nivelul
sistemului care cache-uiește intrările din tabelele de pagini ale proceselor.
Întrucât fiecare acces la memorie necesită de fapt două accese (unul la tabela
de pagini, alta la datele efective), TLB-ul micșorează timpul de acces
simplificând primul acces (la tabela de pagini). TLB îndeplinește astfel rolul
eficientizării accesului la memorie.
16. Care este avantajul principal al folosirii mecanismului copy-on-write la crearea
proceselor folosind fork()?
▪ Răspuns: Prin folosirea mecanismului de copy-on-write la crearea proceselor
folosind fork(), un proces nou este creat foarte rapid. Un proces nou nu va
trebui să aloce spațiu fizic nou/separat ci va partaja spațiul fizic aferent
procesului părinte. Acel spațiu este marcat read-only și va fi duplicat doar în
momentul în care unul dintre procese va scrie (copy-on-write).
17. De ce numărul de pagini virtuale dintr-un sistem este mai mare decât numărul de
pagini fizice?
▪ Răspuns Numărul de pagini fizice este limitat de dimensiunea memoriei RAM,
în timp ce numărul de pagini virtuale este determinat de numărul de procese.
În cazul memoriei partajate de două sau mai multe procese, pot exista mai
multe pagini virtuale mapate pe aceeași pagină fizică.
18. De ce paginarea ierarhică are un overhead de prelucrare mai mare decât paginarea
neierarhică?
▪ Răspuns În cazul paginării neierarhice, numărul paginii virtuale este și
indexul în tabela de pagini, deci va exista un singur acces la memorie pentru
aflarea paginii fizice. În cazul paginării ierarhice se vor face atâtea accese la
memorie, cât numărul de niveluri ierarhice.
19. Care este principală sursă de overhead la schimbarea de context între două procese?
▪ Răspuns Principală sursă de overhead la schimbarea de context între două
procese este repopularea TLB-ului. În urma unei schimbări de context,
intrările din TLB sunt invalidate și este necesară translatarea adreselor
virtuale în adrese fizice pe baza tabelei de pagini a noului proces. Acest lucru
implică numeroase accesări ale memoriei, la o viteză mult mai mică decât
viteza TLB-ului.
20. De ce nu este necesară eliminarea paginilor de memorie ale kernel-ului din TLB în
cazul unei schimbări de context?
▪ Răspuns Eliminarea paginilor de memorie ale kernel-ului din TLB în cazul
unei schimbări de context nu este necesară deoarece toate procesele au
mapate pe aceleași pagini virtuale tot spațiul kernel.
21. Fie un sistem cu paginare ierarhică pe două niveluri, fără TLB. Pot două pagini de
memorie virtuală referi aceeași pagină de memorie fizică?
▪ Răspuns Da, două pagini de memorie virtuală pot referi aceeași pagină.
▪ În această situație, în cadrul unui proces, în tabela de pagini, unor intrări
diferite (indexate de pagina virtuală A și pagina virtuală B) le corespunde
aceeași pagină fizică.
▪ În cazul a două procese este vorba de implementarea mecanismului de
shared memory.
▪ Afirmațiile sunt valabile pentru orice tip de paginare: ierarhică, neierarhică,
inversată, indiferent de prezența / absența TLB-ului.
22. Presupunând dimensiunea unei pagini de 4096, câte pagini fizice (frame-uri) noi vor
fi alocate în urma apelului malloc(6000)? De ce?
▪ Răspuns Apelul malloc folosește demand paging, alocând memorie pur
virtuală, fără suport în memoria fizică. Totuși, pentru alocări de dimensiuni
mici, este posibil să fie alocate și paginile fizice aferente. Deoarece 4096 <
6000 < 2 * 4096, se vor aloca maxim două pagini.
23. De ce este mecanismul de mapare a fișierelor esențial în rularea proceselor pe
sisteme de operare moderne?
▪ Răspuns Mecanismul de mapare a fișierelor este esențial deoarece rularea
proceselor se face prin maparea executabilului în memorie și folosirea
demand-paging pentru încărcarea zonelor de date și cod la nevoie.
24. Fie afirmația “Toate procesele vor genera page fault-uri în urma unui apel din familia
exec().” Precizați și justificați valoarea de adevăr a afirmației.
▪ Răspuns Afirmația este adevărată deoarece în urma apelului exec(), vor fi
încărcate zonele de date și cod, folosind demand-paging.
25. Fie afirmația: “Un apel fork() modifică numărul de pagini virtuale și numărul de
pagini fizice alocate într-un sistem.” Precizați și justificați valoarea de adevăr a
afirmației.
▪ Răspuns Afirmația este adevărată. Numărul paginilor virtuale crește,
deoarece apare un nou spațiu de adresă. Crește și numărul de pagini fizice,
întrucât se alocă structuri interne nucleului, printre care noua tabelă de pagini
pentru procesul copil.
26. În ce mod influențează dimensiunea TLB-ului numărul maxim de spații de adresă
existente în sistem?
▪ Răspuns: TLB-ul menține mapări de pagini fizice și pagini virtuale. Conține
un subset al tabelei de pagini. Numărul maxim de spații de adresă existente
în sistem depinde de numărul de procese existente în sistem. Acest număr nu
este influențat de dimensiunea TLB-ului.
27. Fie un utilizator fără drepturi de administrator. În ce mod poate acesta modifica
numărul de pagini fizice din sistem? Dar numărul de pagini virtuale?
▪ Răspuns:
1. Numărul de pagini fizice din sistem poate fi modificat prin adăugarea/
scoaterea de memorie fizică din sistem sau prin modificarea
dimensiunii paginilor fizice, având suport hardware. Pentru aceste
operații este necesar un administrator, deci un utilizator fără drepturi
de administrator nu poate modifica numărul de pagini fizice din sistem.
2. Un utilizator fără drepturi de administrator poate modifica numărul de
pagini virtuale din sistem prin crearea/omorârea de procese, deoarece
fiecare proces ale paginile virtuale proprii.
28. Dați exemplu de avantaj, respectiv dezavantaj al paginării simple (neierarhice) în
fața paginării ierarhice.
▪ Răspuns:
1. Avantaje:
1. Overhead de prelucrare mic (este parcursă o singură tabelă de
pagini);
2. Complexitate mică de implementare.
2. Dezavantaj: paginarea simplă ocupă mai mult spațiu, deoarece se
ocupă spațiu și pentru zonele de memorie virtuale nevalide.
29. Dați exemplu de avantaj, respectiv dezavantaj al folosirii TLB.
▪ Răspuns:
1. Avantaj: TLB-ul este un cache rapid care menține mapări de pagini
fizice și pagini virtuale. În cazul TLB hit, scade timpul de acces la
memorie.
2. Dezavantaje:
1. În cazul TLB miss, timpul de acces la memorie este mai mare
decât timpul de acces în absența TLB-ului.
2. La fiecare schimbare de context se face TLB flush, astfel că vor
exista mai multe miss-uri imediat după schimbarea de context.
3. Dimensiune mică și cost ridicat.
30. Motivați utilizarea copy-on-write în cadrul apelului fork().
▪ Răspuns: Este utilă folosirea mecanismului de copy-on-write deoarece
crearea unui nou proces se face mai rapid, evitând operațiile de copiere a
memoriei. Foarte probabil, după fork() copilul va face un apel exec() pentru
a lansa un program diferit. Astfel, dacă nu s-ar folosi copy-on-write, s-ar
copia degeaba paginile din spațiul de adresă al părintelui.
31. Prezentați un avantaj și un dezavantaj al mapării fișierelor în memorie.
▪ Răspuns:
1. Avantaje:
1. Se evită apelurile de sistem read() și write(), deci și double-
buffering-ul.
2. Permite partajarea fișierelor între procese.
3. Nu mai este necesar apelul lseek(), căutarea se realizează prin
manipularea pointer-ilor.
2. Dezavantaje:
1. Maparea fișierelor în memorie se face la nivel de pagină, astfel
că apare fragmentare, mai ales în cazul fișierelor mici.
2. Maparea trebuie să se încadreze în spațiul de adresă al
procesului. Pe sisteme pe 32 de biți, sunt dificil de mapat fișiere
mari.
3. Accesul la memorie determină apariția page fault-urilor.
4. Trebuie apelat periodic msync(), deoarece modificările în
memorie nu sunt scrise imediat pe disc.
32. În ce situație folosirea funcției memcpy generează page fault și în ce situație nu
generează page fault?
▪ Răspuns: Funcția memcpy folosește două buffere: sursă și destinație.
Fie S mulțimea paginilor în care se află bufferul sursă și D mulțimea paginilor
în care se află bufferul destinație.
1. Folosirea funcției memcpy generează page fault în următoarele situații:
1. Cel puțin o pagină din S sau D este nevalidă. (nu a fost alocată
în RAM, este pe swap sau un acces nevalid din partea
programatorului)
2. Cel puțin o pagină din D este validă, dar nu sunt drepturi de
scriere. (copy-on-write, mapare PROT_READ)
3. Cel puțin o pagină din S este validă, dar nu sunt drepturi de
citire. (mapare PROT_NONE)
2. Folosirea funcției memcpy nu generează page fault dacă toate paginile
din S sunt valide, cu drept de citire și dacă toate paginile din D sunt
valide, cu drept de scriere.
33. Pe un sistem pe 32 de biți, cu 512MB RAM și 512MB de swap, un proces execută
secvența următoare de cod:
34.int x = read_int_from_user();
35.void *a = malloc(x);
memset(a, 0, x);
La un curs de Sisteme de Operare, profesorul întreabă “Care este valoarea minimă
a lui x pentru care apelul malloc întoarce un pointer valid, dar apelul memset duce la
blocarea sistemului?”. Un student răspunde “1GB + 1B”. Profesorul răspunde “Cam
pe acolo, dar valoarea reală este ceva mai mică”. De ce a spus profesorul acest
lucru?
▪ Răspuns: Apelul malloc folosește demand paging, alocând memorie pur
virtuală, fără suport în memoria fizică. Apelul memset necesită alocarea de
pagini fizice, astfel că sistemul se va bloca în momentul în care nu mai are
pagini fizice disponibile. Sistemul are disponibil 1 GB memorie fizică (512MB
RAM și 512MB de swap) astfel că studentul a considerat că se va bloca la
ocuparea întregii memorii fizice (1GB + 1B). În realitate, sistemul se va bloca
la o valoare “ceva mai mică”, deoarece în memoria fizică se află pagini de
memorie ale kernelului, cât și ale altor procese.
36. Dați exemplu de avantaj, respectiv dezavantaj al paginării ierarhice în fața paginării
simple (neierarhice).
▪ Răspuns:
1. Avantaj: paginarea ierarhică ocupă mai puțin spațiu; se ocupă spațiu
doar pentru zonele de memorie virtuale valide. În cazul paginării
simple, tabela de pagini ocupă același spațiu indiferent de numărul de
pagini virtuale valide.
2. Dezavataje:
1. overhead de prelucrare mai mare (trebuie parcurse mai multe
tabele de pagini, mai multe referențieri);
2. complexitate mai mare de implementare (împărțire mai fină a
unei adrese de memorie).
37. Fie un sistem cu paginare simplă (neierarhică). Pot două pagini de memorie virtuală
referi aceeași pagină de memorie fizică? Dar invers?
▪ Răspuns:
1. Da, două pagini de memorie virtuală pot referi aceeași pagină.
1. În această situație, în cadrul unui proces, în tabela de pagini,
unor intrări diferite (indexate de pagina virtuală A și pagina
virtuală B) le corespunde aceeași pagină fizică.
2. În cazul a două procese este vorba de implementarea
mecanismului de shared memory.
2. Nu, două pagini de memorie fizică nu pot referi aceeași pagină
virtuală. Adresa virtuală este cea folosită de proces. O astfel de adresă
nu poate indica spre două adrese fizice diferite. Este similar cu a spune
că f(X) = A și f(X) = B, unde A != B.
3. Nu are relevanță folosirea paginării simple. Afirmațiile sunt valabile
pentru orice tip de paginare: ierarhică, neierarhică, inversată.
38. Fie struct tlb_entry o structură aferentă unei intrări în TLB. Ce câmpuri conține o
astfel de structură?
▪ Răspuns:
1. TLB-ul este folosit pentru translatarea rapidă a adreselor virtuale în
adrese fizice. Structura conține adresa paginii virtuale și adresa paginii
fizice aferente.
39. De ce este golit (flushed) TLB-ul la o schimbare de context?
▪ Răspuns:
1. TLB-ul conține un subset de intrări din tabela de pagini pentru acces
rapid din partea procesorului. Tabela de pagini este proprie fiecărui
proces (spațiului de adresă al acestuia). La o shimbare de context,
procesul este schimbat și la fel și tabela de pagini. Schimbarea tabelei
de pagini înseamnă invalidarea TLB-ului și este necesară golirea
acestuia.
40. Dați exemplu de situație în care se produce page fault (la nivelul subsistemului de
gestiune a memoriei), fără a rezulta în segmentation fault/excepție la nivelul
procesului care a cauzat page fault-ul.
▪ În cazul demand paging, se alocă o pagină virtuală (page) fără suport fizic
(frame). La apariția unui page fault se va aloca o pagină fizică, fără a rezulta
o excepție.
▪ În cazul copy-on-write, două pagini virtuale (page) (din două procese diferite)
sunt marcate read-only și referă aceeași pagină fizică (frame). În momentul
în care unul dintre cele două procese efectuează o operație de scriere, se
obține page fault, se duplică pagina fizică și se continuă execuția.
▪ În cazul unei pagini virtuale (page) valide, al cărei conținut se găsește
în swap, un acces generează page fault. Conținutul este swapped in într-o
pagină fizică (frame) si procesul își continuă execuția.
41. Care este rolul bitului dirty / modified pentru subsistemul de înlocuire a paginilor?
Bitul este activat în momentul în care o pagină este modificată/scrisă.
▪ Dacă o pagină este modificată atunci o eventuală alegere a acestei pagini
pentru a fi evacuată (swapped out) va înseamnă scrierea acesteia pe disc.
▪ Dacă o pagină nu este modificată (bitul dirty este dezactivat) nu va mai fi
scrisă pe disc în momentul evacuării, rezultând într-un overhead redus al
operației de înlocuire.
▪ În general, algoritmii de înlocuire de pagini din cadrul subsistemelor aferente,
vor ține cont de ultimul acces (fie de scriere, fie de citire al unei pagini) –
LRU, NRU (Least/Not Recently Used). Se vor prefera paginile nereferite
recent, iar dintre acestea, cele care nu au fost scrise (bitul dirtydezactivat) –
vezi NRU.
42. Dați exemplu de situație/scenariu în care apariția unui page fault determină swap
out.
▪ În cazul apariției unui page fault, este posibil ca pagina fizică (frame) aferentă
să nu fie alocată (demand paging sau copy-on-write) sau să fie pe disc
(swapped). În acest caz pagina trebuie adusă în RAM.
▪ Dacă memoria fizică (RAM) este ocupată, va trebui aleasă o pagină fizică
pentru a fi înlocuită. Se aplică un algoritm de înlocuire a paginii.
▪ În momentul aplicării algoritmului de înlocuire a paginii, pagina fizică este
evacuată pe disc (swapped out); conținutul de pe disc aferent paginii ce a
cauzat page fault-ul este adus în memorie (swapped in) în locul paginii fizice
proaspăt evacuate.

SECURITATEA MEMORIEI
1. De ce este relevant, în contextul securității memoriei, faptul că adresa de retur a
unei funcții se reține pe stivă?
▪ Răspuns: Adresa de retur stocată în memorie oferă unui atacator posibilitatea
suprascrierii acesteia și alterarea fluxului normal de execuție al programului.
Pentru aceasta este nevoie de o vulnerabilitate într-un buffer la nivelul stivei. În
general folosirea de adrese pe stive oferă această posibilitate si e de evitat, dar
nu putem face asta în privința adresei de retur; este nevoie de stocarea pe stivă
pentru a putea reveni în stack frame-ul anterior.
2. De ce folosirea DEP (Data Execution Prevention) nu previne atacurile de tipul return-
to-libc?
▪ Răspuns: DEP previne existența simultană a permisiunilor de scriere și
execuție. Adică nu se poate scrie într-o zonă un shellcode (sau ceva similar)
care apoi să se execute. Un atac de tipul return-to-libc presupune
suprascrierea unei adrese (de retur, pointer de funcție) ca să pointeze către o
funcție din biblioteca standard C. Întrucât un atac de tipul return-to-libc nu
presupune scriere și execuție a aceleiași zone, nu poate fi prevenit de DEP.
3. De ce trebuie avut grijă la construcțiile precum cea de mai jos în cadrul unei funcții?
4. int (*fn_ptr)(int, int); /* fn_ptr is a function pointer */

char buffer[128]; /* buffer for storing strings */

▪ Răspuns: În construcția din exercițiu dacă nu se ține cont de dimensiunea


buffer-ului se poate obține un buffer overflow. În urma overflow-ului, se
suprascrie pointer-ul de funcție fn_ptr. Probabil acest pointer va fi folosit la
un moment dat rezultând în execuția arbitrară și alterând fluxul normal de
execuție al programului.
5. De ce, în general, un shellcode se încheie cu invocarea apelului de sistem execve?
▪ Răspuns: Un shellcode încearcă, în general, rularea unui program nou, de
exemplu a unui shell în forma echivalentă a unui apel execve(”/bin/sh”).
Întrucât un apel de bibliotecă este mai dificil de realizat, se preferă o
instrucțiune simplă de apel de sistem (precum int 0x80. Se face un apel de
sistem execve cu un argument de forma unui șir /bin/sh într-un registru
rezultând în crearea unui shell nou.
6. Care este utilitatea ASLR (Address Space Layout Randomization) din perspectiva
securității memoriei?
▪ Răspuns: În cazul unui atac ce exploatează o vulnerabilitate de securitate a
memoriei, atacatorul dorește să deturneze fluxul normal de execuție spre o
funcție/adresă injectată de el (shellcode) sau către una existentă (return to
libc). Pentru aceasta are nevoie de adresa precisă a acelei funcții; dacă
procesul folosește ASLR este foarte dificil (în special pe sistemele pe 64 de
biți) de identificat adresa funcției.
7. Ce înseamnă “stack buffer overflow”?
▪ Răspuns: Stack buffer overflow este depăsirea unui buffer local unei funcții
(alocat pe stivă). Adică în cazul unui buffer cu 10 elemente, accesăm al 15-
lea sau al 20-lea element. Putem suprascrie pointeri sau adresa de retur a
funcției și dând naștere, astfel, unor atacuri de securitate.
8. Ce este un shellcode?
▪ Răspuns: Un shellcode este o secvență de cod binar care se dorește a fi
injectat, prin intermediul unei vulnerabilități de securitate, în codul unui
proces care rulează. Apoi procesului îi este deturnat fluxul de execuție pentru
a executa shellcode-ul. De regulă shellcode-ul urmărește obținerea unui shell
prin execuția unei instrucțiuni de forma exec(“/bin/bash”).
9. Ce înseamnă un atac de tipul “return-to-libc”?
▪ Răspuns: Un atac de tipul “return-to-libc” presupune suprascrierea unui
pointer sau a adresei de retur a unei funcții cu adresa unei funcții din
biblioteca standard C (de obicei funcția system). În acest fel se deturnează
fluxul normal de execuție al programului către o altă adresă încercându-se
obținerea unui shell.
10. De ce NU previne flag-ul NX (No eXecute) atacurile de tipul return-to-libc?
▪ Răspuns Flag-ul NX marchează regiuni specifice (stiva, de exemplu) ca
neexecutabile. Un atac de tipul return-to-libc forțează un jump nevalid la o
adresă din zona de cod (text) care este executabilă și nu este afectată de
prezența sau nu a flag-ului NX.
11. În ce mod protejează chroot împotriva atacurilor de tip shellcode?
▪ Răspuns În chroot jail nu este adăugat executabil de shell (/bin/bash sau /
bin/sh). În acest caz, nu se poate executa un shell dintr-un program chroot-at
și, deci, nici un shellcode.
12. De ce NU este stocată valoarea “salt” și în fișierul /etc/passwd?
▪ Răspuns Deoarece valoarea este corelată cu hash-ul, care se stochează în /
etc/shadow. De asemenea, din considerente de securitate, nu ar trebui să fie
stocată într-un fișier care poate fi citit de orice user.
13. De ce are tehnica ASLR impact redus pe un sistem pe 32 de biți?
▪ Răspuns Tehnica ASLR împiedică atacatorul să cunoască adresa funcției
dorite din cadrul zonei de cod prin maparea acestora în puncte aleatoare din
spațiul de adresă. Pentru sisteme pe 32 de biți, spațiul virtual nu este
suficient de mare, atacatorii putând “căuta” adresa dorită pentru a realiza
atacuri de tipul return-to-libc.
14. De ce este considerată funcția memcpy mai “sigură” decât funcția strcpy?
▪ Răspuns: Deoarece funcția memcpy specifică dimensiunea buffer-ului care
trebuie copiat, în timp ce strcpy presupune că buffer-ele sunt alocate cum
trebuie, astfel încât strlen(sursă) < sizeof(dest).
15. Completați zona punctată a următoarei secvențe de cod cu un apel de bibliotecă,
astfel încât:
▪ a) programul să fie vulnerabil la stack smashing;
▪ b) programul să nu fie vulnerabil la stack smashing.
▪ Motivați alegerile făcute.
▪ #define INPUT "1234"
▪ int main(void)
▪ {
▪ char a[2];
▪ memcpy(a, INPUT, ...);
▪ return 0;
}
▪ Răspuns: Pentru ca programul să fie vulnerabil la stack smashing, trebuie să
permită copierea unui număr de octeți independent de dimensiunea buffer-
ului destinație. Pentru ca programul să nu fie vulnerabil, trebuie să permită
copierea unui număr de octeți mai mare decât dimensiunea buffer-ului
destinație.
1. a) Apeluri de bibliotecă: strlen(INPUT), sizeof(INPUT), etc.
2. b) Apeluri de bibliotecă: sizeof(a), etc.
16. Cum previne tehnica ASLR (Address Space Layout Randomization) atacuri de
tipul return-to-libc?
▪ Un atac de tipul return-to-libc presupune suprascrierea adresei de retur din
cadrul stack frame-ului unei funcții cu o adresă din zona de cod a procesului
(cel mai probabil codul aferent pentrusystem sau exec).
▪ Tehnica ASLR împiedică atacatorul să cunoască adresa funcției dorite din
cadrul zonei de cod prin maparea acestora în puncte aleatoare din spațiul de
adresă. În cazul unui spațiu virtual suficient de mare (spre exemplu în cazul
sistemelor pe 64 de biți), timpul de “căutare” a adresei dorite face impractice
atacuri de tipul return-to-libc.
17. Cum previne flag-ul NX (No eXecute) atacurile de tipul stack buffer overflow?
▪ Atacurile de tipul stack buffer overflow presupun suprascrierea adresei de
retur din cadrul stack frame-ului unei funcții prin trecerea peste limita unui
buffer alocat pe stivă. În general, adresa este suprascrisă chiar cu o adresă de
pe stivă din cadrul buffer-ului.
1. Atacatorul completează în cadrul buffer-ului de pe stivă un shell code
și apoi suprascrie adresa de retur cu adresa din cadrul buffer-ului unde
începe shell code-ul.
▪ Flag-ul NX este un flag/bit hardware ce marchează anumite pagini ca fiind
neexecutabile. Exemple de zone care sunt candidați pentru acest bit sunt
stiva, heap-ul și zonele de date. Marcarea stivei ca fiind neexecutabilă, prin
folosirea flag-ului NX, înseamnă imposibilitatea realizării unui atac de
tipul stack buffer overflow.
18. Cum sunt prevenite atacurile de tipul stack buffer overflow folosind soluții de
tipul stack smashing protection?
▪ Vezi descriere stack buffer overflow mai sus.
▪ Soluțiile de tipul stack smashing protection presupun plasarea unei valori
speciale (canary value) înaintea adresei de retur dintr-un stack frame.
▪ Întrucât majoritatea atacurilor presupun operații pe șiruri, suprascrierea
adresei de retur va însemna și suprascrierea valorii speciale. Înainte de
revenirea de funcție se verifică această valoare. Dacă a fost modificată atunci
se generează eroare.
19. De ce executabilul aferent comenzii su (/bin/su) are bitul setuid activat?
▪ Un proces care ia naștere din cadul executabilului /bin/su execută o serie de
operații privilegiate precum:
1. parcurgerea fișierului /etc/shadow pentru a citi parola;
2. schimbarea user id-ului curent (practic schimbarea utilizatorului).
▪ Pentru executarea operațiilor privilegiate este nevoie de drept de superuser.
Prezența bituluisetuid permite efectuarea acestora.
THREAD-URI & MECANISME DE
SINCRONIZARE
1. Care este un avantaj și un dezavantaj al folosirii unei implementări de thread-uri în
user space (user-level threads)?
▪ Răspuns:
▪ Avantaje pot fi:
▪ timp de creare mai mic decât thread-urile kernel level
▪ schimbări de context mai rapide
▪ control mai bun asupra aspectelor de planificare (totul se întâmplă în
user space, sub controlul programatorului)
▪ Dezavantaje pot fi:
▪ blocarea unui thread duce la blocarea întregului proces
▪ nu poate fi folosit suportul multiprocesor
2. De ce este importantă o instrucțiune de tip TSL (test and set lock) la nivelul
procesorului?
▪ Răspuns: Implementările de mecanisme de sincronizare se bazează pe
instrucțiuni hardware. Fără suportul procesorului pentru operații atomice
(precum TSL sau cmpxchg) nu ar fi posibilă implementarea unor mecanisme
precum spinlock-uri. Astfel de instrucțiuni vor fi disponibile pentru orice procesor
pentru a permite implementarea mecanismelor de sincronizare.
3. Care este un avantaj și un dezavantaj al folosirii unei implementări de thread-uri cu
suport în kernel (kernel-level threads)?
▪ Răspuns:
▪ Avantaje pot fi:
▪ dacă un thread se blochează celelalte thread-uri pot rula
▪ se folosește suportul multiprocesor al sistemului
▪ planificator robust asigurat de sistemul de operare, preemptiv
▪ Dezavantaje pot fi:
▪ timp de creare mai mare (necesită apel de sistem)
▪ schimbare de context mai lentă (overhead datorat trecerii în
kernel space pentru invocarea planificatorului)
4. Precizați un dezavantaj al folosirii primitivelor de sincronizare.
▪ Răspuns: Dezavantaje sunt:
▪ lock contention: mai multe thread-uri așteaptă la un lock (un singur
thread poate accesa regiunea critică protejată de lock) → ineficiență
▪ lock overhead: apelul de lock/unlock produce overhead, de multe ori
însemnând apel de sistem
▪ serializarea codului: codul protejat de un lock este cod serial, accesibil
unui singur thread; nu avem paralelism
▪ deadlock: o folosire necorespunzătoare a primitivelor de sincronizare
duce la deadlock sau livelock
5. De ce dimensiunea spațiului virtual de adresă al unui proces crește în momentul
creării unui thread (chiar dacă thread-ul nu ajuns încă să se execute)?
▪ Răspuns: În momentul creării unui thread se alocă o stivă nouă acelui
thread. În mod implicit, pe sistemele Linux, dimensiunea stivei este de
8 MB de memorie, observând o creștere semnificativă a spațiului virtual de
adresă al procesului.
6. Când este recomandat să folosim un spinlock în locul unui mutex?
▪ R ă s p u n s : S p i n l o c k- u l f o l o s e ș t e b u s y - w a i t i n g ș i a r e o p e r a ț i i
de lock() și unlock() ieftine prin comparație cu mutex-ul. Operațiilor
de lock() și unlock() pe mutex sunt de obicei costisitoare întrucât pot
ajunge să invoce planificatorul. Având operații rapide, spinlock-ul este potrivit
pe secțiuni critice de mici dimensiuni în care nu se fac operații blocante; în
aceste cazuri faptul că face busy-waiting nu contează așa de mult pentru că
va intra rapid în regiunea critică. Dacă am folosi un mutex pentru o regiune
critică mică, atunci overhead-ul cauzat de operațiile pe mutex ar fi relativ
semnificativ față de timpul scurt petrecut în regiunea critică, rezultând în
ineficiența folosirii timpului pe procesor.
7. De ce schimbarea de context între două thread-uri ale aceluiași proces este, în
general, mai rapidă decât schimbarea de context între două procese?
▪ Răspuns: Schimbarea între două thread-uri ale aceluiași proces este mai
rapidă decât schimbarea de context între două procese pentru că nu este
nevoie de schimbarea spațiului de adresă. Schimbarea spațiului de adresă
este relativ costisitoare pentrucă presupune schimbarea tabelei de pagini și
golirea multor intrări din TLB.
8. Care sunt cele două tipuri de operații aferente mecanismelor de sincronizare prin
secvențiere/ordonare?
▪ Răspuns: Cele două operații aferente mecanismelor de sincronizare prin
secvențiere/ordonare sunt:
▪ wait() pentru așteptarea îndeplinirii unei condiții după care thread-ul
curent va rula;
▪ notify() sau signal() pentru a anunța thread-ul/thread-urile blocate
în operația wait() de îndeplinirea condiției.
9. Precizați un avantaj al folosirii thread-urilor în locul proceselor
▪ Răspuns: Avantaje pot fi:
▪ timp de creare mai mic
▪ timp de schimbare de context mai rapid pentru thread-urile aceluiași
proces
▪ partajare facilă a datelor între thread-urile aceluiași proces
▪ în cazul implementărilor cu suport la nivelul nucleului (kernel-level
threads), blocarea unui thread nu blochează întregul proces, ducând la
o productivitate sporită
10. Ce înseamnă “lock contention”?
▪ Răspuns: Lock contention se referă la accesul concurent la un lock/mutex din
partea multor thread-uri. În cazul în care multe thread-uri așteaptă la un
lock, eficiența este scăzută, doar un singur thread putând accesa la un
moment dat regiunea critică protejată de lock.
11. Ce efect are apelul exit() în cadrul unei codului rulat de un thread?
▪ Răspuns: Apelul exit() încheie execuția procesului curent, indiferent de
punctul în care este apelat. Dacă în cadrul funcției unui thread se apelează
exit() atunci procesul aferent thread-ului își încheie execuția (împreună cu
toate thread-urile procesului).
12. Indicați un dezavantaj/neajuns al primitivelor de acces exclusiv chiar și în cazul
folosirii corespunzătoare (în care se asigură coerența datelor).
▪ Răspuns: Dezavantaje sunt:
▪ lock contention: mai multe thread-uri așteaptă la un lock (un singur
thread poate accesa regiunea critică protejată de lock) → ineficiență
▪ lock overhead: apelul de lock/unlock produce overhead, de multe ori
însemnând apel de sistem
▪ serializarea codului: codul protejat de un lock este cod serial, accesibil
unui singur thread; nu avem paralelism
13. Ce se întâmplă în cazul unui acces nevalid la memorie în cadrul codului rulat de un
thread?
▪ Răspuns: În cazul unui acces nevalid la memorie, sistemul de operare
generează o excepție (semnalul SIGSEGV pe Linux) al cărei efect este
încheierea execuției procesului curent. Indiferent de modul în care este
generat accesul (din cadrul funcției unui thread), procesul își încheie execuția,
împreună cu toate thread-urile aferente.
14. Care este dezavantajul folosirii de regiuni critice de mici dimensiuni (granularitate
fină)?
▪ Răspuns: Într-o regiune critică mică, overhead-ul cauzat de apelurile lock și
unlock este semnificativ față de acțiunea efectivă realizată în regiunea critică.
Dacă regiunea critică este accesată foarte des atunci acest overhead devine
semnificativ la nivelul întregului set de acțiuni executate de thread.
1. Precizați un dezavantaj al folosirii thread-urilor în locul proceselor.
▪ Răspuns: Dezavantaje sunt:
▪ dacă un thread execută un apel nevalid la memorie atunci se generează
excepție și întreg procesul își încheie execuția
▪ zonele de memorie folosite la comun impun folosirea mecanismelor de
sincronizare care pot produce probleme dificil de depanat
▪ un număr semnificativ de thread-uri duce la penalizări de performanță față
de alte abordări care nu folosesc mai multe thread-uri sau procese (de
exemplu event-based I/O sau operații neblocante/asincrone)
1. Două thread-uri folosesc două mutex-uri. Cum se poate ajunge la deadlock?
▪ Răspuns: Fie T1, T2 cele două thread-uri și mutex_a și mutex_b cele două
mutex-uri. Situația în care se poate ajunge la deadlock presupune ca thread-ul
T1 să execute un cod de forma lock(mutex_a); lock(mutex_b); iar thread-ul T2
să execute un cod de forma lock(mutex_b); lock(mutex_a); Dacă thread-ul T2
rulează între cele două apeluri lock ale thread-ului T1 atunci acesta va
achiziționat mutex-ul mutex_b. În acea situație T1 va avea achiziționat mutex-ul
mutex_a și va aștepta după eliberarea mutex-ului mutex_b, iar T2 invers. În
această situație nici un thread nu poate trece mai departe, ambele rămânând
blocate: deadlock.
2. De ce este necesară folosirea mutex-urilor, și nu a spinlock-urilor, pentru regiunile
critice cu operații de I/O?
▪ Răspuns Regiunile critice cu operații de I/O sunt, de obicei, lungi. De
asemenea, operatiile I/O pot duce la blocarea thread-ului, caz în care nu
poate fi folosit spinlock-ul.
3. În ce situație este posibilă apariția unui deadlock pe o singură resursă critică?
▪ Răspuns Fie procesul P1 care a acaparat resursa critică și procesele P2, P3,
… , Pn care așteaptă eliberarea resursei respective. Un deadlock pe resursa
respectivă va apărea dacă procesul P1 nu va elibera resursa critică, fie
datorită codului său (nu există instrucțiunea de release/unlock, intră într-un
ciclu infinit, etc.), fie deoarece a fost terminat prin semnal SIGKILL.
4. Descrieți două diferențe între un mutex și un semafor binar.
▪ Răspuns
▪ Mutexul este folosit pentru acces exclusiv, semaforul binar este folosit pentru
sincronizare
▪ Mutexul poate fi eliberat doar de procesul care l-a ocupat, în timp ce orice
proces poate incrementa semaforul.
▪ Mutexul este mereu inițializat unlocked, semaforul binar poate fi inițializat la
valoarea 0.
▪ Mutexul are un overhead mai mic decât semaforul binar.
5. De ce overhead-ul creării unui nou thread este independent de utilizarea
mecanismului copy-on-write?
▪ Răspuns Copy-on-write are sens doar între două tabele de pagini diferite,
adică între două procese diferite, întrucât se partajează paginile fizice între
pagini virtuale din procese diferite. Thread-urile din același proces partajează
tabela de pagină, astfel că overhead-ul creării unui nou thread este
independent de copy-on-write.
6. Câte fire de execuție va avea un proces multithreaded în urma executării unui apel
din familia exec()?
▪ Răspuns În urma executării unui apel din familia exec(), spațiul de adresă al
procesului existent va fi înlocuit cu spațiul de adresă al noului proces. Acesta
va avea inițial n singur fir de execuție.
7. Fie un program multithreaded care rulează pe un sistem uniprocesor cu sistem de
operare cu suport multithreaded. În ce situație este mai eficientă folosirea user-level
threads în fața kernel-level threads?
▪ Răspuns Folosirea user-level threads este mai eficientă în cazul în care
procesul face multe operații CPU intensive, deoarece este mai rapid context
switch-ul.
8. Fie f o funcție care nu este reentrantă. Cum trebuie aceasta apelată pentru a fi
thread-safe?
▪ Răspuns Pentru a fi thread-safe, trebuie apelată folosind un mecanism de
acces exclusiv. Dacă se pune lock, un singur thread va putea accesa funcția,
care devine, astfel, thread-safe.
9. Dați exemplu de avantaj, respectiv dezavantaj al sincronizării folosind semafoare
binare în fața spinlock-urilor.
▪ Răspuns:
1. Avantaj: Deoarece semafoarele binare nu folosesc busy waiting, pot fi
folosite pentru secțiuni critice de orice dimensiune.
2. Dezavantaj: Un proces va ceda procesorul dacă nu poate lua
semaforul. Operația de down are un overhead ridicat, datorită
schimbării de context.
10. Dați exemplu de avantaj, respectiv dezavantaj al sincronizării folosind spinlock-uri în
fața mutex-urilor.
▪ Răspuns:
1. Avantaj: Un proces nu va ceda procesorul dacă nu poate lua spinlock-
ul. Operația de lock are un overhead scăzut.
2. Dezavantaj: Deoarece spinlock-urile folosesc busy waiting, sunt
recomandate doar pentru secțiuni critice mici, ce se execută rapid.
11. Care metodă de sincronizare (mutex sau spinlock) este mai avantajoasă pentru
sincronizarea accesului la următoarea regiune critică și de ce? Prezentați cel puțin un
motiv.
12....

13.// start regiune critica


14.c=a+b;
15.a=b;
16.b=c;
17.// stop regiune critica
...
▪ Răspuns: Deoarece regiunea critică este mică, este mai avantajoasă
folosirea unui spinlock pentru sincronizare, deoarece se evită overhead-ul
unei schimbări de context.
18. Care dintre următoarele operații pot genera schimbare de context cu o cauză diferită
de expirarea cuantei? De
ce? lock(&mutex), unlock(&mutex), spin_lock(&spinlock), spin_unlock(&spinlock)
,down(&semaphore), up(&semaphore).
▪ Răspuns:
1. lock(&mutex) - încercarea de a obține un mutex inaccesibil trece
procesul în starea WAITING -DA
2. unlock(&mutex) - la ieșirea din zona critică, procesele care așteptau la
mutex vor trece din starea WAITING în starea READY - DA
3. spin_lock(&spinlock) - încercarea de a obține un spinlock inaccesibil
nu trece procesul în stareaWAITING - NU
4. spin_unlock(&spinlock) - la ieșirea din zona critică, procesul care
aștepta la spinlock se va debloca, dar va rămâne în starea RUNNING -
NU
5. down(&semaphore) - încercarea de a obține un semafor inaccesibil trece
procesul în stareaWAITING - DA
6. up(&semaphore) - la ieșirea din zona critică, procesele care așteptau la
semafor vor trece din starea WAITING în starea READY - DA
19. Dați 2 exemple de resurse partajate între thread-urile aceluiași proces.
▪ Răspuns: Thread-urile aceluiași proces partajează descriptorii de fișier,
spațiul de adrese (memoria), masca de semnale, deoarece acestea sunt
resurse la nivel de proces, nu de thread.
1. Ele folosesc aceleași segmente de memorie .heap, .data și .bss. (deci
și variabilele stocate în ele)
20. Dați 2 exemple de resurse care NU sunt partajate între thread-urile aceluiași proces.
▪ Răspuns: Fiecare thread al unui proces are un context de execuție propriu,
format din stivă și set de regiștri (deci și un contor de program - registrul
(E)IP). De asemenea, TLS/TSD reprezintă variabile specifice unui thread,
invizibile pentru celelalte thread-uri.
21. Dați exemplu de situație/aplicație în care este mai avantajos să se folosească un
număr X de kernel-level threads într-un proces și o situație/aplicație în care este mai
avantajos să se folosească un număr Y de kernel-level threads, cu X < Y. Ambele
situații se vor raporta la același sistem dat.
▪ Răspuns:
1. Este mai avantajos să se folosească un număr X (mai mic) de kernel-
level threads într-un proces care execută operații CPU-intensive care
folosesc puține apeluri blocante, deoarece utilizarea mai multor kernel-
level threads ar însemna mai multe schimbări de context, care ar
scădea performanța.
2. Este mai avantajos să se folosească un număr Y (mai mare) de kernel-
level threads într-un proces care este I/O-intensive, având multe
apeluri blocante, permițând unui număr mai mare de thread-uri să și
continue execuția în cazul blocării unui thread.
22. Prezentați un avantaj și un dezavantaj al folosirii user-level threads față de kernel-
level threads.
▪ Răspuns:
1. Avantaje :
1. schimbarea de context nu implică kernelul, deci vom avea
comutare rapidă
2. planificarea poate fi aleasă de aplicație; aplicația poate folosi
acea planificare care favorizează creșterea performanțelor
3. firele de execuție pot rula pe orice sistem de operare, inclusiv
pe sisteme de operare care nu suportă fire de execuție la nivel
kernel.
2. Dezavantaje :
1. kernel-ul nu știe de fire de execuție, astfel că dacă un fir de
execuție face un apel blocant toate firele de execuție planificate
de aplicație vor fi blocate
2. nu se pot utiliza la maximum resursele hardware: kernelul va
vedea un singur fir de execuție și va planifica procesul respectiv
pe maximum un procesor, chiar dacă aplicația ar avea mai
multe fire de execuție planificabile în același timp.
23. Se poate implementa un semafor folosind doar mutex-uri? Dar un mutex
folosind doar semafoare?
▪ Răspuns:
▪ Un mutex se poate implementa ca un semafor binar. Răspuns afirmativ la a doua
întrebare.
▪ (Mulțumim lui Răzvan Pistolea pentru observație) Pentru implementarea unui
semafor cu mutex-uri, este nevoie de un mutex care să îndeplinească rolul unei
variabile condiție (pentru notificare). În acest caz, trebuie ca acel mutex să nu poată
fi incrementat de două ori (adică să se apeleze release de două ori). În plus, trebuie
asigurată sincronizarea corespunzătoare, pentru a preveni condițiile de cursă,
precum două thread-uri care trec de semafor în momentul în care valoarea acestuia
este 1.

24. Fie un sistem cu două procesoare. Câte procese pot “aștepta” simultan eliberarea
unui spinlock? Dar a unui mutex?
▪ Răspuns:
1. La un mutex pot aștepta oricâte procese. Dacă mutexul este
achiziționat, procesele se blochează și trec în starea WAITING. Pot
exista oricâte procese în starea WAITING.
2. Dacă un proces a achiziționat un spinlock, cel mult un alt proces
poate “aștepta” (busy waiting) așteptarea acestuia pe un alt procesor.
Dacă cele două procese (ce rulează pe procesor) au ajuns la un
livelock (ambele așteaptă la spinlock) sistemul este “agățat”; ambele
așteaptă în acel moment la spinlock.
1. Un proces care deține un spinlock nu va fi planificat pentru că
atunci alte procese ar aștepta nedefinit și sistemul devine
instabil. Un spinlock protejează o zonă scurtă și rapidă. De
asemenea, un proces care “așteaptă” la un spinlock nu va fi
preemptat pentru că așteptarea este că va aștepta puțin la
procesor (prin busy waiting).
2. În concluzie, pe un sistem cu două procesoare, eliberarea unui
spinlock poate fi așteptată de cel mult două procese.
25. Este nevoie de folosirea unui mecanism de sincronizare la folosirea memoriei
partajate? Dar la folosirea cozilor de mesaje?
▪ Răspuns:
1. Da, la memoria partajată. Două (sau mai multe procese) pot decide să
scrie în memorie partajată și pot rezulta date incorecte. Este nevoie de
protejarea prin folosirea unui mecanism de locking.
2. În cazul folosirii cozilor de mesaje nu este nevoie de folosirea unui
mecanism de sincronizare. Aceasta deoarece operațiile pe cozile de
mesaje sunt atomice și serializate.
1. Scrierea unui mesaj se face după altă scriere, iar citirea unui
mesaj se realizează în momentul în care un mesaj există deja
în coadă (altfel se blochează în așteptare). Nu este necesară
folosirea unei forme de utilizare de genul lock(); send();
unlock();.
26. Dați exemplu de situație în care trei procese care interacționează ajung în deadlock.
▪ Răspuns:
1. Cea mai simplă situație este aceea a unei așteptări circulare a
p r o c e s e l o r. P r o c e s e l e , r e s p e c t i v , P 1 , P 2 , P 3 d e ț i n
resursele R1, R2, R3 fără a le elibera. Apoi procesul P1 solicită accesul
la resursa R2, P2 la R3 iar P3 la R1. Fiecare proces așteaptă la o resursă
deținută de alt proces, fără ca vreunul din ele să o fi eliberat. Toate
sunt blocate – deadlock.
27. Fie un program multithreaded care efectuază multe operații I/O per thread. Este mai
eficientă folosirea user-level threads sau kernel-level threads?
▪ Operațiile I/O sunt, în general, operații blocante. Efectuarea unei operații de
I/O în cadrul unei implementări de thread-uri user-level va bloca întreg
procesul, nu doar thread-ul curent.
▪ În cadrul unei implementări de thread-uri kernel-level, doar thread-ul care a
efectuat operația I/O, celelalte thread-uri putând fi planificate pe procesor.
▪ Implementarea kernel-level threads este, în acest caz mai eficientă, prin
folosirea procesorului/procesoarelor de mai multe thread-uri ale proceselor.
28. Fie un program multithreaded care rulează pe un sistem multiprocesor. Este mai
eficientă folosireauser-level threads sau kernel-level threads?
▪ În cadrul unei implementări user-level, un singur thread rulează pe procesor;
planificatorul de la nivelul nucleului “vede” o singură entitate planificabilă –
procesul corespunzător.
▪ În cadrul unei implementări kernel-level, fiecare thread al procesului poate
rula pe un procesor. Se poate întâmpla ca, pe un sistem cu număr suficient de
procesoare, toate thread-urile unui proces să ruleze pe procesoare.
▪ În concluzie, este mai eficientă folosirea unei implementări kernel-level prin
utilizarea mai bună a procesoarelor, rezultând, astfel, într-o productivitate mai
bună.
29. Fie următoarea secvență de cod:
30.int main(void)

31.{

32. int a = 0;

33. pthread_create(...);

34. a++;

Care va fi valoarea lui a la finalul secvenței?


▪ În urma apelului pthread_create thread-ul principal (main thread, master
thread) își continuă execuția; thread-ul nou creat execută funcția transmisă
ca argument funcției pthread_create.
▪ Variabila a va fi incrementată doar de thread-ul principal, astfel că valoarea
finală a acesteia va fi 1.
▪ O situație diferită este aceea în care a este transmisă într-o formă ca
argument funcției aferente thread-ului nou creat și acesta o modifică; de
asemenea, o valoare diferită rezultă în momentul în care thread-ul nou creat
accesează variabila a pe stiva thread-ului principal (foarte puțin probabil, dar
posibil).
35. În ce situație pot două thread-uri din două procese diferite să partajeze o pagină de
memorie?
▪ Două thread-uri din două procese diferite nu pot partaja o pagină de memorie
decât în cazul în care cele două procese o partajează la rândul lor.
▪ Întrebarea se transformă, așadar, în “când pot două procese diferite să
partajeze o pagină de memorie?”
1. când cele două procese referă au mapată aceeași pagină fizică (folosint
apeluri de formammap);
2. când cele două procese sunt înrudite și referă o pagină prin copy-on-
write;
3. când cele două procese au mapat același executabil/aceeași bibliotecă
(codul acestora este read-only și paginile fizice aferente sunt
partajate).

I/O – DISPOZITIVE DE INTRARE/IESIRE


1. Indicați două obiective ale algoritmilor de planificare a cererilor pentru hard disk, și
dați un exemplu de algoritm care le îndeplinește.
▪ Răspuns: Un obiectiv este performanță ridicată. Un alt obiectiv este fairness:
asigurarea că toate procesele au acces echitabil la resurse și că un proces nu
așteaptă mai mult ca altul accesul la disc. Un algoritm care colectează mai multe
cereri și apoi le sortează și agregă, independent de procesul care le cauzează va
atinge obiectivele. Algoritmi precum C-SCAN sau C-LOOK sau altele satisfac
aceste obiective.
2. Precizați două diferențe între un symbolic link și un hard link.
▪ Răspuns: Un symbolic link are un inode al său, pe când un hard link este un
dentry (un nume și un index de inode). Un symbolic link poate referi directoare
în timp ce un hard link nu; un symbolic link poate fereri un fișier de pe altă
partiție/alt sistem de fișiere, în timp ce un hard link nu.
1. Ce limitează performanța hard disk-ului în cazul accesului aleator la date din diverse
zone ale discului?
▪ Răspuns: Mutarea capului de citire pe sectoarele/zonele necesare. Dacă există
acces aleator, atunci datele vor fi plasate în diverse zone iar o operație va consta
în două suboperații:
a. plasarea capului de citire
b. citirea sau scrierea datelor respective
▪ Dacă datele sunt plasate aleator, operația de plasare va dura mult și va
limita performanța; putem optimiza prin ordonarea cererilor și limitarea
timpului de accesare. Operația de citire și scriere este standard, ține de
mecanica discului, nu o putem optimiza.
1. Un director conține N subdirectoare. Câte hard link-uri pointează la acest director?
▪ Răspuns: Directorul va avea N+2 hard link-uri. N hard link-uri sunt date de
intrarea .. (dot dot) a fiecărui subdirector (link către directorul părinte).
Celelalte două hard link-uri sunt numele directorului și intrarea . (dot) care
referă directorul însuși.
2. De ce la plăcile de rețea de mare viteză are sens folosirea polling în locul
întreruperilor pentru partea de intrare/ieșire?
▪ Răspuns: Având viteze mari, vor veni pachete foarte des și vor fi generate
întreruperi foarte des. În această situație, procesorul va fi ocupat foarte mult
timp rulând rutine de tratare a întreruperilor. Prin trecere la polling,
procesorul interoghează placa de rețea și, dacă are date, le citesțe repede,
fără întreruperi. În restul timpului face și alte lucruri, fără a mai consuma
timp în rutina de tratare a întreruperilor.
3. Care este un avantaj și un dezavantaj al alocării indexate (cu i-node) pentru blocuri
de date pentru fișiere?
▪ Răspuns: Principalul dezavantaj al alocării indexate este limitarea
dimensiunii fișierului la numărul de intrări din lista de indecși (pointeri către
blocuri). Avantajele este accesul rapid la blocuri (se citește indexul) și absența
fragmentării externe: blocurile se pot găsi oriunde și pot fi referite din lista de
indecși. Dezavantajul este compensat prin folosirea indirectării (simple, duble,
triple) ducând la o mai mare dimensiune a fișierului, dar introducând un alt
dezavantaj: timp mai mare de acces pentru blocurile din partea finală a
fișierului; întrucât se trece prin blocurile de indirectare. Un dezavantaj aici
poate fi și ocuparea de blocuri doar cu indecși, în loc să conțină date efective.
4. De ce operația lseek() nu are sens pe dispozitive de tip caracter, ci doar pe
dispozitive de tip bloc?
▪ Răspuns: Pentru că pe dispozitivele de tip caracter datele vin și sunt citite/
scrise octet cu octet, ca într-o țeavă. Nu putem anticipa date și ne putem
plasa mai sus sau mai jos pe banda de date. În cazul dispozitivelor de tip bloc
însă, datele se găsesc pe un spațiu de stocare pe care ne putem plimba/glisa;
putem "căuta" date prin plasarea pe un sector/bloc al dispozitivului de stocare
și atunci operația lseek() are sens.
5. Ce conțin blocurile de date aferente unui inode de tip director?
▪ Răspuns: Conțin un vector de dentry-uri. Un dentry este o structură ce
conține numele fișierului și indexul inode-ului aferent. Fiecare intrare din
director (indiferent de tipul acesteia: fișier, director, link symbolic) are
un dentry.
6. La ce este util buffer/page cache-ul?
▪ Răspuns: Datele accesate recent de pe disc/sistemul de fișiere sunt reținute
în memorie pentru acces rapid. Șansele sunt mari ca acele date să fie
reaccesate în viitor. Memoria fiind mult mai rapidă ca discul, se mărește viteza
de lucru a sistemului.
7. Care este un avantaj al alocării indexate față de alocarea contiguă la nivelul
sistemului de fișiere?
▪ Răspuns: Alocarea indexată reduce fragmentarea externă: un fișier poate
folosi blocuri din poziții aleatoare de pe disc.
8. De ce în cadrul unei plăci de rețea de mare viteză (10Gbit) este problematic să se
folosească un model bazat DOAR pe întreruperi? (în general se folosește un model
hibrid de întreruperi și polling)
▪ Răspuns: Pentru că pachetele vin cu vitează foarte mare și generează multe
întreruperi. În cazul în care s-ar folosi doar un sistem bazat pe întreruperi, ar
exista riscul ca procesorul să fie ocupat doar de rularea de rutine de tratare a
întreruperilor.
9. Ce este un hard link?
▪ Răspuns: Un hard link se referă la situația în care avem mai multe intrari in
directoare (dentry-uri) care pointeaza catre acelasi fișier pe disc (inode). Un
nume sau un dentry denotă un hard link.
10. Care este o caracteristică a unui dispozitiv de tip bloc?
▪ Răspuns: Un dispozitiv de tip bloc permite acces aleator la date, nu
secvențial ca în cazul unui dispozitiv de tip caracter. De asemenea, un
dispozitiv de tip bloc lucrează cu blocuri de date, nu cu câte un caracter/byte
așa cum este cazul unui dispozitiv de tip caracter.
11. De ce este importantă ordonarea cererilor în cadrul unui planificator de disk (disk
scheduler)?
▪ Răspuns: Dacă cererile nu sunt ordonate, se fac multe operații de căutare
(seek) pe disk pentru fiecare cerere, ceea ce înseamnă timp consumat. Prin
ordonarea cererilor timpul de căutare (seek) este minimizat: se trece, în
ordine, de la un bloc la alt bloc.
12. Care este un avantaj al folosirii operațiilor I/O asincrone?
▪ Răspuns: După momentul lansării unei operații I/O asincrone, sistemul/
procesul poate executa alte operații, nu trebuie să se blocheze în așteptarea
încheierii acesteia. Acest lucru conduce la o eficiență sporită a sistemului.
13. Ce este un inode? Ce informații conține (în linii mari)?
▪ Răspuns: Este o structură/tip de date care identifică un fișier pe disc. Un
inode identifică orice fișier (fișier obișnuit, director, link simbolic) și conține
metadate despre un fișier: permisiune de acces, deținător, timestamp-uri,
dimensiune, contor de link-uri, pointeri la blocurile de date etc.
14. Cu ce diferă o operație I/O sincronă de o operație I/O blocantă?
▪ Răspuns O operație sincronă întoarce rezultatele prezente în acel moment,
indiferent dacă operația s-a încheiat sau nu. O operație blocantă blochează
procesul curent.
15. Care sunt cele două argumente ale unei instrucțiuni de tipul IN sau OUT (pentru
lucrul cu port-mapped I/O)?
▪ Răspuns Cele două argumente sunt registrul procesorului și portul
dispozitivului I/O.
16. De ce nu are sens sortarea operațiilor I/O pentru dispozitive caracter (char devices)?
▪ Răspuns Dispozitivele de tip caracter obțin datele în format secvențial, astfel
că operațiile I/O nu pot fi sortate.
17. Prezentați un avantaj al folosirii întreruperilor în fața polling-ului.
▪ Răspuns Polling-ul este un mecanism de tip busy-waiting, deci procesorul va
fi ținut ocupat în așteptarea datelor. În schimb, întreruperile nu țin procesorul
ocupat, oferind o utilizare mai bună a acestuia.
18. Cum se modifică numărul de inode-uri ocupate, respectiv numărul de dentry-uri de
pe o partiție în cazul creării unui fișier nou obișnuit (regular file)? Explicați.
▪ Răspuns În cazul creării unui fișier nou obișnuit (regular file), se va crea un
dentry nou, în cadrul directorului părinte, și va fi ocupat un inode.
19. Fie afirmația “Spațiul ocupat pe disc de un director este constant.” Precizați și
justificați valoarea de adevăr a afirmației.
▪ Răspuns Afirmația este falsă. Spațiului unui director pe disc crește pe
măsură ce apar intrări noi (dentry-uri) în cadrul directorului.
20. Cum se modifică numărul de inode-uri, respectiv dentry-uri de pe o partiție în cazul
creării unui director nou? Explicați.
▪ Răspuns În cazul creării unui director nou, se vor crea trei dentry-uri noi
(unul în cadrul directorului părinte și două: ”.” și ”..” în cadrul directorului nou
creat), și va fi ocupat un inode.
21. Cum se modifică numărul de inode-uri, respectiv dentry-uri de pe o partiție în cazul
creării unui hard-link? Explicați.
▪ Răspuns În cazul creării unui hard-link, se va crea un dentry nou, în cadrul
directorului părinte, fără a se modifica numărul de inode-uri.
22. Fie un program multithreaded cu user-level threads care efectuează multe operații I/
O per thread. Este mai eficientă folosirea operațiilor I/O blocante sau non-blocante?
▪ Răspuns: Efectuarea unei operații blocante în cadrul unei implementări
cu user-level threads va bloca întreg procesul, nu doar thread-ul curent. De
aceea este mai eficientă folosirea operațiilor I/O non-blocante.
23. Fie afirmația “Întreruperile pot fi folosite la fel de bine și pentru dispozitive care
folosesc memory-mapped I/O și pe sisteme care folosesc port-mapped I/O”. Precizați
și justificați valoarea de adevăr a afirmației.
▪ Răspuns: Întreruperile sunt semnale folosite de către dispozitive pentru a
semnala procesorului finalizarea unei operații de I/O. Acesta va salva starea
curentă și va rula rutina de tratare a întreruperii. Utilizarea memory-mapped
I/O sau a port-mapped I/O determină doar modul de adresare a dispozitivelor
în cadrul rutinei de tratare, fără a afecta eficiența utilizării întreruperilor.
Astfel, afirmația este adevărată.
24. Care este principala caracteristică a unui dispozitiv de tip caracter? Dați 2 exemple
de astfel de dispozitive.
▪ Răspuns: Dispozitivele de tip caracter oferă acces secvențial și transfer de
date la nivel de caracter, astfel au viteză redusă.
▪ Exemple: tastatură, mouse, game controller, port serial, terminal,
25. Care sunt cele două caracteristici importante ale unui dispozitiv de tip bloc? Dați 2
exemple de astfel de dispozitive.
▪ Răspuns: Dispozitivele de tip bloc oferă acces aleator și transfer de date la
nivel de bloc, astfel au o viteză ridicată.
▪ Exemple: discuri (hard-disk, floppy, unități optice, unități flash),
sisteme de fișiere, memoria RAM.
26. Care sunt dentry-urile existente în orice director pe un sistem de fișiere ext2/3 și ce
reprezintă acestea?
▪ Răspuns: Orice director are intrările . și .. care sunt dentry-uri către
directorul curent, respectiv directorul părinte.
27. Prezentați un avantaj și un dezavantaj al utilizării sistemelor de fișiere jurnalizate.
▪ Răspuns:
▪ Avantaj: menținerea unui log al acțiunilor făcute asupra sistemului de
fișiere, log ce poate fi consultat în cazul unei defecțiuni și poate ajuta
la recuperarea datelor afectate în timpul defecțiunii.
▪ Dezavantaj: este overhead-ul adus în procesare și spațiul pe suportul
fizic consumat pentru menținerea jurnalului
28. Fie o implementare de inode care reține următoarele valori: permisiuni, număr de
link-uri, uid, gid, dimensiune și zone directe. Fie fișierul ”a” și un hard link către
acesta, ”b”. Prin ce diferă inode-urilecelor două fișiere?
▪ Răspuns: Un hard link reprezintă un nou dentry care referă același inode,
deci inode-urile celor două fișiere sunt identice.
29. Pe un sistem se dorește conservarea numărului de inode-uri. Care din cele două
tipuri de link-uri,hard sau sym, trebuie folosite în acest caz și de ce?
▪ Răspuns: Un hard link nu generează inode-uri noi, pe când un sym link este
un inode nou, astfel că trebuie folosite hard link-uri.
30. Cine/ce generează întreruperi și cine/ce le tratează?
▪ Întreruperile sunt generate de dispozitivele hardware la apariția unui
eveniment specific (primirea de date/eliberarea unui buffer local, caz de
eroare).
▪ Tratarea întreruperilor este realizează de procesor în cadrul unei rutine de
tratare a întreruperii (IRQ handler sau ISR – Interrupt Service Routine).
31. Dați exemplu de funcție de API care efectuează operații I/O sincrone și o funcție care
efectuează operații I/O asincrone.
▪ Operații I/O sincrone: read, write. Sunt funcții la apelul cărora se execută
operația de I/O.
▪ Operații I/O asincrone: aio_read, aio_write, Overlapped I/O, select.
▪ Primele 3 sunt operații asincrone, neblocante. Asincrone – declanșează
o operația I/O fără a urmări “sincron” execuția acesteia; neblocante –
nu se blochează în așteptarea încheierii operației.
▪ select – nu este cu adevărat o operație I/O, ci un apel de control a
acestora. Poate fi considerată asincronă pentru că nu urmărește
execuția unui set de operații; fie sincrone sau asincrone, select este
un apel blocant care notifică încheierea unei operații I/O.
32. Care este scopul sortării cererilor de I/E pe care le face un sistem de operare atunci
când lucrează cu discul?
▪ Sortarea cererilor de I/E rezultă în organizarea acestora după sectorul de pe
disc din care fac parte.
▪ Prin sortarea acestora se minimizează timpul de căutare și poziționarea acului
mecanic pe sector corespunzător. În loc să se rotească discul înainte și înapoi
pentru poziționare pe capătul/sectorul/platanul corespunzător, se parcurg
liniar/secvențial sectoarele descrise în cereri.
▪ Rezultă, astfel, un overhead redus al căutării (seek) pe disc în cadrul cererilor
de I/E – performanță îmbunătățită.
33. Cu ce diferă o operație I/O asincronă de o operație I/O neblocantă?
▪ O operație I/O asincronă este pornită dar nu se așteaptă încheierea acesteia.
În momentul încheierii operației, se trimite o notificare.
▪ O operație I/O neblocantă se întoarce imediat. Dacă este sincronă atunci va
citi/scrie cât îi oferă dispozitivul. Dacă este asincronă, va primi notificare la
încheierea operației.
▪ Diferența constă, în general, în primirea sau nu a unei notificări, la sfârșitul
încheierii operației de I/O. Acest lucru se întâmplă tot timpul la o operație I/O
asincronă; la o operație I/O neblocantă se întâmplă doar dacă aceasta este
asincronă – dacă este sincronă (read cu O_NONBLOCK) atunci nu se primește
notificare, doar se întoarce după ce a scris/citit cât i-a oferit dispozitivul.
34. Care este forma de asociere între dentry și inode? (unu la unu, mai multe la mai
multe etc.)
▪ Un dentry conține un nume și un inode number. Două sau mai multe dentry-
uri pot referi același inode, prin intermediul inode number-ului (denumite și
hard link-uri). Asocierea este mai multe dentry-uri la un inode.
35. De ce este mai eficientă folosirea unei tabele FAT în locul alocării cu liste pentru
gestiunea blocurilor libere ale unui sistem de fișiere?
▪ Alocarea cu liste presupune existența, la sfârșitul fiecărui bloc, a unui pointer
către următorul bloc de date. Acest lucru înseamnă că, pentru a ajunge la al
N-lea bloc de date, trebuie parcurse (citite de pe disc și aduse în memorie)
N-1 blocuri de date.
▪ În cazul tabelei FAT toți pointerii sunt ținuți localizat în cadrul tabelei. La o
adresă specifică unui bloc se găsește un pointer către o altă adresă (specifică
altui bloc). Overhead-ul/timpul de citire a unui bloc de date este, astfel, mult
redus.
▪ Tabela FAT, fiind localizată, poate fi stocată în cache-ul de memorie, mărind
astfel viteza de acces la resursele aferente.
36. De ce nu se poate crea un hard link către un inode de pe alt sistem de fișiere?
▪ Un hard link este un dentry; conține un nume și un număr de inode.
▪ Întrucât două sisteme de fișiere diferite pot avea alocat același inode, este
ambiguă prezența unui număr de inode ce referă al sistem de fișiere. Numărul
de inode din cadrul dentry-ul aferent va referi inode-ul de pe sistemul curent
de fișiere.
37. În cazul alocării indexate, inode-ul conține un număr limitat de pointeri către blocuri
de date, limitând astfel dimensiunea maximă a unui fișier. Cum este rezolvată
această problemă?
▪ Problema este rezolvată prin indirectare. O parte din pointerii din cadrul
inode-ului nu vor referi blocuri de date, ci vor referi blocuri cu pointeri către
blocuri de date (indirectare simplă) sau blocuri cu pointeri către blocuri cu
pointeri către blocuri de date (indirectare dublă) etc. Dezavantajul este o
latență mai mare de acces pentru blocurile finale.
NETWORKING IN SISTEMUL DE
OPERARE
1. O aplicație execută un apel send() cu 1024 de octeți de date, iar
apelul send întoarce 1024. Alegeți varianta corectă de mai jos și argumentați: În
acest moment, aplicația sender poate fi sigură că datele au fost livrate cu succes
către
▪ nucleul SO de pe sistemul destinație
▪ aplicația destinație
▪ datele au fost salvate în send-buffer-ul de pe sistemul transmițătorului
▪ Răspuns: Datele au fost salvate în buffer-ul de send al transmițătorului. În
momentul în care datele au fost scrise acolo, apelul se întoarce. Este posibil ca
datele să nu fi părăsit sistemul, dar apelul se va întoarce. Stiva TCP se va ocupa
de transmiterea datelor din buffer-ul de send către destinație.
2. E x p l i c a ț i m o t i v u l p e n t r u c a r e e s t e i n d i c a t s ă f o l o s i m p e n t r u
apelurile send() și recv() buffere de dimensiuni mari (de exemplu mai mari
decât 100KB).
▪ Răspuns: Ca să transmitem mai multe date o dată si să evităm apelurile de
sistem generate de apelul send și recv. Un apel de sistem va însemna
overhead de timp (intare în kernel mode și apoi revenire în user mode) și
overhead de copiere (transfer de date din buffer-ul din user space în buffer-ul
din kernel space sau invers).
3. În ce situație operația send() pe un socket se blochează?
▪ Răspuns: Apelul send() pe socket se blochează în situația în care buffer-ul
din kernel (send buffer) nu dispune de loc pentru copierea datelor din buffer-
ul de user space. Sau, în anumite cazuri, precum în cazul sockeților non-
blocanți, dacă nu există nici măcar 1 octet liber în buffer-ul de kernel (send
buffer).
4. În cazul unui apel recv() comandat pentru citirea a 789 de octeți, se citesc 123 de
octeți. Cum se explică citirea unui număr mai mic de octeți decât cel comandat?
▪ Răspuns: În momentul citirii datelor, doar 123 de octeți erau disponibili în
buffer-ul din kernel aferent socket-ului (receive buffer). În această situație
apelul recv() se întoarce cu numărul de octeți disponibili (123) deși exista
spațiu mai mare (789) în buffer-ul din user space.
5. În ce situație se poate bloca un apel send pe un socket?
▪ Răspuns: Apelul send pe socket se blochează dacă buffer-ul de send
(transmit, TX) al socketului este plin, adică dacă nu are nici un slot de un
octet disponibil. Buffer-ul este plin pentru că nu au apucat să fie transmise
pachetele pe rețea (placă de rețea lentă, congestie sau receiver-ul are și el
buffer-ul plin).
6. De ce este considerat sendfile un mecanism de tip zero-copy?
▪ Răspuns: sendfile transmite un fișier (sau parte a unui fișier) pe un socket.
Întrucât nu există copieri între user space și kernel space, așa cum ar fi cazul
unor operații de tipul read și send, sendfile este un mecanism de tip zero-
copy.
7. Ce valoare (numărul de octeți transmiși) poate întoarce un apel de forma send(s,
buffer, 1000, 0)? Apelul urmărește transmiterea unui buffer de 1000 de octeți pe
socketul s.
▪ Răspuns: Apelul send poate întoarce între 1 și 1000 de octeți. Întoarce
numărul de octeți disponibili (între 1 și 1000) când are date disponibile. Dacă
nu are date disponibile și celălalt capăt nu a închis conexiunea, se blochează.
La eroare sau când celălalt capăt a închis conexiunea, se întoarce cu eroare
(-1).
8. accept este un apel blocant pe partea server-ului. Ce apel din partea clientului
deblochează apelul accept? De ce?
▪ Răspuns: Apelul connect din partea clientul este cel care stabilește o
conexiune la server. În acest caz apelul accept se întoarce și creează un nou
socket care va fi folosit pentru transmisia datelor.
Sisteme de operare
6 septembrie 2009

Timp de lucru: 70 de minute

NOTĂ: toate răspunsurile trebuie justificate

1. Un sistem dispune de următoarele caracteristici
• magistrala de date pe 64 de biți
• overhead­ul impus de un page fault este de 1ms
• nu dispune de memorie cache
• durata unei operații cu memoria este de 100ns
Pe sistem rulează un sistem de operare în cadrul căruia biblioteca standard C folosește un buffer intern de 64K la nivelul fiecărui handle de 
fișier.
Care din operațiile marcate aldin (bold) de mai jos durează mai mult?
char buf[32*1024]; char buf[32*1024];
f = fopen(“a.dat”, “wb”); int fd;
/* fill buffer */ char *a;
… fd = open(“a.dat”, O_RDWR | O_CREAT | O_TRUNC,
fwrite(f, buf, 32*1024); 0644);
fflush(f); /* fill buffer */
fwrite(f, buf, 32*1024); …
write(fd, buf, 32*1024);
a = mmap(NULL, 32*1024, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
memcpy(a, buf, 32*1024);

Operația fwrite înseamnă copierea datelor din buf în bufferul intern al bibliotecii standard C. Întrucât bufferul intern oferă spațiu pentru  
tot buffer­ul nu va exista nici un apel de sistem și nici pagefault­uri.

Operația memcpy va presupune copierea datelor într­o zonă alocată cu mmap. Durata de copiere este identică celei de mai sus, dar au loc  
page fault­uri la fiecare pagină. Aceasta se întâmplă pentru că mmap alocă memorie virtuală pură (demand paging). Primul acces la o  
pagină va conduce la page fault.

În concluzie, operația memcpy durează mai mult decât fwrite.

2. În cadrul problemei celor 5 filozofi se folosește următoarea implementare (în pseudo­C) a funcției eat():

mutex_t global_mutex;

void eat(int fork1, int fork2)


{
lock(global_mutex);
take_fork(fork1);
take_fork(fork2);
do_eat();
put_fork(fork1);
put_fork(fork2);
unlock(global_mutex);
}

Care este neajunsul acestei implementări?

Folosirea mutexului global înseamnă că un singur filozofi din cei 5 poate mânca la un moment dat. Fiind 5 furculițe, soluția eficientă  
permite ca doi filozofi să mănânce simultan.

3. Pe un sistem rulează 50 de procese. La un moment dat este pornită o mașina virtuală VMware Server pe care rulează 50 de procese. Câte  
procese vor rula pe sistemul gazdă? Dar în cazul pornirii unei mașini virtuale OpenVZ pe care rulează 50 de procese?

O mașină virtuală VMware Server este reprezentată pe sistemul gazdă de un singur proces. Vor exista, în total, 51 de procese.

O mașină virtuală (container) OpenVZ are, pe sistemul fizic, un corespondent pentru fiecare proces. Vor exista, în total, 100 de procese.

4. Pe un sistem de fișiere MINIX se execută operațiile

lseek(fd, SEEK_SET, 32*1024);


write(fd, buffer, 1024);

Descrieți operațiile asociate asupra structurilor sistemului de fișiere (inode, bitfield, data block, dentry etc.)
lseek nu modifică structurile interne ale sistemului de fișiere. Este poziționat cursorul de fișier (corespondent unei structuri din memorie) la  
offsetul specificat.

Se calculează  blocul  aferent adresei 32*1024  prin parcurgerea pointerilor de bloc din inode­ul MINIX.  Întrucât se depășește spațiul  


referibil prin referință directă se va citi blocul aferent pentru referință indirectă.

Se parcurge bitfield­ul și să găsește primul bloc liber. Adresa acelui bloc este scrisă pe poziția aferentă din blocul de referință indirectă. Se  
citește blocul de pe disc în memorie și se scrie informația furnizată din user­space. Se actualizează câmpul size din inode. Ulterior se va  
face flush la bloc din memorie pe disc.

5. Un sistem folosește un planificator round­robin non­preemptiv. În cadrul sistemului rulează 5 procese care execută următoarea secvență:
   [ 10ms rulare | 10ms așteptare | 10ms rulare ]
Cât durează planificarea celor 5 procese?

Planificare round­robin înseamnă că fiecare proces este planificat pe rând. Planificarea se face astfel:
0­10ms: procesul 1
10ms­20ms: procesul 2 (procesul 1 așteaptă)
20ms­30ms: procesul 3 (procesul 2 așteaptă, procesul 1 este gata de execuție)
30ms­40ms: procesul 4 (procesul 3 așteaptă, procesul 1 și procesul 2 sunt gata de execuție)
40ms­50ms: procesul 5 (procesul 4 aștepată, procesele 1, 2 și 3 sunt gata de execuție)
….
Durata de planificare este de 100ms

6. Cum se modifică spațiul de adresă al unui proces la schimbarea de context între două thread­uri?

Nu se modifică. Thread­urile partajează spațiul de adresă al procesului.

7. Are sens folosirea operațiilor asincrone în locul celor sincrone în situația de mai jos (pseudo­cod)?

AIO_TYPE aioArray[32];
InitializeAsyncIoArray(aioArray);
for (i = 0; i < 32; i++)
StartAsyncIo(aioArray[i]);
WaitForAllObjects(aioArray);

Cele 32 de operații asincrone sunt pornite în același timp. Durata de așteptare este durata de rulare/planificare a celei mai lente operații.  
În  cazul  unei   operații   sincrone   (blocante,   secvențiale).   Durata   de  așteptare  ar  fi   fost   suma   duratelor   de  rulare/planificare   a   tuturor  
operațiilor.

8. Descrieți o situație în care operația:
memcpy(a, “12345678901234567890”, 20);
durează mai mult, respectiv mai puțin, decât operația
getpid();
Operația getpid rezultă într­un apel de sistem. Overhead­ul unui page fault este de 5ms, iar a unui apel de sistem de 7ms.

Dacă zona indicată de a (20 de octeți) este poziționată într­o singură pagină overhead­ul este de 5ms în cazul unui page fault (pagina nu  
este prezentă în memoria fizică) sau neglijabil în cazul în care pagina este prezentă în memorie. Durează, astfel, mai puțin decât getpid().

Dacă zona indicată de a (20 de octeți) este poziționată pe două pagini (spre exemplu 8 octeți în prima, 12 în a doua), overhead­ul (în cazul  
absenței pagininilor din memoria fizică) este de 10ms.

9. Pe o arhitectură x86, care registre generale (eax, ebx, ecx, edx, esi, edi, ebp, esp) sunt schimbate în cazul unei schimbări de context între 
două thread­uri? Dar în cazul unei schimbări de context între două procese?

În ambele cazuri se schimbă tot setul de registre, întrucât definesc un nou context.

10. Se presupune că se împlementează la nivel hardware o tehnică ce împiedică accesarea zonelor de memorie nealocate la nivel de octet.  
Cum poate fi folosită această tehnică pentru prevenirea atacurilor de tip buffer overflow la nivelul stivei?

Nu previne. Atacul de tip buffer overflow înseamnă suprascrierea stivei sau a altei regiuni deja alocate și a adresei de retur (și aceasta  
alocată pe stivă). Protecția la accesarea unor zone nealocate nu împiedică acest tip de atac.

11. Într­un sistem de operare, 4 (patru) procese execută operațiile

fd1 = open(“a.txt”, O_RDONLY);


fd2 = open(“b.txt”, O_RDWR);
a1 = mmap(NULL, 4*1024, PROT_READ, MAP_SHARED, fd1, 0);
a2 = mmap(NULL, 4*1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd2, 0);
printf(“%c”, *a1);
*a2 = ‘a’;
Câte pagini de memorie fizică, respectiv virtuală vor fi ocupate în urma operațiilor de mai sus?

Fiecare proces accesează cele două pagini alocate. În urma accesului se realizează un page fault și se mapează paginile virtuale peste  
paginile fizice.

Se vor aloca 4 procese * 2 pagini virtuale = 8 pagini virtuale

Pentru fiecare pointer (a1 sau a2) se mapează aceeași pagină fizică, Maparea este partajată (MAP_SHARED) și toate procesele vor vedea  
același conținut. În cazul particular al mapării a2 (PROT_WRITE) scrierile din cadrul unui proces vor fi vizibile în celelalte procese.

Se vor aloca 1 pagini fizică (prin a2) + 1 pagină fizică (prin a1) = 2 pagini fizice
Sisteme de operare
10 septembrie 2009

Timp de lucru: 70 de minute

NOTĂ: toate răspunsurile trebuie justificate

1. Care din următoarele acțiuni consumă cel mai mult timp în cazul unei schimbări de context între două thread­uri ale aceluiași proces:
• schimbarea registrelor
• flush TLB
• schimbarea tabelei de pagini
• schimbarea tabelei de descriptori de fișier

În cazul schimbării de context între două thread­uri ale aceluiași proces nu se face flush la TLB, nu se schimbă tabela de pagini, nu se  
schimbă   tabela   de descriptori   de  fișier.  În  consecință,  deși  foarte  rapidă,   acțiunea   care  consumă   cel  mai mult   timp  este  schimbarea  
registrelor.

2. Un sistem dispune de magistrală de date și registre pe 32 de biți (sizeof(unsigned long) = 32). Sistemul folosește paginare simplă (non­
ierarhică) și nu are memorie cache, nici TLB. Sțiind că un acces la memorie durează 50ns iar o pagină este de 4KB, cat va dura secvența de 
mai jos? Se presupune că vectorul buffer este alocat în RAM (memoria fizică) și că valoarea contorului i se păstrează într­un registru (nu 
folosește memoria).

unsigned long buffer[32*1024];

for (i = 0; i < 32 * 1024; i++)


buffer[i] = i;

În absența TLB fiecare acces la memoria fizică necesită accesarea tabelei de pagini (aflată tot în memoria fizică). Astfel, pentru fiecare  
dintre cele 32*1024 de accese la buffer vor exista încă 32*1024 accese la tabele de pagini. Rezultă 64*1024 accese la memorie cu o durată  
totală de 64*1024*50ns.

3. Care dintre variantele chroot, respectiv OpenVZ oferă un grad mai mare de securitate?

chroot oferă “încapsulare” (securizare) doar la nivelul sistemului de fișiere. OpenVZ oferă securizare la nivelul proceselor, memoriei,  
procesorului, rețelei, utilizatorilor etc. OpenVZ oferă, așadar un grad mai mare de securitate.

4. În cadrul unui proces cu mai multe thread­uri, un thread execută următoarea secvență de cod (pseudo C):

int esp;
int stack_val;

/* se obtine valorea registrului esp (registru de stiva) al thread-ului planificat anterior */


esp = get_former_esp();
stack_val = *(esp + 4);

Care va fi rezultatul execuției secvenței de mai sus pe un sistem în care stiva crește în jos?

Întrucât stiva crește în jos, valoarea esp+4 va puncta către o zonă alocată din stiva fostului thread. Execuția de mai jos va rezulta în  
obținerea acelei valori (nu se va obtine segmentation fault decât dacă thread­ul anterior și­a încheiat execuția).

5. Pe un sistem de fișiere MINIX se execută operația:

fd = open(“a.txt”, O_RDWR | O_CREAT | O_TRUNC, 0644);

Precizați ce se întâmplă la nivelul sistemului de fișiere (inode, inode bitmap, zone bitmap, data block, dentry etc.) în cazul în care fișierul 
există sau nu există pe disc.

Dacă fișierul există se găsește dentry­ul acestuia și se obține inode­ul aferent și se citește inode­ul în memorie.
Dacă fișierul nu există se creează un inode nou. Pentru aceastase parcurge bitmapul de inode­uri și se alocă un inode. Se completează cu 1  
poziția liberă găsită. Se creează un dentry cu numele “a.txt”.
Dacă fișierul exista este trunchiat.  Se parcurg pointer­ii de blocuri ai inode­ului  și se marchează cu NULL (sau ceva echivalent). Se  
parcurge bitmapul de blocuri și marchează cu 0 pozițiile aferente acelor blocuri. Dacă fișierul avea mai mult de 7 blocuri se citește și 
completează cu NULL blocul pentru dereferențieri simple.

6. Precizați o soluție de sincronizare pentru problema formării moleculei de oxid de fier (Fe2O3) (ca alternativă la problema formării apei).
void fe_fun() void o_fun()
{ {
m.enter(); m.enter();
fe_count++; o_count++;
if (o_count >= 3) { if (o_count >= 3 && fe_count >= 2) {
if (fe_count < 2) m.o_cond.signal();
m.fe_cond.wait(); m.o_cond.signal();
else { m.fe_cond.signal();
m.o_cond.signal(); m.fe_cond.signal();
m.o_cond.signal(); if (o_count > 3) {
m.o_cond.signal(); m.o_cond.signal();
m.fe_cond.signal(); m.o_cond.wait();
fe_count ­= 2; }
} }
} else
else m.o_cond.wait();
m.fe_cond.wait(); m.leave();
m.leave(); }
}

7. Biblioteca standard C oferă programatorului funcția calloc (alocare cu zeroing). De ce este nevoie și de oferirea funcției malloc?

Funcția malloc este mai rapidă – nu face zeroing și permite demand paging.

8. Pe un sistem care dispune de 3 pagini fizice (frames) și folosește un algoritm de înlocuire a paginii de tip NRU se execută următoarea 
secvență:

1r, 2w, 4r, 1w, 3r, 2w, 1w, 4w, 3r, 3w, 1r, 2r, 5r, 2w, 6r, 3w, 1w, 2r

Câte page fault­uri au loc? 1r înseamnă operație de citire în cadrul paginii virtuale 1; 2w înseamnă operație de scriere în cadrul paginii 
virtuale 2.

Conținutul celor 3 pagini fizice, împreună cu evenimentul aferent este prezentat, evolutiv, în tabelul de mai jos; la fiecare două pagefault­
uri se resetează bitul referenced):
frame 1r (R) 1r (R) 1r 1w  3r (R) 3r (R) 3r 3r (R) 3w  3w  2r (R) 2r (R) 2w  6r (R) 6r (R) 6r 2r (R)
1 (W) (RW) (W) (W)
frame 2w  2w  2w  2w  2w  4w  4w  4w  4w  4w  5r (R) 5r (R) 5r 3w  3w  3w 
2 (W) (W) (W) (W) (W) (W) (W) (W) (W) (W) (W) (W) (W)
frame 4r (R) 4r (R) 4r 1w  1w  1w  1w  1w 1w 1w  1w  1w 1w  1w  1w 
3 (W) (W) (W) (W) (RW) (RW) (W) (W) (W) (W) (W) (W)
PF PF PF PF PF PF PF PF PF PF PF PF PF PF

9. Fie secvența de program de mai jos:

int main(void)
{
char *a;
int i;

for (i = 0; i < 10; i++)


a = malloc(1);

return 0;
}

La rulare se observă (prin folosirea unui profiler) că primul apel malloc durează semnificativ mai mult decât celelalte 9. Care este motivul? 
Toate apelurile reușesc (întorc o adresă validă) și nu există nici o modificare adusă apelului malloc.

Primul apel malloc generează un page fault, urmarea fiind alocarea unei pagini fizice întregi (chiar dacă se solicită alocarea unui singur  
octet). Următoarele apeluri vor aloca octeți din cadrul aceleiași pagini – nu mai este generat un page fault și nu se aloca alte pagini.

10. Are sens folosirea operațiilor de tipul Overalapped I/O pe un sistem care dispune de un singur hard­disk?

Da. Operațiile overlapped I/O permit o planificare mai eficientă a operațiilor de I/O la nivelul nucleului și permit aplicației să ruleze 

11. Descrieți o situație în care două procese partajează o pagină virtuală (din spațiul virtual de adrese).

Fiecare proces are propriul spațiu de adrese. Nu există noțiunea de partajare a unei pagini virtuale.
Sisteme de operare
25 iunie 2009
Timp de lucru: 70 de minute
NOTĂ: toate răspunsurile trebuie justificate

1. "Sistemele de operare moderne nu au probleme de fragmentare externă a memoriei fizice alocate din user­space." Indicați și 
motivați valoarea de adevăr a propoziției anterioare. 

Sistemele de operare moderne  folosesc suportul de paginare pus la dispoziție de sistemul de calcul. Folosirea paginării  
înseamnă că se pot aloca ușor pagini de memorie fizică acolo unde sunt libere. Mecanismul de memorie virtuală asigură  
faptul că o alocare rămâne virtual contiguă. În felul acesta dispar problemele de fragmentare externă – adică de găsire a  
unui spațiu continuu pentru alocare (rămân însă problemele de fragmentare internă).
Excepție fac alocările din kernel­space care pot solicita alocare de memorie fizic contiguă sau alocările impuse de hardware  
(de exemplu DMA).

2. Un sistem de operare dispune de un planificator de procese care folosește o cuantă de 100ms. Durata unei schimbări de 
context este 1ms. Este posibil ca planificatorul să petreacă jumătate din timp în schimbări de context? Motivați.

Da, este posibil în situațiile în care procesele planificate execută acțiuni scurte și apoi se blochează determinând schimbări de  
context. Acest lucru se poate întâmpla în cazul sincronizării între procese (un proces P1 execută o acțiune, apoi trezește  
procesul P2 și apoi se blochează, procesul P2 execută o acțiune, apoi trezește procesul P1, etc.), sau în cazul comunicației cu  
dispozitive de I/O rapide (procesul P1 planifică o operație I/O și se blochează, operația se încheie rapid și trezește procesul  
etc.).
O   altă   situație   este   schimbarea   rapidă   a   priorității   proceselor   care   determină   schimbarea   de   context   pentru   rularea  
procesului cu prioritatea cea mai bună.
3. Dați exemplu de funcție care este reentrantă, dar nu este thread­safe. Dați exemplu de funcție care este thread­safe, dar nu 
este reentrantă. 

Toate  funcțiile  reentrante  sunt   thread­safe.   Exemplu   de  funcție  care   este  thread­safe  dar   nu  reentrantă  este  malloc.  Un  
exemplu generic este o funcție care folosește un mutex pentru sincronizarea accesului la variabile partajate între thread­uri:  
funcția este thread­safe, dar nu este reentrantă (nu pot fi executate simultan două instanțe ale acestei funcții). Proprietatea de  
reentranță sau thread­safety se referă la implementarea și interfața funcției, nu la contextul în care este folosită (o funcție  
reentrantă poate fi folosită într­un context unsafe din punct de vedere al sincronizării, dar nu înseamnă că este non­thread  
safe).
4. Într­un sistem de fișiere FAT un fișier ocupă 5 blocuri: 10, 59, 169, 598, 1078. Știind că:
• un bloc ocupă 1024 de octeți 
• o intrare în tabela FAT ocupă 32 de biți 
• tabela FAT NU se găsește în memorie 
• copierea unui bloc în memorie durează 1ms 
cât timp va dura copierea completă a fișierului în memorie?

Un bloc ocupă 1024 de octeți, o intrare în tabela FAT 4 octeți, deci sunt 256 intrări FAT într­un bloc. În tabela FAT intrările  
10, 59, 169 se găsesc în primul bloc, intrarea 598 în al treilea bloc și 1078 în al cincilea bloc. Vor trebui, astfel, citite 3  
blocuri asociate tabelei FAT. Fișierul ocupă 5 blocuri, deci vor fi citite, în total, 8 blocuri. Timpul total de copiere este 8ms.

5. Două procese P1, respectiv P2 ale aceluiași utilizator sunt planificate după cum urmează: 
  
fd = open("/tmp/a.txt", O_CREAT | O_RDWR, 0644); 
write(fd, “P1”, 2); 
  ­­­ schedule ­­­
  ­­­ schedule ­­­
fd = open("/tmp/a.txt", O_CREAT | O_RDWR, 0644); 
write(fd, “P2”, 2);
 
Ce va conține, în final, fișierul /tmp/a.txt? Ce va conține fișierul în cazul în care se folosesc thread­uri în loc de procese? 

Două apeluri open întorc descriptori către structuri distincte de fișier deschis. Acest lucru înseamnă că fiecare descriptor va  
folosi un cursor de fișier propriu. Al doilea apel open va poziționa cursorul de fișier la începutul fișierului și va suprascrie  
mesajul primului proces. În final în fișier se va scrie P2. În cazul folosirii thread­urilor situația este neschimbată pentru că se  
vor folosi, din nou, cursoare de fișier diferite.
6. Are sens folosirea unui sistem de protejare a stivei (stack smashing protection, canary value) pe un sistem care dispune de și 
folosește bitul NX?

Da, are sens. În general, sistemele de tip stack overflow suprascriu adresa de retur a unei funcții cu o adresă de pe stivă. Bitul  
NX  previne   execuția   de   cod   pe  stivă.  Dar   adresa  de  retur  poate  fi  suprascrisă  cu   adresa  unei   funcții   din  zona  de   text  
(return_to_libc attack) sau o adresă din altă zonă care poate fi executată (biblioteci, heap).

7. Pe un sistem quad­core și 4GB RAM rulează un proces care planifică 3 thread­uri executând următoarele funcții: 
 
thread1_func(initial_data) thread2_func()  thread3_func() 
{ {  { 
    for (i = 0; i < 100; i++) {      for (i = 0; i < 100; i++) {      for (i = 0; i < 100; i++) { 
        work_on_data();         wait_for_data_from_thread1();          wait_for_data_from_thread2(); 
        wake_thread2();         work_on_data();          work_on_data(); 
        wait_for_data_from_thread3();          wake_thread3();          wake_thread1(); 
    }      }      } 
}  }  } 

Care este dezavantajul acestei abordări? Propuneți o alternativă.

Codul de mai sus este un cod serial. Folosirea celor trei thread­uri este ineficientă pentru că se execută mai ușor în cadrul  
unui singur thread (apar overhead­uri de creare, sincronizare și schimbare de context între thread­uri). Soluția este folosirea  
unui singur thread sau reglarea algoritmului folosit pentru a putea fi cu adevărat paralelizat.

8. În spațiul de adrese al unui proces, zona de cod (text) este mapată read­only. Acest lucru este avantajos din punct de vedere 
al securității, întrucât împiedică suprascrierea codului executat. Ce alt avantaj important oferă?

Fiind read­only zona poate fi partajată între mai multe procese limitând spațiul ocupat în RAM.

9. Folosind o soluție de virtualizare, se dorește simularea unei rețele formată din: două sisteme Windows, un gateway/firewall  
OpenBSD și un server Linux. Opțiunile sunt VMware Workstation, OpenVZ și Xen. Care variantă de virtualizare permite 
rularea unui număr cât mai mare de instanțe de astfel de rețele pe un sistem dat?

OpenVZ nu poate fi folosit pentru că este OS­level virtualization: toate mașinile virtuale folosesc același nucleu deci pot fi  
folosite mașini virtuale care rulează același sistem de operare ca sistemul gazdă. Xen este o soluție rapidă dar rularea unui  
sistem nemodificat (gen Windows) este posibilă doar în situația în care hardware­ul peste care rulează oferă suport (Intel VT  
sau AMD­V). Vmware Workstation este o soluție mai lentă, în general, decât Xen dar permite rularea oricărui tip de sistem de  
operare guest.
10. Un sistem de operare dat poate fi configurat să folosească un split user/kernel 2GB/2GB al spațiului de adresă al unui  
proces sau un split 3GB/1GB. Sistemul fizic dispune de 1GB RAM. Un proces rulează secvența: 
 
    for (i = 0; i < N; i++) 
        malloc(1024*1024); 
 
Pentru ce valori (aproximative) ale lui N malloc va întoarce NULL în cele două cazuri de split? 

În exemplul de cod de mai sus, malloc alocă memorie pur virtuală (fără suport de memorie fizică). Alocarea de memorie fizică  
se va realiza la cerere (demand paging). malloc va întoarce NULL în momentul  în care procesul rămâne fără memorie  
virtuală în user­space. N va avea, așadar, valori aproximative de 2048 și 3072. Dimensiunea memoriei RAM a sistemului este  
nerelevantă în această situație.

11. Un proces dispune de o tabelă de descriptori de fișiere cu 1024 de intrări. În codul său, procesul deschide un număr mare 
de fișiere  folosind  open. Totuși, al 1010­lea apel  open  se întoarce  cu eroare,  iar  errno  are  valoarea  EMFILE  (maximum 
number of files open). Care este o posibilă explicație?

Procesul realizează 1009  apeluri open cu succes,  rezultând  în 1009  file descriptori deschiși. stderr, stdout, stdin sunt 3  


descriptori inițiali, rezultând 1012 descriptori. Restul au fost creați prin alte metode. File descriptorii pot fi creați și altfel:  
creat (creare fișiere), dup, socket, pipe. O altă situație este aceea în care procesul moștenește un număr de file descriptori de  
la procesul părinte.
Sisteme de operare
26 iunie 2009

Timp de lucru: 70 de minute

NOTĂ: toate răspunsurile trebuie justificate

1. Știind că operațiile de lucru cu pipe­uri sunt atomice, implementați în pseudocod un mutex cu ajutorul pipe­urilor.

lock: read(pipefd[0], &a, 1);
unlock: write(pipefd[1], &a, 1);

a este un char; pipefd este un pipe

2. Durata unei schimbări de context este de 1ms iar overhead­ul unui apel de sistem de 100µs. Totuși, la un moment dat, apelul 
down(&sem); durează doar 1µs. Apelul se realizează cu succes. Care este explicația?

Apelul down este implementat în user­space (de exemplu o implementare de tip futex). Dacă valoarea semaforului este strict  
pozitivă,   atunci   apelul   down   va   decrementa   valoarea   semaforului   și   va   continua   execuția   (fără   apel   de   sistem   și   fără  
schimbare de context). În cazul în care valoarea este egală cu 0 va avea loc un context switch. În cazul unei implementări de  
thread­uri kernel­level, acest lucru va presupune și un apel de sistem (planificatorul este implementat în kernel­space).

3. De ce este mai avantajos ca, pe un sistem uniprocesor, după un apel fork să fie planificat primul procesul fiu?

Pentru a evita posibilele copieri inutile datorate copy­on­write. De multe ori, după fork procesul copil execută exec, rezultând  
în schimbarea completă a spațiului de adresă. Dacă procesul părinte ar fi planificat primul, atunci apelurile de scriere ale  
acestuia vor rezulta în duplicarea paginilor (overhead temporal și consum memorie) datorită copy­on­write. Dacă procesul  
copil face exec, atunci acele duplicate au însemnat un consum inutil de resurse.

4. Există vreo diferență între implementarea simbolului errno în contextul unui proces single­threaded față de un proces multi­
threaded? Argumentați.

Da,   există  diferență.   Fiecare   thread   trebuie   să aibă   acces   la o  variabilă  errno  proprie,  astfel   că  errno   va  fi  de   obicei  
implementat ca variabilă per­thread. Acest lucru se poate realiza cu ajutorul TLS/TSD. O variabilă comună errno pentru  
toate thread­uri ar conduce la incosistența informațiilor referitoare la erorile apărute.

5. Un sistem uniprocesor (single­core) dispune de 64KB L1 cache, 512KB L2 cache și un TLB cu 256 intrări. Pe un sistem de 
operare cu suport în kernel pentru thread­uri, ce durează mai mult: schimbarea de context între două thread­uri sau între două 
procese?

Schimbarea de context între două procese va dura tot timpul mai mult decât schimbarea de context între două procese. În  
momentul schimbării de context între două procese se schimbă întreg spațiul de adresă și resursele asociate. Se schimbă astfel  
tabela   de   pagini,   se   face   flush   la   TLB   etc.   În   cazul   thread­urilor   o   schimbare   de   context   presupune   doar   schimbarea  
registrelor și a informațiilor specifice unui thread.

6. Comanda pmap afișează informații despre spațiul de adrese al unui proces. În urma rulării de mai jos a comenzii  pmap se 
observă următoarele informații despre biblioteca standard C: 
# pmap 1 
base address         size    rights     name
[...]
00007f8c480e6000    1320K    r­x­­      libc­2.7.so 
00007f8c4842f000      12K    r­­­­      libc­2.7.so 
00007f8c48432000       8K    rw­­­      libc­2.7.so 

Presupunând că în sistem rulează 50 de procese care folosesc biblioteca standard C, care este spațiul total de memorie RAM  
ocupat de bibliotecă? 

Ultima zona este o zonă read­write și nu poate fi partajată între două procese. Celelalte două zone sunt read­only și vor fi  
partajate. Biblioteca va ocupa, așadar, 50*8K + 12K + 1320K.

7. Descrieți și explicați în pseudo­asamblare cum acționează suportul de SSP (Stack Smashing Protection) pe un sistem în care 
stiva crește în sus (de la adrese mici la adrese mari).
Pe un sistem pe care stiva crește în sus nu se poate realiza stack overflow din stack­frame­ul curent ci din stack frame­ul  
apelantului. Astfel, dacă o funcție apelează strcpy și un argument este un buffer al funcției, acest buffer poate fi folosit pentru  
a suprascrie  (prin overflow) adresa de retur a funcției strcpy. Valoarea de tip canary trebuie stocată la o adresă mai mică  
decât  adresa de retur a funcției strcpy (practic, la fel ca la o stivă care crește în jos).  Întrucât  apelantul  este cel care  
construiește stack frame­ul apelatului, acesta va trebui să marcheze valoarea de tip canary. În schimb apelatul (aici strcpy),  
înainte de întoarcere va verifica suprascrierea adresei de tip canary.
Stack frame­ul este cel de mai jos:

[ strcpy local  ]
[      ...      ]
[ ret address   ]
[ old_ebp       ]  callee (strcpy) stack frame
[ canary value  ]­­­­
[ strcpy param1 ]
[ strcpy param2 ]
[      ...      ]
[ local buffer  ]  caller stack frame
[      ...      ]
[ local buffer  ]

8. Pe un sistem de fișiere dat un dentry are următoarea structură:
• 1 octet ­ lungimea numelui
• 251 octeți – numele
• 4 octeți ­ numărul inode­ului
O instanță a unui astfel de sistem de fișiere deține un director rădăcină, 5 subdirectoare, iar fiecare subdirector conține 5  
fișiere. Câte dentry­uri deține sistemul de fișiere?

Fiecare intrare în sistemul de fișiere (director sau fișier) conține cel puțin un dentry. Ignorând intrările speciale . și .. rezultă  
(1 +) 5 + 5*5 = 30 (31) intrări. Directorul rădăcină poate să nu aibă dentry. Considerând intrările speciale, rezultă un plus  
de 1 + 2*5 intrări = 11 intrări (directorul rădăcină nu are referință ..).

9. Un sistem pe 64 de biți folosește pagini de 8KB și 43 de biți pentru adresare într­o schemă de adresare ierarhică pe trei  
niveluri cu împărțirea (10 + 10 + 10 + 13). Un proces care rulează în cadrul acestui sistem are, la un moment dat, următoarea 
componență a spațiului de adrese (se începe de la adresa 0): 
        [ text ]             ­ 16 pagini 
        [ data ]             ­ 8 pagini 
        [ spațiu nealocat ]  ­ 8168 pagini 
        [ stivă]             ­ 8 pagini 
Știind că o intrare în tabela de pagini ocupă 64 de biți, câte pagini ocupă tabelele de pagini pentru procesul dat?

O intrare în tabela de pagini ocupă 64 de biți = 8 octeți. Există, astfel, 1024 de intrări într­o pagină. Zona text și data ocupă  
24 de pagini deci vor există 24 de intrări valide în prima pagină de tabelă de pe nivelul 3. Următoarele 8168 pagini vor  
completa intrările din prima tabelă de pe nivelul 3 și vor mai folosi 7 pagini de tabele. Întrucât este spațiu nealocat, cele 7  
pagini de tabele nu vor fi nici ele alocate. A 9­a pagină de tabela va folosi primele 8 intrări pentru a referi paginile de pe  
stivă.
Prima pagină de tabelă de pe nivelul 2 va avea valide doar prima și a 9­a intrare (care vor referi prima și a 9­a pagină de  
tabelă). Pagina de tabelă de pe nivelul 1 va avea validă doar prima intrare către pagina de tabelă de pe nivelul 2. Vor fi,  
astfel, folosite, doar 4 pagini.
Schematic, reprezentarea este următoarea:

[ level 1 page table ] ­­­> [#1 level 2 page table ] ­­­> [#1 level 3 page table] ­­­­> text
                                                     |                             | ...
                                                     |                             +­­> data
                                                     |                               ...
                                                     |               ...
                                                     +­­> [#9 level 3 page table] ­­­­> stack
                                                                                     ...

10.   Dați   exemplu   de   situație   în   care,   pentru   comunicația   cu   dispozitivele   de   I/E,   se   preferă   folosirea   polling   în   loc   de 
întreruperi.

Pollingul se preferă în situațiile în care întreruperile previn funcționarea eficientă a sistemului. Acest lucru se întâmplă în  
cazul în care întreruperile sunt transmise foarte des și procesorul petrece mult timp în rutinele de tratare a întreruperilor.  
Soluția este dezactivarea temporară a întreruperilor și folosirea polling. Acest lucru se întâmplă la dispozitivele de rețea  
foarte rapide, spre exemplu plăcile de rețea.
11. Un program execută secvența de cod din coloana din stânga tabelului de mai jos. În coloana din dreapta este prezentat  
rezultatul rulării programului:
 
/* init array to 2, 0, 0, 0 ... */  before init data1: 1245582962s, 753431us 
static int data1[1024*1024] = {2, }; after  init data1: 1245582962s, 767496us 

static void print_time(char *msg)  before init data2: 1245582962s, 767524us 
// ...  after  init data2: 1245582962s, 776012us

static void init_array(int *a, size_t len) 

    size_t i; 
­­­
    for (i = 0; i < len; i += 1024)  Se observa ca:
        a[i] = 2009;  – durata initializare data1 – 14065us
}  – durata initializare data2 ­  8488us
int main(void) 

    int *data2 = malloc(1024*1024 * sizeof(int)); 

    print_time("before init data1"); 
    init_array(data1, 1024*1024); 
    print_time("after  init data1");

    print_time("before init data2"); 
    init_array(data2, 1024*1024); 
    print_time("after  init data2"); 

    return 0; 

Cum explicați faptul că inițializarea vectorului data1 durează mai mult decât inițializarea vectorului data2?

Data1 se găsește în .data și este stocat în executabil. Zona .data a executabilului va fi mapată în memorie folosind demand  
paging. Drept consecință, un page­fault în momentul inițializării vectorului data1 va forța citirea de pe disc (din executabil).  
De partea cealaltă, data2 va fi alocat direct în RAM la cerere (tot prin demand­paging).
1. Care din următoarele instrucțiuni ar putea suprascrie adresa de retur a unei funcții? (my_func este o funcție) Motivați și
precizați contextul în care se poate întâmpla. (Poate fi un singur răspuns, răspunsuri multiple, nici unul, toate răspunsurile)
   long *a = malloc(30);  /* definire si alocare */

  /* instructiuni */
  a)  a = my_func;
  b)  *(&a + 4) = my_func;
  c)  *(a + 0x4000000) = my_func;
  d)  memcpy(my_func, a, sizeof(void *));

Cuvânt cheie: ar putea. Unele situații sunt improbabile dar posibile.

Înainte de toate:
• a este o variabilă (de tip pointer)
• a rezidă pe stivă
• &a este adresa pe stivă a variabilei a
• a (valoarea a) este o adresă de heap (punctează către zona de 30 de octeți alocată folosind malloc)
• în general, heap-ul crește în sus și stiva crește în jos
• my_func este o funcție deci rezidă în .text (zona de cod)

a) nu are un efect; se suprascrie valoarea lui a cu adresa funcției my_func


b) posibil; dacă există unele variabile între [ret][ebp] și [a] atunci &a+4 poate puncta către adresa unde se găsește valoarea de
retur și *(&a + 4) o poate suprascrie
c) posibil; a+0x4000000 înseamnă adunarea a 0x4000000 la o adresă de heap (valoarea lui a); se poate ajunge (greu probabil,
dar posibil) la o adresă de retur de pe stivă
d) ciudată, se suprascrie o informație din zona de cod (zona read only); pot apărea erori de acces sau comportanent
nedeterminist (în nici un caz nu suprascrierea adresei de retur dintr-o funcție)

2. Un proces este folosit pentru calcularea de transformate Fourier iar un altul este folosit pentru căutarea de informații într-o
ierarhie de fișiere. Care dintre cele două procese va avea prioritatea mai mare? De ce?

Proces care calculează transformate Fourier – CPU intensive. Proces care caută informatii într-o ierarhie de fisiere – I/O
intensive. În general, procesele I/O intensive au prioritate mai mare. Motivele sunt:
• creșterea interactivității
• împiedicarea starvation (fairness); dacă nu ar fi astfel prioritizate, procesele CPU intensive s-ar transforma în “processor
hogs” și ar folosi resursele sistemului
• procesele I/O intensive ocupă timp puțin pe procesor deci întârzierea provocată altor procese este mică

3. Un sistem dispune de un TLB cu 128 de intrări; care este capacitatea maximă a memoriei fizice și a memoriei virtuale pe acel
sistem?

Nu există nici o legătură. TLB-ul menține mapări de pagini fizice și pagini virtuale. Conține un subset al tabelei de pagini.
Memoria fizică și memoria virtuală pot fi oricât de mici/mari. Nu sunt afectate de dimensiunea TLB-ului.

4. Completați zona punctată de mai jos cu (pseudo)cod Linux (POSIX) sau Windows (WIN32) (la alegere) care va conduce la
afișarea mesajului "alfa" la ieșirea standard (standard output) și mesajul "beta" la ieșirea de eroare standard (standard error):
    /* de completat */
    [...]
    fputs("alfa", stderr);
    fputs("beta", stdout);
Nu alterați simbolurile standard fputs (functie), stderr și stdout (FILE *).

Problema este, de fapt, o problemă a paharelor ascunsă. Se dorește ca ieșirea standard să folosească descriptorul 2, iar
ieșirea de eroare standard să folosească descriptorul 1.

În pseudocod Linux, lucurile stau astfel:

int aux_fd;

/* aux_fd punctează către ieșirea standard */


dup2(STDOUT_FILENO, aux_fd);
/* descriptorul de ieșire standard este închis și apoi punctează către ieșirea de eroare standard */
dup2(STDERR_FILENO, STDOUT_FILENO);
/* descriptorul de ieșire de eroare standard este închis și apoi punctează către ieșirea standard (indicată de aux_fd) */
dup2(aux_fd, STDERR_FILENO)

fputs ....

5. Fie următoarea secvență de (pseudo)cod:


    int *a;
    a = mmap(NULL, 4100, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, ­1, 0);
    n = read_int_from_user();
    a[n] = 42;
    n = read_int_from_user();
    a[n] = 42;
Ce efect au valorile introduse de utilizator asupra programului? (page faults, erori, scrieri în memorie) Discuție. (o pagină
ocupă 4 KB; read_int_from user() citește o valoare întregă de la intrarea standard)

Discuția este ceva mai amplă. Prerequisites:


• mmap lucrează la nivel de pagină
• mmap nu alocă memorie fizică “din prima”; se alocă la acces (demand paging) în urma unui page fault

Nu am considerat necesar să se observe că a este int* și că referirea primei pagini se face cu 0 <= n <= 1024. Au fost
considerată validă și observația 0 <= n <= 4096 pentru prima pagină.

Se solicită alocarea a 4100 de octeți (> 4096, < 8192) deci se vor aloca 2 pagini.

Primul read:
• n < 0, proababil eroare (SIGSEGV) în cazul în care pagina anterioară este nevalidă (destul de probabil)
• n >= 2048 (peste cele două pagini), probabl eroare (SIGSEGV)
• 0 <= n < 1024 page fault și alocare spațiu fizic și validare pagină pentru prima pagină; fără erori
• 1024 <= n < 2048 la fel ca mai sus pentru a doua pagină; fără erori (chiar și pentru n >= 1025 (4100/4))

Al doilea read:
• n < 0 idem
• n >= 2048 idem
• n este în aceeași pagină ca mai sus nu se întâmplă nimic
• n în cealaltă pagină atunci page fault, alocare spațiu fizic și validare pagină

Practic mmap( ..., 4100, ....) este echilvalent cu mmap(..., 8192, ...).

6. Care este avantajul configurării întreruperii de ceas la valoarea de 1ms? Dar la valoarea de 100ms?

1ms
• timp de răspuns scurt, interactivitate sporită, fairness, sisteme desktop (întreruperi dese, se diminuează timpul de așteptare
pentru fiecare proces
100ms
• productivitate (throughtput) sporită, sisteme server (mai puține context switch-uri, mai mult timp pentru “lucru efectiv”)
1. Un proces execută la un moment dat:
sigaction(SIGUSR1, &sa, NULL);
iar la un moment ulterior
    write(fd, buffer, 4);
În care din situații este mai probabilă înlocuirea majorității intrărilor din TLB? Motivați. (Argumentele și modul de folosire a
funcțiilor se prespun corecte.)

Problema se tratează cel mai bine de la coadă la cap. Care sunt situațiile în care se înlocuiesc majoritatea intrărilor din TLB
(eventual un flush – golire)? Răspuns: în cazul unei schimbări de context. Se schimbă tabelele de pagini între procesul
preemptat și cel planificat, mai puțin partea kernel. TLB-ul se golește (dacă arhitectura și/sau sistemul de operare permite)
atunci unele intrări rămân active (zone de memorie partajată comune procesulelor, zone din spațiul kernel). De aici cuvântul
“majorității”.

Când se realizează un context switch?


• la terminarea unui proces
• la expirarea cuantei de rulare a unui proces
• în momentul în care prioritatea procesului curent (cel ce rulează) este sub prioritatea unui proces READY
• în cazul blocării procesului curent

În cazul celor două apeluri doar ultima variantă are sens (blocarea procesului curent). Acest lucru se poate întâmpla doar în
cazul apelului write, care este un apel blocant.

2. Care este limita de spațiu de swap pentru un sistem cu magistrala de adrese de 32 de biți cu spațiul de adrese împărțit
2GB/2GB (user/kernel). Dar pentru un sistem cu magistrala de adrese de 64 de biți?

Nu există nici o limitare. Singura limitare este cea impusă de hardware.

3. O funcție signal-safe este o funcție care poate fi apelată fără restricții dintr-o rutină de tratare a unui semnal (signal handler).
De ce nu este malloc o funcție signal-safe? Oferiți o secvență de cod pentru exemplificare.

După cum s-a menționat în cateva rezolvări (fără a aduce o contribuție în cadrul răspunsului, însă) funcțiile signal-safe sunt
practic echivalente cu funcțiile reentrante. O funcție non-signal-safe este o funcție care folosește variabile statice, astfel că,
dacă un semnal întrerupe funcția în programul principal și funcția este rulată în handler este posibil să apară inconsistențe
(exact cum se întâmplă în momentul în care un thread este întrerupt și rulează alt thread fără asigurarea accesului exclusiv și
consistent la date).

Dacă un semnal întrerupe funcția malloc și, în handler, rulează funcția malloc structurile interne folosite de libc pentru gestiunea
alocării memoriei procesului vor fi date peste cap. Funcția printf este, în mod similar, o funcție non-signal-safe pentru că
folosește buffer-ele interne ale libc. Mai multe informații aici (https://www.securecoding.cert.org/confluence/x/34At)

Scenariul de exemplificare este de forma:

void sig_handler(int signo)
{
void *p = malloc(100);
}

int main(void)
{
              ....
void *a = malloc(BUFSIZ);    /* aici sosește semnalul */
              ...
}

Răspunsul “malloc poate genera SIGSEGV când deja rulează un signal handler” nu este valid. Malloc nu generează SIGSEGV;
cel mult rămâne fără memorie și întoarce NULL. SIGSEGV este generat în momentul accesării unei regiuni invalide a memoriei.

4. Un program execută următoarea secvență de cod:


    for (i = 0; i < BUFLEN; i++)
        printf("%c", buf[i]);
  iar altul
    for (i = 0; i < BUFLEN; i++)
        write(1, buf+i, 1);
Care secvență durează mai mult? De ce?

Funcția printf folosește buffering-ul din libc. Acest lucru înseamnă că, până la îndeplinirea uneia dintre cele trei acțiuni de mai
jos, nu se face apel de sistem:
• se umplu buffer-ele
• se apelează fflush(stdout)
• se transmite newline (\n)
Apelul de sistem write face apel de sistem de fiecare dată rezultând un overhead important.

Pentru convingere puteți rula testul de aici (http://anaconda.cs.pub.ro/~razvan/school/so/test_printf_write.c). Mai jos este un
exemplu de rulare, primul folosind printf al doilea folosind write (se alterează macro-ul USE_PRINTF). Rezultatele sunt, în
opinia mea, edificatoare.

razvan@valhalla:~/school/2008­2009/so/examen$ time ./test_printf_write > out.txt 
real 0m0.076s 
user 0m0.060s 
sys 0m0.020s 

razvan@valhalla:~/school/2008­2009/so/examen$ time ./test_printf_write > out.txt 
real 0m5.930s 
user 0m0.052s 
sys 0m5.868s 

5. Un proces P se găsește în starea READY. Precizați și motivați două acțiuni care determină trecerea acestuia in starea
RUNNING.

Fie Q procesul care rulează în acest moment pe procesor. Situații de trecere a lui P din READY în RUNNING:
• Q se încheie și P este primul din coada de procese READY
• lui P îi este crescută prioritatea peste a lui Q
• lui Q îi expiră cuanta și P este primul în coada de procese READY
• Q efecuează o operație blocantă (trece în blocking) și P este primul proces în coada de procese READY

6. De ce obținerea ordonată/ierarhică a lock-urilor previne apariția deadlock-urilor, respectiv apariția fenomenului de starvation?

Ordonarea modului de obținere (achiziție) a lock-urilor în particular și a resurselor în general previne apariția de cicluri în graful
de alocare a resurselor și deci apariția deadlock-urilor.

În absența ordinii de obținere un proces P1 poate face Lock(1) și apoi Lock(2). Înainte de Lock(2) este preemptat și procesul P2
face Lock(2) și apoi încearcă Lock(1). Ambele procese rămân blocate în așteptare mutuală (deadly embrace) = deadlock.

Nu există nici o legătură directă în lock-uri și fenomenul de starvation. Fenomentul de starvation caracterizează o durată de
așteptare foarte mare pentru un proces gata de rulare. Alte procese îi iau tot timpul “fața” și procesul nu ajunge pe procesor. Se
spune că sistemul nu este “fair” (echitabil). Principala formă de asigurare a echității este folosirea noțiunii de cuantă și, în lumea
Linux, de epocă și folosirea priorității dinamice a proceselor. Orice formă de locking duce la creșterea nivelului de starvation. Un
proces care așteaptă la un semafor intrarea într-o regiune critică dar alte procese intră înaintea sa. Asigurarea fairness-ului
poate fi asigurată prin strategii de tipul FIFO. Dar, obținerea ierarhică a lock-urilor nu are un efect vizibil diferit față de folosirea
în orice fel a lock-urilor din perspectiva starvation.
Sisteme de operare
20 iunie 2010

Timp de lucru: 90 de minute

NOTĂ: toate răspunsurile trebuie justificate

1. Câte inode­uri va folosi un hard­link către un fișier aflat pe un sistem de fișiere diferit?

Nu se pot crea hard­link­uri către un fișier aflat pe un alt sistem de fișiere.  Un hard­link conține un nume și un număr de  
inode. Inode­ul referit corespunde sistemului local de fișiere, nu altui sistem de fișiere (nu există un identificator al sistemului  
de fișiere, se presupune cel local).

2.  Fie următoarea secvență de comenzi:
touch a.txt
ln a.txt b.txt
ln -s b.txt c.txt
Comanda  ln fără  opțiuni  creează  hard  link­uri,  iar  comanda  ln cu  opțiunea  ­s creează   symbolic  link­uri.  Câte inode­uri, 
respectiv dentry­uri vor fi create în urma rulării comenzilor de mai sus?

Un hard­link se asociază cu un dentry. La fel un nume de fișier. Un symbolic link are asociat un inode, inode ce conține  
numele fișierului referit. Se vor crea astfel următoarele:
touch a.txt → 1 dentry (a.txt) și 1 inode (aferent fișierului proaspăt creat)
ln a.txt b.txt → 1 dentry (b.txt) ca hard­link la a.txt
ln ­s b.txt c.txt → 1 dentry (c.txt) și 1inode (aferent simbolic link­ului proaspăt creat)
 
Se creează 3 dentry­uri și 2 inode­uri.

3. Un proces execută secvența următoare în două situații diferite:
 

a = malloc(5000);
memset(a, 0, 5000);
Într­una din situații rezultă două page fault­uri, iar în alta trei page fault­uri. Explicați acest comportament.

Pentru alocarea celor 5000 de octeți se folosește demand­paging. Accesele la acea zonă conduc la generarea de page fault­
uri. Se generează un page fault pentru fiecare pagină. Depinzând de alinierea celor 5000 de octeți pot rezulta două sau trei  
page fault­uri.

De exemplu, în cazul în care se alocă [500 octeți, 4096 octeți, 404 octeți] pe parcursul a trei pagini, vor rezulta trei page  
fault­uri după ce se accesează octetul cu indexul 0, octetul cu indexul 500, octetul cu indexul 4596.

În cazul în care se alocă [4096, 904] pe parcursul a două pagini vor rezulta două page fault­uri după ce se accesează octetul  
cu indexul 0 și octetul cu indexul 4096.

4. Un program citește un fișier de pe disc, operație care durează T1. Imediat după prima rulare, se execută din nou programul 
și durează T2. T2 este semnificativ mai mic decât T1. Cum explicați?

Citirea unui  fișier de pe disc presupune,  pe sistemele de operare moderne,  interacțiunea  cu un subsistem de caching   în  


memorie (buffer cache) al datelor de pe disc. La prima rulare, nu există date în cache­ul din memoria fizică (buffer cache) și  
toate datele sunt citite de pe disc. La a doua rulare, datele se regăsesc în cache și timpul de citire va fi redus – diferența de  
acces la memorie față de disc este mare.
 
5. Care proces este părintele proceselor zombie?

Un proces zombie este un proces care și­a încheiat execuția dar a cărui stare nu a fost “analizată” de procesul părinte –  
adică procesul părinte nu a apelat wait pentru culegerea de informații despre procesul copil. Drept urmare, procesul zombie  
are același proces părinte ca procesul obișinuit înainte să­și fi încheiat execuția – nu există un proces specializat care să fie  
părintele proceselor zombie.
 
6. Precizați o situație în care accesarea unei adrese virtuale valide produce segmentation fault.
În cazul în care pagina referită de adresă este marcată de tip read­only (fără a fi vorba de copy­on write),   un acces de  
scriere la acea pagină va genera un page fault. Sistemul de operare aanalizează tipul de page fault; fiind vorba de un acces  
de scriere la o adresă dintr­o zonă marcată read­only (non copy­on­write), conchide că este vorba de un acces invalid.  
Rezultă transmiterea unui semnal SIGSEGV (pe un sistem Unix) către procesul care a generat accesul, adică afișarea unui  
mesaj de tipul “Segmentation fault”.

7. Este utilă folosirea "canary value" (stack smashing protection) în cadrul funcției de mai jos? Justificați.
 

void f(char *msg)


{
char *buffer = malloc(10);
strcpy(buffer, msg);
}
...
f("supercalifragilisticexpialidocious");
...

Stack smashing protection se referă la protejarea stivei prin detectarea situațiilor în care adresa de retur a unei funcții este  
probabil să fie suprascrisă.  În cazul particular al secvenței de cod de mai sus, se realizează un buffer overflow la nivelul  
heap­ului,   adică   la   nivelul   variabilei   buffer   (alocată   pe   heap   folosind   malloc).   Drept   urmare,   folosirea   stack   smashing  
protection nu are nici o utilitate.

8. Un sistem S1 folosește segmentare. Timpul de translatare a unei adrese virtuale într­o adresă fizică este T1. Un sistem S2 
folosește paginare, iar timpul de translatare este T2. Care dintre timpii T1 și T2 este mai mare?

În cazul paginării, translatarea unei adrese virtuale  în adrese fizică duce la interogarea tabelei de pagini, care rezidă în  
memorie; diminuarea overhead­ului de acces la memorie se realizează prin folosirea TLB. În cazul unei paginări ierarhice  
timpul de acces este mai mare.

Dacă descriptorii/selectorii de segment sunt menținuți în registre ale procesorului atunci timpul T1 este mai mic decât timpul  
T2.

Dacă descriptorii/selectorii de segment sunt menținuți în memorie, atunci T1 este aproximativ egal cu T2 în cazul folosirii  
unui sistem cu adresare neierarhică și mai mic decât T2 în cazul folosirii unui sistem cu adresare ierarhică.
 
9. Ce se întâmplă cu sistemul de bază (host) în cazul în care apare o eroare fatală la nivelul nucleului:
 a) unei mașini virtuale VMware Workstation;
 b) unui container OpenVZ.
 
Dacă   apare   o   eroare   la   nivelul   unei   mașini   virtuale   VMware,   mașina   virtuală   trebuie   repornită   (este   într­o   stare  
inconsistentă). Sistemul de bază nu este afectat în vreun fel.

OpenVZ este o soluție de operating system level virtualization. Drept urmare, containerele OpenVZ partajează același nucleu  
de sistem  de operare (Linux) cu sistemul de bază (denumit și container­ul 0). Astfel o eroare de nucleu apărută în nucleul  
unui  continaer OpenVZ  se manifestă la nivelul  tuturor container­elor  și a sistemului  de bază – este,  de  fapt, impropriu  
exprimarea “nucleul unui container OpenVZ” ­ nucleul este comun tutoror container­elor și sistemului de bază. O astfel de  
eroare va fi, deci, fatală și sistemului de bază și acesta trebuie repornit.

10. Fie următoarele două secvențe de programe
 

/* S1 */ /* S2 */
fd = open(“a.txt”, O_RDWR | O_CREAT, 0644); void *thread_handler(void *arg)
{
pid = fork(); write(fd, "a", 1);
if (pid == 0) { close(fd);
write(fd, "a", 1); return NULL;
close(fd); }
exit(EXIT_SUCCESS);
} fd = open(“a.txt”, O_RDWR | O_CREAT, 0644);
pthread_create(&tid, thread_handler, NULL);
wait(&status); pthread_join(&tid, NULL);
write(fd, "b", 1); write(fd, "b", 1);
 

În cazul secvenței S2 apelul write(fd, "b", 1); se întoarce cu eroarea EBADF. Care este explicația? De ce în primul caz 


nu se întâmplă același lucru?
În   cazul   secvenței   S1,   după   apelul   fork,   procesul   copil   folosește   un   descriptor   propriu  (duplicat   al   descriptorului   fd  al  
procesului părinte). Operația close(fd) conduce la închiderea descriptorului doar în procesul copil.

În cazul secvenței S2, thread­ul principal și thread­ul nou creat partajează resursele procesului și, deci, tabela de descriptori  
a procesului.  În consecință,  operația close(fd)  are sens la nivelul   întregului   proces   și va   închide  descriptorul. Operația  
write(fd, “b”, 1) executată în thread­ul principal  după  ce thread­ul creat a închis fișierul folosește un descriptor nevalid.  
Operația va întoarce EBADFD (Bad file descriptor).

11. Fie următoarea secvență de operații:
for (i = 0; i < N; i++)
a[i] = 1;
Secvența este rulată pe două sisteme diferite care nu dispun de TLB sau memorie cache. Pe un sistem au loc N accese la  
memorie iar pe un alt sistem 2*N accese. Secvența este identică și rulată în aceleași condiții (același program) pe ambele 
sisteme. Cu ce diferă cele două sisteme?

Secvența de mai sus conduce la N accese la elemente ale vectorului a[i], aflat în memorie. Într­un caz se produc N accese,  
deci fiecare acces la un element al vectorului înseamnă 1 acces la memorie. În al doilea caz se produc 2*N accese, deci  
fiecare acces la un element al vectorului înseamnă 2 accese la memorie.

Pentru primul caz (N accese) sistemul nu dispune de memorie virtuală – în acest caz un acces în limbajul C se traduce printr­
un acces la memoria fizică.

Pentru al doilea caz (2*N accese) sistemul dispune de memorie virtuală cu adresare neierarhică. Sistemul nu dispune de TLB  
astfel că fiecare acces la un element al vectorului va însemna un prim acces la tabela de  pagini  și apoi unul la zona de  
memorie aferentă elementului, ambele localizate în memorie fizică (RAM) a sistemului.
Sisteme de operare
22 iunie 2010

Timp de lucru: 90 de minute

NOTĂ: toate răspunsurile trebuie justificate

1. Care dintre secțiunile de memorie de mai jos sunt proprii unui proces dar nu unui program/executabil? Justificați. 
text, rodata, data, bss, heap, stack

Un executabil definește secțiunile text, rodata, data și, fără a aloca spațiu, bss. Secțiunea bss este populată cu zero­uri în  
momentul creării procesului (load­time). Zonele heap și stack  (stivă)  sunt zone pur dinamice  ­  țin de evoluția  procesului – 
alocarea memoriei; alocarea pe heap se realizează prin malloc iar alocarea pe stack se realizează în contextul apelurilor de 
funcții.

2. Precizați o situație în care accesarea unei adrese virtuale valide produce page fault, fără a produce segmentation fault. 

Dacă adresa este validă, dar pagina fizică nu este prezentă în RAM (este în swap sau a fost alocată folosind demand­paging),  
va rezulta page fault, și apoi pagina va fi adusă în RAM sau alocată. Dacă pagina este marcată read­only dar de tip copy­on­
write (după un fork) atunci un acces de scriere la pagină va conduce la obținerea unui page fault; page fault­ul va conduce la  
alocarea unei pagini fizice noi și marcarea acesteia cu drepturi de scriere.

3. Presupunem  că  avem  3 page  frames la dispoziție,  toate inițial goale.  Se realizează  următorul  șir de accese  (numerele 
reprezintă pagini virtuale): 3 2 1 0 3 2 4 3 2 1 0 4. Câte page faulturi vor rezulta în urma folosirii algoritmului FIFO? Dar dacă  
se mărește numărul de page frames la 4? 

În tabelul de mai jos, cele 3 linii conțin, respectiv, pagina virtuală aferentă fiecărei pagini fizice (se folosesc 3 frame­uri).
frame1 3 3 3 0 0 0 4 4 4 4 4 4
frame2 ­ 2 2 2 3 3 3 3 3 1 1 1
frame3 ­ ­ 1 1 1 2 2 2 2 2 0 0

În tabelul de mai jos, cele 4 linii conțin, respectiv, pagina virtuală aferentă fiecărei pagini fizice (se folosesc 4 frame­uri).
frame1 3 3 3 3 3 3 4 4 4 4 0 0
frame2 ­ 2 2 2 2 2 2 3 3 3 3 4
frame3 ­ ­ 1 1 1 1 1 1 2 2 2 2
frame4 ­ ­ ­ 0 0 0 0 0 0 1 1 1

Cu font aldin (bold) au fost marcate paginile virtuale accesate, iar cu font roșu dacă acel acces a generat un page fault. În  
cazul folosirii a 3 page frame­uri, se obțin 9 page fault­uri, iar în cazul folosirii a 4 page  frame­uri se obțin 10 page fault­uri.  
Acest fenomen poartă numele de anomalia lui Belady (http://en.wikipedia.org/wiki/Belady's_anomaly).

4. Se consideră următoarea schemă de segmentare:

Segment Base Length


----------- ------ --------
0 100 1000
1 1200 250
2 1800 300
3 2200 500
4 3000 800

Care dintre următoarele reprezintă adrese logice valide? Adresele sunt de forma (segment, offset) .
a. (0, 820) 
b. (1, 430) 
c. (2, 13) 

În cazul opțiunilor prezentate contează dacă offsetul în cadrul segmentului depășește dimensiunea segmentului (length). Se  
observă că doar a doua opțiune (b) conduce la depășirea lungimii segmentului  (430 > 250), deci nu reprezintă o adresă  
logică validă.

5. Dați exemplu de situație în care operația lock(&mutex) conduce la invocarea scheduler­ului și un exemplu de situație în care 
nu conduce la invocarea scheduler­ului. 

Dacă apelul este blocant va conduce la invocarea scheduler­ului. Dacă apelul nu este blocant și nu se produce o analiză a  
priorităților proceselor, nu va conduce la invocarea scheduler­ului. Apelul este blocant în momentul în care mutex­ul este  
deja achiziționat. Apelul este neblocant dacă mutexul nu este achiziționat (adică este liber). În caz particular, dacă mutex­ul  
este implementat în user space (de tip  futex)  și este liber, nu va genera apel de sistem și nu există “riscul” replanificării  
acestuia din cauza priorității proceselor sau a altor euristici de planificare ale nucelului.

6. Un sistem de fișiere dispune de un bitmap pentru inode­uri (un bit specifică folosirea sau nu a unui inode) de 32KB. Câte 
symlink­uri pot fi create? (este suficient ordinul de mărime și justificarea răspunsului) 

32KB = 32*2^10*8biti = 256*2^10 intrări în bitmap. Pot fi create 256*2^10 inode­uri. Întrucât un symbolic link ocupă un  
inode pot fi create 256*2^10 inode­uri. Se pot scădea câteva inode­uri aferente directorului rădăcină și fișierelor reale către  
care punctează symbolic link­urile.

7. Se dă următoarea secvenţă de execuţie :

Thread A Thread B
-------------- -------------
work_a1 work_b1
work_a2 work_b2

Realizaţi sincronizarea celor 2 fire de execuţie folosind semafoare astfel încât work_a1 să se execute înainte de work_b2 şi 
work_b1 să se execute înainte de work_a2. Folosiți primitivele:  sem_init(sem_t *sem, int count), sem_up(sem_t
*sem), sem_down(sem_t *sem). 

Presupunem folosirea a două semafoare (s_a și s_b) cu următoarele roluri:
        * s_a, thread­ul A a ajuns la punctul de întâlnire 
        * s_b, thread­ul B a ajuns la punctul de întâlnire. 

Soluția de sincronizare este cea de mai jos:

sem_t s_a, s_b;


sem_init(&s_a, 0);
sem_init(&s_b, 0);

Thread A Thread B
------------------ ------------------
work_a1 work_b1
sem_up(&s_a); sem_up(&s_b);
sem_down(&s_b); sem_down(&s_a);
work_a2 work_b2

8.   Într­o   aplicație   multi­threaded   există   două   threaduri.   Primul   execută   o   funcție   CPU   intensive,   iar   celălalt   execută 
preponderent operații I/O. De ce folosirea implementării threadurilor la nivel user nu este de dorit? 

În cazul unei implementări de thread­uri la nivel user, blocarea unui thread conduce la blocarea întregului proces. Thread­ul  
care execută operații I/O va avea parte de situații dese de blocare (operațiile I/O sunt, în general, blocante). Blocarea acestui  
thread va conduce la blocarea întregului proces. În acest caz, thread­ul CPU intensive, deși ar putea rula și executa acțiuni  
utile, este blocat.  Acest lucru duce la folosirea necorespunzătoare a procesorului: un thread este pregătit pentru execuție  
(READY) dar nu poate rula. Pe un sistem cu implementare de thread­uri la nivel kernel, acest lucru nu ar avea loc.

9. De ce, înainte de a realiza un apel exec(), e recomandat să se închidă toate fișierele de care nu are nevoie procesul copil? 

Un proces copil moștenește descriptorii procesului părinte. Acest lucru atrage două dezavantaje importante:
* securitate: un proces poate citi, parcurge sau corupe datele din fișierele unui alt proces
* resurse: menținerea descriptorilor deschiși duce la ocuparea unui număr mare de descriptori de fișier; în cazul în care se  
creează procese în continuare, tabela de descriptori de fișiere este ocupată în mare măsură de fișiere deschise de alte procese

10. Care este numărul minim de apeluri de sistem generate de următoarea secvență de pseudocod? (toate apelurile de funcții se 
întorc cu succes)

acquire_mutex(&m);
write(fd, "abcd", 4);
free(p);
release_mutex(&m);

În cazul unei implementări de mutex­uri în user space (de tipul futex)  și a unui mutex neocupat,  funcțiile acquire_mutex și 


release_mutex nu generează apel de sistem.

Apelul  de bibliotecă  free poate să nu genereze apel de sistem (de obicei nu generează) depinzând de implementarea din  


biblioteca standard C. Atât apelul malloc cât și free alocă, respectiv eliberează, anumite dimensiuni de memorie din heap. În  
momentul în care se “cumulează” o zonă suficient de mare (de alocat sau eliberat), se realizează apel de sistem (brk).

Apelul de bibliotecă write conduce la invocarea apelului de sistem aferent (sys_write pe Linux).

În consecință, numărul minim de apeluri de sistem generate este 1 (unu), generat de apelul write.

11. De ce nu se poate implementa un mecanism de memorie partajată pentru un sistem cu paginare inversată?

Într­un sistem cu paginare inversată, intrările din tabela de pagini conțin PID­ul procesului  și pagina virtuală aferentă.  
Indexul intrării în tabelă reprezintă frame­ul aferent.  Partajarea unei pagini se poate realiza în măsura în care se poate  
asocia unui frame (unei  pagini fizice) mai multe pagini  virtuale.  Într­un sistem cu paginare inversată,  o singură pagină  
virtuală poate corespunde unei pagini fizice, și nu se poate implementa partajarea memoriei.

Dacă sistemul permite alocarea unei liste de elemente de tip (PID, pagină virtuală) în cadrul unei intrări în tabela de pagini  
atunci  partajarea  memoriei  se poate  implementa,  cu  dezavantajul  unui  timp de căutare ridicat  (problemă  care se poate  
rezolva prin folosirea de tabele hash).
Sisteme de operare
26 iunie 2010

Timp de lucru: 90 de minute

NOTĂ: toate răspunsurile trebuie justificate

1. Câte procese copil, respectiv părinte poate avea un proces la un moment dat? 

Un proces poate avea, la un moment dat, un singur proces părinte și oricâte procese copil, în limita resurselor sistemului.  
Procesul init poate fi considerat un proces particular care nu are un proces părinte.

2. Un proces execută secvența: 

for (i = 0; i < 42; i++)


a++;

În timpul execuției secvenței, procesul este preemptat și este planificat alt proces. Dați exemplu de o situație care poate genera 
preemptarea. 

Întrucât secțiunea de mai sus nu este blocantă, procesul poate fi preemptat dacă îi expiră cuanta de timp sau dacă în sistem  
există un proces cu prioritate superioară pregătit pentru execuție. Această situație este declanșată de apariția întreruperii de  
ceas.

3. Daţi exemplu de o funcţie thread safe dar non­reentrantă. Explicaţi. 

O funcție thread safe dar non­reentrantă permite rularea acesteia în context multithreaded dar nu permite existența simultană  
a două fluxuri de execuție în contextul aceluiași thread/proces. Un exemplu este o funcție care folosește locking. De forma:

int my_function(void)
{
lock(&mutex);
… /* TODO */ …
unlock(&mutex);
}

4. Se consideră următorul cod: 

void f()
{
int *z = malloc(sizeof(int));
[...]
printf("z = %p\n", z);
printf("&z = %p\n", &z);
}

După rularea secțiunii se afișează mesajul: 
z = 0x12345678
&z = 0x87654321

Asociați adresele z, &z cu secțiunile spațiului de adresă al unui proces: .text, .data, .bss, heap și stack. 

z este o variabilă de tip pointer – conținutul acesteia este o adresă. Adresa punctează către o zonă din heap (fiind rezultatul  
întors de apelul malloc).

&z reprezintă adresa variabilei v. Variabila este o variabilă locală unei funcții deci este alocată pe stivă.

Avem o variabilă alocată pe stivă (adresa ei este o adresă din stivă), iar conținutul acelei variabile (pointer) este o adresă  
întoarsă de apelulul malloc, adică o adresă din heap.

5. Explicați modul în care se poate produce starvation pe un sistem cu planificare SRTF (Shortest Remaining Time First). 
Dacă   în   cadrul   sistemului   apar   în   coada   ready   procese   cu   timp   de   rulare   redus,   acestea   vor   fi   planificate   primele.  
Presupunând un flux continuu de procese cu timp de rulare redus, procesele cu timp de rulare mare vor ajunge să se execute  
foarte rar sau deloc, adică să se producă fenomenul de starvation.

6. După schimbarea contextului  între două thread­uri, care clase registre au valori diferite (înainte și după schimbarea de 
context): registrele generale, registrul de stivă, registrele de segment.

La schimbarea de context între două thread­uri, majoritatea registrelor se schimbă. Fiecare thread dispune de valori proprii  
ale registrelor. Astfel, registrele generale și registrul de stivă se schimbă. Sistemele de operare moderne folosesc rar registrele  
de segment, astfel că în general acestea nu vor fi schimbate.

În plus, anumite registre interne procesorului, inaccesibile din user space (ring3 pe o arhitectură x86) își pot păstră valorile  
în cazul thread­urilor diferite (registre precum cr2, cr3 pe o arhitectură x86).

7. De ce anumite zone din bibliotecile partajate sunt mapate read­write? Dati un exemplu. 

Bibliotecile partajate conține zone r­x (cod), r­­ (read­only data) și rw­ (date). Zonele read­write conțin variabile care pot fi  
scrise de procesul ce folosește biblioteca, precum variabila errno în cazul bibliotecii standard C.

8. Exceptând apelurile de sistem, dați exemplu de situație în care procesorul comută în kernel space. 

Procesorul execută cod kernel în cazul unor solicitări din user space (apel  de sistem) sau  de la hardware (întreruperi).  


Procesorul execută, astfel, cod kernel, exceptând apelurile de sistem, în momentul sosirii unei întreruperi.

9. Un sistem dispune de N procese. Fiecare proces dispune de M pagini virtuale nealocate. Sistemul dispune de o singură 
pagină fizică disponibilă. Care este numărul maxim de pagini virtuale care pot fi asociate cu pagina fizică? 

Oricâte pagini virtuale pot fi asociate cu o pagină fizică. Implementări de tipul mmap permit maparea unei zone de memorie  
virtuale peste o zonă de memorie fizică. În situația de mai sus, un proces poate mapa toate cele M pagini virtuale proprii peste  
acea pagină fizică. În total, pentru cele N procese, se pot mapa N*M pagini virtuale.

10. Explicați de ce nu se poate implementa mecanismul de swapping pe un procesor fără unitate de management al memoriei. 

Unitatea de management a memoriei  (MMU)  este responsabilă cu translatarea paginilor virtuale în pagini fizice.  Absența  


MMU conduce la absența mecanismului de memorie virtuală. Mecanismul de memorie virtuală permite existența unui spațiu  
virtual de adrese care depășește spațiul fizic – spațiul suplimentar poate fi furnizat de disc, prin intermediul  swap­ului.  În  
cazul absenței mecanismului de memorie virtuală, nu se poate referi/folosi spațiul de swap, deci nu se poate implementa  
mecanismul de swapping (evacuare pe disc și recuperarea paginilor de pe disc).

11. Un inode dispune de 10 pointeri de indirectare simplă a blocurilor de date. Un bloc ocupă 4096 de octeți. Știind că un 
dentry ocupă 64 de octeți, câte intrări poate avea maxim un director?

10 pointeri de indirectare simplă punctează către blocuri care conțin, la rândul lor, pointeri. Considerând că un pointer  
ocupă 4 de octeți, rezultă că un bloc de pointeri conține 4096/4 = 1024 de pointeri.

Cei 10 pointeri de indirectare simplă vor referi 10 blocuri care conțin, la rândul lor, 10*1024 pointeri adică 10240.

Fiecare dintre cei 10240 pointeri punctează către un bloc de date. Fiecare bloc de date ocupă 4K. Rezultă așadar, că un  
inode poate referi 10240*4KB de date.

În cazul unui director, datele sale sunt un vector (array) de dentry­uri. Numărul maxim de dentry­uri se obține împărțind  
spațiul   maxim   ce   poate   fi   referit   de   un   inode   la   dimensiunea   unui   dentry.   În   consecință,   un   director   poate   conține  
10240*4KB / 64 dentry­uri, adică 10240 * 64 = 655360.
1. Două procese (P1, P2) folosesc o zonă de memorie partajată, rulează într-un sistem preemptiv și execută secvenţa de cod de mai jos. Știind
că valoarea inițială indicată de pointerul counter este 0 şi că acesta indică în zona de memorie partajată, care este valoarea indicată de
pointerul counter la finalul execuţiei celor două procese?
P1 P2
if (*counter == 0) if (*counter == 0)
(*counter)++; (*counter)++;

Dacă cele două procese se execută secvențial atunci primul proces va incrementa variabila pe 1, iar al doilea nu va executa
instrucțiunea de incrementare din if. Valoarea finală va fi 1.

În varianta în care procesul P1 este preemptat de procesul P1 după verificarea if (*counter == 0), dar înainte de (*counter)++
atunci procesul P2 va testa, la rândul său, ca fiind adevărată condiția (*counter == 0). În consecință, va incrementa valoarea
contorului. P1 va incrementa, de asemenea, valoarea contorului, rezultând valoarea finală 2.

2. Pe un sistem dual processor cu 128MB de RAM şi swap de 256MB rulează un sistem Linux. Câte procese se pot găsi la un moment dat în
starea RUNNING, READY respectiv WAITING?

Caracteristica ce influențează numărul de procese din starea RUNNING este numărul de unități de execuție. Fiind vorba de un
sistem dual procesor, pot exista maxim 2 procese în coada RUNNING.
În coada READY și WAITING se pot găsi oricâte procese , limita fiind dată de constrângerile și resursele sistemului.

3. Un sistem folosește TLB în care fiecare intrare conține trei câmpuri (pid, frame, pagină virtuală). Care este avantajul acestei implementări fa ță
de o implementare care conține doar două câmpuri (frame, pagină virtuală)?

Prezența câmpului pid înseamnă că se poate realiza o selecție după proces a intrărilor din TLB. Acest lucru este util în cazul
schimbărilor de context. În cazul unei schimbări de context cea mai mare parte a intrările din TLB sunt anulate. Prezența unui
câmp pid înseamnă că, în cazul unei schimbări de context, doar intrările specifice procesului vor fi eliminate rezultând într-un
număr mai mic de accese directe la tabela de pagini.

4. Descrieţi în pseudocod cum se poate determina dacă stiva crește de la adrese mari la adrese mici sau invers.

O soluție ține cont de construirea stack frame-urilor pentru funcții:

int *p_fcaller;
int *p_fcalled;

void f2(void)
{
int f2_local;
p_fcalled = &f2_local;

if (p_fcalled > p_fcaller)


printf(“stack grows up\n”);
else
printf(“stack grows down\n”);
}
void f1(void)
{
int f1_local;
p_fcaller = &f1_local;
f2();
}

Exemplu complet aici: http://swarm.cs.pub.ro/git/?p=razvan-


code.git;a=blob;f=tests/stack_grow.c;h=3ebc0408a4cf30cb8317714fb5c473c35ca7bb3d;hb=21d876d26d3db2145f6be96423cb
c14a7ddaf21a

5. Câte pagini fizice vor ocupa stivele celor două procese rezultate în urma apelului fork exact înainte de apelul exit? Apelul fork se întoarce cu
succes.
int main(void)
{
char buf[3 * PAGE_SIZE];
buf[0] = 'a';
buf[PAGE_SIZE] = 'z';
fork();
buf[0] = 'b';
exit(EXIT_SUCCESS);
}
Bufferul buf este alocat folosind demand paging. Acest lucru înseamnă că nu vor fi alocate pagini fizice până în momentul
accesului. După buf[0] = 'a' și buf[PAGE_SIZE] = 'z' rezultă două page fault-uri care vor genera alocare a două pagini fizice.

După fork paginile sunt marcate copy-on-write.

După apelul fork() atât procesul părinte cât și procesul copil accesează buf[0]. Acest lucru va rezulta într-un page fault (se
duplică pagina și pagina nouă si pagina veche primesc drept de scriere). Se va aloca astfel o pagină fizică nouă.

În final stivele celor două procese vor ocupa 3 pagini fizice.

6. În urma rulării secvenței de cod de mai jos fișierul a.txt conține șirul 222313. După fiecare apel write actualizaţi următorul vector (cursor fd1,
cursor fd2, cursor fd3, conţinut fişier). Exemplu: inainte de primul write vectorul va fi (0, 0, 0, '').
fd1 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); switch (pid) {
fd2 = open("a.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); case 0:
fd3 = dup(fd1); write(fd1, "1", 1);
write(fd2, "2", 1);
write(fd1, "1", 1); write(fd3, "3", 1);
write(fd2, "2", 1); break;
write(fd3, "3", 1); default:
wait(&status);
pid = fork(); write(fd1, "1", 1);
write(fd2, "2", 1);
write(fd3, "3", 1);
/* continuat pe coloana a doua */ break;
}

inițial w1 w2 w3 w4 w5 w6 w7 w8 w9
f1.cursor 0 1 1 2 3 3 4 5 5 6
f2.cursor 0 0 1 1 1 2 2 2 3 3
f3.cursor 0 1 1 2 3 3 4 5 5 6
conținut “” “1” “2” “23” “231” “221” “2213” “22131” “22231” “222313”

Procesul părinte și fiu partajează descriptorii de fișier și cursorul de fișier. Orice modificare a cursorului în procesul copil va fi
vizibilă în procesul părinte și invers.
fd3 partajează cursorul de fișier cu fd1 ca urmare a apelului dup. Orice incrementare a cursorului folosind descriptorul fd1 va fi
vizibila descriptorului fd3 și invers.
1. Fie următoarele procese și timpii lor de execuție și de intrare (process, timp execuție, timp intrare): (P1, 8, 1), (P2, 4, 2 ), (P3,
3, 3 ), (P4, 7, 4 ). Determinați timpii medii de așteptare pentru First Come First Served (FCFS) și Shortest Job First (SJF).

În cadrul răspunsului, am marcat cu (a, b) intervalul în care un proces așteaptă și cu [a, b] intervalul în care un proces este activ
pe procesor

= FCFS =

P1: [1,9]
P2: (2, 9) [9,13]
P3: (3, 13) [13,16]
P4: (4, 16) [16,23]

Procesul P1 intră în sistem la momentul 1 și, fiind primul proces, lucrează 8 unități de timp până la momentul 9. Nu așteaptă.
Procesul P2 intră în sistem la momentul 2 și așteaptă procesul P1; va fi planificat înaintea proceselor P3 și P4. Va aștepta până
la încheierea procesului P1 adică intervalul (2, 9) Începe să lucreze în intervalul [9,13] pentru 4 unități de timp.
Procesul P3 intră în sistemul la momentul 3 și așteaptă procesul P1 și P2, adică intervalul (3,13). Va lucra în intervalul [13,16]
pentru 3 unități de timp.
Procesul P4 intră în sistemul la momentul 4 și așteaptă procesul P1, P2 și P3, adică intervalul (4,16). Va lucra în intervalul
[16,23] pentru 7 unități de timp.

Timp mediu de așteptare = ( 0 + 7 + 10 + 12) / 4 = 7.25

= SJF =

P1: [1,9]
P2: (2,12)[12,16]
P3: (3,9)[9,12]
P4: (4,16)[16,23]

* Procesul P1 întră în sistem la momentul 1 și rulează pe procesor [1,9].


* La momentul 2 intră procesul P2 al cărui timp de execuție este 4. Întrucât primul proces rulează, nu va preempta procesorul.
* La momentul 3 intră procesul P3 al cărui timp de execuție este 3. Întrucât primul proces rulează, nu va preempta.
* La momentul 4 intră procesul P4 al cărui timp de execuție este 7. Nu preemptează nici un proces și așteaptă.
* La momentul 9, P1 își încheie execuția. Se planifică procesul cu timpul de execuție cel mai scurt. Se planifică P3 (timp de
execuție 3). P3 rulează pe procesor în intervalul [9,12].
* La momentul 12, P3 își încheie execuția. Se planifică procesul cu timpul de execuție cel mai scurt. Se planifică P2 (timp de
execuție 4). P3 rulează pe procesor în intervalul [12,16].
* La momenutl 16, P2 își încheie execuția. Se rulează procesul P4.
* La momentul 23, procesul P4 își încheie execuția.

Timp mediu de așteptare = (0 + 10 + 6 + 12) / 4 = 7

2. Câte procese se vor crea în urma execuției secvenței de mai jos (se exclude procesul curent)? Toate apelurile fork se întorc
cu succes.
l1) fork();
l2) if (fork() == 0)
l3) fork();

Apelul fork() întoarce 0 în procesul copil și cu PID-ul procesului copil în părinte


Fie P1 primul proces (cel existent).
După linia 1 (l1) primul proces creează un proces copil, P2. Întrucât nu se verifică valoarea de ieșire a apelului fork, ambele
procese (P1 și P2) vor continua cu linia l2
În cadul liniei 2 cele două procese de mai sus creează câte un proces copil. Fie P3 procesul copil pentru P1 și P4 procesul copil
pentru P2. În cadrul liniei se verifică valoarea întoarsă de apelul fork. Doar procesele copil proaspăt create vor satisface condiția
if (fork() == 0). Adică foar procesele P3 și P4 vor continua la linia l3.
În cadrul liniei l3, procesele P3 respectiv P4 creează câte un proces copil; fie acestea P5 și P6.
La sfârșitul secțiunii vor exista 6 procese: procesul inițial (P1) și 5 noi procese create (P2, P3, P4, P5, P6).

3. Fie secvenţele de pseudocod de mai jos. Care din cele două abordări este optimă?
mutex_lock(&mutex); spin_lock(&spinlock);
a++; a++;
mutex_unlock(&mutex); spin_unlock(&spinlock);

Secvențele de mai sus urmăresc accesul exclusiv pentru incrementarea variabilei a. Incrementarea variabilei a este o operație
rapidă și puțin consumatoare de timp. Se dorește, așadar, un mecanism de sincronizare/asigurare a accesului exclusiv cât mai
rapid. Acest mecansim este asigurat de spinlock-uri care operează foarte rapid în cadrul primitivelor spin_lock și spin_unlock. În
vreme ce o operație pe un mutex impune contactarea planificatorului, spinlock-ul impune o operație de busywaiting care va fi,
însă, foarte rapidă ținând cont de dimensiunea redusă a regiunii critice (necesită doar incrementarea variabilei a).
4. În secvența de cod de mai jos, PAGE_SIZE reprezintă dimensiunea unei pagini, iar apelul mmap reușește. În urma rulării a
10 instanţe ale programului de mai jos se ocupă 11 pagini fizice în zonele de date (date inițializate, heap, readonly (.rodata),
bss). Argumentați acest comportament.
const char x[PAGE_SIZE] = {1, };

int main(void)
{
char *z = mmap(NULL, PAGE_SIZE, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf(“%c %c\n”, x[0], z[0]);
while (1) /* wait forever */
;
return 0;
}

Vectorul x ocupă o pagină și fiind declarat const este read-only. Acest lucru înseamnă că pagina fizică aferentă acestuia poate fi
partajată între mai multe procese. Nu este nevoie de copie separată pentru fiecare proces în parte.

Vectoul z ocupă, de asemnea, o pagină. Fiind vorba de o mapare privată fiecare proces va dispune de o pagină separată.

Alocările se realizează folosind demand-paging. Doar în momentul accesului la date, se vor aloca pagini fizice.

Pentru fiecare instanță de proces, se alocă, în momentul accesului z[0] o pagină fizică nouă pentru z. Pagina fizică aferenta lui
x va fi identică pentru toate procesele, întrucât este read-only. După rularea a 10 procese vor rezulta 10 * 1 pagină (pentru z) +
1 pagină (pentru x) = 11 pagini fizice.

5. Care dintre apelurile sigaction și sigemptyset va dura mai mult? sigaction actualizează rutina de tratare a unui semnal, iar
sigemptyset initializeaza setul de semnale descris de structura sigset_t la 0.

sigaction este apel de sistem și are un overhead inerent. sigemptyset operează la nivelul biților dintr-o zonă de memorie,
operație foarte rapidă și care nu dispune de un overhead suplimentar. În consecință, durata sigaction este vizibil mai mare
decât durata sigemptyset. Puteti verifica pe codul de aici: http://swarm.cs.pub.ro/git/?p=razvan-
code.git;a=blob;f=tests/measure_time_syscall.c;h=f7046aedb4b8af9ffe65cc84533ca43c3888a1d6;hb=refs/heads/examples

6. Fie un sistem pe 32 biți cu 100MB RAM, 100MB de swap și pagini de 4KB. Spațiul de adresă virtual este împărțit 3GB
programe utilizator / 1 GB kernel. Un program aflat în execuție rulează:
void *ptr = malloc(1024 * 1024 * 1024);
Apelul se realizează cu success, returnând un pointer valid. Motivați de ce malloc se întoarce cu succes.
Ce se întâmplă și de ce, dacă se rulează:
memset(ptr, 0, 1024 * 1024 * 1024);

malloc alocă memorie folosind demand-paging. În concluzie se alocă memorie pur virtuală fără a dispune de suport fizic.
Memoria fizică nu este folosită și apelul reușește.

În cadrul apelului memset, se produc page-fault-uri la accesarea diferitelor adrese indicate de ptr. Pentru fiecare page-fault se
alocă o nouă pagină fizică. În jurul valorii de 200MB, spațiul din memoria RAM și cel de pe swap se vor fi umplut, drept pentru
care sistemul va rămâne fără memorie, se va activa un sistem de management de tipul OOM (Out of memory)
(http://en.wikipedia.org/wiki/Out_of_memory) care va începe să omoare procese. În final, sistemul va suferi crash/se va bloca
din cauza absenței memoriei fizice.
Nume s, i grupă:

Sisteme de Operare
2 septembrie 2013
Timp de lucru: 100 de minute
Notă: Toate răspunsurile trebuie justificate

1. (1 punct) De ce NU este posibil ca un planificator să fie, simultan, echitabil (fair) s, i


productiv (să ofere un throughput mare)? Commented [In1]: It seems you can add comments here,
• Pentru că ai un overhead la schimbarea de task-uri. Ca să fii echitabil, fiecare proces too.

trebuie să aibă un timp mai mult sau mai puțin egal pe procesor, însă schimbarea de
pe un proces pe altul durează -> nu poți fi productiv.

2. (1 punct) De ce este afectat sistemul gazdă (host) ı̂n cazul aparit, iei unei erori fatale la
nivelul nucleului unui container OpenVZ?

3. (1 punct) Funct, ia malloc alocă memorie. Funct, ia calloc alocă memorie s, i completează
spat, iul cu zero-uri. De ce un apel calloc generează, de obicei, mai multe page fault-uri decât
malloc?
Pentru că malloc nu verifică dacă memoria este alocată sau nu, și nu îți dai seama de asta decât atunci
când faci o scriere. Calloc scrie 0 în fiecare bit, deci dacă apar probleme de permisiuni, calloc o să țipe (not the
most decent explanation)

4. (1 punct) De ce este preferată folosirea apelurilor asincrone, acolo unde există, ı̂n locul
celor sincrone?
• Pentru că prin folosirea apelurilor sincrone, procesul este blocat, așteptând răspunsul
apelului. La folosirea unui apel asincron, procesul poate executa alte operații (sau
poate fi înlocuit pe procesor de alt proces) în timp ce așteaptă după răspunsul la apel)

5. (1 punct) Trei thread-uri afis, ează continuu respectiv mesajul "red", "green", "blue".
Descriet, i, t, inând cont de sincronizare, cele trei funct, ii aferente thread-urilor astfel ı̂ncât să se
afis, eze "red green blue red green blue red green blue ...".

6. (1 punct) De ce este indicat, din punct de vedere al securităt, ii, să fie folosit apelul
printf("%s", argv[1]) ı̂n locul apelului printf(argv[1])?
• Pentru că în primul apel printf o să afișeze un string. În
al doilea apel, printf poate afișa orice (inclusiv cifre, ce
ar cauza un overflow sau type mismatch)

7. (1 punct) Creat, i un paragraf adevărat, informativ s, i argumentat din domeniul sistemelor


de operare care să cuprindă ca subiecte principale not, iunile de fis, ier mapat ı̂n memorie s, i apel
de sistem.

8. (1 punct) Creat, i un paragraf adevărat, informativ s, i argumentat din domeniul sistemelor


de operare care să cuprindă ca subiecte principale not, iunile de hard link, symbolic link s, i inode.

9. (1 punct) Care sunt asocierile dintre clasele asincron, zero-copying, sincron blocant s, i
apelurile/tehnologiile read, readv, ReadFile, mmap, sendfile, Completion Ports, io submit?

10. (1 punct) Care sunt asocierile dintre perechile de mai jos (unul la unul, unul la mai multe,
1
Nume s, i grupă:
mai multe la unul, mai multe la mai multe)?
(procesor, registru)
- unul la mai multe
(spat, iu de adresă, stivă)
- unul la mai multe
(procesor, proces) -
unul la mai multe
(hard link, inode)
-mai multe la unul
(mutex, thread-uri ı̂n as, teptare)
- unul la mai multe

11. (2.5 puncte) Se dă un server TCP pe care trebuie să ı̂l caracterizat, i din punct de vedere
al performant, ei. În general, asta ı̂nseamnă să măsurat, i, de exemplu:

• numărul de cereri servite pe secundă


• numărul maxim de client, i servit, i simultan

2
• throughput de date
Nume s, i grupă:
s, i cum variază aceste trei mărimi una ı̂n funct, ie de cealaltă.
Descriet, i cum realizat, i o arhitectură de sistem (topologie, legături, scenarii de folosire) care
să poată măsura aces, ti parametri. Elaborat, i având ı̂n vedere faptul că software-ul de server
rulează pe un calculator server mult mai performant decât calculatoarele pe care le avet, i la
dispozit, ie pentru testare.

12. (2.5 puncte) Pentru aceeas, i situat, ie ca la punctul anterior, descriet, i arhitectura software
a software-ului de măsură: single threaded, multi-threaded, multi-proces? Ce alt, i parametri
at, i mai putea măsura?

3
Nume s, i grupă:
10 iunie 2013

1. (1 punct) S, tiind că overhead-ul unui apel de sistem este de 7 ms s, i overhead-ul tratării
unui page fault este de 2 ms, ı̂n ce situat, ie un apel memcpy va dura mai mult decât un apel de
sistem?

2. (1 punct) De ce este o eroare de tip Segmentation fault urmată de o schimbare de context?


Pentru că după ce se rulează handler-ul întreruperii de segfault, fie se va pasa contextul
sistemului de operare, în cazul în care programul nu are handler pentru segfault, fie contextul
va deveni cel al programului în cauză, pentru a își putea reveni din situația creată

3. (1 punct) De ce putem crea un container OpenVZ ı̂n cadrul unui mas, ini virtuale VMware
Workstation, dar nu s, i invers?

4. (1 punct) Precizat, i s, i justificat, i valoarea de adevăr a următoarei afirmat, ii: Un apel write
blocant apelat de un proces multithreaded cu implementare ı̂n user-space NU va cauza un TLB
flush.

Nu va cauza un TLB flush pentru că apelul este executat în user-space, deci se face buffered

5. (1 punct) Un proces execută secvent,a:

for (i = 0; i < 42; i++)


a++;
În ce situat, ie poate genera această secvent, ă un TLB flush?

6. (1 punct) Majoritatea planificatoarelor I/O (I/O scheduler-ul ) realizează operat, ii de tip


sorting and merging pe cererile de lucrul cu discul. De ce aceste planificatoare sunt utile pentru
discuri IDE, dar nu sunt potrivite pentru dispozitive de stocare de tip SSD (Solid State Drive )?
Pentru ca SSD-urile sunt construite pe o cu totul alta tehnologie fata de HDD-uri, tehnologie
in care nu se foloseste un cap de citire deci nu exista latenta la seek, algoritmii de disk-
scheduling nu isi au rostul. In orice ordine s-ar realiza operatiile, overhead-ul e acelasi.

7. (1 punct) Creat, i un paragraf adevărat, informativ s, i argumentat din domeniul sistemelor


de operare care să cuprindă ca subiecte principale not, iunile de ASLR (Address Space Layout
Randomization ) s, i shellcode.
Folosirea ASLR împiedică rularea de shellcode-uri de pe stivă prin poziționarea stivei în
diverse zone din spațiul de adresă; atacatorul nu poate ști (ușor) care este adresa de start
a shellcode-ului. Are eficiență în special pe sistemele pe 64 de biți. Pe cele pe 32 de biți se
poate folosi brute force.

8. (1 punct) Creat, i un paragraf adevărat, informativ s, i argumentat din domeniul sistemelor


de operare care să cuprindă ca subiecte principale not, iunile de stack overflow s, i thread.
Folosirea unui număr mare de thread-uri în cadrul unui proces poate conduce mai rapid la
stack overflow. Fiecare thread are stiva proprie, cu dimensiunea fixată la crearea. Dacă se
creează prea multe thread-uri, se va ocupa foarte mult stiva. Stivele thread-urilor vor fi
apropiate unele de altele astfel că, în cazul unui flux de apeluri mare (apeluri recursive, de
4
Nume s, i grupă:
exemplu), există riscul ca stiva unui thread să suprascrie stiva altui thread.

9. (1 punct) Fie următoarea comandă rulată ı̂ntr-un shell. Identificat, i procesele, relat, iile
părinte-copil, descriptorii de fis, ier s, i redirectările existente.

cat examen | grep raspuns > corect


cat examen | grep raspuns > corect

Procese:
Bash
Cat
Grep
Relații
Bash – părinte la cat
Cat – părinte la grep
Descriptori de fișier:
Pipe va avea doi descriptori de
fișier (output-ul de la cat examen și
input-ul de la GREP
Mai avem un descriptor către
fișierul corect, în care scrie grep

Redirectări
Stdout de la cat la stdin grep
Stdout grep la corect

10. (1 punct) Ce drepturi (citire, scriere, execut, ie) au următoarele zone din spat, iul de adresă
al unui proces: text, data, rodata, bss, stivă, heap, biblioteci mapate? Justificat, i.
Executable files include four canonical sections called, by convention, .text, .data, .rodata, and .bss.
The .text section contains executable code and is packed into a segment which has the read and execute access
rights. The .data and .bss sections contain initialized and uninitialized data respectively, and are packed into
a segment which has the read and write access rights.

11. (2.5 puncte) Se cere să construit, i un server care ascultă pe un socket TCP s, i serves, te
cereri efectuate ı̂ntr-un limbaj propriu ı̂mpachetat in XML. Este nevoie să ofere suport pentru
maxim 1000 de client, i simultan s, i să servească 10.000 de cereri pe secundă ı̂n total din partea
acestor client, i. Alcătuit, i schema bloc a acestui server s, i detaliat, i blocul de comunicat, ie peste
TCP ı̂n pseudocod. Explicat, i alegerile făcute.

Blocurile/modulele implicate intro diagrama, despre care ar trebui discutat, sunt:

1. TCP communication
2. I/O model (async vs threading)
3. XML parser

Daca le luam de la coada la cap, parserul XML trebuie sa fie lightweight si sa implementeze
un subset necesar comunicarii intre client si server.

Modelul de I/O, daca ne uitm la numarul maxim de clienti cerut de specificatii, de 1000,

5
Nume s, i grupă:
putem sa implementam folosind 1000 de threaduri care asculta pe acelasi socket. Daca
insa ne uitam la faptul ca trebuie sa serveasca un numar relativ mare de tranzactii pe
secunda, probabil ca un model async I/O ar fi mai potrivit, eliminand context switchurile
dintre cele 1000 de threaduri din celalalt model. In cele din urma, probabil ca un model
hibrind cu un thread care asculta pe un socket si foloseste async IO si un numar de
threaduri care este o functie de numarul de core-uri este cel mai eficient.

Modulul de TCP are o particularitate interesanta. Din starea de listen, la aparitia unei
conexiuni noi se creeaza un fd de date. TCPul fiind un protocol de tip stream, sunt doua
posibilitati. Daca clientii trimit cereri care au toate nevoie de raspuns, atunci este ok,
serverul citeste de pe socket pana cand se poate forma un mesaj XML corect, apoi il trimite
mai departe catre procesare si trimite raspunsul inapoi pe aceeasi conexiune de date, dupa
care inchid conexiunea. Cazul mai complicat este cand un client poate trimite notificari
catre server fara sa astepte raspuns, caz in care este necesar sa se detecteze si sa se
delimiteze mai multe requesturi in acelasi buffer. In acest caz este posibil sa apara si
desincronizari intre client si server, daca clientul trimite notificari foarte rapid, bufferul de
TCP poate sa contina requesturi incomplete, ceea ce complica detectia de mesaje bine
formate.

6
12. (2.5 puncte)
Nume s, i grupă:
Un program are nevoie să stocheze 1.000.000 de fis, iere pe disc, fiecare de
10MB, care să poată fi accesate pe bază de identificatori numerici unici. Alcătuit, i schema bloc
a unui sistem de stocare care să ofere suport pentru acest volum de informat, ii s, i detailat, i blocul
de identificare a fis, ierului pe disc.
Aici problema consta in faptul ca nu este scalabil sa stochezi 1,000,000 de fisiere in acelasi
director, si nici intro baza de date nu prea are sens sa stochezi ca bloburi toata povestea
asta care insumeaza 10TB de date. Asadar, e clar ca fisierele vor trebui stocate pe disc in
foldere separate, cel mai bine intro structura arborescenta, care sa se poata intinde pe
oricate filesystemuri, deoarece e vorba de o capacitate mai mare decat discurile uzuale.
Asadar, blocurile implicate in aceasta solutie sunt doua:

1. Bloc de identificare
2. Bloc de stocare

Blocul de identificare se poate implementa cu o tabela simpla in orice sistem de baze de


date, care face o mapare de la un identificator unic la o cale de fisier pe disc.

Blocul de stocare are un API simplu, prin care i se cere, pentru un nou fisier de stocat,
locul in care se va stoca. Pentru a face asta, trebuie sa stie ce discuri exista in sistem, ce
capacitati au si ce filesystem au, pentru a determina un numar optim de intrari in director
pentru fiecare dintre ele. Cand se cere un slot pt un fisier nou, sistemul cauta pe volumele
existente cel mai bun loc in termeni de spatiu disponibil, intrari in director, etc. Se poate
implementa si un load balancer care sa distribuie fisierele cele mai cerute pe discuri
diferite, etc.

În conformitate cu ghidul de etică al Departamentului de Calculatoare,


declar că nu am copiat s, i nu voi copia la această lucrare. De asemenea,
nu am ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

7
Nume s, i grupă:

Sisteme de Operare
14 iunie 2013
Timp de lucru: 100 de minute
Notă: Toate răspunsurile trebuie justificate

1. (1 punct) Fie T1 timpul de mutare a unui fis, ier pe aceeas, i partit, ie s, i T2 timpul de mutare
a aceluias, i fis, ier pe o altă partit, ie. Ce relat, ie există ı̂ntre T1 s, i T2 s, i de ce?

T1 < T2
In cazul in care se muta un fisier pe aceeasi partitie, tot ce se intampla este crearea
unui nou hard link catre inode-ul referentiat de fisier si stergerea vechiului link. In situatia in
care se muta fisiere intre partitii, datele chiar se muta. Astfel, T2 este mai mare deoarece are
overhead de la mutarea datelor.
2. (1 punct) Bibliotecile partajate cont, in zone de memorie mapate ı̂n cadrul proceselor cu
următoarele permisiuni: r-x (cod), r-- (read-only data) s, i rw- (date). Care din aceste zone
NU trebuie să fie partajate ı̂ntre mai multe procese care folosesc aceeas, i bibliotecă s, i de ce?

3. (1 punct) Două sisteme identice din punct de vedere al hardware-ului s, i al sistemului de


operare sunt folosite ı̂n scopuri diferite. Primul este folosit pentru calculul de transformate
Fourier, iar al doilea este folosi drept server (web, e-mail, ssh, dns, dhcp, etc.). Care dintre cele
două va petrece mai mult timp ı̂n schimbări de context s, i de ce?
Al doilea!

There are three potential triggers for a context switch:

Multitasking[edit]
Most commonly, within some scheduling scheme, one process needs to be switched out of the CPU so another
process can run. This context switch can be triggered by the process making itself unrunnable, such as by waiting
for an I/O or synchronization operation to complete. On a pre-emptive multitasking system, the scheduler may
also switch out processes which are still runnable. To prevent other processes from being starved of CPU time,
preemptive schedulers often configure a timer interrupt to fire when a process exceeds its time slice. This
interrupt ensures that the scheduler will gain control to perform a context switch.

Interrupt handling[edit]
Modern architectures are interrupt driven. This means that if the CPU requests data from a disk, for example, it
does not need to busy-wait until the read is over; it can issue the request and continue with some other
execution. When the read is over, the CPU can be interrupted and presented with the read. For interrupts, a
program called an interrupt handler is installed, and it is the interrupt handler that handles the interrupt from
the disk.

When an interrupt occurs, the hardware automatically switches a part of the context (at least enough to allow
the handler to return to the interrupted code). The handler may save additional context, depending on details of
the particular hardware and software designs. Often only a minimal part of the context is changed in order to
minimize the amount of time spent handling the interrupt. The kernel does not spawn or schedule a special
8
Nume s, i grupă:
process to handle interrupts, but instead the handler executes in the (often partial) context established at the
beginning of interrupt handling. Once interrupt servicing is complete, the context in effect before the interrupt
occurred is restored so that the interrupted process can resume execution in its proper state.

User and kernel mode switching[edit]


When a transition between user mode and kernel mode is required in an operating system, a context switch is
not necessary; a mode transition is not by itself a context switch. However, depending on the operating system,
a context switch may also take place at this time.

4. (1 punct) În ce situat, ie pot două procese copil ale aceluias, i proces să aibă acelas, i PID?
Neither fork() nor vfork() keep the same PID although clone() can in one scenario (*a). They are all
different ways to achieve roughly the same end, the creation of a distinct child.

clone() is like fork() but there are many things shared by the two processes and this is often used to enable
threading.

vfork() is a variant of clone in which the parent is halted until the child process exits or executes another
program. It's more efficient in those cases since it doesn't involve copying page tables and such. Basically,
everything is shared between the two processes for as long as it takes the child to load another program.

Contrast that last option with the normal copy-on-write where memory itself is shared (until one of the
processes writes to it) but the page tables that reference that memory are copied. In other words, vfork() is
even more efficient than copy-on-write, at least for the fork-followed-by-immediate-exec use case.

But, in most cases, the child has a different process ID to the parent.

*a
Things become tricky when you clone() with CLONE_THREAD. At that stage, the processes still have
different identifiers but what constitutes the PID begins to blur. At the deepest level, the Linux scheduler doesn't
care about processes, it schedules threads.

A thread has a thread ID (TID) and a thread group ID (TGID). The TGID is what you get from getpid().

When a thread is cloned without CLONE_THREAD, it's given a new TID and it also has its TGID set to that value
(i.e., a brand new PID).

With CLONE_THREAD, it's given a new TID but the TGID (hence the reported process ID) remains the same as the
parent so they really have the same PID. However, they can distinguish themselves by getting the TID from
gettid().

There's quite a bit of trickery going on there with regard to parent process IDs and delivery of signals (both to
the threads within a group and the SIGCHLD to the parent), all which can be examined from the clone() man
page.

5. (1 punct) Două procese scriu s, i citesc un fis, ier, ı̂n mod sincronizat, prin următoarea secvent, ă
de pseudo-cod:
9
Nume s, i grupă:

acquire_mutex (&m);
if (condition)
write_to_file (content);
else
read_from_file(content);
release_mutex(&m);
Care este numărul minim de apeluri de sistem care sunt generate ı̂n secvent, a de pseudo-cod de
mai sus?
6. (1 punct) La un moment dat un proces accesează o adresă de memorie, fără a rezulta page
fault. După un timp, accesează din nou acea adresă s, i rezultă page fault. S, tiind că ı̂ntre cele
două accese descrise nu au existat alte accese la pagina aferentă, explicat, i de ce s-a produs acel
page fault.

7. (1 punct) Creat, i un paragraf adevărat, informativ s, i argumentat din domeniul sistemelor


de operare care să cuprindă ca subiecte principale not, iunile de handler de semnal s, i mutex.
Nu este recomandat să se folosească mutex-uri într-un handler de semnal. Dacă se face
lock pe mutex și, în programul principal, s-a făcut de asemenea, lock pe mutex, există riscul
unui deadlock; handler-u de semnal întrerupe programul principal în timp ce acesta încă
are ocupat mutex-ul.

8. (1 punct) Creat, i un paragraf adevărat, informativ s, i argumentat din domeniul sistemelor


de operare care să cuprindă ca subiecte principale not, iunile de pagină fizică (frame) s, i fis, ier
mapat ı̂n memorie.
Un fișier mapat în memorie ocupă pagini fizice (frame-uri) care sunt mapate apoi în spațiuil
de adresă al procesului. Un fișier poate fi mapat de mai multe procese, caz în care paginile
fizice (frame-urile) aferente pot fi partajate. Scrierile în spațiul de adresă vor ajunge în
spațiul fizic și vor fi flushed doar la apeluri specifice sau la închiderea mapării.

9. (1 punct) Care sunt asocierile dintre sect, iunile de memorie de mai jos s, i programe/executabile,
procese, respectiv thread-uri? (unul la unul, unul la mai multe, etc.)
text, rodata, data, bss, heap, stack

10. (1 punct) Asociat, i conceptele de mai jos cu solut, iile de virtualizare VMware Workstation,
KVM, LXC: kernel development, bare-metal virtualization, modul de kernel, native virtualiza-
tion, full virtualization, containers, disk image file.

10
11. (2.5 puncte) Un programator implementează o bază de date care va stoca obiecte de
Nume
dimensiune s, i grupă:
fixă de 1MB. Fiecare obiect are un identificator numeric unic (ID), s, i este salvat
pe disc (HDD) ca un fis, ier separat, folosind sistemul de fis, iere ext2. Numărul total de obiecte
poate fi foarte mare, fiind limitat doar de dimensiunea HDD-ului (e.g. 10TB 10 milioane
fis, iere).
Baza de date trebuie optimizată astfel ı̂ncât să acceseze cât mai repede un fis, ier identificat cu
un anumit ID.
Vi se cere să sugerat, i optimizări posibile pentru a atinge acest scop. Explicat, i care sunt fac-
torii care limitează performant, a, s, i argumentat, i optimizările alese. Desenat, i o schemă bloc a
sistemului propus.

Pentru deschiderea unui fisier e nevoie sa localizam fisierul in dentry - costul este liniar in
nr. de fisiere in director in ext2. Idee de baza: ca sa reducem timpul de acces, trebuie sa
ne asiguram ca subiectele sunt stocate in directoare care au un numar redus de fisiere.

Pentru a implementa baza de date, avem nevoie de doua lucruri:

• o tabela de hash care mapeaza ID-ul fisierului in directorul care il contine. Aceasta
tabela va fi stocata in memorie. Presupunand ca numarul de directoare este relativ
mic, dimensiunea tabelei este data de nr de fisiere * (dim_id +
pointer_nume_director) ~ 8 sau 16 octeti. 10 milioane de fisiere ar ocupa numai
160MB de RAM.
• o ierarhie de directoare in care fiecare director are un numar maxim de intrari X
(prestabilit).

Se pot folosi multiple structuri ierarhice cu adancime prestabilita. Se creaza astfel mai
multe directoare:

• un director “direct” care contine un numar de fisiere X


• un director “indirect” care contine X alte directoare fiecare cu X fisiere
• un director dublu indirect
• un director triplu indirect etc.

Se vor folosi directoarele “directe” dupa care cele indirecte, dublu-indirecte, etc.

Cum alegem X? Timpul de acces la un fisier depinde de X si de adancimea structurii de


directoare, citirea unui director dureaza: X(logXN + 1). Stiind N (e.g. 10 milioane), se
poate optimiza X alegand valoarea care minimizeaza formula de mai sus.

12. (2.5 puncte) Client, ii Youtube se conectează la servere via TCP s, i transmit o cerere care
cont, ine numele fis, ierului dorit, offset-ul de ı̂nceput s, i dimensiunea dorită (tipic, câteva zeci de
KB, pentru a evita download-ul cont, inutului ı̂n mod inutil, dacă utilizatorul ı̂nchide clipul).
Dacă serverul ı̂nchide conexiunea, clientul se va reconecta atunci când are nevoie de următoarea
secvent, ă de clip s, i va relua download-ul. Altfel, clientul t, ine conexiunea deschisă s, i doar va
emite o nouă cerere.
Vi se cere să implementat, i un server Youtube care să permită unui număr cât mai mare de
utilizatori să privească clipuri simultan, astfel:

• desenat, i o schemă bloc cu sistemul propus;


• explicat, i cum va fi tratat fiecare client (proces nou / thread / etc.) s, i motivat, i alegerea
făcută;
11
• explicat, i ce API vet, i folosi pentru a citi / scrie din sockets TCP (nonblocking, event-based,
Nume s, i grupă:
blocking?) s, i de ce;
• atunci când cres, te numărul de client, i, ce resurse vor limita scalabilitatea sistemului pe
care l-at, i construit? (e.g. nr. de descriptori, nr. de procese/thread-uri, ret, eaua)?
Ideea principala este de a trata o cerere de continut cat mai repede si de a inchide
conexiunea – astfel numarul de clienti conectati simultan este mic si acest lucru reduce
stresul asupra resurselor sistemtului (thread-uri, descriptori).

Se poate folosi un pool de thread-uri; atunci cand vine o cerere noua se aloca cererea
unuia din thread-urile din pool. Folosirea pool-ului de thread-uri minimizeaza costurile de
startup, si reduc costurile de switching (no tlb flush), insa toti clientii vor folosi acelasi
spatiu de adresa – totusi potentialele probleme de securitate par minore (read-only video
data).

Se va folosi blocking API pt. citire din sockets – e cea mai usor de folosit. Non-blocking
nu prea are sens cu thread-uri. Event based la fel. Cel mai probabil reteaua va deveni un
bottleneck. Din cauza ca folosim un pool de thread-uri nr de thread-uri nu e o problema,
si nici cel de descriptori (presupunand ca nu se executa accept atunci cand nu se poate
repartiza cererea clientului unui thread).

În conformitate cu ghidul de etică al Departamentului de Calculatoare,


declar că nu am copiat s, i nu voi copia la această lucrare. De asemenea,
nu am ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

Sisteme de Operare
27 mai 2013
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. Comanda ”ulimit -s unlimited” stabiles, te ca dimensiunea stivei să fie maximă posibilă.
De ce această comandă poate fi folosită pentru a ı̂nlesni atacuri de tip return-to-libc pe sisteme
cu suport de ASLR?
Cu cat stiva este mai mare cu atat posibilitatile de pozitionare a acesteia in spatiul de
adresa scad. Astfel, se poate face brute force mai usor.

2. Care este asocierea dintre not, iunea de spat, iu de adresă s, i proces? Dar ı̂ntre spat, iu de adresă s,
i thread-uri?
Fiecare proces are propriul spatiu de adresa -->( spatiu de adresa, proces ) - unu la unu
Threadurile unui proces partajeaza acelasi spatiu de adresa (spatiu de adresa, thread) - unul
la mai multe
12
Nume s, i grupă:
3. Folosind mecanismul de memorie virtuală, se pot partaja zone din spat, iul de adresă din user
space al unui proces cu zone din spat, iul kernel. Dat, i exemplu de situat, ie ı̂n care o astfel de
abordare este utilă.
A processor in a computer running Windows has two different modes: user mode and kernel mode. The
processor switches between the two modes depending on what type of code is running on the processor.
Applications run in user mode, and core operating system components run in kernel mode. Many drivers run
in kernel mode, but some drivers run in user mode.

When you start a user-mode application, Windows creates a process for the application. The process provides
the application with a private virtual address space and a private handle table. Because an application's virtual
address space is private, one application cannot alter data that belongs to another application. Each
application runs in isolation, and if an application crashes, the crash is limited to that one application. Other
applications and the operating system are not affected by the crash.

In addition to being private, the virtual address space of a user-mode application is limited. A processor
running in user mode cannot access virtual addresses that are reserved for the operating system. Limiting the
virtual address space of a user-mode application prevents the application from altering, and possibly
damaging, critical operating system data.

All code that runs in kernel mode shares a single virtual address space. This means that a kernel -mode driver
is not isolated from other drivers and the operating system itself. If a kernel-mode driver accidentally writes to
the wrong virtual address, data that belongs to the operating system or another driver could be compromised.
If a kernel-mode driver crashes, the entire operating system crashes.

This diagram illustrates communication between user-mode and kernel-mode components.

4. De ce numărul de intrări ı̂n TLB este de ordinul sutelor? De ce nu se alocă dimensiuni mai
mari pentru TLB (număr de intrări de ordinul miilor sau zecilor de mii)?
I'd be interested to experiment with different TLB sizes, to see what effect

that has on performance. But I suspect that lack of TLB contexts mean that we

wind up flushing the TLB more often than real hardware does, and therefore a

larger TLB merely takes longer to flush.

Hardware TLBs are limited in size primarily due to the fact that increasing their sizes increases their access
latency as well. but software tlb does not

suffer from that problem. so i think the size of the softtlb should be not influenced by the size of the hardware
tlb.

13
Nume s, i grupă:
Flushing the TLB is minimal unless we have a really really large TLB, e.g. a TLB with 1M entries. I vaguely
remember that i see ~8% of the time is spent in

the cpu_x86_mmu_fault function in one of the speccpu2006 workload some time ago. so if we increase the
size of the TLB significantly and potential getting

rid of most of the TLB misses, we can get rid of most of the 8%. ( there are still compulsory misses and a few
conflict misses, but i think compulsory

misses is not the major player here).

5. De ce se preferă folosirea spinlock-urilor pentru regiuni critice de dimensiuni mici iar mutex-
urile pentru regiuni critice de dimensiuni mari?
Deoarece spinlock-ul foloseste busy-waiting este indicat sa fie folosit pe portiuni mici unde
timpul de asteptare este mic si unde folosirea unui mutex ar aduce un overhead mult prea
mare. Mutex-urile sunt folosite pe portiuni critice mari deoarece costul unui busy waiting pe
un timp lung e mult mai mare decat asteptarea intr-o coada pana la un unlock pe mutex.
6. Care este legătura s, i diferent, a dintre not, iunile de ”drepturi de creare a unui fis, ier” s, i ”drep-
turi de deschidere a unui fis, ier”?
7. Care este un avantaj al fiecăreia dintre formele de virtualizare: full virtualization s, i paravir-
tualization?
Full virtualization:
*AVANTAJE
One of the most common reasons for implementing a full virtualization solution is for
operational efficiency. It allows organizations to use existing hardware more efficiently by
placing a greater load on each computer. This means that servers using full virtualization can
use more of the computer’s processing and memory resources than servers running a single
OS instance and a single set of services.
Another reason to use full virtualization is to facilitate desktop virtualization, in which a single
PC runs more than one OS instance. There are a number of reasons to do so. This can offer
support for applications that only run on a particular OS. It can also allow changes to be
made to an OS and later on, revert to the original if necessary. Desktop virtualization has
also proven to support better control of OSs, in order to ensure that they meet basic security
requirements
*DEZAVANTAJE
• Adds layers of technology, which increase the security management burden as it
requires additional security controls.
• Combining a number of systems onto a single physical computer causes a larger
impact, should a security compromise occur.
• It’s relatively easy to share information between virtualization systems, which can
facilitate attack vectors, if not carefully controlled or regulated.
• The dynamic aspect of virtualized environments renders creating and maintaining the
necessary security boundaries more complex.
.PARAVIRTUALIZATION

14
Nume s, i grupă:
*AVANTAJE
Performance is the most well known advantage that paravirtualization has, however with
paravirtualized device drivers in a fully virtualized OS this advantage is actually getting
smaller over time.
However, compared to traditional full virtualization, where the virtualization software
emulates a complete computer and a completely unmodified guest operating system is run,
paravirtualization has very significant performance advantages.

8. De ce un proces care reprezintă un server web are prioritate mai mare decât un proces care
este folosit pentru ı̂nmult, iri de matrice?
CPU INTENSIVE VS I/O
9. Procesul P foloses, te thread-uri hibride, având 9 thread-uri user-level, mapate pe 3 thread-
uri kernel-level (câte trei user-level threads pe un kernel-level thread). Fie thread-ul user-level
TU1 mapat pe thread-ul kernel-level TK1. În ce mod vor fi afectate celelalte thread-uri mapate
pe TK1, ı̂n cazul ı̂n care TU1 realizează un acces nevalid la memorie? Dar thread-urile mapate
pe celelalte thread-uri kernel?
10. De ce este necesară operat, ia de ”Safe Remove” a dispozitivelor USB, ı̂n urma copierii unor
fis, iere noi pe acestea?
Obviously, yanking out a drive while it's being written to could corrupt the data. However, even if the
drive isn't actively being written to, you could still corrupt the data. By default, most operating
systems use what's called write caching to get better performance out of your computer. When you
write a file to another drive—like a flash drive—the OS waits to actually perform those actions until it
has a number of requests to fulfill, and then it fulfills them all at once (this is more common when
writing small files). When you hit that eject button, it tells your OS to flush the cache—that is, make
sure all pending actions have been performed—so you can safely unplug the drive without any data
corruption.

În conformitate cu ghidul de etică al Catedrei de Calculatoare, declar că nu


am copiat s, i nu voi copia la această lucrare. De asemenea, nu am ajutat s, i
nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

15
Sisteme de Operare
24 mai 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. (7 puncte) Precizat, i o diferent, ă ı̂ntre un thread s, i un proces.


Procesele au propriul spatiu de adresa pe cand
threadurile unui proces sharuiesc acelasi spatiu de adresa.
Processes are the abstraction of running programs: A binary image, virtualized memory, various
kernel resources, an associated security context, and so on. Threads are the unit of execution in a
process: A virtualized processor, a stack, and program state. Put another way, processes are running
binaries and threads are the smallest unit of execution schedulable by an operating system's process
scheduler.

A process contains one or more threads.

Both processes and threads are independent sequences of execution. The typical difference
is that threads (of the same process) run in a shared memory space, while processes run in
separate memory spaces.

2. (7 puncte) Care este legătura ı̂ntre memorie virtuală s, i spat, iu de swap?


Memoria virtuală este memoria pe care o vede un proces care
rulează în sistem. Pentru că în RAM nu încape întreg spațiul adresat de
memoria virtuală (~4GB pentru procesoarele pe 32 de biți) extindem
memoria cu un cache pe hard disk, pe care îl numim swap.

Virtual memory is a combination of RAM and disk space that running processes can use.

Swap space is the portion of virtual memory that is on the hard disk, used when RAM is
full.

3. (7 puncte) Dat, i exemplu de dispozitiv de tip caracter. De ce este acesta un dispozitiv de


tip caracter?
Tastatura este un dispozitiv de tip caracter pentru ca trasferul de date se realizeaza
caracter cu caracter.
A character device is any device that can have streams of characters read from or written
to it. A character device has a character device driver associated with it that can be used
for a device such as a line printer that handles one character at a time. However, character
drivers are not limited to performing I/O a single character at a time (despite the name
``character'' driver). For example, tape drivers frequently perform I/O in 10K chunks. A
character device driver can also be used where it is necessary to copy data directly to or
from a user process. Because of their flexibility in handling I/O, many drivers are character
drivers. Line printers, interactive terminals, and graphics displays are examples of devices
that require character device drivers.

(si despre block character, sa-l avem acolo)


1
exemplu: hard disk

A device, such as a magnetic tape drive or disk drive, that conveys data in blocks through the
buffer management code.

A block device is one that is designed to operate in terms of the block I/O supported by Digital
UNIX. It is accessed through the buffer cache. A block device has an associated block device
driver that performs I/O by using file system block-sized buffers from a buffer cache supplied by
the kernel. Block device drivers are particularly well-suited for disk drives, the most common block
devices.

4. (7 puncte) Descriet, i, ı̂n pseudocod sau literal, un scenariu ı̂n care are loc un atac de tipul
buffer overflow. Precizat, i ı̂n ce condit, ii se realizează acest atac.
exemplU, avem urmatorul program:
#include <stdio.h>

#include <string.h>

int main(void) {

char buff[15];

int pass = 0;

printf("\n Enter the password : \n");

gets(buff);

if(strcmp(buff, "thegeekstuff")) {

printf ("\n Wrong Password \n");

} else {

printf ("\n Correct Password \n");

pass = 1; }

if(pass) { /* Now Give root or admin rights to user*/ printf ("\n


Root privileges given to the user \n"); } return 0; }

Daca rulam programul cu parola: thegeekstuff, se produce ceea ce ne asteptam, si primim drepturi de root.

Acest program insa are posibilitatea de a produce buffer overflow. Functia gets() nu verifica marginile unde
scrie si poate scrie string-uri de lungime mai mare decat marimea celui in care scrie el de fapt. Asadar, daca
atacatorul da o parola mai lunga decat marimea buffer-ului, acesta poate sa ajunga sa scrie peste zona de
memorie a variabilei "pass", devenind ceva diferit de 0 => drepturi de root pentru atacator.

5. (7 puncte) Un fis, ier are două link-uri hard (a.txt s, i b.txt). Ce se ı̂ntâmplă dacă s, tergem
unul dintre link-uri (rm a.txt)?
When you create a second, third, fourth, etc link, the counter is incremented (increased ) each
2
time by one. When you delete (rm) a link the counter is decremented ( reduced ) by one. If the link
counter reaches 0 the filesystem removes the inode and marks the space as available for use.

In short, as long as you do not delete the last link the file will remain.

Edit: The file will remain even if the last link is removed. This is one of the ways to ensure
security of data contained in a file is not accessible to any other process. Removing the data from
the filesystem completely is done only if the data has 0 links to it as given in its metadata and is
not being used by any process.

raspunzi cam la orice intrebare soft links vs hard links:


Underneath the file system files are represented by inodes (or is it multiple inodes not sure)

A file in the file system is basically a link to an inode.


A hard link then just creates another file with a link to the same underlying inode.

When you delete a file it removes one link to the underlying inode. The inode is only deleted (or deletable/over-
writable) when all links to the inode have been deleted.

A symbolic link is a link to another name in the file system.

Once a hard link has been made the link is to the inode. deleting renaming or moving the original file will not
affect the hard link as it links to the underlying inode. Any changes to the data on the inode is reflected in all files
that refer to that inode.

Note: Hard links are only valid within the same File System. Symbolic links can span file systems as they are
simply the name of another file.

6. (10 puncte) De ce este necesară prezent, a bitului setuid (suid) pe executabilul /usr/bin/passwd?
Pentru a executa passwd user-ul nu are neaparat nevoie de privilegii de root (ex: vrea sa isi
schimbe parola). Cu toate astea, procesul are nevoie de privilegii pentru a modifica fisierul
/etc/passwd. Aici intervine bitul setuid care seteaza effective user id-ul la owner-ul executabilului -
root.

7. (10 puncte) Într-un sistem rulează la un moment dat 100 de procese. Câte tabele de pagini
sunt alocate? Justificat, i.
By giving each process its own page table, every process can pretend that it has access to the
entire address space available from the processor. It doesn't matter that two processes might use
the same address, since different page-tables for each process will map it to a different frame of
physical memory. Every modern operating system provides each process with its own address
space like this.

Over time, physical memory becomes fragmented, meaning that there are "holes" of free space in
the physical memory. Having to work around these holes would be at best annoying and would
become a serious limit to programmers. For example, if you malloc 8 KiB of memory; requiring

3
the backing of two 4 KiB frames, it would be a huge inconvience if those frames had to be
contiguous (i.e., physically next to each other). Using virutal-addresses it does not matter; as far
as the process is concerned it has 8 KiB of contiguous memory, even if those pages are backed
by frames very far apart. By assigning a virtual address space to each process the programmer
can leave working around fragmentation up to the operating system.

8. (10 puncte) În urma a două apeluri accept() ı̂n codul unui server sunt creat, i doi socket, i
care au aceeas, i adresă IP s, i port. Cum diferent, iază sistemul de operare socketul căruia ı̂i va fi
livrat un pachet dat?
Socketii sunt identificati printre altele de urmatoarele atribute: ip sursa, port sursa, ip
destinatie, port destinatie. In cazul de fata, desi doua dintre atribute vor fi identice pentru
ambii socketi celelalte doua vor putea identifica peer-ul asociat (port sursa, ip sursa).

9. (10 punct) De ce este de preferat folosira unei cuante de timp mai mari pentru planificatorul
de procese al unui sistem de tip server, s, i a unei cuante de timp mai mici pentru planificatorul
de procese al unui sistem de tip laptop?

10. (10 puncte) Apelul lseek() actualizează cursorul de fis, ier. De ce această actualizare nu
produce nici o modificare a inode-ului fis, ierului?
If it were associated with the inode, then you would not be able to have multiple processes accessing a file
in a sensible manner, since all accesses to that file by one process would affect other processes.
Thus, a single process could have track many different file positions as it has file descriptors for a given file.

Per the lseek docs, the file position is associated with the open file pointed to by a file descriptor, i.e. the thing that
is handed to your by open. Because of functions like dup and fork, multipledescriptors can point to a single
description, but it's the description that holds the location cursor.
File position is associated with an open file description, not a file descriptor. Many different file descriptors can
refer to the same open file description, due to fork, dup, etc.

11. (15 puncte) Avet, i la dispozit, ie un sistem cu mai multe core-uri s, i vret, i să dezvoltat, i o
bibliotecă de video transcoding (CPU-bound) pentru stream-uri video dintr-un fis, ier. Presupu-
nem că avet, i detaliile unui algoritm de transcoding paralel. Acest algoritm permite efectuarea
operat, iei de transcoding separat pe blocuri diferite din stream (transcode_block()). Este
ı̂nsă nevoie, la finalul trascodingului unui bloc de o operat, ie de unificare la final pentru două
blocuri adiacente (merge_adjacent_stream_blocks()); ı̂n această parte de unificare se ,,li-
pesc”, respectiv, părt, ile de ı̂nceput s, i sfârs, it ale celor două blocuri. Blocurile sunt citite s, i
scrise dintr-un/ı̂ntr-un fis, ier. Dimensiunea blocului este prestabilită.
Care vor fi principiile de proiectare a bibliotecii? Vet, i folosi thread-uri sau procese? Câte? Ce
mecanisme de comunicare s, i sincronizare vet, i folosi ı̂n cadrul bibliotecii? Care sunt factorii de
overhead din implementare?

4
În conformitate cu ghidul de etică al Departamentului de Calculatoare, de-
clar că nu am copiat s, i nu voi copia la această lucrare. De asemenea, nu am
ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

5
Sisteme de Operare
3 iunie 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. (7 puncte) Precizat, i două entităt, i diferite pe care le poate referi un descriptor de fis, ier ı̂n
Linux.

Entati diferite ce le poate referi: standard input/output/error, fisier, pipe, resursa


input/output
if there are 100 files opened in your OS then there will be 100 entries in OS (somewhere in kernel). These
entries are represented by integers like (...100, 101, 102....). This entry number is the file descriptor. So it is just
an integer number that uniquely represents an opened file in operating system. If your process opens 10 files
then your Process table will have 10 entries for file descriptors.
Similarly when you open a network socket, it is also represented by an integer and it is called Socket
Descriptor.

2. (7 puncte) În urma unui buffer overflow adresa de retur este suprascrisă. Către ce zonă
poate pointa adresa suprascrisă pentru a genera un atac, ı̂n cazul ı̂n care sistemul are DEP
(Data Execution Prevention )? Aleget, i dintre text, stack, data. Justificat, i.

3. (7 puncte) De ce este utilă paginarea ierarhică?


The inverted page table keeps a listing of mappings installed for all frames in physical memory. However,
this could be quite wasteful. Instead of doing so, we could create a page table structure that contains
mappings for virtual pages. It is done by keeping several page tables that cover a certain block of virtual
memory. For example, we can create smaller 1024-entry 4K pages that cover 4M of virtual memory.

This is useful since often the top-most parts and bottom-most parts of virtual memory are used in running a
process - the top is often used for text and data segments while the bottom for stack, with free memory in
between. The multilevel page table may keep a few of the smaller page tables to cover just the top and
bottom parts of memory and create new ones only when strictly necessary.

Now, each of these smaller page tables are linked together by a master page table, effectively creating a
tree data structure. There need not be only two levels, but possibly multiple ones.

A virtual address in this schema could be split into three parts: the index in the root page table, the index in
the sub-page table, and the offset in that page.

Multilevel page tables are also referred to as hierarchical page tables.

Alt raspuns:

You will appreciate the space optimization of multi-level page tables when we go into the 64-bit address space.

Assume you have a 64-bit computer ( which means 64 bit virtual address space ), which has 4KB pages and 4
GB of physical memory. If we have a single level page table as you suggest, then it should contain one entry
per virtual page per process.
6
One entry per virtual page – 264 addressable bytes / 212 bytes per page = 252 page table entries

One page table entry contains: Access control bits ( Bits like Page present, RW etc ) + Physical page number

4 GB of Physical Memory = 232 bytes.

232 bytes of memory/212 bytes per page = 220 physical pages

20 bits required for physical page number.

So each page table entry is approx 4 bytes. ( 20 bits physical page number is approx 3 bytes and access control
contributes 1 byte )

Now, Page table Size = 252 page table entries * 4 bytes = 254 bytes ( 16 petabytes ) !

16 petabytes per process is a very very huge amount of memory.

Now, if we page the pagetable too, ie if we use multi level page tables we can magically bring down the
memory required to as low a single page. ie just 4 KB.

Now, we shall calculate how many levels are required to squeeze the page table into just 4 KB. 4 KB page / 4
bytes per page table entry = 1024 entries. 10 bits of address space required. i.e 52/10ceiled is 6. ie 6 levels of
page table can bring down the page table size to just 4KB.

6 level accesses are definitely slower. But I wanted to illustrate the space savings out of multi level page tables.

4. (7 puncte) Cu ce diferă apelul fork() din Linux fat, ă de apelul CreateProcess() din
Windows?
In Windows when you call
CreateProcess() it does just that.
It creates a new "clean" process
ready to run what you say.

In Linux fork() clones the


process's page table and sets
the child process's pages to
copy-on-write--so no, there is no
measurable "waste of time and
resources". And if you're really
that concerned about
performance, you can use
vfork() instead, which doesn't
even bother to clone the page
table. In fact, if you'd done your
homework at all you would know
that spawning new processes on
Linux is quite significantly faster
7
than on Windows.

5. (7 puncte) Când folosim mutex-uri ı̂n loc de operat, ii atomice pentru asigurarea accesului
serial la date?
6. (10 puncte) Fie instruct, iunea:

a = b;
În ce situat, ie instruct, iunea generează două page fault-uri fără a conduce la terminarea proce-
sului curent?
DEFINITIE:
A page fault is a trap to the software raised by the hardware when a program accesses a page that is
mapped in the virtual address space, but not loaded in physical memory.

That's not entirely correct, as explained later in the same article (Minor page fault). There are soft page faults,
where all the kernel needs to do is add a page to the working set of the process. Here's a table from the
Windows Internals book (I've excluded the ones that result in an access violation):

• Reason for Fault - Result


• Accessing a page that isn’t resident in memory but is on disk in a page file or a mapped file -Allocate a
physical page, and read the desired page from disk and into the relevant working set
• Accessing a page that is on the standby or modified list - Transition the page to the relevant process,
session, or system working set
• Accessing a demand-zero page - Add a zero-filled page to the relevant working set
• Writing to a copy-on-write page - Make process-private (or session-private) copy of page, and replace
original in process or system working set

Page faults can occur for a variety of reasons, as you can see above. Only one of them has to do with reading
from the disk. If you try to allocate a block from the heap and the heap manager allocates new pages, then
accesses those pages, you'll get a demand-zero page fault. If you try to hook a function in kernel32 by writing
to kernel32's pages, you'll get a copy-on-write fault because those pages are silently being copied so your
changes don't affect other processes.

Now to answer your question more specifically: Process Hacker only seems to have page faults when updating
its service information - that is, when it calls EnumServicesStatusEx, which RPCs to the SCM (services.exe). My
guess is that in the process, a lot of memory is being allocated, leading to demand-zero page faults (the service
information requires several pages to store, IIRC).

Several reasons:
1. Processes are created by memory mapping the code sections from the file. Reading sections of a
memory mapped file that aren't yet in memory causes page faults, so each process will at least have
the page faults of reading in its own executable and any DLLs whose code wasn't yet in memory.
Other memory mapped files used by a process will also cause page faults.
2. When a process requests memory with VirtualAlloc, no physical frames are actually committed to
the process until the allocated pages are 'touched' for the first time. This also causes page faults.
3. Even when memory is not full, Windows will trim infrequently used pages from the process' working
set and lazily page them out to disk. This enables Windows to better respond to sudden demands for
8
large amounts of memory. When a process attempts to access such a page, it causes a page fault. In
situations where memory consumption is low enough, the page will probably still be in memory so no
disk read is necessary, but a page fault is still triggered. This is called a soft page fault.

7. (10 puncte) În ce situat, ie folosit, i apeluri de tip read/write ı̂n loc de maparea fis, ierului ı̂n
memorie?
It really depends on what you're trying to do. If all you need to do is hop to a known offset and read out a small
tag, read() may be faster (mmap() has to do some rather complex internal accounting). If you are planning
on copying out all 200mb of the MP3, however, or scanning it for some tag that may appear at an unknown
offset, then mmap() is likely a faster approach.

read() on the other hand involves an extra memory-to-memory copy, and can thus be inefficient for large
I/O operations, but is simple, and so the fixed overhead is relatively low. In short, use mmap()for large bulk
I/O, and read() or pread() for one-off, small I/Os.

8. (10 puncte) În cadrul unei conexiuni TCP, transmit, ătorul realizează apelul:

send(s, send_buffer, 5000, 0); /* send 5000 bytes */


iar receptorul realizează apelul

recv(s, recv_buffer, 7000, 0); /* receive 7000 bytes */


Cât, i octet, i va primi receptorul (la ı̂ntoarcerea din apelul recv)?
Receptorul poate primi oricati octeti intre 1 si 5000, presupunand ca niciun alt send nu s-a realizat sau se va
realiza din partea tansmitatorului. In functie de congesia retelei, a incarcarii bufferelor din kernel si cele de pe
placa de retea aceasta valoare variaza intre 1 si 5000. Daca transmitatorul inchide conexiunea inainte ca datele
sa fie transmise, apelul recv se va intoarce cu -1.

9. (10 punct) Un thread foloses, te malloc pentru a aloca memorie. Precizat, i un set de pas, i (ı̂n
pseudocod) ı̂n care alt thread al aceluias, i proces accesează zona de memorie alocată de primul
thread.
• malloc() and free() are not thread-safe functions. You need to protect the calls to those functions with
a mutex.
• You need to protect all shared variables with a mutex as well. You can use the same one as you use for
malloc/free, one per variable.
• You need to declare variables shared between several threads as volatile, to prevent dangerous
optimizer bugs on some compilers. Note that this is no replacement for mutex guards.
• Are the buffers arrays, or two-dimensional arrays (like arrays of C strings)? You have declared all
buffers as potential two-dimensional arrays, but you never allocate the inner-most dimension.
• Never typecast the result of malloc in C. Read this and this.
• free(bufferaction), not free(&bufferaction).
• Initialize all pointers to NULL explicitly. After free(), make sure to set the pointer to NULL. Before the
memory is accessed by either thread, make sure to check the pointer against NULL.

10. (10 puncte) Un sistem de fis, iere ext2 poate folosi fis, iere cu dimensiune de până la 16GB.
Ce limitează dimensiunea maximă a fis, ierelor?
There are various limits imposed by the on-disk layout of ext2. Other limits are imposed by the current
9
implementation of the kernel code. Many of the limits are determined at the time the filesystem is first
created, and depend upon the block size chosen. The ratio of inodes to data blocks is fixed at filesystem
creation time, so the only way to increase the number of inodes is to increase the size of the filesystem.

The 2TiB file size is limited by the i_blocks value in the inode which indicates the number of 512-bytes sector
rather than the actual number of ext2 blocks allocated.

This limit was also overcome ages ago by the use of a flag in the inode that indicates that the i_blocks value is,
in fact, in units of block size rather than 512 bytes. The triple indirect block structure though, can only address
just over 4 TiB using a 4k block size.

11. (15 puncte) Dorim să implementăm un proxy server pentru conexiuni web. Un proxy ser-
ver serves, te cereri din cache-ul local dacă paginile web se găsesc ı̂n cache; altfel face cereri către
serverul web destinat, ie, obt, ine pagina s, i apoi o cache-uies, te. Facem următoarele presupuneri:

• Paginile cerute sunt, ı̂n mare parte, de dimensiuni mici (ordinul kilooctet, ilor).
• Paginile se modifică greu; nu este nevoie să vă gândit, i la expirarea paginilor ı̂n cache.

10
• Se poate folosi pentru caching atât memorie cât s, i spat, iu pe disc, ambele limitate.
• Există un număr mare de cereri pe secundă pe care le primes, te proxy serverul.

Ce tehnologii vet, i folosi ı̂n proiectarea proxy serverului? (operat, ii asincrone, multiplexare,
multithreading, multiproces, etc.). Justificat, i alegerea.
Ce politică de ı̂nlocuire a paginilor ı̂n cache vet, i folosi?
Ce pagini vet, i plasa ı̂n cache-ul de memorie s, i ce pagini vet, i plasa ı̂n cache-ul de pe disc?
Cum vet, i asigura accesul sincronizat/consecvent/coerent la datele din cache?

În conformitate cu ghidul de etică al Departamentului de Calculatoare, de-


clar că nu am copiat s, i nu voi copia la această lucrare. De asemenea, nu am
ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

11
Sisteme de Operare
5 iunie 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. (7 puncte) Când are loc un flush de TLB (Translation Lookaside Buffer )?


Are loc la orice schimbare asupra structurii paginilor.

If another processor could also be affected by a page table write (because of shared
memory, or multiple threads from the same process), you must also flush the TLBs on those
1processors.

2. (7 puncte) Ce cont, in blocurile de date aferente unui director?

3. (7 puncte) De ce considerăm tastatura un dispozitiv de tip caracter?


a mai fost
4. (7 puncte) De ce este timpul de creare a unui thread al aceluias, i proces mai mic decât
timpul de creare a unui proces?
• Less time to create a new thread than a process, because the newly created thread uses the current
process address space.

5. (7 puncte) Câte tabele de descriptori de fis, iere are un proces cu 10 thread-uri?


una!
there is only one file descriptor table per process, and it's shared among all the threads.

The file descriptors are shared between the threads. If you want "thread specific" offsets, why not have each
thread use a different file descriptor (open(2) multiple times) ?

6. (10 puncte) Care este o legătură ı̂ntre planificatorul de procese s, i sistemul de ı̂ntreruperi?
Sistemul de intreruperi furnizeaza intreruperea de ceas, ce determina planificatorul sa ia
decizii privind procesul ce va rula in urmatoarea cuanta de timp.

7. (10 puncte) În cadrul unei conexiuni TCP un client trimite către un server mesaje ı̂ntr-o
buclă:

while (1) {
send(s, buffer, 8192, 0); /* send 8192 bytes */
}
La un moment dat, apelul send se blochează (pentru o perioadă de timp). Care este o cauză
posibilă pentru această blocare?
Toate bufferele intre transmitator si destinatar s-au umplut ori reteaua este congestionata.
Prin buffere ma refer la atat la bufferele din kernel cat si la cele de pe placile de retea.

8. (10 puncte) Descriet, i o situat, ie ı̂n care un buffer overflow pe un array aflat ı̂n zona de date
globale conduce la un exploit.
The principle of exploiting a buffer overflow is to overwrite parts of memory
which aren't supposed to be overwritten by arbitrary input and making the
process execute this code. To see how and where an overflow takes place, lets
12
take a look at how memory is organized. A page is a part of memory that uses its
own relative addressing, meaning the kernel allocates initial memory for the
process, which it can then access without having to know where the memory is
physically located in RAM. The processes memory consists of three sections:

- code segment, data in this segment are assembler instructions that the
processor executes. The code execution is non-linear, it can skip code, jump, and
call functions on certain conditions. Therefore, we have a pointer called EIP, or
instruction pointer. The address where EIP points to always contains the code
that will be executed next.

- data segment, space for variables and dynamic buffers

- stack segment, which is used to pass data (arguments) to functions and as a


space for variables of functions. The bottom (start) of the stack usually resides at
the very end of the virtual memory of a page, and grows down. The assembler
command PUSHL will add to the top of the stack, and POPL will remove one item
from the top of the stack and put it in a register. For accessing the stack memory
directly, there is the stack pointer ESP that points at the top (lowest memory
address) of the stack.

’Lets assume that we exploit a function like this:

void lame (void) { char small[30]; gets (small); printf("%s\n", small); }


main() { lame (); return 0; } Compile and disassemble it: # cc -ggdb blah.c
-o blah /tmp/cca017401.o: In function `lame': /root/blah.c:1: the `gets'
function is dangerous and should not be used. # gdb blah /* short
explanation: gdb, the GNU debugger is used here to read the binary file and
disassemble it (translate bytes to assembler code) */ (gdb) disas main Dump
of assembler code for function main: 0x80484c8 : pushl %ebp 0x80484c9 :
movl %esp,%ebp 0x80484cb : call 0x80484a0 0x80484d0 : leave 0x80484d1 : ret
(gdb) disas lame Dump of assembler code for function lame: /* saving the
frame pointer onto the stack right before the ret address */ 0x80484a0 :
pushl %ebp 0x80484a1 : movl %esp,%ebp /* enlarge the stack by 0x20 or 32.
our buffer is 30 characters, but the memory is allocated 4byte-wise
(because the processor uses 32bit words) this is the equivalent to: char
small[30]; */ 0x80484a3 : subl $0x20,%esp /* load a pointer to small[30]
(the space on the stack, which is located at virtual address
0xffffffe0(%ebp)) on the stack, and call the gets function: gets(small); */
0x80484a6 : leal 0xffffffe0(%ebp),%eax 0x80484a9 : pushl %eax 0x80484aa :
call 0x80483ec 0x80484af : addl $0x4,%esp /* load the address of small and
the address of "%s\n" string on stack and call the print function:
printf("%s\n", small); */ 0x80484b2 : leal 0xffffffe0(%ebp),%eax 0x80484b5
: pushl %eax 0x80484b6 : pushl $0x804852c 0x80484bb : call 0x80483dc
0x80484c0 : addl $0x8,%esp /* get the return address, 0x80484d0, from stack
and return to that address. you don't see that explicitly here because it
is done by the CPU as 'ret' */ 0x80484c3 : leave 0x80484c4 : ret End of

13
assembler dump. 3a. Overflowing the program # ./blah
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- user input xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ./blah xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx <- user input
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Segmentation fault (core dumped) # gdb
blah core (gdb) info registers eax: 0x24 36 ecx: 0x804852f 134513967 edx:
0x1 1 ebx: 0x11a3c8 1156040 esp: 0xbffffdb8 -1073742408 ebp: 0x787878
7895160

EBP is 0x787878, this means that we have written more data on the stack than the input
buffer could handle. 0x78 is the hex representation of 'x'. The process had a buffer of 32
bytes maximum size. We have written more data into memory than allocated for user
input and therefore overwritten EBP and the return address with 'xxxx', and the process
tried to resume execution at address 0x787878, which caused it to get a segmentation
fault.

9. (10 punct) Fie secvent, a de instruct, iuni de mai jos:

printf("%d", *a);
*a = 42;
unde a este un pointer la un ı̂ntreg (int *). Dat, i exemplu de situat, ie ı̂n care prima instruct, iune
(printf) NU cauzează page fault, dar a doua (*a = 42) cauzează page fault.

10. (10 puncte) Un set de thread-uri lucrează cu o structură de tip listă dublu ı̂nlănt, uită.
Unele thread-uri modifică lista (adaugă, s, terg elemente), altele doar parcurg lista. De ce trebuie
asigurat accesul exclusiv la listă pentru ambele tipuri de thread-uri, nu doar pentru cele care
modifică lista?

11. (15 puncte) Dorim să implementăm o bibliotecă de tip engine de baze de date. Această
bibliotecă va oferi un API de adăugare, s, tergere, inserare, modificare elemente ı̂n baza de date s,
i va realiza s, i stocarea fiecărei baze de date ı̂ntr-un fis, ier pe disc. Un program care va folosi
biblioteca va putea să stocheze informat, ii ı̂ntr-o bază de date dintr-un fis, ier pe disc ı̂ntr-un
format intern. Biblioteca trebuie să fie thread safe. Trebuie ca operat, iile executate ı̂n thread-
uri diferite ale procesului să ment, ină datele coerente.

14
Definit, i schematic API-ul pe care ı̂l va expune biblioteca: structuri de date s, i funct, ionalităt, i
expuse ca interfat, ă pentru programul ce va folosi biblioteca. Gândit, i-vă doar la interfat, a expusă
nu la internele implementării.
Cum vet, i asigura ı̂n cadrul implementării bibliotecii, ı̂n mod eficient, partea de thread safety?
Cum propunet, i să asigurat, i o viteză bună de lucru cu fis, ierul de bază de date s, i ı̂n acelas, i timp
să oferit, i o asigurare cât mai bună că datele ajung pe disc?
Cum vet, i implementa partea de tranzact, ie? Adică un set de operat, ii să fie executate atomic ı̂n
cadrul bibliotecii.

În conformitate cu ghidul de etică al Departamentului de Calculatoare, de-


clar că nu am copiat s, i nu voi copia la această lucrare. De asemenea, nu am
ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

15
Sisteme de Operare
8 iunie 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. (7 puncte) Un apel mmap() rezervă 16 pagini de memorie virtuală. Câte pagini de memorie
fizică alocă apelul?

2. (7 puncte) De ce este dezavantajoasă folosirea unei cuante de timp prea mari pentru
planificarea proceselor?

3. (7 puncte) Cu ce diferă un spinlock de un mutex?


Mutex Vs Spinlock
Criteria Mutex Spinlock

Mechanism Test for lock. Test for lock.

If available, use the resource If available, use the resource.

If not, go to wait queue If not, loop again and test the lock
till you get the lock

When to use Used when putting process is not Used when process should not be
harmful like user space programs. put to sleep like Interrupt service
routines.
Use when there will be
considerable time before process Use when lock will be granted in
gets the lock. reasonably short time.

Drawbacks Incur process context switch and Processor is busy doing nothing till
scheduling cost. lock is granted, wasting CPU cycles.

The Theory

In theory, when a thread tries to lock a mutex and it does not succeed, because the mutex is
already locked, it will go to sleep, immediately allowing another thread to run. It will continue
to sleep until being woken up, which will be the case once the mutex is being unlocked by
whatever thread was holding the lock before. When a thread tries to lock a spinlock and it
does not succeed, it will continuously re-try locking it, until it finally succeeds; thus it will not
allow another thread to take its place (however, the operating system will forcefully switch to
another thread, once the CPU runtime quantum of the current thread has been exceeded, of
course).

The Problem

The problem with mutexes is that putting threads to sleep and waking them up again are both
rather expensive operations, they'll need quite a lot of CPU instructions and thus also take
16
some time. If now the mutex was only locked for a very short amount of time, the time spent
in putting a thread to sleep and waking it up again might exceed the time the thread has
actually slept by far and it might even exceed the time the thread would have wasted by
constantly polling on a spinlock. On the other hand, polling on a spinlock will constantly waste
CPU time and if the lock is held for a longer amount of time, this will waste a lot more CPU
time and it would have been much better if the thread was sleeping instead.

The Solution

Using spinlocks on a single-core/single-CPU system makes usually no sense, since as long


as the spinlock polling is blocking the only available CPU core, no other thread can run and
since no other thread can run, the lock won't be unlocked either. IOW, a spinlock wastes only
CPU time on those systems for no real benefit. If the thread was put to sleep instead, another
thread could have ran at once, possibly unlocking the lock and then allowing the first thread
to continue processing, once it woke up again.

On a multi-core/multi-CPU systems, with plenty of locks that are held for a very short amount
of time only, the time wasted for constantly putting threads to sleep and waking them up
again might decrease runtime performance noticeably. When using spinlocks instead,
threads get the chance to take advantage of their full runtime quantum (always only blocking
for a very short time period, but then immediately continue their work), leading to much higher
processing throughput.

The Practice

Since very often programmers cannot know in advance if mutexes or spinlocks will be better
(e.g. because the number of CPU cores of the target architecture is unknown), nor can
operating systems know if a certain piece of code has been optimized for single-core or multi-
core environments, most systems don't strictly distinguish between mutexes and spinlocks. In
fact, most modern operating systems have hybrid mutexes and hybrid spinlocks. What does
that actually mean?

A hybrid mutex behaves like a spinlock at first on a multi-core system. If a thread cannot lock
the mutex, it won't be put to sleep immediately, since the mutex might get unlocked pretty
soon, so instead the mutex will first behave exactly like a spinlock. Only if the lock has still not
been obtained after a certain amount of time (or retries or any other measuring factor), the
thread is really put to sleep. If the same code runs on a system with only a single core, the
mutex will not spinlock, though, as, see above, that would not be beneficial.

A hybrid spinlock behaves like a normal spinlock at first, but to avoid wasting too much CPU
time, it may have a back-off strategy. It will usually not put the thread to sleep (since you don't
want that to happen when using a spinlock), but it may decide to stop the thread (either
immediately or after a certain amount of time) and allow another thread to run, thus
increasing chances that the spinlock is unlocked (a pure thread switch is usually less
expensive than one that involves putting a thread to sleep and waking it up again later on,
though not by far).

Summary
17
If in doubt, use mutexes, they are usually the better choice and most modern systems will
allow them to spinlock for a very short amount of time, if this seems beneficial. Using
spinlocks can sometimes improve performance, but only under certain conditions and the fact
that you are in doubt rather tells me, that you are not working on any project currently where
a spinlock might be beneficial. You might consider using your own "lock object", that can
either use a spinlock or a mutex internally (e.g. this behavior could be configurable when
creating such an object), initially use mutexes everywhere and if you think that using a
spinlock somewhere might really help, give it a try and compare the results (e.g. using a
profiler), but be sure to test both cases, a single-core and a multi-core system before you
jump to conclusions (and possibly different operating systems, if your code will be cross-
platform).

4. (7 puncte) Dat, i exemplu de apel de sistem care poate conduce la o schimbare de context
ı̂ntre procese. Explicat, i inclusiv ı̂n ce condit, ii se produce schimbarea de context.
• Interrupts - When the CPU is interrupted to return data from a disk read.
schimbare de context la apel de sistem read file
5. (7 puncte) Apelurile accept() s, i recv() au o sintaxă de forma:

accept(sockfd1, ...)
recv(sockfd2, ...)
unde sockfd1 s, i sockfd2 sunt descriptori de socket. Cu ce diferă cei doi socket, i?
sockfd1 - socket de tip listen asupra caruia se poate efectua doar operatia de
accept deoarece campurile port sursa si ip sursa nu sunt completate
sockfd2 - socket normal cu ajutorul caruia se realizeaza comunicatia.
Caracterizat de atributele: ip + port sursa, ip + port destinatie
6. (10 puncte) Într-un executabil sunt definite sect, iunile text (codul programului), data
(variabile globale) s, i rodata (variabile read-only). Care sect, iuni vor fi partajate de două procese
pornite separat din acest executabil?

7. (10 puncte) Ignorând câmpurile de tip timestamp din cadrul unui inode, dat, i un exemplu
de apel de sistem de lucru cu fis, iere (din forma open(), read(), write(), seek(), close(),
chmod(), stat() etc.) care modifică un inode s, i altul care nu modifică un inode.

write() - modifica inode-ul aferent fisierului deoarece se poate modifica dimensiunea sau, mai
exact, numarul de block-uri din care este alcatuit fisierul.
seek() - nu modifica inode-ul deoarece cursorul de fisier, ca si entitate, apartine de catre
structura ce defineste un fisier deschis si nu de file control block - FCB - inode.

8. (10 puncte) Un sistem are suport DEP (Data Execution Prevention ) dar nu are suport
ASLR (Address Space Layout Randomization ). Precizat, i cum se face un atac de tipul return-
to-libc. Cum se obt, ine adresa/adresele necesare?
Address space layout randomization (ASLR) makes this type of attack extremely unlikely to succeed on 64-bit
machines as the memory locations of functions are random. For 32-bit systems ASLR provides little benefit
since there are only 16 bits available for randomization, and they can be defeated by brute force in a matter of
minutes

18
DEP effectiveness (without ASLR)
In a previous blog post series we went into detail on what DEP is and how it works[part 1, part 2]. In
summary, the purpose of DEP is to prevent attackers from being able to execute data as if it were
code. This stops an attacker from being able to directly execute code from the stack, heap, and other
non-code memory regions. As such, exploitation techniques like heap spraying (of shellcode) or
returning into the stack are not immediately possible.

The effectiveness of DEP hinges on the attacker not being able to 1) leverage code that is already
executable or 2) make the attacker's data become executable (and thus appear to be code). On
platforms without ASLR (that is, versions of Windows prior to Windows Vista), it is often
straightforward for an attacker to find and leverage code that exists in modules (DLLs and EXEs) that
have been loaded at predictable locations in the address space of a process. Return-oriented
programming (ROP) is perhaps the most extensive example of how an attacker can use code from
loaded modules in place of (or as a stepping stone to) their shellcode [3,1]. In addition to loaded
modules, certain facilities (such as Just-In-Time compilers) can allow an attacker to generate
executable code with partially controlled content which enables them to embed shellcode in otherwise
legitimate instruction streams ("JIT spraying")[2].

The fact that modules load at predictable addresses without ASLR also makes it possible to turn the
attacker's data into executable code. There are a variety of ways in which this can be accomplished,
but the basic approach is to use code from loaded modules to invoke system functions like
VirtualAlloc or VirtualProtect which can be used to make the attacker's data become executable.

Summary: DEP breaks exploitation techniques that attackers have traditionally relied upon, but DEP
without ASLR is not robust enough to prevent arbitrary code execution in most cases.

ASLR effectiveness (without DEP)


Attackers often make assumptions about the address space layout of a process when developing an
exploit. For example, attackers will generally assume that a module will be loaded at a predictable
address or that readable/writable memory will exist at a specific address on all PCs. ASLR is designed
to break these assumptions by making the address space layout of a process unknown to an attacker
who does not have local access to the machine. This prevents an attacker from being able to directly
and reliably leverage code in loaded modules.

The effectiveness of ASLR hinges on the entirety of the address space layout remaining unknown to
the attacker. In some cases memory may be mapped at predictable addresses across PCs despite
ASLR. This can happen when DLLs or EXEs load at predictable addresses because they have not opted
into ASLR via the /DYNAMICBASE linker flag. Prior to Internet Explorer 8.0 it was also possible for
attackers to force certain types of .NET modules to load at a predictable address in the context of the
browser[6]. Attackers can also use various address space spraying techniques (such as heap spraying
or JIT spraying) to place code or data at a predictable location in the address space.

In cases where the address space is initially unpredictable an attacker can attempt to discover the
location of certain memory regions through the use of an address space information disclosure or
19
through brute forcing[5]. An address space information disclosure occurs when an attacker is able to
coerce an application into leaking one or more address (such as the address of a function inside a
DLL). For example, this can occur if an attacker is able to overwrite the NUL terminator of a string and
then force the application to read from the string and provide the output back to the attacker [4]. The
act of reading from the string will result in adjacent memory being returned up until a NUL terminator
is encountered. This is just one example; there are many other forms that address space information
disclosures can take.

Brute forcing, on the other hand, can allow an attacker to try their exploit multiple times against all of
the possible addresses where useful code or data may exist until they succeed. Brute forcing attacks,
while possible in some cases, are traditionally not practical when attacking applications on Windows
because an incorrect guess will cause the application to terminate. Applications that may be
subjected to brute force attacks (such as Windows services and Internet Explorer) generally employ a
restart policy that is designed to prevent the process from automatically restarting after a certain
number of crashes have occurred. It is however important to note that there are some circumstances
where brute force attacks can be carried out on Windows, such as when targeting an application
where the vulnerable code path is contained within a catch-all exception block.

Certain types of vulnerabilities can also make it possible to bypass ASLR using what is referred to as
apartial overwrite. This technique relies on an attacker being able to overwrite the low order bits of an
address (which are not subject to randomization by ASLR) without perturbing the higher order bits
(which are randomized by ASLR).

Summary: ASLR breaks an attacker's assumptions about where code and data are located in the
address space of a process. ASLR can be bypassed if the attacker can predict, discover, or control the
location of certain memory regions (particularly DLL mappings). The absence of DEP can allow an
attacker to use heap spraying to place code at a predictable location in the address space.

9. (10 punct) Fie secvent, a de instruct, iuni de mai jos:

printf(\%d\n", *a);
printf(\%d\n", *(a+1));
unde a este un pointer la un ı̂ntreg (int *). Dat, i exemplu de situat, ie ı̂n care prima instruct, iune
NU cauzează page fault, dar a doua cauzează page fault.

10. (10 puncte) Care este un avantaj, respectiv un dezavantaj al folosirii suportului de huge
pages ? Adică pagini de 2MB (2 megabytes ) ı̂n locul paginilor de 4KB (4 kilobytes ).
Avantaje:
• Increased performance through increased TLB hits.
• Pages are locked in memory and are never swapped out which guarantees that
shared memory like SGA remains in RAM.
• Contiguous pages are preallocated and cannot be used for anything else but for
System V shared memory (e.g. SGA)
• Less bookkeeping work for the kernel for that part of virtual memory due to larger page
sizes

20
Dezavantaje:
The amount of wasted memory will increase as a result of internal fragmentation;

extra data dragged around with sparsely-accessed memory can also be costly.

Larger pages take longer to transfer from secondary storage, increasing page fault latency (while
decreasing page fault counts).

The time required to simply clear very large pages can create significant kernel latencies.

11. (15 puncte) Dorim să implementăm un alocator ı̂mbunătăt, it de memorie. Alocatorul
va expune funct, iile malloc(), calloc(), realloc() s, i free(), apeluri standard ı̂n lucrul cu
memoria. În back end va folosi apelurile de sistem de tip mmap() sau brk() expuse de sistemul
de operare pentru rezervarea de memorie virtuală. Cerint, ele alocatorului sunt viteză foarte
bună s, i thread safety.

21
Ce structuri interne vet, i folosi ı̂n cadrul alocatorului pentru gestiunea alocărilor?
Ce probleme posibile (de viteză/eficient, ă) pot apărea la nivelul alocatorului?
Cum vet, i asigura viteză bună de alocare/dezalocare?
Ce fel de aplicat, ii/scenarii de test vet, i folosi pentru a testa alocatorul?

În conformitate cu ghidul de etică al Departamentului de Calculatoare, de-


clar că nu am copiat s, i nu voi copia la această lucrare. De asemenea, nu am
ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

22
Sisteme de Operare
4 septembrie 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. (7 puncte) Care este un avantaj al folosirii tabelei de pagini ierarhice fat, ă de tabela de
pagini simplă?
A mai fost inca o data.

2. (7 puncte) De ce un sistem este cu atât mai ı̂ncărcat cu cât numărul de procese din cozile
READY cres, te (adică mai multe procese ı̂n cozile READY ı̂nseamnă ı̂ncărcare mai mare)?
In a real time system, admitting too many processes to the "ready" state may lead to oversaturation and
overcontention for the systems resources, leading to an inability to meet process deadlines.

overcontention: Bus contention, in computer design, is an undesirable state of the bus in which more than one
device on the bus attempts to place values on the bus at the same time.

3. (7 puncte) În ce situat, ie o operat, ie de tip lock() pe un mutex blochează thread-ul curent s,
i ı̂n ce situat, ie nu ı̂l blochează?
And for default mutexes, attempting to lock a mutex that has been locked by the calling thread leads to
undefined behaviour:

If the mutex type is PTHREAD_MUTEX_DEFAULT, attempting to recursively lock the mutex results in undefined
behaviour.

lock()

Locks the mutex. If another thread has locked the mutex then this call will block until that thread has unlocked
it.

Calling this function multiple times on the same mutex from the same thread is allowed if this mutex is
arecursive mutex. If this mutex is a non-recursive mutex, this function will dead-lock when the mutex is locked
recursively.

4. (7 puncte) De ce au dispozitivele de tip bloc nevoie de operat, ii mai rapide (cu throughput
mai mare) decât dispozitivele de tip caracter?

5. (7 puncte) Cu ce diferă un apel de bibliotecă de un apel de sistem?


System calls are operating system functions, like on UNIX, the malloc() function is built on top of the
sbrk() system call (for resizing process memory space).

Libraries are just application code that's not part of the operating system and will often be available on more
than one OS. They're basically the same as function calls within your own program.

The line can be a little blurry but just view system calls as kernel-level functionality.

23
6. (10 puncte) În ce situat, ie se poate rula cod pe stivă, chiar ı̂n cazul folosirii unui mecanism
de stack smashing protection (canary value )?
Stack-smashing protection is unable to protect against certain forms of attack. For example, it
cannot protect against buffer overflows in the heap. There is no sane way to alter the layout
of data within a structure; structures are expected to be the same between modules,
especially with shared libraries. Any data in a structure after a buffer is impossible to protect
with canaries; thus, programmers must be very careful about how they organize their
variables and use their structures.

7. (10 puncte) La montarea unui sistem de fis, iere se poate folosi opt, iunea noatime. Opt, iunea
ı̂nseamnă că nu va fi actualizat câmpul atime (timestamp de acces) al unui inode ı̂n momentul
accesării (citirii sau scrierii inode-ului). De ce este avantajoasă această opt, iune ı̂n cadrul unui
sistem de fis, iere ı̂ncărcat (cu accese dese la fis, iere)?

This basically means that the number of writes to a disk for relatime mount is close to
double relative to a noatime mount other thing being equal. It is a serious concern for
partitions on flash memory devices.

8. (10 puncte) Fie secvent, a de instruct, iuni de mai jos:

*a = 42; /* first dereferencing */


sleep(5); /* sleep for 5 seconds */
*a = 42; /* second dereferencing */
unde a este un pointer la un ı̂ntreg (int *). Dat, i exemplu de situat, ie ı̂n care prima dereferent, iere
nu cauzează page fault, dar a doua dereferent, iere cauzează page fault.

9. (10 punct) Un proces ı̂n Linux are, ı̂n mod obis, nuit, tabela de descriptori de fis, iere limitată
la 1024 de intrări. De ce ı̂n cazul unui server TCP ı̂ncărcat, care primes, te multe conexiuni, este
nevoie de cres, terea acestei limite?

10. (10 puncte) De ce este mai probabilă aparit, ia unui stack overflow ı̂n cazul unui proces
multi-threaded fat, ă de un proces single-threaded? (stack overflow = depăs, irea limitei stivei ı̂n
cadrul spat, iului de adrese al unui proces)
Folosirea unui număr mare de thread-uri în cadrul unui proces poate conduce mai rapid la
stack overflow. Fiecare thread are stiva proprie, cu dimensiunea fixată la crearea. Dacă se
creează prea multe thread-uri, se va ocupa foarte mult stiva. Stivele thread-urilor vor fi
apropiate unele de altele astfel că, în cazul unui flux de apeluri mare (apeluri recursive, de
exemplu), există riscul ca stiva unui thread să suprascrie stiva altui thread.

11. (15 puncte) Ne propunem implementarea unui framework de messaging (message queue
framework). Cu ajutorul acestui framework, aplicat, ii diferite pot comunica unele cu celelalte.
Există patru concepte importante: Publishers (cei care produc mesaje), Consumers (cei care
consumă mesaje din cozi), Exchanges (cei care primesc mesajele ı̂n cozi de la Publishers s, i apoi
le transmit către Consumers), Queues (cozi de mesaje, create de Consumers s, i care stochează
mesajele). Framework-ul trebuie să asigure scalabilitate s, i performant, ă. În general, Consumers,
Exchanges s, i Publishers se găsesc pe sisteme fizic diferite; sunt conectat, i prin Internet/ret, ea.

24
Definit, i, la nivel de pseudocod, metodele framework-ului folosite de Publishers s, i Consumers.
Ce probleme de scalabilitate pot apărea la nivelul framework-ului? (adică de la un nivel ı̂n sus
se vor resimt, i probleme de performant, ă)
Ce solut, ii de rezolvare a problemelor de scalabilitate există? (atât la nivelul framework-ului,
cât s, i la nivelul infrastrcturii folosite ı̂n instalare)
Cum asigurat, i o performant, ă ridicată a framework-ului s, i pentru o utilizare simplă (fără pro-
bleme de scalabilitate)?

În conformitate cu ghidul de etică al Departamentului de Calculatoare, de-


clar că nu am copiat s, i nu voi copia la această lucrare. De asemenea, nu am
ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă:

Semnătură:..............................

25
Sisteme de Operare
13 septembrie 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate

1. (7 puncte) Câte tabele de pagini se găsesc la un moment dat ı̂ntr-un sistem de operare?
Intr-un sistem se gasesc la un moment dat atatea tabele de pagini catre procese exista.

2. (7 puncte) De ce overhead-ul unui apel de sistem (oricât de simplu ar fi apelul de sistem)


este, ı̂n majoritatea cazurilor, semnificativ mai mare decât overhead-ul unui apel de bibliotecă?
Presupunem că vorbim de un apel de bibliotecă care nu face ı̂n spate apel de sistem

A system call is a call to the kernel for something and acts as an entry point into
the operating system. A system call executes in kernel address space and counts
as part of the system time. System calls have a high overhead because of the
switch to kernel and back, they are specific to each operating system and
generally are not portable.

A library call is a call to a routine in a library, such as printf, and is linked with
the program. It executes in the user address space that is passed out by the
operating system for user programs and has a much lower overhead than a
system call. Library calls can be bundled up with a program so that they are
portable.

3. (7 puncte) În ce situat, ie o operat, ie de tip down() pe un semafor blochează thread-ul curent s,
i ı̂n ce situat, ie nu ı̂l blochează?
If a semaphore has the value 0, a down operation on it will block until someone releases a resource and
increments the semaphore.

A non-blocking semaphore does not block on a down operation if the resource is unavailable, but rather yields
an error. This can be useful if the program needs that resource immediately or without suspending execution,
and if the resource isn't available, the program logic can rather do something else.

4. (7 puncte) De ce fiecare thread al unui proces dispune de o stivă proprie?


Thread-ul reprezinta unitatea minimala ce descrie un fir de executie. Fara o
stiva nu s-ar putea mentine si memora contextul unui thread atunci cand se
face context switching.

5. (7 puncte) Un proces CPU intensive este pornit s, i rulează timp de 30 de minute. La


ı̂ncheierea sa, se observă că acesta a consumat doar 20 de secunde timp de procesor. Cum se
explică acest lucru?

6. (10 puncte) În general, nu se pot crea hard link-uri la directoare. Cu toate acestea numărul
de link-uri aferente unui director diferă ı̂ntre directoare diferite; putem observa acest lucru prin
rularea comenzii stat pe diverse directoare: /, /home, /usr/lib. De ce diferă numărul de
26
link-uri ı̂ntre directoare?
Numarul de link-uri al unui director este reprezentat de numarul total de subdirectoare
pentru ca fiecare dintre ele au un link catre .. + cele doua intrari default: . si ..
Astfel, putem sa zicem ca numarul de link-uri difera intre directoare deoarece difera numarul
de subdirectoare continute.

7. (10 puncte) De ce codul dintr-un shellcode se ı̂ncheie, ı̂n general, cu o instruct, iune pentru
realizarea unui apel de sistem (de forma int 0x80)?
8. (10 puncte) Un utilizator rulează, ı̂n terminal, de mai multe ori cele două comenzi de mai
jos (rulăm de mai multe ori ca să fie informat, iile citit cache-uite s, i să nu afecteze rezultatul):

find /usr > find.out


find /usr
Utilizatorul observă că prima comandă (cu redirectare ı̂n fis, ier) durează semnificativ mai put, in
decât a doua (fără redirectare, care afis, ează pe terminal). De ce?

9. (10 punct) Care este avantajul unui server web care foloses, te mai multe procese pentru
servirea cererilor fat, ă de unul care foloses, te mai multe thread-uri?
1. Threads will use up much less resident memory than Processes. Yes, with dynamically
linked libraries a lot of memory is shared between the Apache Control Process and it's
child Processes, however each new Process will need to instantiate all of the modules
you have enabled.

2. This is easily testable by comparing the memory usage of each Process where you
have, for example, either 5 Processes and 1 Thread each or 5 Processes and 25
Threads each. In my case here, each child Process takes about 7 MBs regardless of
the amount of Threads.

3. +For Threads

4. It takes longer to initiate in terms of time and cpu cycles to load a new Process than it
does a Thread. This can be tested by verifying avg amount of pages served via 'ab'.

5. +For Threads

6. A Processes Threads all depend on the Process .. The biggest concern here, is that if
something happens to the Process it will affect all the Threads that are associated with
it. If you're running with a single Process with a bunch of Threads, then when the
Process dies so will the Threads. More Processes would therefore cause a better
separation, and thus greater "fault" tolerance if you will.

7. +For Processes

8. Related to (3), for modules such as PHP, their memory is loaded by the Process and
shared across all of the Threads. This means that if you have php with memory_limit
set to 100Mbs with 25 Threads below, then at max load technically each Thread would
be able to allocate a maximum of 4MBs each ( course it won't happen this way, some
will hog, some will starve ).

27
So in the end, it really depends on your use case .. That being said, you'll want to maximize
the amount of Threads used so as to diminish memory usage and increase responsiveness.
However, you'll have to balance that with a proper amount of Processes for better fault
tolerance.

Course I'm no expert here as I've only recently have had to become concerned with this, so I
look forward to see what other answers might pop up here !

10. (10 puncte) Un executabil are zona de cod de 1MB. Cu toate acestea, un proces creat din
acest executabil ocupă, pe parcursul rulării, maxim 100KB de memorie RAM. Cum explicat, i?

11. (15 puncte) Dorim să implementăm o ret, ea peer-to-peer. Ret, eaua va pune pret, mai mult
decât orice pe disponibilitatea cont, inutului (availability ). Fiecare peer va rezerva un spat, iu
dat pe hard disk-ul propriu pentru fis, iere care nu sunt ale sale dar care ne propunem să fie
disponibile.
Perioada de timp cât un peer este activ s, i lăt, imea sa de bandă sunt factori ı̂n stabilirea nivelului
de implicare (involvement ) al acestui peer. Un peer implicat va putea descărca mai rapid date
de la alt, i peeri; fiecare peer ı̂s, i controlează banda de upload s, i acordă mai mult peerilor implicat, i.

28
Ret, eaua este folosită pentru transferuri de fis, iere mici (muzică, documentat, ie, mici fis, iere video).
Transferul se realizează doar ı̂ntre un peer s, i alt peer.
Care vor fi primitivele protocolului de comunicare ı̂ntre peeri?
Cum at, i ret, ine, ı̂n cadrul ret, elei peer-to-peer, informat, ile despre implicarea unui peer (involve-
ment), pentru a fi accesate de peeri? Motivat, i alegerea.
Ce facilităt, i vet, i folosi pentru o performant, ă cât mai bună a aplicat, iei specifică unui peer

29
(pentru transferul fis, ierelor pe ret, ea, stocarea fis, ierelor)?
Ce facilităt, i oferă ret, eaua pentru a asigura disponibilitatea cont, inutului? Adică fiecare fis,
ier să fie stocat ı̂n cât mai multe locuri.

În conformitate cu ghidul de etică al Departamentului de Calculatoare,


de- clar că nu am copiat s, i nu voi copia la această lucrare. De asemenea,
nu am ajutat s, i nu voi ajuta pe nimeni să copieze la această lucrare.

Nume s, i grupă: Semnătură:.....................

30
FARA NEGRU

Carina
CristianM
Prj Rosu <3
Remus ​TODO
Vlaicu
Cosminel
Mares ​Orange
Tanase
Iuga ​Turcu’
Anda
Baronescu <!>
Fabi
Trump

Puteti sa adaugati si din alti ani, la final.


Oricine doreste acces, sa ceara.

8 iunie 2016

1. Precizati un apel care modifica cursorul de fisier si un apel care modifica dimensiunea
unui fisier.
Modifica dimensiunea: truncate(), write(), ​open() ​cu O_TRUNC​, close()
Muta cursorul: lseek, read, write, open - pozitioneaza la inceput sa la sfarsit.

2. Metrica pentru un nivel de interactivitate al proceselor pt un planificator.


Timpul de asteptare (waiting time) = timpul de asteptare al unui proces in coada READY
a planificatorului; acesta trebuie sa fie cat mai scurt pentru a avea un sistem interactiv/responsiv
Nr. de procese pe secundă
Cuanta de timp - mica -> interactivitate
3. Fie sectiunea de cod de mai jos:
char arr[128];

arr[140] = ’\0’;
In ce situatie instructiunea de atribuire rezulta in eroare de acces la memorie (de tip
segmentation fault) si in ce situatie nu?
Instructiunea de atribuire rezulta in eroare de acces la memorie (segfault) atunci
cand​ adresa din memorie ce trebuie accesată este într-o pagină la care nu avem drepturi de
scriere (sau nu a fost alocată - ​cuvantul potrivit cred ca e paginata​) ​si nu rezulta in segfault
atunci cand​ adresa referită e în aceeași pagină cu vectorul alocat, la care avem drepturi (sau
într-o altă pagină, eventual cea următoare, care să fie alocată și la care să avem drepturi).

4. Apelul malloc() aloca memorie, in vreme ce apelul calloc() o si zeroizeaza (umple


spatiul alocat cu valori de zero). De ce in cazul apelului calloc() spatiul de memorie
fizica/rezidenta (resident set size) a procesului creste, dar in cazul malloc() nu (sau
foarte putin)?
Apelul malloc face doar rezervare de pagini în memoria virtuală, fără a face
corespondențe cu spațiu de memorie fizică (care se va întâmpla ulterior, la accesarea efectivă a
paginilor virtuale rezervate).
COMPLETARE: ..
Calloc e la la baza malloc() + memset, iar memset implica mai multe apeluri de sistem,
deci overhead

5. Pe un sistem de operare dat, stiva unui thread este limitata la 8MB. Care este un avantaj
si un dezavantaj al cresterii acestei limite (de exemplu la 32MB)?

Dezavantaj: Poti face mult mai putine threaduri, capra serverasul lui perju ​(sugi
Avantaj: Faci recursivitate pana vrea neamul lui tanase si ii trece si lui tema la SD

6. (10 puncte) Un sistem pe 64 de biti are suport pentru DEP (Data Execution Prevention)
si ASLR (Address Space Layout Randomization). Presupunand ca identificam intr-un
program o vulnerabilitate de tip buffer overflow, ce actiuni urmarim pentru a putea obtine
un shell?
Daca are DEP, sigur nu se poate injecta cod si apoi sa il executam, prin urmare va trebui
sa gasim/cautam locul in care aslr-ul a palasat biblioteca libc pentru a reusi sa apelam /bin/bash
prin suprascrierea adresei de return dintr-o finctie catre /bin/bash.
Alternative answer​: Putem injecta cod chiar daca are DEP dar doar cod read-only ,
Dep te impiedica doar sa executi cod read-write. Deci am putea injecta un cod compilat si sa il
setam cu mprotect ca readonly iar pentru a scapa de ASLR putem introduce la inceputul acestui
cod un block de no-opuri si suprascriind adresa de return de unde se gaseste vulnerabilitatea
cu adresa mijlocului blocului de no-opuri din codul nostru aslr-ul o sa dea o adresa de mai sus
sau mai jos cel mai probabil din acel bloc de no-oprui si programul nostru va incepe executia.
C: PS putem chiar injecta cod intr​-​o pagina protejata read only si dupa sa ii dam drepturi cu
mprotect

7. (10 puncte) Precizati un scenariu ın care doua fisiere (inode-uri) diferite ajung sa refere
acelasi bloc de date.
Se intampla asta daca acele inoduri sunt linkuri simbolice -> pot referi acelasi bloc de
date si, simbolic link sunt inoduri de sine statatoare
8. (10 puncte) Un buffer circular este un buffer pentru care operatiile ajunse la sfarsitul sau,
continua de la inceput (daca ai ajuns la ultimul element al buffer-ului, continua de la
primul element). Buffer-ul are doua capete (doi indecsi): in si out. Un consumator citeste
de la out, iar un procucator scrie la in. Precizat, in pseudocod o implementare
sincronizata a functiilor consume() si produce(item) folosind un buffer circular.
Sem N = 1, M = 0 ​// cred(99%) ca N e initial dimensiunea bufferului circular, nu 1
in,out
Producator:
While true
Aux = produce()
P(N) // acquire
Buffer[in] = aux
In = in mod max + 1
V(M) // release

Consumator:
While true:
P(M)
Consuma
Out = out mod max + 1
V(N)

9. (10 puncte) Pentru un program/executabil putem folosi functionalitatea de setuid pentru


actiuni privilegiate. Intrucat aceasta functionalitate este de tip totul sau nimic (obtii
ıntregul set de privilegii ale utilizatorului administrativ), se recomanda folosirea de
capabilitati. O capabilitate este avantajoasa pentru ca permite doar un anumit tip de
actiune privilegiata. De ce nu are sens sa existe o capabilitate pentru actiuni privilegiate
cu sistemul de fisiere (adica scrierea si citirea oricarui fisier)?

Un astfel de privilegiu ar permite acces la fisiere precum /etc/shadow, /etc/passwd, iar


scrierea si citirea acestor fisiere de catre oricine ar putea avea consecinte nedorite in
securitatea sistemului.

10. Load average este o metrica care este proportionala cu numarul de procese aflate in
starea READY. De ce load average-ul este mai mare pentru un sistem cu mai multe
procese de tip CPU intensive?

Spre deosebire de procesele I/O intensive, care deseori se vor afla în starea WAITING,
procesele CPU intensive nu au nevoie de interacțiuni cu utilizatorul și deci vor solicita
mereu accesul la procesor, deci vor fi în READY dacă procesoarele sunt deja ocupate.
Astfel, mai multe procese CPU intensive conduc la un load average mai mare.
10 iunie 2016

1. (7 puncte) Fie urmatoarea sectiune de cod:


char a[128];
...
a[900] = ’\0’;
Intructiunea de atribuire​ nu rezulta​ in exceptie de memorie (segmentation fault). Acest lucru se
ıntampla, insa in momentul folosirii instructiunii a[901] = ’\0’.
Cum explicati?
Instructiunea de atribuire rezulta in eroare de acces la memorie (segfault) atunci
cand​ adresa din memorie ce trebuie accesată este într-o pagină la care nu avem
drepturi de scriere (sau nu a fost alocată) ​si nu rezulta in segfault atunci cand​ adresa
referită e în aceeași pagină cu vectorul alocat, la care avem drepturi (sau într-o altă
pagină, eventual cea următoare, care să fie alocată și la care să avem drepturi).
____
Adresa ce trebuie accesata nu se afla intr-o pagina cu drepturi de scriere (sau nu a fost
alocata);
Adresa a[900] se afla chiar la sfarsitul paginii cu drept de write, iar a[901] la inceputul
unei pagini fara drept de write.

2. (7 puncte) In ce situatie un apel de tipul write() modifica atat cursorul de fisier cat si
dimensiunea fisierului?
write(int​ ​fd​, const void *​buf​, size_t​ ​count​); - ​Write() scrie acolo unde este
pozitionat cursorul, nr de octeti trimisi ca argument, iar cursorul ramane acolo unde
a terminat de scris. In cazul in care, argumentul count nu este 0, iar scrierea se
face cu succes, atat cursorul cat si dimensiunea se modifica.
Completare:​ daca cursorul este pozitionat la inceputul fisierului, de exemplu, sau
in mijloc si scriem un numar x de caracter x < numarul de caractere ramase pana
la sfarsitul fisierului , write va ​suprascrie​ cele x numar de caractere existente deja
in fiser si nu v-a modifica dimensiunea acestuia. Deci write modifica dimensiunea
fisierului doar atunci cand cursoru trece de SEEK_ENDUL initial (dinainte de
scrierei).

3. (7 puncte) Precizati si justificati o metrica numerica pentru nivelul de productivitate al unui


planificator de procese.
Turnaround time cat mai mic. Numar de context switch-uri cat mai mic.
Numarul de context switch-uri într-o unitate de timp. Cu cat sunt mai multe, cu atat
nivelul de productivitate este mai mare(​?!!!????????????????!??!!?!??!?!?!?!?!?!?!?!?!?​)
C:??????^
Cred ca voia sa zica de interactivitate, nu de productivitate
C:agree

4. (7 puncte) De ce operatia de tipul seek() nu are sens pe dispozitive de tip caracter si pe


socketi, ci doar pe dispozitive de tip bloc?
Pentru ca pe dispozitive de tip caracter se primesc datele una cate una si nu ai pe ce sa
faci seek, in schimb daca ai un bloc de date, ai unde sa faci seek

Pentru că pe dispozitivele de tip caracter datele vin și sunt citite/ scrise octet cu octet, ca
într-o țeavă. Nu putem anticipa date și ne putem plasa mai sus sau mai jos pe banda de date. În
cazul dispozitivelor de tip bloc însă, datele se găsesc pe un spațiu de stocare pe care ne putem
plimba/glisa; putem "căuta" date prin plasarea pe un sector/bloc al dispozitivului de stocare și
atunci operația lseek() are sens. Pe sockeți se primesc in pachetele datele si nu ai cum sa faci
seek la ce se afla in capatul celalalt

5. (7 puncte) De ce este de preferat sa folosim spinlock-uri pentru regiuni critice mici si mutexuri
pentru regiuni critice mari?
Spinlock-ul folosește busy-waiting și are operații de lock() și unlock() ieftine prin
comparație cu mutex-ul. Operațiilor de lock() și unlock() pe mutex sunt de obicei costisitoare
întrucât pot ajunge să invoce planificatorul. Având operații rapide, spinlock-ul este potrivit pe
secțiuni critice de mici dimensiuni în care nu se fac operații blocante; în aceste cazuri faptul că
face busy-waiting nu contează așa de mult pentru că va intra rapid în regiunea critică. Dacă am
folosi un mutex pentru o regiune critică mică, atunci overhead-ul cauzat de operațiile pe mutex
ar fi relativ semnificativ față de timpul scurt petrecut în regiunea critică, rezultând în ineficiența
folosirii timpului pe procesor.

Se folosesc spinlock-uri atunci cand codul din regiunea critica se va executa foarte rapid
si celelalte threaduri nu vor astepta foarte mult sa ia lock-ul in busy-waiting.
6. (10 puncte) Fisierele executabile contin o sectiune numita PLT (sau function stubs) prin care
se intermediaza apelul functiilor de biblioteca. Atacurile de tipul ret-to-plt urmaresc sa
foloseasca PLT pentru apelul functiilor de biblioteca. De ce sunt de interes aceste atacuri pe
sisteme care folosesc DEP (Data Execution Prevention) si ASLR (Address Space Layout
Randomization)?
Sunt de interes deoarece prin intermediul acelei sectiuni putem afla unde a plasat ASLR-ul
bibliotecile libc, astfel putem realiza atacuri return-to-libc

ret-to-plt este folosit pentru a trece peste DEP si ASLR. In loc sa faca return la o functie din libc,
a carei adresa este randomizata de ASLR, atacatorul face return la un plt a11l functiei, a carui
adresa nu este randomizata. Pentru ca function@PLT nu este randomizata, atacatorul nu mai
trebuie sa ghiceasca adresa de baza a libc, putand face simplu return la function@PLT pentru a
invoca functia function.

7. (10 puncte) Fisierul /etc/shadow contine rezumate hash (functii de tip one-way) ale parolelor.
De ce se urmareste ca functiile care calculeaza rezumatul hash al unei parole sa fie costisitoare
din punctul de vedere al timpului: o rulare de functie (care calculeaza un hash pentru o parola)
sa dureze, sa nu fie obtinut rezultatul instant?
Sa nu fie obtinute parolele prin brute force (dar nu sunt sigura)​ ​(e bine, am gasit pe net “bcrypt”)
Altfel ai putea usor implementa un brute force. Iei un string gol, pui oricare caracter (256
posibile) si compari hash-ul obtinut de tine cu cel pt care cauti parola. Si concatenezi caractere
in continuare pana ai match. Deci incerci 256^nr_caractere_parola

8. (10 puncte) Thrashing este o problema a unui mecanism de stocare in care componentele
acestuia (pagini, intrari) sunt foarte des schimbate, ducand la o performanta scazuta; in loc sa
fie folosite, acele componente sunt foarte des schimbate. Cum are loc TLB thrashing?
Cred: ​ca TLB thrashing are loc cand se realizeaza alocari alternative de blocuri de
memorie mai mici cu blocuri de memorie mai mari, de EX: daca in tlb avem mapate pagini
pentru date de dimensiune mica(tlb plin) folosite des si avem nevoie de a aceesa un bloc de
date mai mare , tlb-ul va mapa multitudinea de pagini asociate blocului mare de date si va
scoate paginile asociata blocurilor mici iar cand v-a venii randul de acces al blocurilor mici se
vor cauza tlb-misses.
Cred ca e mai simplu de atat: tlb are o dimensiune limitata(desi nowadays e un monstru
pe multe nivele). Daca procesul are multe pagini mapate peste ram, atunci se va umple tlb si vei
pune info peste inrtarile veche..t

9. (10 puncte) Un kernel preemptiv este unul in care poate intrerupe un proces de pe un
procesor atunci cand ii expira cuanta sau este un alt proces prioritar, iar procesul ruleaza in
spatiul kernel (kernel space). Un kernel nepreemptiv poate “ıntrerupe procesul” in acele conditii
doar daca ruleaza in spatiul utilizator (user space). Un kernel preemptiv este mai util pentru
interactivitate sau pentru productivitate?
Interactivitate (procesele nu trebuie sa astepte mult timp pentru a rula pe procesor, se tine cont
de o cuanta de timp, de prioritate)

10. (10 puncte) a.txt si b.txt sunt doua link-uri (nume) la acelasi inode. Inode-ul are contorul de
link-uri egal cu 2. Pe un sistem dat, dupa operatia mv a.txt /boot/ contorul de link-uri ajunge la 1
pentru inode. Cum explicati?

/boot se afla pe alta partitie ​.


Poate cineva sa mai dezvolte? E valabil pt cazul in care a este fisierul si b un hard link la fisier si
in acest caz b fiind hard link nu poate referi ceva aflat pe alta partitie?
Pana la urma cred ca pe ambele poti sa le interpretezi ca fiind hard link-uri, pentru ca de fapt
doar pointeaza catre acelasi inode si nu conteaza, de fapt orice fisier e un hard link catre un
inode(chiar daca e doar unul). Mai era o intrebare de genul, de ce un fiser mare se muta pe
aceeasi partitie instant iar pe alta partitie dureaza, tocmai pentru ca de fapt pe aceeasi partitie e
doar un “hard link” si doar se muta link-ul, inode-ul ramane tot acolo. (E CINEVA CARE CREDE
ALTFEL???????????????????????????? POATE GRESESC EU)

16 iunie 2016

1. (7 puncte) Ce tip de apel de sistem nu are loc in cazul folosirii operatorului & (ampersand) in
shell (operatorul de rulare in background)?
Apelul wait() nu mai are loc. (parintele nu mai asteapta procesul copil)

2. (7 puncte) O functie intr-un program consuma un procent semnificativ din timpul de rulare si
ne propunem sa paralelizam implementarea acesteia. De ce nu are sens folosirea de thread-uri
cu implementare user-level?
Threadurile user level nu au suport multi-core, fiind necesara o implementare cu kernel
level threads.

3. (7 puncte) Un program are o vulnerabilitate de tip buffer overflow si foloseste canary values
(stack smashing protection). Cum putem totusi ataca programul pentru a-i altera fluxul de
executie?
Canary, DEP, ASLR
Facem un handler pentru semnalul de stack smashing
Daca reusesti cumva sa gasesti valoarea Canary-ului si in momentul in care suprascrii
folosindu-te de buffer overflow sa incepi cu valoarea gasita, am inteles ca a spus ceva de genul
la curs, stie cineva sa spuna cum???????

4. (7 puncte) De ce pipe-urile anonime (create cu ajutorul apelului pipe()) pot fi folosite doar
intre procese inrudite?
Deoarece procesele copil mostenesc file descriptorii deschisi de procesul parinte prin
apelul pipe(). Daca se doreste comunicarea intre procese ce nu sunt inrudite este nevoie
named pipes.

5. (7 puncte) De ce numarul de schimbari de context intre procese este mai mare atunci cand
se foloseste un planificator de procese cu prioritati (fata de folosirea unui planificator de procese
fara prioritati)?
Deoarece procesele IO bound au o prioritate mai mare decat cele CPU bound (foarte
posibil sa faca un apel blocant) deci numarul de schimbari de context creste, astfel
obtinandu-se interactivitate mare.

6. (10 puncte) memcached este un sistem de caching al obiectelor in memorie folosit in special
in conjunctie cu sisteme de baze de date. Pentru o performanta superioara se recomanda
folosirea Huge Pages, adica pagini de 2MB in loc de 4KB (pe x86_64). De ce? Alocarile si
dezalocarile afecteaza putin performanta, mare parte din alocari facandu-se la inceputul rularii
iar dezalocarile sunt rare.
Cautarea in TLB este mult mai rapida. Paginile fiind mai mari e nevoie de mai putine intrari in
TLB (ai destula memorie RAM intr-un server).
Cred ca ar mai trebui ceva aici.

7. (10 puncte) Un utilizator porneste Firefox dupa un reboot al sistemului. Dupa un timp inchide
procesul Firefox si apoi il reporneste la scurt timp dupa. Observa ca a doua oara procesul a
pornit semnificativ mai rapid decat prima oara. Cum explicati?
C: probabil anumite blocuri de date sunt ramase prin nivelele de cache L2/L3 sau ram, si nu mai
este nevoie de aducerea lor de pe disk (foarte lenta, mai repede iti vine pizza de la dominos) .
In Windows (nu stiu sigur de Linux) este mecanismul de prefetch care face posibil acest lucru

8. (10 puncte) Un read-write lock este un mecanism de sincronizare care protejeaza o regiune
critica tinand cont de tipul thread-urilor: thread-uri de tip reader (care citesc date) sau threaduri
de tip writer (care modifica date). Exista doua posibilitati:
i. in regiunea critica se gasesc doar thread-uri de tip reader, oricat de multe;
ii.in regiunea critica se gaseste un singur thread de tip writer (si doar acesta, fara alte
thread-uri).
Fat De ce, in general, implementarile de read-write lock sunt de forma write-biased? Adica daca
in regiunea critica se gasesc thread-uri reader si sosesc la regiunea critica thread-uri writer si
thread-uri reader, thread-urile writer nou sosite vor avea prioritate in fata thread-urilor reader
nou sosite.

Daca sosesc la regiunea critica si threaduri writer si reader, prioritate vor avea cele
writer deoarece acestea vor actualiza informatia ce va fi citita de threadurile reader. Daca
threadurile reader ar avea prioritate acestea ar citi date ce vor fi modificate deci date invalide.
Starvation … Daca prioritizezi threadurlei reader, e posibil ca threadul writer sa nu intre
niciodata. ​( se stie!!! ;) )

9. (10 puncte) De ce dimensiunea tabelei FAT nu este afectata de numarul de fisiere aflat pe o
partitie formatata FAT32?
Numarul de entries in tabela FAT e stabilit de la ineput. (Nu stiu sigur)

10. (10 puncte) Un atacator identifica o vulnerabilitate intr-un program si o exploateaza obtinand
un shell pe un sistem la distanta; shell-ul nu este de utilizator privilegiat. Programul ınsa ruleaza
ˆıntr-un chroot jail prevenind atacatorul sa distruga sau sa obtina informat, ii relevante de pe
sistem. Cum poate continua atacatorul atacul?
1. Open a file handle to the root of the jail
2. Create a sub directory in the jail and chroot yourself there (you are root, so you are allowed to
chroot). You’re now even deeper in the jail.
3. Change directory using the file handle to the root of the old jail. You’re now outside of the chroot
jail you created in the sub directory. You’re free! Well, kind of. Actually all locations starting with
‘/’ are still mapped to that sub directory. You don’t want this.
4. cd ..; cd ..; …until you reach the real root.
5. Chroot yourself in the real root. Now you’re properly free.
6. (​DE AICEA AM LUAT​)

11. (25 puncte) O firma proiecteaza si implementeaza un sistem de publicare de imagini. Firma
doreste sa atraga clienti si publicitate folosind ca diferentiator performanta sistemului sau. Vi se
cere sa proiectati si sa implementati o aplicatie/sistem care sa evalueze performanta sistemului
de publicare de imagini.

a. Ce metrici veti urmari pentru a evalua performanta sistemului? (5 puncte)


b. Ce scenarii de utilizare veti folosi pentru a masura acele metrici? Cum veti folosi aplicatia
voastra pentru testarea sistemului? (6 puncte)
c. Ce particularitati de proiectare si implementare va avea sistemul vostru pentru realizarea
scenariilor de mai sus? Cum urmariti testarea limitelor sistemului de publicare de imagini? (7
puncte)
d. Ce veti recomanda firmei proiectante sa modifice (fine tuning) la nivelul sistemului de
publicare de imagini pentru a putea determina ce valori sunt adecvate pentru o performanta
ridicata? (7 puncte)
17 iunie 2016
1. (7 puncte) Explicati daca si cum poate fi exploatata o vulnerabilitate de tip buffer overflow
intr-un program scris in Java.
[Intrebare lucrare curs CA]
De ce nu sunt posibile buffer overflows în Java?
Răspuns: In Java, toate accesele la memorie sunt verificate sa fie “in bounds”. ​C’est
magnifique
Alte opinii?

2. (7 puncte) Ce efect va avea comanda rm a.txt ˆıntr-un sistem de fisiere Unix? (a.txt este un
fisier, iar comanda se intoarce cu succes)
C: Se va sterge fisierul de pe disc, se va reduce numarul de inoduri din folderul in care se gasea
a.txt ​(DACA MAI EXISTA UN HARD-LINK CATRE ACELASI INODE???????)

3. (7 puncte) Ce se intampla daca un proces shell foloseste doar apelul exec(), nu si fork(), in
rularea unei comenzi?
C: intreaga imagine a procesului curent va fi suprascrisa cu imaginea executabilului de la calea
data in exec. ( nu stiu sigur ce se intampla cu stiva procesului vechi)

4. (7 puncte) De ce este recomandat ca procesul parinte sa execute un apel wait() sau waitpid()
pentru fiecare dintre procesele copil?
C: pentru a afla daca procesul copil s-a terminat cu succes sau nu. Astfel procesul copil nu va
ramane zombie si procesul parinte stie daca treaba procesului copil a fost executata bine si
daca poate folosi date modificate de acel proces
Cred ca e recomandat si ca sa poata sa elibereze resursele alocate proceselor copil.

5. (7 puncte) Explicati de ce intr-un handler de semnal nu este recomandat sa folosim variabile


globale sau functii ne-reentrante.
C: pentru ca un handler de semnal poate fi apelat de mai multe threaduri la un moment-dat,
deci trebuie sa fie o functie reentranta ( sa se poata fi apelata de mai multe ori in paralel si ea
sa scoata acelasi output)
6. (10 puncte) In procesul P1, descriptorul d are offset-ul 0 intr-un fisier cu continutul abc.
Procesul P1 executa fork() si rezulta procesul P2. Procesul P2 citeste din d caracterul a.
Procesul P1 doarme o secunda dupa fork(), dupa care citeste un octet din d. Ce caracter va citi
procesul P1?
C: va citi caracterul b deoarece procesul P1 va avea acelasi cursor de fisier ca si procesul P2
(mostenit de la parinte) si in momentul in care P2 citeste un caracter, cursorul din fisier se va
muta si el cu o pozitie)

7. (10 puncte) Explicatii cum este partajat TLB-ul (Translation Lookaside Buffer ) intre procesele
care ruleaza pe acelasi sistem de calcul.
Fiecare proces are propriul TLB. No sharing. Se face flush la fiecare context switch.

8. (10 puncte) Descriptorul s reprezinta un socket TCP conectat, iar apelul send(s,buf,1000,0)
ˆıntoarce valoarea 500. Care e num˘arul minim de octeti pe care i-a primit receptorul pana la
ˆıntoarcerea apelului send()?
C: 0 deoarece send se intoarce in momentul cand a pus datele in bufferul de send din kernel si,
intoarce numarul de octeti scrisi in acel buffer. Insa este posibil ca mesajul sa nu fie inca trimis
cu adevarat catre destinatie sau sa nu fi ajuns inca. In concluzie numarul minim de octeti pe
care i-a primit receptorul este 0.

9. (10 puncte) O aplicatie transmitator foloseste urmatorul protocol peste TCP: fiecare mesaj
incepe cu 4 octet, i care reprezinta dimensiunea mesajului, urmat, i de mesajul efectiv (maxim
1000 octet, i). Dup˘a fiecare mesaj, se as,teapt˘a primirea unei confirm˘ari de la receptor.
Explicat, i problemele care vor ap˘area atunci cˆand transmit,˘atorul execut˘a dou˘a apeluri
send() per mesaj (unul pentru dimensiune s, i altul pentru mesaj) ˆın loc s˘a fac˘a un singur apel
send().

10. (10 puncte) ˆIntr-un sistem cu paginare, tabela de pagini ajut˘a la translatarea din adrese
virtuale ˆın adrese fizice. Tabela de pagini, ˆıns˘a, este stocat˘a tot ˆın memorie. Cum afl˘a MMU
(Memory Management Unit) adresa fizic˘a a tabelei de pagini?
C: ​Adresa din memoria RAM a tabelei de pagini a procesului curent este dată de un registru dedicat numit generic
PTBR (Page Table Base Register). Acest registru este încărcat de sistemul de operare cu valoarea aferentă
procesului curent și este interogat de MMU.

11. (25 puncte) Un programator a implementat un server care primes,te cereri de la client, i
cont, inˆand numele unui fisier dorit, s, i transmite fis, ierul folosind sendfile(). Fis, ierele sunt
stocate pe un disc de tip SSD atas,at serverului. Serverul foloses,te un pool de thread-uri pentru
a procesa aceste cereri. Programatorul crede c˘a un singur parametru va afecta performant,a
sistemului: num˘arul T de thread-uri din thread pool. Ajutat, i programatorul s˘a m˘asoare
performant,a sistemului s˘au s, i s˘a ˆıl optimizeze.
a. Ce metric˘a este cea mai potrivit˘a pentru m˘asurarea performant,ei server-ului? (5 puncte)
b. Explicat, i cum poate fi m˘asurat˘a aceast˘a metric˘a la server. (5 puncte) Programatorul
dores,te s˘a s,tie dac˘a bottleneck-ul ˆıl reprezint˘a SSD-ul, CPU-ul sau placa de ret,ea.
c. Explicat, i ce experimente poate rula pentru a afla acest bottleneck. Ce valoare trebuie s˘a
foloseasc˘a pentru T dac˘a exist˘a N procesoare ˆın sistem? (8 puncte)
d. Argumentat, i de ce implementarea unui cache de fis, iere ˆın memorie nu ar ˆımbun˘at˘at, i
performant,a server-ului. (7 puncte)

C: M-am plictisit rauuuu si nu am chef sa invat la PM :(((


30 August 2016

1. (7 puncte) ˆIn urma rul˘arii comenzii stat pe un fis, ier obt, inem c˘a dimensiunea acestuia
este 1GB, dar num˘arul de blocuri ocupate este 0. Cum explicat, i?
C: Ce apare acolo la dimensiunea fisierului poate fi modificat printr-o functie ( truncate) dar nu
ocupa si spatiul real.

2. (7 puncte) Priorit˘at, ile statice nu pot fi schimbate pe parcursul rul˘arii unui proces. Care este
problema cu un planificator care foloses,te doar priorit˘at, i statice?
Se poate ajunge la starvation (procesele cu prioritate mica vor sta foarte mult timp in coada de
asteptare)

3. (7 puncte) Thrashing-ul este un fenomen negativ al sistemului de memorie virtual˘a ˆın care
se fac schimburi continue ˆıntre memoria principal˘a s, i disc (swap in s, i swap out). Descriet, i
un scenariu care duce la aparit, ia thrashing-ului.
Thrashing-ul poate fi cauzat de programe sau workload-uri care nu prezinta localitatea referintei
(datele accesate frecvent nu se afla in aceeasi parte a memoriei). Astfel, daca intreg setul de
date pe care se lucreaza nu poate incapea in memoria fizica, atunci se poate produce swapping
constant (thrashing).

4. (7 puncte) De ce un proces intr-un sistem de operare modern nu va dispune niciodata de mai


multe pagini fizice decat pagini virtuale?
C: deoarece sistemele moderne sunt majoritatea pe 64 de biti -> un spatiu foarte mare de
adrese virtuale, mult mai mare decat ai putea aveea rami.

5. (7 puncte) De ce putem afirma ca paginarea ierarhica este un compromis spatiu-timp (space


time tradeoff )?
C: deoarece prin paginarea ierarhica se miscoreaza dimensiunea tabelelor de pagini, insa este
ingreunata cautarea. (a doua parte not sure)

6. (10 puncte) De ce este indicat sa folosim capabilitati in loc de setuid pentru executabile care
au nevoie de privilegii?
Din motive de securitate. E mai safe folosirea de capabilitati, decat schimbarea userului. Un
exemplu e comanda “ping” care trebuie sa execute actiuni privilegiate(deschidere de scoketur)
si care e executata folosind capabilitati

7. (10 puncte) De ce unele sisteme pe 32 de bit, si care folosesc memory mapped I/O pot avea
maxim 3GB de memorie RAM (numit si 3GB barrier ) in loc de maxim 4GB?
C: pentru ca in acel 1 G lipsa sunt mapate functii din kernel pentru a nu fi necesar un
context-switch la fiecare apel de sistem

8. (10 puncte) De ce mecanismele de sincronizare sunt mai complexe si mult mai frecvent
folosite ˆın codul care ruleaz˘a ˆın kernel mode fat,˘a de codul care ruleaz˘a ˆın user mode?
C: wtf?

9. (10 puncte) ˆIn ce situat, ie apelul send(sockfd, buffer, 1000) ˆıntoarce o valoare cuprins˘a
ˆıntre 0 s, i 1000 (excluzˆand 0 s, i 1000)?
C: 0 nu cred ca are sens, cat despre valorile intre 0 si 1000, apelul send va intoarce numarul de
octeti pusi in bufferul send_buffer din kernel, buffer din care urmeaza sa fie trimisi catre
destinatie. Datele odata puse acolo sunt considerate ca si trimise (asigura TCP), deci poate sa
intoarca oricat. Legat de dimensiunea maxima nu stiu sigur.

10. (10 puncte) Pe un sistem dat un proces poate dispune de un num˘ar maxim de thread-uri.
Cum putem cres,te aceast˘a limitare?
Micșorând dimensiunea stivei.

11. (25 puncte) Vi se cere s˘a proiectat, i s, i s˘a implementat, i o aplicat, ie de reverse
engineering s, i binary analysis pentru executabile, care s˘a fie capabil˘a atˆat de analiz˘a
static˘a cˆat s, i de analiz˘a dinamic˘a. Se presupune c˘a nu avet, i acces la codul surs˘a s, i c˘a
dorit, i s˘a aflat, i cˆat mai rapid informat, ii legate de executabil s, i de modul s˘au de funct,
ionare. Urm˘arit, i act, iuni precum dezasamblare, construire de graf de flux de control (control
flow graph), investigarea apelurilor de sistem, de bibliotec˘a, acoperirea codului (coverage),
zone de cod hot s, i zone cold (apelate des, respectiv rar).
a. Propunet, i o arhitectur˘a de principiu a aplicat, iei (ce componente va avea s, i cum se vor
conecta ˆıntre ele). (5 puncte)
b. Ce funct, ionalit˘at, i trebuie s˘a ofere sistemul de operare pentru a putea implementa
cerint,ele aplicat, iei? (7 puncte)
c. Cum at, i implementa ˆın cadrul aplicat, iei descoperirea zonelor de cod hot s, i cold? (6
puncte)
d. Cum at, i folosi aplicat, ia pentru descoperirea vulnerabilit˘at, ilor de memorie care ar putea
conduce la execut, ia de cod arbitrar? (7 puncte)

5 septembrie 2016
1. (7 puncte) Cum putem preveni aparitia de atacuri de tipul fork bomb?
Limitam nr de procese ale unui user (comanda ulimit)

2. (7 puncte) De ce o vulnerabilitate la nivelul nucleului sistemului de operare este mult mai


periculoasa decat una la nivelul unei aplicatii ın user space?
C: Deoarece nucleul sistemului de operare ruleaza in acces privilegiat, deci poate executa
comenzi ce necesita drept de root. Pe cand o vulnerabilitate a unei aplicatii din user space nu
este asa de periculoasa deoarece nu poate executa comenzi ca si root, neruland in mod
privilegiat.

3. (7 puncte) Are sens ca un sistem pe 32 de bit, i (cu 4GB spat, iu virtual de adrese pentru un
proces) s˘a aib˘a mai mult de 4GB de memorie fizic˘a (RAM)? Justificat, i.
Magistrala de adrese procesor-ram are tot 32 de biti, nu poti adresa mai mult de 4gb de ram.
Deci nu are sens sa ai mai mult de 4gb de ram.

4. (7 puncte) De ce, ˆın general, nu are sens operat, ia lseek() pe dispozitive de tip caracter?
C: deoarece lseek poate inainta cursorul de citire/scriere intr-un bloc de date, iar in cazul
dispozitivelor de tip caracter, datele sunt primite secvential, nu in blocuri, deci nu am avea pe ce
saface seek
16 iunie 2016 - intrebarea 4

5. (7 puncte) De ce este preferat un sistem de fisiere FAT32 in fata unui sistem de fisiere NTFS
pentru sisteme de fisiere pentru dispozitive mici (de forma USB flash drive)?
In principal pentru compatibilitatea intre platforme, iar fiind vorba si de dispozitive mici nu e o
problema asa de mare limitarea de 4Gb a formatului.

6. (10 puncte) De ce este avantajos ca, in momentul planificarii unei noi entitati, planificatorul sa
aleaga un thread din acelasi proces cu al thread-ului care a rulat anterior?
Overhead mai mic de context switch?
Nu se mai face tlb flush, inlocuirea tabelei de pagini.
7. (10 puncte) In ce situatie operatia send() la transmit,˘ator s, i operat, ia recv() la receptor vor fi
simultan blocate ˆın cadrul unei conexiuni TCP?
C: pot fi simultam blocate daca pachetele se pierd undeva pe retea, astfel recv va ramane
blocat fiindca e blocand, iar send se va bloca deoarece se va umple bufferul de send din kernel.
TCP obliga SO sa pastreze cadrele pana se primeste ACK pentru ele.
Receiverul are bufferul de receive gol iar senderul are bufferul de send plin. S-a petrecut ceva
pe traseu - la middle box uri

8. (10 puncte) Descriet, i un scenariu prin care dou˘a intr˘ari din tabela de pagini a unui proces
refer˘a aceeas, i pagin˘a fizic˘a.
C: daca ele refera o biblioteca, acea biblioteca se va gasi intr-o singura pagina fizica(frame), iar
cele 2 intrari din tabela de pagini virtuale a unui proces pot referi acea biblioteca ( apeluri de
functii maybe?)
9. (10 puncte) Folosim spinlock-uri sau mutex-uri pentru realizarea accesului exclusiv la o
resurs˘a comun˘a ˆıntre thread-uri cu implementare kernel level. De ce implementarea de
spinlockuri poate fi realizat˘a ˆın user space (s, i nu necesit˘a apel de sistem) pe cˆand
implementarea mutex-urilor are nevoie de suport ˆın kernel space?
C: cred ca deoarece spinlock-urile fac busy waiting si ele incearca acolo sa inttre mereu, pe
cand mutexurile realizeaza apelul wait, si asta inseamna ca vor fi trecute din running in coada
waiting pana se primeste semnalul ce trebuie sa le deblocheze

10. (10 puncte) Dispozitivul /dev/mem permite accesul din user space la toat˘a memoria fizic˘a a
sistemului. Scrierea la al N-lea octet din /dev/mem ˆınseamn˘a scrierea la adresa N de memorie
fizic˘a. De ce doar utilizatorul privilegiat (root) are acces la acest dispozitiv?
C: cred ca are voie decat root pt ca din /dev/mem se pot rescrie si chiestii ce tin de
kernel/sistem de operare si nu ar trebui sa poata fi scrise/modificate de oricine
/dev/mem is a character device file that is an image of the main memory of the computer. It
may be used, for example, to examine (and even patch) the system.
Daca orice tamp ar putea scrie in ram atunci nu si-ar mai avea sens separarea user
space-kernel space si tot ce tine de securitatea so ului.

11. (25 puncte) Vi se cere s˘a proiectat, i s, i s˘a implementat, i un remote execution gateway,
adic˘a un sistem care preia cereri s, i le execut˘a pe alte sisteme de execut, ie (de tip backend)
ˆın medii virtualizate. Gateway-ul primes,te task-uri, care cont, in informat, ii despre ce trebuie
rulat (de exemplu, teste pentru teme), s, i o specificat, ie de mas, in˘a virtual˘a. Apoi transmite
task-ul s, i comand˘a mas, ina virtual˘a pe unul dintre sistemele de execut, ie (backend).
Rezultatele rul˘arii sunt apoi trimise clientului care a comandat task-ul. Serverul trebuie s˘a
r˘aspund˘a la un num˘ar mare de cereri.
a. Realizat, i o diagram˘a bloc a componentelor software ale gateway-ului s, i a leg˘aturilor
dintre ele. (5 puncte)
b. Stabilit, i cum arat˘a protocolul de comunicare ˆıntre clientul care trimite task-ul s, i gateway.
Gˆandit, i-v˘a la trimiterea task-ului s, i la primirea rezultatului. (7 puncte)
c. Stabilit, i cum arat˘a protocolul de comunicare ˆıntre gateway s, i sistemele de execut, ie. (6
puncte)
d. Precizat, i cum va ar˘ata sistemul de fis, iere la nivelul gateway-ului pentru a ment, ine
informat, ii despre task-urile ˆın rulare s, i despre rezultatele obt, inute. Presupunet, i c˘a exist˘a
mai mult, i utilizatori s, i trebuie s˘a fie p˘astrat˘a separat, ia sigur˘a ˆıntre submisiile utilizatorilor
diferit, i. (7 puncte)

Puteti sa adaugati si din alti ani


11 iunie 2015
1. (7 puncte) Ce cauzeaz˘a trecerea din user mode ˆın kernel mode?
Mecanismul de trecere din user mode in kernel mode este declansat de apelurile de sistem,
revenirea in user mode facandu-se prin intoarcerea din apelul de sistem.

Separatia kernel mode - user mode este importantăpentru căasigurăun mod privilegiat de executie
(kernel mode) pentru operatii critice. Un mod privilegiat în care ruleazăsistemul de operare
înseamnăcăoperatiile critice (IPC, lucrul cu I/O, lucrul cu memoria) vor fi validate de sistemul de operare
si un proces (aplicatie user space) nu poate face pagube sistemului. Pentru operatii privilegiate va fi
necesarătrecerea în kernel mode prin intermediul unui apel de sistem, si astfel invocarea sistemului de
operare care actioneazăca un gardian al operatiilor, garantând securitatea si integritatea sistemului.

2. (7 puncte) Ce cont, ine, ˆın mod uzual, o intrare ˆın tabela de pagini a unui proces?
Mapare între adresa virtuală a unei pagini și adresa unui frame fizic(RAM). Poate conține și bit
de prezență, dirty bit(de modificare) și ID de proces.

3. (7 puncte) De ce este, ˆın general, mai rapid˘a schimbarea de context ˆıntre dou˘a thread-uri
ale aceluias, i proces decˆat schimbarea de context ˆıntre dou˘a procese?
Nu se face TLB flush.​ Dezvolta
Schimbarea de context intre 2 procese presupune inlocuirea tabelei de pagini si flush la TLB =>
overhead. La schimbarea de context intre 2 threaduri nu este necesar acest lucru deoarece
threadurile partajeaza tabela de pagini a procesului si au acelasi spatiu de adresa.

4. (7 puncte) Descrieti secventa de apeluri Linux prin care descriptorul 3 din tabela de
descriptori de fisier a unui proces va referi iesirea standard, iar descriptorul 1 va referi fisierul
a.txt.
dup2(1, 3);
​ _WRONLY​);
int fd = open(“a.txt”, O
dup2(1, fd); ​// nu e dup2(fd, 1); ???

5. (7 puncte) Ce se ˆıntˆampl˘a ˆın cazul operat, iei up() pe un semafor?


Operatia up incrementeaza valoarea semaforului, daca nu exista nici un proces blocat pe acel
semafor. Daca exista, valoarea semaforului ramane zero, dar unul din procesele (de
exemplu, la intamplare) blocate vor fi deblocate. Operatiile up si down sunt, evident, atomice
(ce este o operatie atomica?)

6. (10 puncte) Pe un sistem de fisiere un inode contine 7 pointeri directi catre blocuri de date si
2 pointeri cu indirectare simpla. Stiind ca un bloc are 4096 de octeti si ca un pointer ocupa 4
octeti, care este dimensiunea maxima a unui fisier pe acest sistem de fisiere?
Logic ar fi să fie 7*4096+2*4 (not sure).

7. (10 puncte) ˆIntr-o zon˘a a unui program exist˘a urm˘atoarea secvent,˘a:


char arr[100]; [...]
/* instructiuni oarecare nu afecteaza array-ul arr */
arr[23] = ’a’;
arr[24] = ’b’;
ˆIn ce situat, ie prima instruct, iune de atribuire NU cauzeaz˘a page fault, dar a doua instruct,
iune de atribuire cauzeaz˘a page fault?

8. (10 puncte) Un programator a scris un program ˆın limbajul C care foloseste apelul recv(s,
buf, 2000) iar dimensiunea buf este de 1000 de octeti. Sistemul ın discutie foloseste tehnicile
Address Space Layout Randomization (ASLR) si Data Execution Prevention (DEP). Poate fi
acest bug exploatat? Daca da, explicati cum.

9. (10 punct) Dat, i exemplu de trei mecanisme ale sistemului de operare care permit
restrictionarea daunelor pe care le poate provoca un proces controlat de un atacator.
1.Sandboxing (chroot jail), îl pune într-un spațiu de unde nu poate să iasă și nu poate face
damage mult.
2.Nu se permite schimbarea ID-ului(cu setuid).
3....

10. (10 puncte) Explicat, i cum s,tie sistemul de operare c˘arui proces ˆıi este destinat un
segment TCP primit. Diferentiat, i ˆıntre segmentele cu bit-ul SYN activat sau dezactivat.

11. (25 puncte) Unui programator i se cere s˘a implementeze un server de imagini care va
deservi un num˘ar mare de utilizatori. Fiecare utilizator poate ˆınc˘arca sau desc˘arca imagini de
dimensiune mic˘a (<1MB). Sistemul trebuie s˘a poat˘a sust, ine simultan minim 1000 de
utilizatori (upload s, i download), iar timpul maxim de upload sau download trebuie s˘a fie 1s per
fis, ier. Sistemul trebuie s˘a ret, in˘a pentru fiecare utilizator num˘arul de octet, i inc˘arcat, i sau
desc˘arcat, i, precum s, i num˘arul total de octet, i pentru tot, i utilizatorii.
a. Ajutat, i programatorul s˘a aleag˘a hardware-ul minim necesar pentru acest server. Aleget, i
placa de ret,ea de vitez˘a, dimensionat, i memoria RAM, aleget, i num˘arul de core-uri/procese,
HDD vs. SSD, etc. Explicat, i toate alegerile facute. (7 puncte)
b. Specificat, i arhitectura serverului: dac˘a vor fi folosite procese sau thread-uri (cˆate?), apeluri
blocante sau neblocante, etc. (8 puncte)
c. Pentru arhitectura aleas˘a, descriet, i ˆın pseudocod codul de tratare a unui client s, i modul
ˆın care statisticile sunt actualizate. (10 puncte)

15 iunie 2015

1. (7 puncte) De ce procesele I/O intensive primesc, ˆın general, o prioritate mai mare decat
procesele CPU intensive?
Pentru ca stau foarte putin in starea running, efectuand o operatie blocanta si trecand in
waiting. Astfel nu ocupa mult timp pe procesor, lasand loc altor procese.

2. (7 puncte) De ce consideram apelul chroot() o forma de sandboxing?


Multe sisteme folosesc chroot extensiv în etapa de dezvoltare și build pentru a detecta
problemele generate de dependințele
dintre diversele module software. Această metodă de build poartă uneori numele de
sandboxing.
Comanda chroot poate fi utilizată pentru a crea o copie virtuală a sistemului de operare.
Aceasta poate fi utilizată pentru:

Testare și dezvoltare
Un mediu de test poate fi setat cu chroot, evitându-se astfel rularea testului pe un sistem aflat
deja în producție.
Controlul dependințelor
chroot este o modalitate foarte bună de control al dependințelor dintre diferite module software
aflate în dezvoltare.
Compatibilitate
Uneori este nevoie să rulăm software mai vechi care necesită versiuni mai vechi ale unor
biblioteci. Un mediu chroot este ideal pentru rularea acestui software.
Recuperare
Sisteme care nu mai pot fi pornite de pe hard disc, se pot uneori porni rapid într-un mediu
chroot plecând de la un Live CD sau alt mediu de pornire.
Separarea privilegiilor
Programe care în mod potențial constituie o problemă de securitate se pot rula într-un mediu
chroot. Se aplică în general serverelor.

3. (7 puncte) Ce se ˆıntˆampl˘a ˆın cazul operat, iei down() pe un semafor?


Marcheaza semaforul ca este ocupat si astfel nu poate fi accesat de alt proces.
Operația de down are un overhead ridicat, datorită schimbării de context.

4. (7 puncte) De ce schimbarea de context ˆıntre doua user level threads ale aceluiasi proces
este mai rapida decat schimbarea de context ıntre doua kernel level threads ale aceluiasi
proces?
Threadurile user level nu necesită schimbarea spațiului de memorie și permisiunilor. Ele
schimbă doar stackul specific.

5. (7 puncte) Ce contine o intrare ˆın TLB (Translation Lookaside Buffer )?


TLB-ul menține mapări de pagini fizice și pagini virtuale. Conține un subset al tabelei de
pagini.(care contine pointeri catre structuri de fisiere deschise)
6. (10 puncte) Pe un sistem cu suport DEP (Data Execution Prevention), un atacator urmareste
sa realizeze un apel mprotect(..., PROT_EXEC, ...) pe un set de pagini apartinand stivei. Ce
urmareste atacatorul cu un astfel de apel? Cum il va ajuta ın continuarea atacului?
vrea sa obtina drept de executie asupra acelei zone de memorie

7. (10 puncte) Un programator masoara durata celor doua instruct, iuni de mai jos, realizate
consecutiv: p = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE |
MAP_ANONYMOUS, -1, 0); p[0] = ’a’; ˆIn urma masuratorii observa ca prima instructiunie
(mmap) dureaza 900 de nanosecunde iar a doua dureaza 3000 de nanosecunde (adica mai
mult decat prima). Ce putem spune despre overhead-ul de timp cauzat de un apel de sistem
fat,˘a de ovehead-ul de timp cauzat de page fault handler?

8. (10 puncte) ˆIntr-un program se realizeaz˘a apelul: fd = open("/home/student/myfile",


O_RDONLY); Apelul reus,es,te. S,tim c˘a /home/student/myfile este un o leg˘atur˘a simbolic˘a
(symbolic link). Cˆate inode-uri sunt citite ˆın cadrul apelului open() de mai sus?
Ar trebui să fie citite toate inodeurile începând de la myfile până la inodeul final.

9. (10 puncte) De ce, ˆın general, pornirea unui proces dureaz˘a mai mult la prima sa pornire
decˆat la urm˘atoarele porniri?

10. (10 puncte) ˆIn ce situat, ie atˆat apelul send() pe transmit,˘ator, cˆat s, i apelul recv() pe
receptor (ˆın aceeas, i conexiune TCP) sunt simultan blocate?

11. (25 puncte) Se propune implementarea unui profiler de aplicat, ii multithreading cu obiectivul
de a obt, ine informat, ii legate de profilul de performant,˘a al acestor aplicat, ii.
a. Ce informat, ii/metrici sunt utile de a fi furnizate de profiler dezvoltatorului aplicat, iei? ˆIn ce
form˘a sunt utile s˘a fie furnizate acestea dezvoltatorului (numere, medii, grafice, tabele)? (7
puncte)
b. Ce facilit˘at, i trebuie s˘a ofere sistemul de operare s, i hardware-ul pentru funct, ionarea
profilerului? (6 puncte)
c. Cum va accesa profilerul facilit˘at, ile oferite de sistemul de operare s, i hardware (apeluri de
sistem, memorie partajat˘a, sisteme de fis, iere, dispozitive virtuale etc.)? (5 puncte)
d. Ce facilit˘at, i/metrici vor fi de interes pentru dezvoltatorul unei aplicat, ii de transcoding
(conversie video)? Cum va fi folosit profilerul de c˘atre dezvoltator pentru a profila/optimiza
aplicat, ia de transcoding? (7 puncte)

1 septembrie 2015

1. (7 puncte) De ce, ˆın general, un prim apel open() reus, it ˆın cadrul unui proces pe un sistem
de operare Unix ˆıntoarce valoarea 3?
2. (7 puncte) Care este necesitatea spat, iului de swap ˆıntr-un sistem de operare?

3. (7 puncte) Numit, i un avantaj s, i un dezavantaj al mecanismului de memorie virtual˘a.

4. (7 puncte) Care este un avantaj s, i un dezavantaj al folosirii chroot() ˆın detrimentul


virtualiz˘arii pentru izolarea unui grup de procese?

5. (7 puncte) De ce este recomandat s˘a folosim, cˆand se poate, operat, ii atomice pentru
sincronizare ˆın loc s˘a folosim alte mecanisme de sincronizare ˆın cadrul unui proces
multi-threaded?

6. (10 puncte) Ce limiteaz˘a, ˆın general, num˘arul maxim de thread-uri ce pot fi create ˆın cadrul
unui proces? Oferit, i o estimare de formul˘a (nu ceva specific, ci aproximativ) pentru calculul
num˘arului maxim de thread-uri ce pot fi create pe un sistem dat.

7. (10 puncte) Un sistem de fis, iere dispune de limit˘ari la num˘arul maxim de fis, iere care pot fi
create (inode-uri), num˘arul maxim de nume de fis, iere (dentry-uri), dimensiunea maxim˘a a
unui fis, ier s, i spat, iul total ocupat de toate fis, ierele. Un utilizator creeaz˘a ˆıntr-o bucl˘a
infinit˘a hard link-uri. Care din limit˘arile de mai sus va cauza oprirea cre˘arii de hard link-uri?
Dar ˆın cazul cre˘arii de symbolic link-uri?

8. (10 puncte) Un proces aloc˘a un buffer de dimensiunea unei pagini: char buffer[PAGE_SIZE];
ˆIn ce situat, ie operat, ia de mai jos rezult˘a ˆın primirea unei except, ii de tip Segmentation fault
s, i ˆın ce situat, ie nu se ˆıntˆampl˘a acest lucru? buffer[PAGE_SIZE+100] = ’a’;

9. (10 puncte) ˆIntr-un sistem de operare un proces A este planificat des pe procesor dar pentru
intervale scurte de timp, ˆın vreme ce un proces B este planificat mai rar, atunci cˆand A nu
ruleaz˘a, dar timpul total de rulare pe procesor este mai mare decˆat ˆın cazul procesului A. Ce
putet, i spune despre cele dou˘a procese?

10. (10 puncte) Un program are o vulnerabilitate de tip buffer overflow pe un buffer de pe stiv˘a.
Sistemul dispune de suport DEP (Data Execution Prevention). Cum poate fi exploatat˘a
vulnerabilitatea?

11. (25 puncte) Se propune crearea unei solut, ii de application sandboxing pe un sistem de
operare dat. O astfel de solut, ie trebuie s˘a asigure o cˆat mai bun˘a izolare a aplicat,
iilor/proceselor sistemului ˆın as,a fel ˆıncˆat s˘a fie diminuate pagubele cauzate de potent, iale
vulnerabilit˘at, i de securitate. O astfel de solut, ie trebuie s˘a asigure acces limitat la sistemul de
fis, iere, la componente de I/O, la primitive de tip IPC, la ret,ea etc. Solut, ia trebuie s˘a permit˘a
definirea ˆın cadrul unor fis, iere a profilelor de sandboxing care apoi s˘a fie folosite pentru
sandboxing-ul unei aplicat, ii date. Un profil de sandboxing poate fi folosit de mai multe aplicat,
ii.
a. Propunet, i un model de specificare a profilului de sandboxing: ce va cont, ine fis, ierul de
profil, cum va fi completat acest fis, ier de profil de sandboxing? (6 puncte)
b. Ce funct, ionalit˘at, i trebuie s˘a ofere nucleul sistemului de operare pentru a putea folosi
funct, ionalitatea de sandboxing pe baza profilului? Nucleul este cel care va gestiona aplicarea
specificat, iilor din profil. (7 puncte)
c. Cum va fi asociat un profil de sandboxing unui proces dat? Din punctul de vedere al
utilizatorului acesta trebuie s˘a porneasc˘a o aplicatie s, i aceasta va folosi automat profilul de
sandboxing definit de administrator pentru acea aplicat, ie. (7 puncte)
d. Cum vet, i proteja fis, ierele cu profilele de sandbox? Acestea trebuie s˘a fie editate/accesate
doar de administrator. (5 puncte)

10 septembrie 2015

1. (7 puncte) Explicat, i de ce apelul printf nu este un mod robust de a face debugging unui
program care primes,te semnalul SIGSEGV.

2. (7 puncte) Dat, i un exemplu in care planificatorul de procese Shortest Job First rezult˘a
ˆıntr-o alocare inechitabil˘a a resurselor procesorului.

3. (7 puncte) Numit, i un avantaj s, i un dezavantaj al folosirii memoriei virtuale.

4. (7 puncte) Un atacator descoper˘a o vulnerabilitate ˆıntr-un program care-i permite s˘a


execute cod arbitrar. De ce sunt interesante pentru atacator apelurile read s, i write?

5. (7 puncte) Dat, i dou˘a avantaje ale implement˘arii unui server de web cu mai multe procese
fat,˘a de mai multe thread-uri.

6. (10 puncte) Explicat, i de ce este necesar˘a starea zombie pentru un proces ˆın sistemele
Unix.

7. (10 puncte) Un sistem Unix are spat, iu de adrese pe 32 bit, i s, i spatiu de swap de 40GB.
Stiva unui thread are minim 4KB pe acest sistem. a. Estimat, i num˘arul maxim de thread-uri ce
pot fi create de un proces pe acest sistem. b. Estimat, i num˘arul maxim de procese ce pot fi
create pe acest sistem, presupunˆand c˘a nu exist˘a limit˘ari la num˘arul de descriptori sau
identificatori de proces.

8. (10 puncte) Un proces este blocat in apelul de sistem recvmsg. Un pachet este primit la placa
de ret,ea. Descriet, i secvent,a de pas, i pe care ˆıi execut˘a placa de ret,ea s, i sistemul de
operare pˆan˘a cˆand apelul recvmsg se va ˆıntoarce s, i aplicat, ia ˆıs, i va continua execut, ia.

9. (10 puncte) Explicat, i cum s,tie sistemul de operare c˘arui proces ˆıi este destinat un
segment TCP primit. Diferentiat, i ˆıntre segmentele cu bit-ul de SYN activat sau dezactivat.
10. (10 puncte) Un programator foloses,te apelul de sistem write pentru a actualiza un fis, ier.
Dup˘a o pan˘a de curent, programatorul descoper˘a ca unele informat, ii au fost scrise cu write
dar nu apar ˆın fis, ier. Explicat, i cauza problemelor s, i dat, i o posibil˘a solut, ie.

11. (25 puncte) Vi se cere s˘a implementat, i dou˘a programe, un sender s, i un receiver, care
sunt capabile s˘a transmit˘a date ˆıntre dou˘a calculatoare folosind dou˘a interfet,e de ret,ea de
10Gbps. Datele de transmis sunt ˆın memoria sender-ului, s, i se presupune c˘a sunt infinite
(e.g. dac˘a se ajunge la sfˆars, itului bufferului ˆın care sunt stocate se va transmite de la
ˆınceputul bufferului). Aplicat, ia receiver are nevoie s˘a stocheze datele ˆın memorie ˆın
aceeas, i ordine ˆın care au fost citite de aplicat, ia receiver. Vi se cere s˘a folosit, i API-ul de
sockets s, i protocolul TCP pentru a utiliza cele dou˘a interfet,e disponibile la maxim (throughput
total 20Gbps):
a. Detaliat, i protocolul pe care ˆıl va folosi aplicat, ia pentru a impr˘as,tia datele pe cele dou˘a
c˘ai s, i a le pune ˆın ordine la receiver. (7 puncte)
b. Descriet, i apelurile de sistem necesare pentru stabilirea conexiunii pe mai multe interfet,e. (3
puncte)
c. Descriet, i in pseudocod implementarea pentru sender, atunci cˆand conexiunea este deja
deschis˘a. Avet, i ˆın vedere folosirea corect˘a a operat, iei send. (5 puncte)
d. Descriet, i in pseudocod implementarea pentru receiver, atunci cˆand conexiunea este deja
deschis˘a. Avet, i ˆın vedere folosirea corect˘a a operat, iei receive. (5 puncte)
e. Analizat, i solut, ia propus˘a, discutˆand minim 2 posibile probleme de performant,˘a care pot
ap˘area ˆın practic˘a, s, i oferind solut, ii de remediere. (5 puncte)
Introducere

SO: Curs 1

SO: Curs 1: Introducere 1


Cuprins

• De ce (un curs) de SO?


• Despre cursul de SO
• Despre sisteme de operare
• Sisteme de calcul. Hardware
• Concepte importante în SO
• Structura unui SO

SO: Curs 1: Introducere 2


DE CE (UN CURS DE) SO?

SO: Curs 1: Introducere 3


Un mic joc

Scrieți pe o foaie de hârtie:


• O aplicație (desktop, mobile, web) pe care o
folosiți sau care vi se pare utilă/interesantă
• Un dispozitiv hardware care vă place și vi se
pare interesant

Participați la discuția cu titularul de curs

SO: Curs 1: Introducere 4


Sisteme de operare

• Gestiunea resurselor sistemului (hardware)


• Interfața dintre aplicații și hardware
• Medierea accesului la hardware
• Securitatea și integritatea sistemului

SO: Curs 1: Introducere 5


De ce sisteme de operare?
• Independent de job/limbaj de programare/tip
de aplicație
– Depanare
– Performanță
– Securitate
• SO ofera primitive care izoleaza aplicatiile de
detaliile hardware
– POSIX
– Procese, fisiere, thread-uri, memorie virtuala.
SO: Curs 1: Introducere 6
De ce e nevoie de un curs de SO?
Ca sa invatam istoria calculatoarelor?
• System V, Solaris, HP-UX, AIX
• CP/M, MS-DOS
• Linux, FreeBSD, MacOS, Windows
• Mozilla OS, ClickOS, ChromeOS, ...

Ca sa invatam din greselile istoriei?


SO: Curs 1: Introducere 7
De ce e nevoie de un curs de SO?
A aparut noul, magnificul OS!

Ca sa evitam confuzia si intrebari ca:


• Dar de ce e nevoie de inca un OS?
• Eu nu am timp sa invat inca un OS!
• Mai bine ma fac boss.
• E prea de tot globalizarea SO, votez cu Brump!

SO: Curs 1: Introducere 8


De ce e nevoie de un curs de SO?
Are sens!

SO: Curs 1: Introducere 9


DESPRE CURSUL DE SO

SO: Curs 1: Introducere 10


Cursuri de SO

USO user interface

SO system API

user space

kernel space

SO2

SO: Curs 1: Introducere 11


Legături cu alte discipline
Cunoștințe utile din Cunoștințe utile pentru
• USO • SO2
• Programare • CPL
• SD • BD
• IOCLA • APP
• CN • SPRC
• PC

SO: Curs 1: Introducere 12


Cine suntem
• Curs
– Costin Raiciu (CA), Mihai Carabaș (CB), Răzvan Deaconescu
(CC)
• Coordonator curs: Mihai Carabaș
• Laborator:
– Laura Ruse, Costin Carabaș, Răzvan Nițu, Răzvan Crainea,
Elena Mihăilescu, Alexandru Jercăianu, Ioana Ciornei, Irina
Preșa, Alexandru Rotaru, George Muraru, Octavian
Grigorescu, Iustin Dumitrescu, Costin Lupu, Mihai
Bărbulescu, Adrian Șendroiu, Vladimir Diaconescu, Darius
Mihai, Vladimir Olteanu, Bogdan Purcareata, Alex Morega
• Colaboratori:
– Robert Baronescu
SO: Curs 1: Introducere 13
Resursele cursului de SO
• echipa
• wiki: http://ocw.cs.pub.ro/courses/so/
• moodle: http://cs.curs.pub.ro
• listă de discuții
• slide-uri cursuri
• catalog și calendar Google
• mașini virtuale
• interfața vmchecker
• documentație
• pagina de Facebook
SO: Curs 1: Introducere 14
Comunitatea SO

• întrebări, discuții, sugestii, feedback


• live (cursuri, laboratoare), listă, Facebook
– să parcurgeți indicațiile de comunicare pe listă
• colaborare în timpul laboratorului
– încurajăm să discutați și vă ajutați între voi

SO: Curs 1: Introducere 15


Conținut
Cursuri Laboratoare
• 13 cursuri • 12 laboratoare
• slide-uri – corelate cursurilor
– după curs
• suport bibliografic
– foarte important de parcurs • practice
• bogat în informații • joc, discuții, exerciții
• interactiv • tutorial, task-based, learn
by doing
• încurajăm colaborarea

SO: Curs 1: Introducere 16


Teme
Conținut Dezvoltare
• Cross-platform • Linux și Windows
development • aprofundare API
• Mini-shell • 8-20 de ore pe temă
• Memorie virtuala • teste publice
• Planificator de thread-uri – testare pe vmchcker
• Web server asincron • întrebări și suport pe listă

SO: Curs 1: Introducere 17


Despre rezolvare teme

• Temele te ajută să înțelegi mai bine conceptele


• Temele îți dezvoltă abilitățile de programare
• Pot fi dificile, dar e “rewarding” când îți ies
• Temele sunt individuale
– Puteți discuta între voi în limitele bunului simț
– Întrebați cu încredere pe lista de discuții
– Parcurgeți regulile de realizare ale temelor
• Folosiți GitLab sau BitBucket sau GitHub (privat)

SO: Curs 1: Introducere 18


Bibliografie

• Curs
– Operating System Concepts
– Modern Operating Systems
• Laborator
– The Linux Programming Interface
– Windows System Programming

SO: Curs 1: Introducere 19


Bibliografie suplimentară

• Beginning Linux Programming


• Advanced Programming in the Unix
Environment
• Linux System Programming
• Win32 Programming
• Programming Windows
• Linkers and Loaders

SO: Curs 1: Introducere 20


DESPRE SISTEME DE OPERARE

SO: Curs 1: Introducere 21


Ce este un SO?

OSCE, fig. 1.1, pg. 4


SO: Curs 1: Introducere 22
Ce este un SO?

• un set de programe
• vedere top-down: extensie a mașinii fizice
• vedere bottom-up: gestionar al resurselor
fizice
• scris în general în C
• relativ transparent utilizatorului (“trebuie să
meargă)

SO: Curs 1: Introducere 23


Legătura SO - hardware

• SO este primul nivel software peste hardware


• Un SO performant
– folosește facilitățile hardware
– expune facilitățile hardware aplicațiilor
• Hardware-ul are nevoie de SO pentru a putea
fi folosit (pentru a construi aplicații/software)
• SO evoluează pe măsură ce evoluează
hardware-ul

SO: Curs 1: Introducere 24


SISTEME DE CALCUL. HARDWARE

SO: Curs 1: Introducere 25


Sistem de calcul

• Sistemul fizic (hardware) + sisteme de operare


și aplicații
• Baza pentru a dezvolta aplicații, a folosi
aplicații, a construi sisteme
• În general interconectat cu alte sisteme
• De la servere cu mii de core-uri la dispozitive
mici din zona IoT (Internet of Things)

SO: Curs 1: Introducere 26


Hardware-ul unui sistem de calcul
• Procesor (CPU)
– rulează codul (instrucțiunile) proceselor
• Memorie de lucru (RAM)
– stochează datele și codul proceselor
• Magistrale
– leagă CPU, module de memorie, dispozitive de I/E
• Dispozitive periferice (de intrare/ieșire, I/E, I/O)
– comunicarea cu exteriorul: utilizator, alte sisteme
de calcul, alte dispozitive
• Spațiu de stocare (disc-uri, flash, ROM, NVRAM)
– Programe (din care vor lua naștere procese)
– Date pentru procese
– Informații pentru utilizator (fișiere)
SO: Curs 1: Introducere 27
Procesor și memorie

OSCE, fig. 1.7, pg. 16

SO: Curs 1: Introducere 28


Magistrale

https://en.wikipedia.org/wiki/Bus_%28computing%29
SO: Curs 1: Introducere 29
SO: Curs 1: Introducere 30
Memorie

OSCE, fig. 1.11, pg. 28


SO: Curs 1: Introducere 31
CONCEPTE IMPORTANTE ÎN SO

SO: Curs 1: Introducere 32


Shell

• Interfața utilizator-sistem de operare


• CLI sau GUI
• Bash vs. Windows Explorer / GNOME / KDE
• Un proces care permite pornirea de alte
procese/aplicații
– “shell spawns a process”

SO: Curs 1: Introducere 33


Procese
• Iau naștere dintr-un program executabil
• Program în execuție, entitate dinamică
– Noțiunea de “runtime” (timpul rulării) se referă la
proces
• Date și cod în memorie, se execută pe CPU
• Are asociate resurse: spațiu de adrese de
memorie, fișiere deschise, sockeți
• SO oferă protecție și comunicare inter-proces
• Ierarhie de procese la nivelul SO
Mai multe la cursurile 3 și 4
SO: Curs 1: Introducere 34
Memoria unui proces

Mai multe la cursurile 3 și 5 https://en.wikipedia.org/wiki/Virtual_address_space


SO: Curs 1: Introducere 35
Memorie virtuală
• Un proces are un spațiu virtual (inexistent de
fapt) de memorie
– Procesul are impresia că toată memoria îi aparține
• Memoria virtuală decuplează vederea procesului
de memoria sistemului
• Procesele lucrează cu adrese virtuale (din spațiul
fizic de adrese)
– Adresele fizice sunt adrese din memoria fizică
• SO mapează/asociază spațiul virtual al proceselor
cu memorie fizică
Mai multe la cursurile 6 și 7
SO: Curs 1: Introducere 36
Thread-uri

• Un proces poate avea mai multe thread-uri


• Thread-urile partajează spațiul virtual de
adrese al procesului
• Utile să faci mai multe lucruri cu aceleași date
(din spațiul virtual de adrese al procesului)
• Permit folosirea facilităților hardware multi-
procesor

Mai multe la cursul 8


SO: Curs 1: Introducere 37
Concurență și sincronizare

• Thread-urile sau procesele pot concura la


achiziția de resurse (date în memorie)
• Accesele concurente pot duce la date
inconsecvente sau blocaje
• Sincronizarea garantează accesul consecvent și
ordonat la date

Mai multe la cursul 9


SO: Curs 1: Introducere 38
Moduri de execuție
• Procesorul are un mod privilegiat: supervisor mode,
system mode, kernel mode
– Pentru acțiuni privilegiate
– Aici rulează sistemul de operare
• Aplicațiile rulează în modul neprivilegiat: user mode
– Nu pot comunica cu hardware-ul sau cu alte procese
– Pot doar acționa asupra spațiului de memorie propriu
• Nucleul intermediază accesul proceselor la hardware
sau la resursele altor procese
• Separația modurilor asigură securitatea sistemului
• Tranziția user mode -> kernel mode = apel de sistem
Mai multe la cam toate cursurile următoare ☺
SO: Curs 1: Introducere 39
Traziția user mode – kernel mode

OSCE, fig. 1.10, pg. 22

Mai multe la cam toate cursurile următoare ☺


SO: Curs 1: Introducere 40
Apeluri de sistem

Mai multe la cam toate cursurile următoare ☺


SO: Curs 1: Introducere
OSCE, fig. 2.6, pg. 56 41
Fișiere
• Unități de stocare
• Deschise și folosite de procese
• Fișier pe disc (static) și fișier deschis (dinamic,
în cadrul unui proces)
• Fișier pe disc: nume, dimensiune, permisiuni,
timestamp-uri
• Fișier deschis: handle de fișier, cursor de fișier,
drepturi de deschidere, operații pe fișier
Mai multe la cursurile 2 și 12 SO: Curs 1: Introducere 42
STRUCTURA UNUI SO

SO: Curs 1: Introducere 43


Stiva software pentru un sistem de
calcul

Aplicații

Framework-uri, biblioteci, servicii

Utilitare de sistem, biblioteci low-level

Nucleu (kernel)

SO: Curs 1: Introducere 44


Nucleul (kernel-ul)
• Strict tehnic, nucleul este sistemul de operare
– Windows are ca nucleul WindowsNT din imaginea
ntoskrnl.exe
– Linux este nucleul unei distribuții GNU/Linux și
Android
– Mac OS X și iOS au ca nucleu XNU
• Încărcat la bootare apoi pornește primele aplicații
și gestionează hardware-ul
• Răspunde apelurilor de sistem ale proceselor
• Gestionează resursele hardware
• Garantează integritatea sistemului
SO: Curs 1: Introducere 45
Structura uzuală a SO (monolitic)

SO: Curs 1: Introducere


OSCE, fig. 2.13, pg. 69 46
SO monolotic vs. SO microkernel

https://en.wikipedia.org/wiki/Microkernel
SO: Curs 1: Introducere 47
SO monolotic vs. SO microkernel (2)

Monolitic Microkernel
• Eficient • Mai lent (comunicare între
• Coeziunea codului/datelor servicii)
• Mai puțin flexibil • Componentizabil, flexibil,
• TCB (Trusted Computing modular
Base) mai mare (design mai • TCB redus (design mai sigur)
puțin sigur)

SO: Curs 1: Introducere 48


Mașini virtuale

SO: Curs 1: Introducere


OSCE, fig. 2.17, pg. 74 49
Tendințe curente în SO

• Securitate
• Dispozitive de mici dimensiuni (tinification)
• Scalare (CPU, memorie, disc), mașini virtuale
• Performanță, suport hardware pentru operații
specifice

SO: Curs 1: Introducere 50


Reminder: Resursele cursului de SO
• echipa
• wiki: http://ocw.cs.pub.ro/courses/so/
• moodle: http://cs.curs.pub.ro
• listă de discuții
• slide-uri cursuri
• catalog și calendar Google
• mașini virtuale
• interfața vmchecker
• documentație
• pagina de Facebook
SO: Curs 1: Introducere 51
Concluzie
• Cunoștințele de SO sunt utile pentru
– securitate
– depanare
– performanță
• Conceptele de SO sunt relativ complexe
• Sunt foarte importante legăturile între concepte
• Vom insista pe fiecare concept în următoarele cursuri
• Sistemul de operare este strâns legat de hardware
• Nucleul SO gestionează hardware-ul și asigură
securitatea sistemului

SO: Curs 1: Introducere 52


Cuvinte cheie
• sistem de operare (SO) • mod de execuție
• comunitatea SO • kernel mode
• sistem de calcul • user mode
• hardware • apel de sistem
• procesor • fișiere
• memorie • nucleu/kernel
• procese • SO monolitic
• spațiu virtual • SO microkernel
• memorie virtuală • mașină virtulă

SO: Curs 1: Introducere 53


Procese

SO: Curs 3

SO: Curs 3: Procese 1


Sisteme de fișiere
• De ce este nevoie de sisteme de fișiere?
• Ce este un fișier?
• Cu ce diferă un fișier obișnuit de un director?
• Ce este un descriptor de fișier?
• Ce poate fi referit printr-un descriptor de fișier?
• Ce informații conține o structură de fișier deschis?
• Ce operații (API C) pe fișier modifică cursorul de fișier?
• Ce operații (API C) pe fișier modifică câmpul size din
metadatele fișierului?
• Ce se întâmplă cu tabela de descriptori la redirectare?
SO: Curs 3: Procese 2
De ce e nevoie de procese?

• Partajarea resurselor unui sistem de calcul


– între mai mulți utilizatori
– între acțiunile diferite ale aceluiași utilizator

SO: Curs 3: Procese 3


Proprietăți necesare ale proceselor
• să grupeze resursele necesare pentru efectuarea unei
acțiuni
– cod, date
– memorie folosită, registre procesor
– I/O
• să crească utilizarea resurselor sistemului
• să asigure izolare; un proces nu poate accesa
– memoria unui alt proces
– sockets ai altui proces
– fișierele altui proces
SO: Curs 3: Procese 4
Ce este un proces?

• Un program aflat în execuție

• Încapsularea/abstractizarea execuției în SO

• Abstractizare peste procesor, memorie, I/O

SO: Curs 3: Procese 5


Cuprins

• Rolul proceselor
• Atributele unui proces
• Planificarea proceselor
• Crearea unui proces
• Alte operații cu procese
• API pentru lucrul cu procese

SO: Curs 3: Procese 6


Suport de curs
• OSCE
– Capitolul 3: Processes
• MOS
– Capitolul 2: Processes and Threads (Secțiunea 1)
• LSP
– Capitolul 5: Process Management
– Capitolul 6: Advanced Process Management
• WSP
– Capitolul 6: Process Management
SO: Curs 3: Procese 7
ROLUL PROCESELOR

SO: Curs 3: Procese 8


Proces 1 Proces 2 Proces 3

Nucleul SO

Procesor Memorie I/O


Time

Storage 9
SO: Curs 3: Procese
API folosit de procese

memorie descriptori
thread-uri
virtuală de fișier

procesor memorie I/O

vom reveni în cursurile 5, 6 și 8


SO: Curs 3: Procese 10
Proces și procesor

• Un proces are unul sau mai multe thread-uri


– Un thread rulează instrucțiuni pe un procesor

• De obicei procesele sistemului sunt mai multe


decât procesoarele sistemului
– E nevoie de planificarea proceselor pe procesoare

SO: Curs 3: Procese 11


Proces și memorie

• Un proces are o memorie proprie (izolată de


alte procese)
• Cod/instrucțiuni și date
• Instrucțiunile sunt aduse din memoria RAM în
procesor și executate
– Spunem că procesul se execută pe procesor

SO: Curs 3: Procese 12


Spațiul (virtual) de adrese

SO: Curs 3: Procese 13


Spațiul (virtual) de adrese (2)
• Fiecare proces are un spațiu (virtual) de
adrese
• Asigură izolarea față de alte procese
• Procesul are impresia folosirii exclusive a
memoriei
• Toate resursele de memorie (cod, date,
biblioteci, stivă) sunt referite prin zone în
spațiul virtual de adrese
mai multe în cursul 5
SO: Curs 3: Procese 14
Proces și I/O
• Un proces comunică cu exteriorul: disc, rețea,
tastatură, monitor
• Comunicarea se face, de regulă, prin decriptori de
fișier
• Un proces are o tabelă de descriptori de fișier
• Un descriptor de fișier referă un fișier, socket,
terminal, dispozitiv etc.
• Operațiile cu I/O blochează procesul
– I/O este mai lent decât procesorul
– procesul așteaptă încheierea operației

SO: Curs 3: Procese 15


Tabela de descriptori

• Proprie fiecărui proces


• Interfața pentru I/O a unui proces
• Vectori de pointeri către structuri de fișiere
deschise
• Structurile pot referi fișiere, sockeți,
dispozitive speciale (terminale)

SO: Curs 3: Procese 16


Tipuri de procese
• CPU bound (CPU intensive)
– rulează des pe procesor
• I/O bound (I/O intensive)
– rulează rar pe procesor
– fac operații de I/O -> se blochează

• Interactive (foreground)
– interacționează cu utilizatorul
• Neinteractive (batch, background)
– servicii, daemoni

SO: Curs 3: Procese 17


ATRIBUTELE UNUI PROCES

SO: Curs 3: Procese 18


Cum arată un proces la nivelul SO?
• O structură de date
– PCB (Process Control Block)
– Descrie un proces la nivelul SO
• Informații legate de resursele folosite
• Un identificator (PID: process identifier)
• Legături cu celelalte structuri
• Informații de securitate, monitorizare,
contabilizare
SO: Curs 3: Procese 19
Resursele unui proces

• Timp de rulare pe procesor


• Memorie (cod și date)
• Tabelă de descriptori de fișier
• Unele resurse pot fi partajate cu alte procese

SO: Curs 3: Procese 20


Atribute ale unui proces

• PID
• parent PID
• pointeri către resurse
• stare (de rulare, așteptare)
• cuantă de timp de rulare
• contabilizare resurse consumate
• utilizator, grup

SO: Curs 3: Procese 21


PLANIFICAREA PROCESELOR
mai multe în cursul 4

SO: Curs 3: Procese 22


Procese și procesoare

• Pentru a rula un proces are nevoie de un


procesor
• Procesorul rulează instrucțiunile procesului
• Procesele într-un sistem sunt mai multe decat
procesoarele
• Sistemul de operare asigură accesul echilibrat
al proceselor la procesoare

SO: Curs 3: Procese 23


Multitasking
• SO schimbă rapid procesele pe procesoare
• După un timp (cuantă, time slice) un proces este
scos de pe procesor și pus altul în loc
– se spune că “expiră cuanta”
– acțiunea este numită “schimbare de context” (context
switch)
• Cuantă de timp de ordinul milisecundelor
– se schimbă foarte rapid procesele
– impresia de rulare simultan
• Un proces poate fi întrerupt în momentul
executării unei secvențe
SO: Curs 3: Procese 24
Starea unui proces

• rulare (RUNNING)
– Procesul rulează pe un procesor
• așteptare (WAITING)
– procesul a executat o acțiune blocantă (de exemplu citire
I/O) și așteaptă sosirea datelor; nu poate rula
• gata de execuție (READY)
– procesul poate să ruleze pe procesor
• Câte procese se pot găsi în cele trei stări?
• Cum ați asigura gestiunea proceselor în cele trei stări?

SO: Curs 3: Procese 25


Tranziții între stările unui proces

SO: Curs 3: Procese 26


Tranziții între stările unui proces (2)
• RUNNING -> READY
– procesului i-a expirat cuanta
– există un alt proces în starea READY cu prioritate
superioară
• RUNNING -> WAITING
– procesul a executat o operație blocantă
• WAITING -> READY
– evenimentul așteptat de proces s-a produs
• READY -> RUNNING
– s-a eliberat un procesor
– procesul este primul din coada de procese READY
SO: Curs 3: Procese 27
Planificarea unui proces (scheduling)

• Un proces este adus din starea READY în


starea RUNNING
• Este adus dacă există un procesor liber
• Un procesor este liber dacă procesul de pe
procesor l-a eliberat
• Se spune că are loc o schimbare de context
(context switch)

SO: Curs 3: Procese 28


Schimbarea de context

SO: Curs 3: Procese 29


Schimbarea de context (2)
• Un proces este înlocuit pe procesor cu alt
proces
• Se salvează procesul/contextul vechi
• Se restaurează procesul/contextul nou
• O schimbare de context înseamnă overhead
– mai multe schimbări de context: mai mult
overhead
– mai puține schimbări de context: mai puțină
interactivitate

SO: Curs 3: Procese 30


Planificatorul de procese

• Componentă a SO
• Responsabilă cu planificarea proceselor
– asigurarea accesului proceselor la procesoare
– compromis (trade-off) între echitate și
productivitate

mai multe la cursul 4

SO: Curs 3: Procese 31


CREAREA UNUI PROCES

SO: Curs 3: Procese 32


Cum ia naștere un proces?

• Din cadrul unui executabil (program)


• Un alt proces (părinte) creează un proces
(copil)
– noul proces (procesul copil) își populează
memoria cu informații din executabil
• Acțiunea se mai cheamă loading, load time
– realizată de loader

SO: Curs 3: Procese 33


Ierarhia de procese

• Un proces poate crea unul sau mai multe


procese copil
• Un proces poate avea un singur proces părinte
• În Unix, procesul init este în vârful ierarhiei de
procese
– init este creat de sistemul de operare la boot
– init creează apoi procesele de startup
– procesele de startup creează alte procese etc.

SO: Curs 3: Procese 34


Procese și executabile
• Unul sau mai multe procese iau naștere dintr-
un fișier executabil
• Fișierul executabil conține în mod esențial
datele și codul viitorului proces
• Procesul are zone de memorie care nu sunt
descrise în executabilul aferent
– stiva
– heap-ul (malloc)
– zone pentru bibliotecile dinamice
SO: Curs 3: Procese 35
Funcționarea unui shell
• Se scrie la stdin un șir
• Șirul este interpretat de shell într-o cale de
executabil (și argumente)
• Procesul shell creează un nou proces (copil)
• Procesul copil “încarcă” (load) datele și codul
din executabil
• Procesul copil este planificat de SO
• Părintele procesului copil este procesul shell
SO: Curs 3: Procese 36
fork și exec

• Separare între crearea unui nou proces și


încărcarea datelor dintr-un executabil
• fork: creare nou proces (copil) (aproape
identic procesului părinte)
• exec: încărcarea informațiilor dintr-un
executabil în memoria procesul copil

SO: Curs 3: Procese 37


fork, exec și shell-ul
parent

shell

child
fork()
shell

exec(“/bin/ls”)

child
ls

parent
shell wait()

SO: Curs 3: Procese 38


fork, exec și redirectare

pid = fork();
if (pid == 0) { /* child process */
fd = open(“a.txt”, O_WRONLY|O_CREAT|O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
execl(“/bin/ls”, “/bin/ls”, NULL);
}

SO: Curs 3: Procese 39


ALTE OPERAȚII CU PROCESE

SO: Curs 3: Procese 40


Încheierea execuției unui proces

• Procesul își încheie execuția


– a ajuns la sfârșitul programului
– a apelat exit()
• Decizia este a procesului
• În final, procesul nu mai există în sistem

SO: Curs 3: Procese 41


Terminarea unui proces

• Sistemul de operare încheie procesul


– la cererea altui proces (kill)
– procesul a efectuat o operație nevalidă
(segmentation fault)
• Se trimite un semnal (Unix) sau o excepție
(Windows)
• În final, procesul nu mai există în sistem

SO: Curs 3: Procese 42


Așteptarea încheierii unui proces

• Sincronizarea acțiunii unor procese


– faci acțiunea X după acțiunea Y
– procesul care face acțiunea X așteaptă încheierea
execuției procesului care face acțiunea Y
• Se cheamă “waiting for a process”
• Sunt utile informațiile legate de încheierea
procesului
– valoarea de retur, în shell variabila $?

SO: Curs 3: Procese 43


Operatorul shell &

• În mod obișnuit shell-ul așteaptă încheierea


proceselor create
• Operatorul &: shell-ul nu mai așteaptă
încheierea procesului
– procesul rulează în background
– shell-ul controlează stdin

SO: Curs 3: Procese 44


Proces orfan

• Un proces al cărui părinte și-a terminat


execuția
• Își pierde legătura în ierarhia de procese
• Este adoptat de un proces dedicat (init pe
Unix)

SO: Curs 3: Procese 45


Proces zombie
• Un proces care s-a încheiat dar nu a fost
așteptat
• Rămân informații reziduale care vor putea fi
folosite de procesul părinte
• Dacă un proces zombie rămâne și orfan, este
adoptat de init și apoi este încheiat
• Procesele zombie pe durată mai lungă ocupă
(degeaba) resurse ale sistemului
SO: Curs 3: Procese 46
Comunicare interprocese
• Transfer de date între procese
• Partajare de date (de obicei memorie) între
procese
• Comunicarea prin fișiere este modul cel mai
simplu (și barbar)
• De avut în vedere sincronizarea proceselor
– un proces citește dacă altul a scris
– un proces poate scrie dacă altul e pregătit să citească
– de obicei se folosesc buffere (zone de memorie
intermediare)
SO: Curs 3: Procese 47
pipe-uri
• Pipe-uri anonime (spre deosebire de FIFO-uri,
pipe-uri cu nume)
• Buffere de comunicare între procese
• Expuse ca o “țeavă” între două procese
– un capăt de scriere
– un capăt de citire
– cele două capete sunt descriptori de fișier
• Se folosesc operațiile de tip read și write
• Procesele trebuie să fie “înrudite”
SO: Curs 3: Procese 48
API DE LUCRU CU PROCESE

SO: Curs 3: Procese 49


Lucrul cu procese în shell

• Se creează prin rularea de comenzi


• Se încheie la încheierea rulării comenzii
• Se termină folosind kill, pkill, Ctrl+c, Ctrl+\
• Shell-ul este procesul părinte al proceselor
nou create

SO: Curs 3: Procese 50


Lucrul cu procese în ANSI C

system("ps -u student");

FILE *f = popen("ps -u student", "r");


pclose(f);
• nu e ANSI, e POSIX, dar are suport si pe
Windows

SO: Curs 3: Procese 51


Lucrul cu procese în POSIX

pid = fork();
switch (pid) {
case -1: /* fork failed */
perror(“fork”);
exit(EXIT_FAILURE);
case 0: /* child process */
execlp("/usr/bin/ps", "ps", "-u", "student", NULL);
default: /* parent process */
printf(“Created process with pid %d\n”, pid);
}

pid = wait(&status);

SO: Curs 3: Procese 52


Lucrul cu procese în Win32

PROCESS_INFORMATION pi;
CreateProcess(NULL, “notepad”, NULL, NULL, ..., &pi);

WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &retValue);

SO: Curs 3: Procese 53


Lucrul cu procese în Python

p = subprocess.Popen(["ps", "-u", "student"],


shell=True, bufsize=bufsize,
stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
(child_stdin,
child_stdout,
child_stderr) = (p.stdin, p.stdout, p.stderr)

SO: Curs 3: Procese 54


Lucrul cu procese în Java

ProcessBuilder builder = new ProcessBuilder("ps","-u","student");

Process p = builder.start();
InputStream is = p.getInputStream();
OutputStream os = p.getOutputStream();

SO: Curs 3: Procese 55


Cuprins

• Rolul proceselor
• Atribute ale unui proces
• Planificarea proceselor
• Crearea unui proces
• Alte operații cu procese
• API pentru lucrul cu procese

SO: Curs 3: Procese 56


Recapitulare

• De ce este nevoie de procese?


• Ce resurse are un proces?
• Câte procese copil sau părinte are un proces?
• Ce este un proces zombie?
• Ce se întâmplă cu un proces orfan?
• Cu ce diferă implementarea pentru rularea
“ls” față de rularea “ls &” într-un shell?

SO: Curs 3: Procese 57


Cuvinte cheie
• proces • READY, RUNNING, WAITING
• procesor • schimbare de context
• memorie • planificare
• I/O • cuantă de timp
• CPU bound • ierarhie de procese
• I/O bound • fork, exec
• PCB • terminare proces
• PID • așteptare proces
• spațiu virtual de adrese • zombie
• multitasking • orfan
• stare proces • pipe

SO: Curs 3: Procese 58


Securitatea memoriei

SO: Curs 07
Cuprins
• Procese și executabile
• De la proces la executabil
• Alterarea spațiului de adresă
• Vulnerabilități și atacuri
• Exploatarea memoriei
• Metode ofensive și mecanisme defensive

SO: Curs 07: Securitatea memoriei 2


Suport de curs
• Jon Erickson - Hacking: The Art of Exploitation, 2nd
Edition
– Section 0x270. Memory Segmentation
– Chapter 0x300. Exploitation
• https://io.netgarage.org/
• Aleph One – Smashing the Stack for Fun and Profit
• Understanding the Stack (CMSC 311, Computer
Organization)

SO: Curs 07: Securitatea memoriei 3


PROCESE ȘI EXECUTABILE

SO: Curs 07: Securitatea memoriei 4


Procese
• Program în execuție
• Entitate planificabilă
• CPU, memorie, I/O
• Descriptori de fișier, semnale
• Cuantă de timp, prioritate, stare
• Spațiu virtual de adrese, zone de memorie
– date (variabile), zone read+write
– cod (instrucțiuni), zone read+executable

SO: Curs 07: Securitatea memoriei 5


Spațiul virtual de adrese
• Spațiu de adresare unic procesului
• Toate informațiile de memorie (cod, date)
• Adrese virtuale, pagini virtuale
– mapare peste pagini fizice
• Programatorul lucrează doar cu adrese
virtuale

SO: Curs 07: Securitatea memoriei 6


Spațiul virtual de adrese (2)
$ pmap -p $(pidof exec-addr)
6293: ./exec-addr
0000000000400000 4K r-x-- /home/exec-addr
0000000000600000 4K rw--- /home/exec-addr
00007fa6d730f000 1664K r-x-- /lib/x86_64-linux-gnu/libc-2.18.so
00007fa6d74af000 2044K ----- /lib/x86_64-linux-gnu/libc-2.18.so
00007fa6d76ae000 16K r---- /lib/x86_64-linux-gnu/libc-2.18.so
00007fa6d76b2000 8K rw--- /lib/x86_64-linux-gnu/libc-2.18.so
00007fa6d76b4000 16K rw--- [ anon ]
00007fa6d76b8000 128K r-x-- /lib/x86_64-linux-gnu/ld-2.18.so
00007fa6d789c000 12K rw--- [ anon ]
00007fa6d78d3000 16K rw--- [ anon ]
00007fa6d78d7000 4K r---- /lib/x86_64-linux-gnu/ld-2.18.so
00007fa6d78d8000 4K rw--- /lib/x86_64-linux-gnu/ld-2.18.so
00007fa6d78d9000 4K rw--- [ anon ]
00007fffd9b4d000 132K rw--- [ stack ]
00007fffd9bfe000 8K r-x-- [ anon ]
ffffffffff600000 4K r-x-- [ anon ]
total 4068K

SO: Curs 07: Securitatea memoriei 7


Crearea unui proces
• Dintr-un proces existent
• CreateProcess() pe Windows
• fork() + exec() pe Linux
• La exec() se înlocuiește imaginea de executabil
– Spațiul de memorie (date, cod) este înlocuit
• La exec() informațiile sunt preluate dintr-un
fișier în format de executabil

SO: Curs 07: Securitatea memoriei 8


Fișiere în format executabil
• Obținute din compilarea surselor și link-
editarea modulelor obiect
• Descrierea zonelor de memorie: date și cod
• Simbolurile definite în program
– variabile
– funcții
– un simbol cuprinde: nume, tip, adresă, spațiu
ocupat

SO: Curs 07: Securitatea memoriei 9


Formate de executabile
• ELF: Formatul standard pe Unix
• PE: Windows
• Mach-O: Mac OS X
• Listare secțiuni
objdump --headers <executable-file>
• Listare simboluri
objdump --syms <executable-file>
• Dezasamblare (zone cu instrucțiuni)
objdump --disassemble <executable-file>

SO: Curs 07: Securitatea memoriei 10


DE LA EXECUTABIL LA PROCES

SO: Curs 07: Securitatea memoriei 11


Fazele prin care trece un program
• Compile-time
– se creează modul obiect dintr-un fișier cod sursă
• Link-time
– se creează fișier executabil din module obiect și
biblioteci
• Load-time
– se creează un proces dintr-un executabil
• Run-time
– procesul execută acțiuni definite în codul său
SO: Curs 07: Securitatea memoriei 12
Maparea zonelor de executabil
• .text, .data, .rodata, .bss
• Se întâmplă la load-time
• Se folosește un apel de forma mmap pentru a
mapa zonele de cod și date în memoria
procesului
• În Linux se pot observa folosind utilitarul
pmap

SO: Curs 07: Securitatea memoriei 13


Maparea bibliotecilor dinamice
• .so pe Linux, .dll pe Windows, .dylib pe Mac OS X
• Au tot format de executabil
• Au date și cod
• Maparea este similară dar ...
– pot fi mapate la adrese diferite depinzând de executabil
– sau chiar la adrese diferite la diferite rulări ale procesului
• PIC: Position Independent Code

SO: Curs 07: Securitatea memoriei 14


ASLR
• Codul și datele bibliotecilor sunt mapate la
adrese diferite la fiecare rulare
• Rațiuni de securitate
• Address Space Layout Randomization
• Poate fi observat folosind utilitarul pmap
• ... sau folosim ldd cu argument un executabil

SO: Curs 07: Securitatea memoriei 15


ALTERAREA SPAȚIULUI DE ADRESĂ
LA RUN-TIME

SO: Curs 07: Securitatea memoriei 16


Stiva
• Este folosită pentru a reține informații legate
de apelurile de funcții
• “Stack frame” creat pentru fiecare funcție
• Un stack frame conține informații despre
apelant (caller) și apelat (callee)
– parametrii funcției
– adresa de retur
– fostul frame pointer
– variabile locale
SO: Curs 07: Securitatea memoriei 17
Alocarea pe stivă
• Pentru variabile locale unei funcții
• Dinamică, la run-time
• Automată (alocarea și dezalocarea)
– alocare la intrarea în funcție/bloc
– dezalocarea la ieșirea din funcție/bloc
• Se mai alocă, implicit, valoarea de retur, fostul
frame pointer și parametrii funcției

SO: Curs 07: Securitatea memoriei 18


Heap-ul
• Alocare dinamică, la run-time
• Apelurile malloc/free
• Atenție la
– memory leak-uri
– omiterea apelării free
– dangling pointers
– double free

SO: Curs 07: Securitatea memoriei 19


Maparea memoriei
• Alocare dinamică, la run-time
• mmap, VirtualAlloc (Windows)
• Permite mapare de fișiere, partajare de
memorie
• Control al alocării/dezalocării
• Permisiuni de acces
• Granularitate la nivel de pagină

SO: Curs 07: Securitatea memoriei 20


VULNERABILITĂȚI ȘI ATACURI LA
MEMORIA UNUI PROCES

SO: Curs 07: Securitatea memoriei 21


Execuția codului într-un proces
• Zone de cod
• Instruction pointer
– poziția curentă
– incrementat cu dimensiunea unei instrucțiuni
• Fluxul se schimbă la branch-uri sau apeluri de
funcții
• Pointeri de funcții

SO: Curs 07: Securitatea memoriei 22


Bug-uri și vulnerabilități
• Ce este un bug?
• Când este un bug o vulnerabilitate?
• Ce obiective sunt în exploatarea unei
vulnerabilități?

SO: Curs 07: Securitatea memoriei 23


Tipuri de vulnerabilități la nivelul
memoriei
• Suprascrierea datelor cu date de atac
– suprascriere variabile (verificate în if)
– suprascriere pointeri de funcții
• Alterarea fluxului de execuție
• Executarea de apeluri arbitrare
• Executarea de cod arbitrar (code injection)

SO: Curs 07: Securitatea memoriei 24


exec(“/bin/bash”)
• Sau echivalentul system(“/bin/bash”)
• Obținerea unui shell într-o aplicație existentă
• Obiectivul inițial al unui atac
• Atacatorul se va folosi de vulnerabilități ale
memoriei pentru a porni un shell
• Ulterior:
– obținere informații confidențiale
– denial of service
– privilege escalation
SO: Curs 07: Securitatea memoriei 25
STACK BUFFER OVERFLOW

SO: Curs 07: Securitatea memoriei 26


Injectare de cod
• Punere de cod într-o zonă writable și
executarea sa
• Să putem scrie cod
– Folosim funcții de citire input (fgets, scanf)
• Să putem executa cod de acolo
– zonă executabilă
– jump la acea adresă
• E nevoie de o zonă simultan writable +
executable
SO: Curs 07: Securitatea memoriei 27
Shellcode
• Instrucțiuni în cod binar
• Se scriu într-o zonă / buffer
• Se “sare” aici pentru execuție
• De obicei realizează exec(“/bin/bash”)
• Se încearcă injectarea acestuia într-o zonă
accesibilă

SO: Curs 07: Securitatea memoriei 28


Structura stivei (reminder)
• Adresă de retur
• Fostul frame pointer
• Variabile locale (buffere incluse)

• Dacă un buffer este neîncăpător (overflow)


putem suprascrie date, eventual chiar adresa
de retur

SO: Curs 07: Securitatea memoriei 29


Stack buffer overflow
• Scrierea în buffer peste dimensiunea alocată
• De ce merge?
– avem spațiu alocat pe stivă
• Ce suprascriem?
– alte variabile
– adresa de retur
• Ce obținem
– alterarea fluxului de execuție
– rulare de cod arbitrar
SO: Curs 07: Securitatea memoriei 30
Suprascrierea adresei de retur
• Adresa pe care o va folosi apelatul la
încheierea funcției
– este adresa instrucțiunii următoare față de cea
folosită de apelant în momentul apelului
• Se găsește pe stivă
• Stack buffer overflow poate conduce la
suprascrierea adresei de retur
• Se pune adresa shellcode-ului
• Shellcode-ul poate sta pe stivă
SO: Curs 07: Securitatea memoriei 31
Funcții de lucru cu șiruri
• str*
• Împreună cu funcțiile de citire de intrare (fgets) sunt
principalele funcții exploatabile
• NBTS (NUL-terminated byte strings)
– dacă un șir nu e NUL-terminat, putem suprascrie dincolo
de dimensiunea sa
• Lungimea unui șir
– trebuie știută tot timpul
– presupunei legate de lungimea șirului pot conduce la
vulnerabilități de securitate
SO: Curs 07: Securitatea memoriei 32
METODE OFENSIVE ȘI MECANISME
DEFENSIVE

SO: Curs 07: Securitatea memoriei 33


Injectare de cod
• Zone writable + executable
• Se poate plasa cod acolo și executa
• Acest cod se numește “shellcode”
• Clasic: pe stivă + stack buffer overflow
– citire buffer de la standard input
– variabilă de mediu

SO: Curs 07: Securitatea memoriei 34


DEP
• Data execution prevention
• O zonă writable nu este executable
• W^X
• NX flag pe arhitecturile moderne
• Bypass: apel mprotect, VirtualProtect

SO: Curs 07: Securitatea memoriei 35


return-to-libc
• Executarea unei funcții existente
• Clasic, system(“/bin/bash”)
• Nu este nevoie de injectare de cod
• Trebuie știută adresa funcției

SO: Curs 07: Securitatea memoriei 36


Canary value
• Plasată pe stivă între variabile locale și adresa
de retur
• Se verifică coerența informației la părăsirea
funcției
• Previne efectul stack buffer overflows pentru a
suprascrie adresa de retur

SO: Curs 07: Securitatea memoriei 37


Suprascriere de alte date
• Nu suprascriem adresa de retur
• Variabile locale alterează fluxul de lucru
• Pointeri de funcții
• Suprascriere pointeri de gestiune a heap-ului

SO: Curs 07: Securitatea memoriei 38


ASLR
• Randomizează plasarea bibliotecilor
– dificil de găsit adresa funcțiilor de bibliotecă
– dificil de realizat return-to-libc
• Randomizează plasarea stivei
– adresa de retur se suprascrie cu o adresă efectivă,
nu cu o funcție cu jump relativ
– adresa buffer-ului de pe stivă variază la fiecare
rulare
– dificil de știut unde să sari pe stivă
SO: Curs 07: Securitatea memoriei 39
CONCLUZII

SO: Curs 07: Securitatea memoriei 40


Safe coding
• Atenție la funcții de lucru pe șiruri
• Folosiți suport DEP, ASLR, canary value
• Analiză statică pentru verificarea de bug-uri
sau vulnerabilități

SO: Curs 07: Securitatea memoriei 41


Cuvinte cheie
• proces • bug
• spațiu virtual • vulnerabilitate
• executabil • exec(“/bin/bash”)
• biblioteci • injectare de cod
• pmap • stack buffer overflow
• zone de memorie • alterarea fluxului
• ASLR • shellcode
• load-time • adresa de retur
• aun-time • funcții pe șiruri
• alocare la run-time • return-to-libc
• stivă • DEP
• stack frame • canary value
• heap • safe coding
SO: Curs 07: Securitatea memoriei 42
Sincronizare

SO: Curs 09
Cuprins

• Recapitulare thread-uri
• Contextul sincronizării
• Implementarea mecanismelor de
sincronizare
• Sincronizarea multi core
• Structuri de sincronizare

SO: Curs 09: Sincronizare 2


Suport de curs

• Operating Systems Concepts


– Capitolul 6 - Process Synchronization
• Modern Operating Systems
– Capitolul 2 - Processes and Threads
• Secțiunea 2.3 - Interprocess Communication
• Allen B. Downey - The Little Book of
Semaphores

SO: Curs 09: Sincronizare 3


RECAPITULARE THREAD-URI

SO: Curs 09: Sincronizare 4


Ce este un thread?
• Instanță de execuție (planificabilă)
• Instruction Pointer + Stack Pointer + stare
(registre)
• Partajează cu alte thread-uri resursele
procesului: fișiere, memorie
• Permite paralelism
• Necesită sincronizare (partajare date)
• User-level threads / kernel-level threads
SO: Curs 09: Sincronizare 5
De ce thread-uri?
• Lightweight: timp de creare scurt, timp de
comutare scurt
– nu se face schimbare de tabelă de pagini
(+TLB flush) la context switch
• Comunicare rapidă (memorie partajată)
• Calcule multiprocesor cu date comune
• Thread pool pentru prelucrarea de task-uri
– mai ușor de programat față de un model
asincron
SO: Curs 09: Sincronizare 6
De ce nu thread-uri?

• O problemă a unui thread afectează


întregul proces
• Nevoie de sincronizare
– date incoerente, comportament incoerent
– probleme ale sincronizării
– reentranță
• Mai puțin relevante pentru probleme
seriale

SO: Curs 09: Sincronizare 7


CONTEXTUL SINCRONIZĂRII

SO: Curs 09: Sincronizare 8


Context

• Sisteme multiproces
- procese, thread-uri date comune
• Sisteme multicore
- magistrală comună, memorie comună
• Nevoie de comunicare
- canale de comunicație, date partajate

SO: Curs 09: Sincronizare 9


Situații posibile

• read before write


• time of check to time of use (TOCTOU)
• interleaved access

SO: Curs 09: Sincronizare 10


Condiții de cursă

• race conditions
• output-ul unui sistem depinde de timp sau
de evenimente
• dacă ordinea nu e cea dorită e un bug
• nedeterminism în execuție
• date inconsecvente/neintegre
• poate fi vulnerabilitate exploatabilă

SO: Curs 09: Sincronizare 11


Date inconsecvente
unsigned long sum = 0;

void thread_func(size_t i)
{
sum += i * i * i;
print(“sum3(%zu): %lu\n”, i, sum);
}

int main(void)
{
size_t i;
for (i = 0; i < NUM_THREADS; i++)
create_thread(thread_func, i);
[...]
}

Ce probleme sunt în codul de mai sus?


Cum le-am rezolva?
SO: Curs 09: Sincronizare 12
TOCTOU
if (access("file", W_OK) != 0) {
exit(1);
}

/* done by another process */


. . . . .
symlink("/etc/passwd", "file");
. . . . .

fd = open("file", O_WRONLY);
write(fd, buffer, sizeof(buffer));

https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use#Examples

SO: Curs 09: Sincronizare 13


Sincronizare

• Asigurarea accesului corect la date


– date integre
– determinism
• Acces exclusiv / serializare / atomizare
– regiune critică
• Secvențiere / ordonare
– read after write, write after write, use after
create

SO: Curs 09: Sincronizare 14


Primitive de sincronizare

• Ordonare
– wait()
– notify()

• Acces exclusiv
– lock()
– unlock()

SO: Curs 09: Sincronizare 15


Mecanisme de sincronizare
• Ordonare
– semafoare
– cozi de așteptare
– variabile condiție
– monitoare

• Acces exclusiv
– variabile atomice
– spinlock-uri
– mutex-uri
SO: Curs 09: Sincronizare 16
IMPLEMENTAREA MECANISMELOR
DE SINCRONIZARE

SO: Curs 09: Sincronizare 17


Atomicitatea unei operații

• Atomicitate: operația se execută fără


intervenția unui alt proces sau core

• Este a += 5 atomică în C?

SO: Curs 09: Sincronizare 18


Single core / multi core

• a += 5
• x86: add [ebp-12], 5
- atomică single core
- înseamnă read-update-write
- poate fi “întreruptă” de alt core
- neatomică multi core
• ARM: load, add, store
- neatomică nici pe single core

SO: Curs 09: Sincronizare 19


Adunare atomică

• __sync_fetch_and_add (GCC)
• https://gcc.gnu.org/onlinedocs/gcc-4.1.0/
gcc/Atomic-Builtins.html
• pe x86 multi core pune lock pe magistrală
• pe ARM leagă face operații tranzacționale
• tranzațional: totul sau nimic
• dacă nu iese, încearcă iar

SO: Curs 09: Sincronizare 20


Atomic pe multi core

• un singur core să poată folosi exclusiv


magistrala
• prefixul lock pe x86
• ldrex, strex pe ARM

SO: Curs 09: Sincronizare 21


Locking (implementare naivă)

lock = 0; /* init */

while (lock == 1)
; /* do nothing */
lock = 1; /* get lock */

SO: Curs 09: Sincronizare 22


Compare and Swap (CAS)

• Compare and Exchange


• Prevenirea TOCTOU pentru locking naiv
• Operație atomică simultană de verificare și
actualizare
• compare_and_exchange(lock, 0, 1);

SO: Curs 09: Sincronizare 23


Compare and Swap (CAS) (2)

• compare_and_exchange(lock, 0, 1);
• atomic for:

if (value == to_compare)
value = to_update;
return value;

SO: Curs 09: Sincronizare 24


Locking using CAS

• show demo

SO: Curs 09: Sincronizare 25


Spinlock

• basic exclusive access primitive


• CAS-based, în general optimizat
• suport multi core, access exclusiv la magistrală
• uses busy waiting
• spin_lock(), spin_unlock()
• show demo

SO: Curs 09: Sincronizare 26


Mutex

• primitivă de acces exclusiv


• blocantă, are o coadă de așteptare pentru
procese
• adecvat pentru regiuni critice mai mari

SO: Curs 09: Sincronizare 27


Implementare mutex

• o structură
• un câmp stare internă
• o coadă de așteptare de procese
• un spinlock pentru protejarea structurii interne
• dacă regiunea critică nu e ocupată (not
contended) nu intră în coada de așteptare (fast
path)
• futex: implementare cu suport user-space în Linux
- evită apeluri de sistem pentru not contended

SO: Curs 09: Sincronizare 28


Spinlock vs. mutex
Spinlock Mutex
• Busy-waiting • Blocant
• Simplu • Coadă de așteptare
• Pentru regiuni critice • Pentru regiuni critice
scurte mari sau în care thread-ul
se blochează

SO: Curs 09: Sincronizare 29


SINCRONIZAREA MULTI CORE

SO: Curs 09: Sincronizare 30


SMP

• Symmetric Multi Processing


- memorie comună
- magistrală comună de acces la memorie
• sincronizarea pe un cor nu ține cont de
magistrală
- de exemplu: dezactivare întreruperi
• pe multi core: acces exclusiv la magistrală

SO: Curs 09: Sincronizare 31


Acces exclusiv pe magistrală

• prefixul lock pe x86


lock add [ebp-12], 5
• ldrex, strex pe ARM
ldrex …

strex …
cmp … ; if not transactional, try again
• overhead de serializare a accesului

SO: Curs 09: Sincronizare 32


Sincronizarea cu lock multi core

• lock-ul este o variabilă comună


• este încăcată în memoria cache a fiecărui core
• modificările unui core (lock, unlock) se fac în
cache-ul local
• modificările se propagă la celelalte cache-uri

SO: Curs 09: Sincronizare 33


Cache thrashing

• modificări într-un cache se propagă în celelalte


cache-uri
• intrările în celelalte cache-uri se invalidează
• spinlock-urile pe core-uri diferite duc la
modificări pe un core și invalidări pe celelalte
• are loc cache thrashing
- invalidări frecvente
- citiri din memoria principală
- overhead

SO: Curs 09: Sincronizare 34


Variabile per-CPU

• câte o variabilă per procesor


• nu e nevoie de sincronizare inter-core
• exemplu: procesul curent care rulează
• partiționare a datelor, eliminarea overhead-ului
de sincronizare

SO: Curs 09: Sincronizare 35


STRUCTURI DE SINCRONIZARE

SO: Curs 09: Sincronizare 36


Acces la date comune

• acces concurent
- nevoie de acces exclusiv
• acces comunicativ/colaborativ
- nevoie de ordonare citire/scriere
- producător-consumator

SO: Curs 09: Sincronizare 37


Producător consumator

• zonă comună partajată


• unul sau mai mulți scriitori (producători)
• unul sau mai mulți cititori (consumatori)
• buffer/zonă cu mai multe celule
- diferență de viteză producători/consumatori
- networking, I/O, message passing

SO: Curs 09: Sincronizare 38


Implementare producător consumator

producer consumer

lock(mutex); lock(mutex);
if (is_buffer_full()) if (is_buffer_empty())
wait(buffer_not_full,mutex); wait(buffer_not_empty, mutex);
produce_item(); consume_item();
signal(buffer_not_empty); signal(buffer_not_full);
unlock(mutex); unlock(mutex);

SO: Curs 09: Sincronizare 39


Buffer circular

• circular buffer, ring buffer

https://blog.grijjy.com/2017/01/12/expand-
your-collections-collection-part-2-a-generic-ring-
buffer/

SO: Curs 09: Sincronizare 40


Buffer circular

• buffer obișnuit cu “wrap around”


• consume() / produce() sau get() / put()
• index = (index + 1) % BUFFER_SIZE
• read_index, write_index, capacity, size
• demo
• exemplu implementare: kfifo în nucleul
Linux

SO: Curs 09: Sincronizare 41


CONCLUZII

SO: Curs 09: Sincronizare 42


Contextul sincronizării

• Sisteme multiproces
- procese, thread-uri date comune
• Sisteme multicore
- magistrală comună, memorie comună
• Nevoie de comunicare
- canale de comunicație, date partajate
• Nevoie de ordonare, determinism, date
integre
SO: Curs 09: Sincronizare 43
Mecanisme de sincronizare
• necesită suport hardware
• compare and swap (CAS)
• acces exclusiv/serial
- variabile atomice
- spinlock
- mutex
• secvențiere/determinism/ordonare
- semafoare
- cozi de așteptare
- variabile condiție
- monitoare

SO: Curs 09: Sincronizare 44


Sincronizare SMP

• acces exclusiv pe magistrală (lock)


• cache thrashing
• variabile per-CPU

SO: Curs 09: Sincronizare 45


Buffer circular

• folosit pentru producător-consumator


• viteze diferite de scriere și citire
• buffer obișnuit cu “wrap around”

SO: Curs 09: Sincronizare 46


Probleme de sincronizare

• race conditions
• TOCTOU
• deadlock: așteptare mutuală
• livelock: “așteptare” cu spinlock-uri

SO: Curs 09: Sincronizare 47


Overhead de apel

• Apelurile de lock, unlock, wait, notify sunt


costisitoare
• În general înseamnă apel de sistem
• De multe ori invocă planificatorul
• Mai multe apeluri, mai mult overhead
• Lock contention generează mai mult
overhead
• De preferat, unde se poate, operații atomice

SO: Curs 09: Sincronizare 48


Cuvinte cheie
• sincronizare • CAS (compare and swap)
• multi core • cmpxchg()
• determinism • lock()
• condiție de cursă • unlock()
• inconsecvență • signal()
• TOCTOU • wait()
• acces exclusiv • locking pe magistrală
• atomicitate • cache thrashing
• secvențiere • per-cpu variables
• operații atomice • ring buffer
• mutex • producător-consumator
• spinlock • overhead de sincronizare

SO: Curs 09: Sincronizare 49


Networking  in    
Sistemul  de  Operare  

Sisteme  de  Operare  


Curs  11  

1  
Cum  folosim  Internetul?  
•  Aplica<ile  doresc  sa  transmita  si  sa  
recep<oneze  datele  
•  Cum  facem  asta  folosind  modelul  best-­‐effort  
oferit  de  IP?  

2  
Transmisia  de  date  in  Internet  

Calculatorul  Meu   Google  

Web  Client   Web  Server  


5.6.7.8   1.2.3.4  

Skype   SSH  Server  

3  
Bit 0
Packet  
Bit 15 Bit 16
I P   Bit 31

Version IHL TOS ECN Total Length

Identification Flags Fragment Offset

TTL Protocol Header Checksum

Source IP

Destination IP

TRNASPORT PROTOCOL

4  
Transmisia  de  date  in  Internet  

Calculatorul  Meu   Google  

Web  Client   Web  Server  


SRC:  5.6.7.8,     5.6.7.8   1.2.3.4  
DST:  1.2.3.4  
Skype   SSH  Server  

5  
Transmisia  de  date  in  Internet  

Calculatorul  Meu   Google  

Web  Client   Web  Server  


5.6.7.8  SRC:  5.6.7.8,     1.2.3.4  
DST:  1.2.3.4  
Skype   SSH  Server  

6  
Transmisia  de  date  in  Internet  

Calculatorul  Meu   Google  

Web  Client   Web  Server  


5.6.7.8   SRC:  5.6.7.8,     1.2.3.4  
DST:  1.2.3.4  
Skype   SSH  Server  

7  
Cum  demul<plexam  pachetele?  

Calculatorul  Meu   Google  

Web  Client   Web  Server  


5.6.7.8   SRC:  5.6.7.8,  1.2.3.4  
 
DST:  1.2.3.4  
Skype   SSH  Server  

8  
Cum  demul<plexam  pachetele?  

Calculatorul  Meu   Google  

?  
Web  Client   Web  Server  
5.6.7.8   SRC:  5.6.7.8,  1.2.3.4  
 
DST:  1.2.3.4  
Skype   SSH  Server  

9  
Porturi:  adresele  nivelului  transport  

Calculatorul  Meu   Google  

Web  Client   Web  Server:    


PORT  1102   5.6.7.8   PORT  80  
SRC:  5.6.7.8,  1.2.3.4  
 
DST:  1.2.3.4  
Skype   SSH  Server  
PORT  22  

10  
Bit 0 Bit 15 Bit 16 Bit 31

Version IHL TOS ECN Total Length

Identification Flags Fragment Offset

TTL Protocol Header Checksum

Source IP

Destination IP

Source Port Destination Port

TRANSPORT PROTOCOL

11  
Porturi:  adresele  nivelului  transport  

Calculatorul  Meu   Google  

Web  Server:    
Web  Client   PORT  80  
5.6.7.8   1.2.3.4  
SRC:  5.6.7.8,DST:  1.2.3.4  
SRC_PORT:  1102  DST_PORT:  80  
Skype   SSH  Server  
PORT  22  

12  
Socket  API  
•  Interfata  pentru  servicii  de  transport  
•  Oferit  ca  biblioteca  u<lizator  sau  func<i  OS  
•  Foloseste  descriptori  (ca  la  fisiere)  

•  Socket  API    este      


–  Originar  din  Berkeley  BSD  UNIX    
–  Disponibil  pe  Windows,  Solaris,  etc.    

•  Standard  de  facto  


13  
Transmission  Control  Protocol  
•  Protocol  orientat  conexiune  
•  In  cadrul  unei  conexiuni  garanteaza  transmisie  
in  ordine  si  sigura  a  datelor  
•  Realizeaza  controlul  conges3ei  adaptand  
viteza  de  transmisie  la  condi<ile  retelei  
•  Interfata  oferita:    
   transmisie  si  recep3e  de  sir  de  octe3  

14  
API  Conexiuni  TCP:  Sumar  

Web  Client   Google  


PORT  ?   ASTEPT  CONEXIUNI  PE  PORTUL  80  

Skype  
Web  Server:    
1.2.3.4   PORT  80  

Web  Client  
PORT  ?  

15  
Transmisie/recep<e  date  cu  TCP  
Pasi  necesari  server:   Pasi  necesari  client:  
1.  Creaza  socket:   1.  Creaza  socket:  
int  ls  =  socket  (AF_INET,   int  s  =  socket  (AF_INET,  
SOCK_STREAM,   SOCK_STREAM,  
IPPROTO_TCP);   IPPROTO_TCP);  
2.  Seteaza  port  pentru  socket:   2.  Conecteaza-­‐te  la  server:  
bind(ls,  &addr,  sizeof(addr));   connect(s,  &addr,  sizeof(addr));  
3.  Declara  nr.  de  clien<:  
listen(ls,  5);  
4.  Asteapta  conexiune:  
int  s  =  accept(ls,NULL,NULL);    
16  
Inchiderea  conexiunii  TCP  
•  Elibereaza  resursele  asociate  conexiunii  
•  Informeaza  capatul  celalalt  de  inchiderea  
conexiunii  
•  API  
–  shutdown(s,SHUT_RD/SHUT_RDWR/SHUT_WR)  
–  close(s)  

17  
TCP  asigura  transmisie  sigura,  in  
ordine    a  unui  sir-­‐de-­‐octe3:    
•  Applica<ile  trimit  un  numar  arbitrar  de  octe<  
–  Sa  zicem  100.000B  
•  TCP  imparte  octe<i  in  segmente  
–  Pentru  ca  reteaua  func<oneaza  cu  pachete  de  
dimensiune  fixa  
•  Le  trimite  in  retea  
–  Segmentele  pot  fi  pierdute  sau  reordonate  
•  Receptorul  TCP  trebuie  sa  recep<oneze  datele  
in  ordine  
18  
Transmisia  de  date  TCP  

Browser:    Send  3KB  of  data   HTTP  Server:    


Read  Request  

19  
Transmisia  de  date  TCP  

Data:  0-­‐1000  

Browser:    Trimit  3KB  de  date   HTTP  Server:    


Citesc  cerere  

20  
Transmisia  de  date  TCP  

Data:  
Data:  0-­‐1000  
1000-­‐2000  

Browser:    Trimit  3KB  de  date   HTTP  Server:    


Citesc  cerere  

21  
Transmisia  de  date  TCP  

Data:   Data:  
Data:  1-­‐1000  
2001-­‐3000   1001-­‐2000  

Browser:    Trimit  3KB  de  date   HTTP  Server:    


Citesc  cerere  

22  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  
SEQ   Data:   SEQ   Data:   SEQ  
2001   1001   1   Data:  1-­‐1000  
2001-­‐3000   1001-­‐2000  

Browser:    Trimit  3KB  de  date   HTTP  Server:    


Citesc  cerere  

23  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

SEQ   Data:   SEQ   Data:  


2001   2001-­‐3000   1001   1001-­‐2000  

Browser:    Trimit  3KB  de  date   ACK  1001   HTTP  Server:    


Citesc  cerere  

24  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

SEQ   Data:  
2001   2001-­‐3000  

ACK  1001   ACK  2001  

Browser:    Trimit  3KB  de  date   HTTP  Server:    


Citesc  cerere  

25  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

ACK  1001   ACK  2001   ACK  3001  

Browser:    Trimit  3KB  de  date   HTTP  Server:    


Citesc  cerere  

26  
Recep<a  de  date  cu  TCP  
recv  (s,  buf,  max_len,  flags)  

•  Intoarce  numarul  de  octe<  trimisi  


–  Poate  fi  mai  mic  decat  max_len!  
•  Cod  corect:  
int  t  =  0;  
while  (t<len){    
 d  =  recv(s,  buf+t,  len-­‐t,0);  
 if  (d<0)  break;  
 t  +=  d;    
}   27  
Cum  implementam  recv  in  SO?  
•  recv()  –  intoarce  datele  in  ordine  
•  Ce  se  intampla  daca  se  pierd  segmente?  
–  S<va  trebuie  sa  pastreze  pachetele  cu  numar  de  
secventa  mai  mare  
–  Este  nevoie  de  un  receive  buffer  
–  Ce  se  intampla  daca  se  umple  bufferul?  
•  Flow  control,  sender-­‐ul  se  opreste  din  transmisie  

28  
Recep<a  datelor:  receive  buffer  

SEQ   Data:   SEQ   Data:   SEQ  


2001   1001   1   Data:  1-­‐1000  
2001-­‐3000   1001-­‐2000  

Receive  buffer  
29  
Recep<a  datelor:  receive  buffer  

SEQ   Data:   SEQ   Data:  


2001   2001-­‐3000   1001   1001-­‐2000  

ACK  1001,  
WND  2000  

1-­‐1000  
Receive  buffer  
30  
Recep<a  datelor:  receive  buffer  

SEQ   Data:  
2001   2001-­‐3000  

ACK  1001,   ACK  2001  


WND  2000   WND  1000  

1000-­‐  
1-­‐1000  
2000  
Receive  buffer  
31  
Recep<a  datelor:  receive  buffer  

ACK  1001,   ACK  2001   ACK  3001  


WND  2000   WND  1000   WND  0  

1000-­‐   1000-­‐  
1-­‐1000  
2000   3000  
Receive  buffer  
32  
Recep<a  datelor:  receive  buffer  

Ca<  octe<  va  ci<  apelul  urmator?  


ACK  1001,   ACK  2001   ACK  3001  
recv  (buf,  3000,  0)  
WND  2000   WND  1000   WND  0  

1000-­‐   1000-­‐  
1-­‐1000  
2000   3000  
Receive  buffer  
33  
Transmisia  de  date  cu  TCP  
send  (s,  buf,  len)  

•  Intoarce  numarul  de  octe<  trimisi  


–  Poate  fi  mai  mic  decat  len!  
•  Cod  corect:  
int  t  =  0;  
while  (t<len){    
 d  =  send(s,  buf+t,  len-­‐t);  
 if  (d<0)  break;  
 t  +=  d;    
}  
34  
Cum  implementam  send  in  SO?  
•  send()  –  garanteaza  ca  datele  acceptate  vor  fi  
transmise,  cat  <mp  conexiunea  merge  
•  Ce  se  intampla  daca  se  pierd  segmente?  
–  Sender-­‐ul  trebuie  sa  pastreze  mesajele  pana  sunt  
confirmate  
–  Este  nevoie  de  un  send  buffer  
–  Ce  se  intampla  daca  se  umple  bufferul?  
•  Send  se  blocheaza  

35  
Transmisia  datelor:  send  buffer  

Care  este  valoarea  intoarsa  de  send?  


send  (buf,  5000,  0);  
0   4000  

Send  buffer  

36  
Transmisia  datelor:  send  buffer  

1000-­‐   2000-­‐   3000-­‐  


1-­‐1000  
2000   3000   4000   CWND=3,  RWND=4    
Send  buffer  

37  
Transmisia  datelor:  send  buffer  

Flight_size                    CWND  RWND  


         0        <    min(    3    ,    4    )  ?  
1000-­‐   2000-­‐   3000-­‐  
1-­‐1000  
2000   3000   4000   CWND=3,  RWND=4    
Send  buffer  

38  
Transmisia  datelor:  send  buffer  

SEQ  
1   Data:  1-­‐1000  

1  <  min(3,4)  ?  
1000-­‐   2000-­‐   3000-­‐  
2000   3000   4000   CWND=3,  RWND=4    
Send  buffer  

39  
Transmisia  datelor:  send  buffer  

SEQ   Data:   SEQ  


1001   Data:  1-­‐1000  
1001-­‐2000   1  

2  <  min(3,4)  ?  
2000-­‐   3000-­‐  
3000   4000  
Send  buffer  

40  
Transmisia  datelor:  send  buffer  

SEQ   Data:   SEQ   Data:   SEQ  


2001   2001-­‐3000   1001   Data:  1-­‐1000  
1001-­‐2000   1  

3  <  min(3,4)  ?  
3000-­‐  
4000  
Send  buffer  

41  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

SEQ   Data:   SEQ   Data:  


2001   2001-­‐3000   1001   1001-­‐2000  

ACK  1001  

3000-­‐  
4000  
Send  buffer  

42  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

SEQ   Data:  
2001   2001-­‐3000  

ACK  1001   ACK  2001  

3000-­‐  
4000  
Send  buffer  

43  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

ACK  1001   ACK  2001   ACK  3001  

3000-­‐  
4000  
Send  buffer  

44  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

Ack  =>flight_size—  
ACK  2001   ACK  3001  

2  3000-­‐  
<  min(3,4)  ?  
4000  
Send  buffer  

45  
Transmisia  de  date  TCP:    
Numere  de  secventa  si  confirmari  

SEQ   Data:  
3001   3001-­‐4000  

ACK  2001   ACK  3001  

Send  buffer  

46  
Analiza<  codul  urmator  
Sender   Receiver  
char  buf[1000];  …   char    buf[1000];…  
for    (i=0;i<1000;i++){   recv(s,  buf,  1000);  
 send(s,  b+i,  1);  
}  
Ca<  octe<  va  primi  recv?  

47  
Imbunata<rea  performantei:  batching  
•  Dimensiunea  datelor  pentru  send/receive  
•  Dimensiunea  send  si  receive  buffers  
–  Recomandat  la  minim  2  *  Bandwidth  *  Delay  
–  Controlabil  cu  sysctl  tcp.rmem  /  tcp.wmem  

48  
Batching  (2)  
•  Batching  in  nucleul  SO  
–  S<va  lucreaza  cu  segmente  mari,  de  64KB  
–  TCP  Segmenta3on  Offload:  Placa  de  retea  
fragmenteaza  segmentele  inainte  sa  le  puna  pe  fir  
–  Large  Receive  Offload:  opera<a  opusa,  la  receiver  
•  Exista  si  variante  soqware:  (gso)  generic  
segmenta<on  offload  
•  Controlabile  cu  ajutorul  u<litarului  ethtool  
•  Fara  TSO/LRO  Linux  nu  a<nge  10Gbps  cu  TCP!  
49  
Considera<i  de  performanta  
•  Evitarea  copierilor  inu<le  
–  Sendfile  

50  
Thread-­‐uri  vs.  event  I/O  
•  Discu<e  la  tabla!  

51  
Securitatea sistemului

SO: Curs 13
Cuprins
• Sisteme sigure
• Separarea privilegiilor
• Securitatea fișierelor
• Cel mai mic privilegiu
• Identitatea utilizatorilor

SO: Curs 13: Securitatea sistemului 2


Suport de curs
• Operating Systems Concepts
– Capitolul 14 – Protection
– Capitolul 15 – Security
• Secțiunile 15.1, 15.2, 15.5
• Modern Operating Systems
– Capitolul 9 – Security
• Secțiunile 9.4, 9.6

SO: Curs 13: Securitatea sistemului 3


SISTEME SIGURE

SO: Curs 13: Securitatea sistemului 4


Sistem sigur
• Face ce este proiectat să facă
• Face doar ce este proiectat să facă
• Folosește corespunzător resursele date
• Asigură confidențialitate informațiilor
• Un sistem nesigur
– Are comportament nedeterminist
– Are comportament exploatabil (controlabil de atacator)
– Folosește necorespunzător/abuziv resurele sistemului
– Permite accesul la date confidențiale

SO: Curs 13: Securitatea sistemului 5


Bug și vulnerabilitate
• Bug: problemă de implementare/proiectare
– Generează comportament nevalid, în general
eroare
• Vulnerabilitate (bug de securitate)
– Problema poate fi exploatată
– Un atacator poate folosi/controla bug-
ul/vulnerabilitatea
– Un model de atack (attack vector) rezultă în
atacator obținând acces sau controlând accesul la
resurse
SO: Curs 13: Securitatea sistemului 6
Elemente de proiectare a unui
sistem sigur
• Separarea privilegiilor
– Procesele au anumite privilegii
– O acțiune diferită -> un proces nou cu alte privilegii
– Un proces compromis nu compromite altele
• Cel mai mic privilegiu
– Nu lași unui proces mai multe privilegii (permisiuni) decât
are nevoie
– Dacă un proces este compromis pagubele sunt conținute
• Limitarea resurselor
– Nu se oferă mai multe resurse decât este necesar
SO: Curs 13: Securitatea sistemului 7
Elemente de proiectare a unui
sistem sigur (2)
• Identitatea utilizatorilor
– Autentificare, credențiale robuste
• Monitorizare
– Procese
– Folosirea resurselor
– Acționat în momentul în care sunt probleme de securitate
• Confidențialitate: criptare, integritate
• Asigurarea calității
– Secure coding, secure programming, defensive programming, secure
by design
– Security auditing, fuzz testing
– Software updates
SO: Curs 13: Securitatea sistemului 8
SEPARAREA PRIVILEGIILOR

SO: Curs 13: Securitatea sistemului 9


Separarea privilegiilor
• componente separate au roluri separate
• cuplat cu principiul celui mai mic privilegiu
• exemplu: Postfix; master (root) + smtpd,
pickup, cleanup, qmgr (postfix)
• folosirea utilizatorului nobody
• privilegii administrative (complete): kernel-
mode sau root
– de folosit rar
– de delegat roluri (sudo)
SO: Curs 13: Securitatea sistemului 10
Separația kernel mode – user mode
• Instrucțiunile privilegiate sunt executate în
spațiul kernel
– accesul la I/O
– alocarea de resurse
– handler-ele de întrerupere
– gestiunea sistemului
• Suportul procesorului
– niveluri de privilegiu (rings)
– x86: nivelul 0 (kernel), nivelul 3 (user)

SO: Curs 13: Securitatea sistemului 11


Utilizatorul administativ (root)
• Are acces complet la sistem
• Are privilegiile nucleului în sistemul de
operare
• Procesele administrative rulează cu permisiuni
de root
• Accesul trebuie limitat
• Dispozitivele mobile în general nu oferă acces
de tipul “root” în mod direct
SO: Curs 13: Securitatea sistemului 12
Utilizatori și permisiuni
• Roluri dedicate sunt oferite altor utilizatori
• Un utilizator are un subset de privilegii și acces
limitat la resursele sistemului
• Accesul la sistemul de fișiere se realizează prin
permisiuni la sistemul de fișiere
• Dacă un proces al unui utilizator este
compromis pagubele sunt limitate la
permisiunile acelui utilizator

SO: Curs 13: Securitatea sistemului 13


Stabilirea permisiunilor/privilegiilor
• DAC: Discretionary access control
– Există noțiunea de owner
– Owner-ul poate stabili permisiuni/privilegii
– Permisiunile pe sistem de fișiere
• MAC: Mandatory access control
– Kernel-ul/Sistemul/Administratorul decide întregul set de
privilegii
– Nu există owner
– Modificările sunt efectuate doar de sistem/root
– SELinux
– Suport și pe sistemele mobile (Android, iOS)
SO: Curs 13: Securitatea sistemului 14
SECURITATEA FIȘIERELOR

SO: Curs 13: Securitatea sistemului 15


Permisiuni pe sistemul de fișiere
• o formă de separare de privilegii
• controlul accesului
• anumiți utilizatori au anumite drepturi pe
resursele din sistemul de fișiere
• matrice de control al accesului
• liste de acces

SO: Curs 13: Securitatea sistemului 16


Drepturi pe fișiere
• Asocierea drepturilor de acces pentru
utilizatori la fișiere
• Citire, scriere, ștergere, execuție
• Creare fișier, listare, ștergere fișier, parcurgere

SO: Curs 13: Securitatea sistemului 17


Matrice de acces

SO: Curs 13: Securitatea sistemului 18


Drepturi pe fișiere în Unix
• Matrice de acces
• Domeniile sunt
– utilizator (user) - deținătorul fișierului
– grup (group) – grupul deținător al fișierului
– alții (others)
• Drepturi
– read (r) – citire, listare
– write (w) – scriere, creare fișier
– execute (x) – execuție, parcurgere

SO: Curs 13: Securitatea sistemului 19


Liste de acces

SO: Curs 13: Securitatea sistemului 20


Liste de acces (2)
• POSIX ACL
– implementate pe sisteme de fișiere Linux cu
extended attributes
– getfacl, setfacl
• Drepturi pe fișiere în Windows
– ACL pe NTFS
– read, write, list, read and execute, modify, full
control

SO: Curs 13: Securitatea sistemului 21


Capabilități
• O cheie asociată unor acțiuni privilegiate sau unor drepturi
de acces
• Un mod de delegare de permisiuni din partea utilizatorului
root către un utilizator neprivilegiat
• Pot fi interschimbate între entități
– nu este un lucru obișnuit în sistemele de operare actuale
• Capabilități POSIX (IEEE 1003.1e)
– CAP_NET_BIND_SERVICE
– CAP_SYS_CHROOT
– CAP_NET_RAW
• man 7 capabilities
– nu se poate accesa un director/fișier din afara ierarhiei impuse
de noul director rădăcină

SO: Curs 13: Securitatea sistemului 22


Escaladarea privilegiilor
• permiterea obținerii de permisiuni suplimentare
pentru acțiuni privilegiate
– sudo
– setuid
• de obicei sunt atacate conturile privilegiate
• sunt exploatate programele care rulează ca root -
atenție specială executabilelor cu bitul setuid activat
– bug în aplicații - obținerea unor drepturi necuvenite

SO: Curs 13: Securitatea sistemului 23


Bitul setuid
• set user-ID on execution bit
• Real user ID
• Effective user ID
• Bitul setuid (chmod 4777)
– permite configurarea euid ca utilizatorul ce deține
executabilul
• setuid
– total privilege revocation (real user ID, effective user ID)
• seteuid
– temporary privilege revocation (effective user ID)

SO: Curs 13: Securitatea sistemului 24


main() în ping.c
int
main(int argc, char **argv)
{
struct hostent *hp;
int ch, hold, packlen;
int socket_errno;
u_char *packet;
char *target, hnamebuf[MAXHOSTNAMELEN];
char rspace[3 + 4 * NROUTES + 1]; /* record route space */

icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);


socket_errno = errno;

uid = getuid();
if (setuid(uid)) {
perror("ping: setuid");
exit(-1);
}
[...]

SO: Curs 13: Securitatea sistemului 25


CEL MAI MIC PRIVILEGIU

SO: Curs 13: Securitatea sistemului 26


Cel mai mic privilegiu
• Un proces nu are acces la mai multe privilegii
sau resurse decât are nevoie
• Rulează ca utilizator cu permisiuni reduse
• Rulează într-un mediu izolat (jail, sandbox), cu
acces doar la anumite resurse
• Rulează cu limitări: poate folosi resurse doar
până într-o limită

SO: Curs 13: Securitatea sistemului 27


Permisiuni reduse
• Nu rulează ca root
• Utilizatorul care rulează nu are permisiuni de citire
sau scriere pe fișiere de care nu are nevoie
• Procesul nu are capabilități de care nu are nevoie
• Procesului nu i se permite escaladarea de privilegii
• Dacă are nevoie de permisiuni de citire, nu se dau
permisiuni de scriere
• Pentru permisiuni distincte se creează o altă entitate
(separare de privilegii)

SO: Curs 13: Securitatea sistemului 28


Mediu izolat
• Acces strict la resursele necesare
• Poate fi vorba de sistemul de fișiere sau de operații
posibile: pe sockeți, apeluri de sistem, IPC-uri
• Jailing sau sandboxing
• În general avut în vedere pe dispozitivele mobile;
aplicațiile rulează în câte un sandbox
• În cazul unui atac pagubele sunt limitate la nivelul
sandbox-ului
• chroot, containere, virtualizare

SO: Curs 13: Securitatea sistemului 29


chroot
• Modifică directorul rădăcină asociat
procesului.
– nu se poate accesa un director/fișier din afara
ierarhiei impuse de noul director rădăcină
– chroot jail
• Comanda chroot
• Apelul chroot
chroot(“/var/spool/postfix”);

SO: Curs 13: Securitatea sistemului 30


Containere
• Namespace dedicat de procese și alte resurse
• chroot++ (nu doar la nivel de sistem de fișiere)
• Procesele pot să se vadă doar între ele într-un
container
• Se partiționează accesul la anumite resurse
• OpenVZ, LXC, docker

SO: Curs 13: Securitatea sistemului 31


Virtualizare
• Mai multe avantaje pentru folosirea
virtualizării
– Un avantaj este izolarea
• Un întreg sistem de operare este izolat
• Orice probleme (bug-uri, vulnerabilități,
performanță) sunt limitate la nivelul mașinii
virtuale
• VMware, VirtualBox, KVM, Xen, Hyper-V

SO: Curs 13: Securitatea sistemului 32


Limitări
• Resursele să fie accesate până într-un prag
• Throttling
– nu se poate trece de o viteză (un prag instantaneu de
folosire): maxim 20% din procesor, maxim 512Kbps, maxim
10 procese pornite simultan
• Capping
– nu se poate trece peste o limită totală de folosire a unei
resurse: maxim 3GB de spațiu pe disc ocupat, maxim 2 ore
timp pe procesor
• Previne atacuri de tipul denial of service

SO: Curs 13: Securitatea sistemului 33


getrlimit/setrlimit
• Apelurile folosite pentru limitarea resurselor în Unix
• În general o limită soft și o limită hard
– Limita soft este cea folosită
– Limita soft poate fi schimbată până în limita hard
– Limita hard este impusă de sistem
• Exemple: dimensiune maximă spațiu de adresă,
număr maxim de procese, număr maxim de fișiere
deschise, dimensiunea maximă a unui fișier
• Folosit de /etc/security/limits.conf și comanda ulimit
• Apelul getrusage pentru contabilizarea folosirii
resursei SO: Curs 13: Securitatea sistemului 34
Cote
• limitări la nivelul sistemului de fișiere
– număr maxim de fișiere care să fie create de un utilizator
– dimensiunea maximă a spațiului ocupat de fișierele unui
anumit utilizator
• în Linux 4 valori de configurat la nivel de
utilizator/grup
– limitarea numărului de fișiere/inode-uri (soft/hard)
– limitarea spațiului ocupat la nivel de blocuri (soft/hard)
– soft: limită soft, se trimite un warning
– hard: limită hard, se interzice trecerea peste
• necesită suportul sistemului de fișiere
SO: Curs 13: Securitatea sistemului 35
IDENTITATEA UTILIZATORILOR

SO: Curs 13: Securitatea sistemului 36


Autentificarea utilizatorilor
• Accesul utilizatorilor în sistem
• Parolă
• Cheie publică
• Voice recognition, identificatori biometrici

SO: Curs 13: Securitatea sistemului 37


/etc/passwd + /etc/shadow
• /etc/passwd
– user:password_hash:uid:gid:...
– problemă
• accesul utilizatorilor (nevoie de informații diferite de
password_hash)
• /etc/shadow
– user:password_hash:...
– security enforcing
• număr de zile între schimbat parola
• număr de zile după care contul este dezactivat
• ....

SO: Curs 13: Securitatea sistemului 38


Password hash în Unix
• man 3 crypt
• Implicit DES
• $id$salt$encrypted
• ID: 1 (MD5), 2a (Blowfish), 5 (SHA-256), 6
(SHA-512)
• salt este folosit pentru a adăuga un nivel
suplimentar de criptare a parolei
– un salt pe 12 biți înseamnă 4096 de posibilități de
criptare

SO: Curs 13: Securitatea sistemului 39


Problema parolelor
• Pot fi ghicite, uitate, slabe
• 6.46 million LinkedIn passwords leaked online (June 2012)
– http://www.zdnet.com/article/6-46-million-linkedin-passwords-
leaked-online/
• $12,000 computer (Project Erebus v2.5), 8 AMD Radeon
HD7970 GPU cards (August 2012)
– are nevoie de 12 ore pentru a folosi brute force pe întreg spațiul de
parole de 8 caractere printabile
– http://arstechnica.com/security/2012/08/passwords-under-assault/

SO: Curs 13: Securitatea sistemului 40


Forme de enforcing pentru parole
• Anumite tipuri de caractere
• Expirarea parolei după un interval
• Passphrase în loc de parole
• Folosirea altor forme de autentificare (chei
publice)
• One Time Password (OTP)

SO: Curs 13: Securitatea sistemului 41


Autentificarea prin chei publice
• Cheie publică + cheie privată
• Cheia publică este pe sistem (server)
• Cheia privată este folosită pentru autentificare
• Legătură matematică
– one way function
– de fapt este two-way, insa nu este computațional
fezabil să se calculeze inversul funcției (încă..)
• RSA, DSA
SO: Curs 13: Securitatea sistemului 42
OTP
• One Time Password
• Time-synchronized OTP
– RSA SecurID
• Algoritm matematic
– s – initial seed
– f – one-way function
• cryptographic hash function
– it is easy to compute the hash value for any given message,
– it is infeasible to find a message that has a given hash,
– it is infeasible to modify a message without changing its hash,
– it is infeasible to find two different messages with the same hash.
– f(f(f(f(...f(s)...))), ..., f(s)

SO: Curs 13: Securitatea sistemului 43


Cuvinte cheie
• sistem sigur • permisiuni
• bug • cel mai mic privilegiu
• vulnerabilitate • sandboxing
• atac • chroot
• separarea privilegiilor • containere
• escaladarea privilegiilor • virtualizare
• kernel mode • limitări
• root • setrlimit/getrlimit
• setuid • ulimit
• utilizatori • Cote
• DAC • /etc/passwd, /etc/shadow
• MAC • password hash
• liste de acces • OTP
SO: Curs 13: Securitatea sistemului 44
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Laborator 01 ­ Introducere

Scop

introducerea în tematica laboratorului
familiarizarea cu mediul și uneltele folosite în cadrul laboratorului

Cuvinte cheie
programare de sistem, C, compilare, depanare, biblioteci
gcc, make, gdb
cl, nmake, Visual Studio

Materiale ajutătoare

lab01­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab01­slides.pdf]
lab01­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab01­refcard.pdf]
Visual Studio Tutorials [http://elf.cs.pub.ro/so/res/tutorial/asist­visual­studio/]
Video Introducere [http://elf.cs.pub.ro/so/res/tutorial/lab­01­introducere/]

Nice to read

TLPI ­ Chapter 3, System Programming Concepts
WSP4 ­ Chapter 1, Getting started with Windows

Desfășurarea laboratorului
Laboratorul de Sisteme de Operare este unul de programare de sistem
[http://en.wikipedia.org/wiki/System_programming] având drept scop aprofundarea conceptelor prezentate la
curs și prezentarea interfețelor de programare oferite de sistemele de operare (system API). Un
laborator va prezenta un anumit set de concepte și va conține următoarele activități:

prezentare teoretică
parcurgerea exercițiilor rezolvate
rezolvarea exercițiilor propuse

Pentru o desfășurare cât mai bună a laboratorului și o înțelegere deplină a conceptelor vă
recomandăm să parcurgeți conținutul laboratorului de acasă. De asemenea, pentru consolidarea
cunoștințelor folosiți suportul de laborator prezentat în paragraful următor.

Suport de laborator

adăugați ca bookmark secțiunea Resurse [https://elf.cs.pub.ro/so/res/doc/]
Linux
The Linux Programming Interface [http://nostarch.com/tlpi] ­ TLPI
Windows
Windows System Programming 4th Edition [http://www.amazon.com/Windows­Programming­
Addison­Wesley­Microsoft­Technology/dp/0321657748]  ­ WSP4
General

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 1/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

lista de discuții [http://cursuri.cs.pub.ro/cgi­bin/mailman/listinfo/so]
canalul de IRC dedicat cursului #cs_so, de pe serverul freenode
[http://webchat.freenode.net/].

Prezentare
Pentru a oferi o arie de cuprindere cât mai largă, laboratoarele au ca suport familiile de sisteme de
operare Unix și Windows. Instanțele de sisteme de operare din familiile de mai sus alese pentru acest
laborator sunt GNU/Linux, respectiv Windows 7.

În cadrul acestui laborator introductiv va fi prezentat mediul de lucru care va fi folosit în cadrul
laboratorului de Sisteme de Operare cât și în rezolvarea temelor de casă.

Laboratorul folosește ca suport de programare limbajul C. Pentru GNU/Linux se va folosi suita de
compilatoare GCC, iar pentru Windows compilatorul Microsoft pentru C/C++ cl. De asemenea, pentru
compilarea incrementală a surselor se vor folosi GNU make (Linux), respectiv nmake (Windows).
Exceptând apelurile de bibliotecă standard, API­ul folosit va fi POSIX
[http://www.opengroup.org/onlinepubs/9699919799/], respectiv Win32 [http://msdn.microsoft.com/en­
us/library/aa163326.aspx].

Linux

GCC
GCC este suita de compilatoare implicită pe majoritatea distribuțiilor Linux. Pentru mai multe detalii
despre proiectul GCC apăsați pe butonul Click to display (de acum înainte secțiunile suplimentare
vor fi ascunse folosind astfel de butoane).

GCC este unul dintre primele pachete software dezvoltate de organizația „Free Software Fundation” în
cadrul proiectului GNU (Gnu's Not Unix). Proiectul GNU a fost inițiat de Richard Stallman ca un protest
împotriva software­ului proprietar la începutul anilor '80.

La început, GCC se traducea prin “GNU C Compiler”, pentru că inițial scopul proiectului GCC era
dezvoltarea unui compilator C portabil pe platforme UNIX. Ulterior, proiectul a evoluat astăzi fiind un
compilator multi­frontend, multi­backend cu suport pentru limbajele C, C++, Objective‐C, Fortran,
Java, Ada. Drept urmare, acronimul GCC înseamnă, astăzi, “GNU Compiler Collection”.
La numărul impresionant de limbaje de mai sus se adaugă și numărul mare de platforme suportate
atât din punctul de vedere al arhitecturii hardware (i386, alpha, vax, m68k, sparc, HPPA, arm, MIPS,
PowerPC, etc.), cât și al sistemelor de operare (GNU/Linux, DOS, Windows 9x/NT/2000, *BSD, Solaris,
Tru64, VMS, etc.). La ora actuală, GCC­ul este compilatorul cel mai portat.

În cadrul laboratoarelor de Sisteme de Operare ne vom concentra asupra facilităților oferite de
compilator pentru limbajul C. GCC are suport pentru standardele ANSI, ISO C, ISO C99, ISO C11,
POSIX, dar și multe extensii folositoare care nu sunt incluse în niciunul din standarde; unele dintre
aceste extensii vor fi prezentate în secțiunile ce urmează.

Utilizare GCC
Vom folosi pentru exemplificare un program simplu care tipărește la ieșirea standard un șir de
caractere.

hello.c

#include <stdio.h> 
  
int main(void)  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 2/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]
int main(void)  

      printf("SO, ... hello world!\n"); 
  
      return 0; 
}

GCC folosește pentru compilarea de programe C comanda gcc. O invocare tipică este pentru
compilarea unui program dintr­un singur fișier sursă, în cazul nostru hello.c.

so@spook$ ls  so@spook$ ls 
hello.c  hello.c 
so@spook$ gcc hello.c                                   so@spook$ gcc hello.c ‐o hello                           
so@spook$ ls  so@spook$ ls                       
a.out  hello.c  hello  hello.c 
so@spook$ ./a.out  so@spook$ ./hello 
SO, ... hello world! SO, ... hello world!

Așadar, comanda gcc hello.c a fost folosită pentru compilarea fișierului sursă hello.c. Rezultatul a
fost obținerea fișierului executabil a.out (nume implicit utilizat de gcc). Dacă se dorește obținerea
unui executabil cu un alt nume se poate folosi opțiunea ‐o.

Fazele compilării
Compilarea se referă la obținerea unui fișier executabil dintr­un fișier sursă. După cum am văzut în
paragraful anterior comanda gcc a dus la obținerea fişierului executabil hello din fişierul sursă
hello.c. Intern, gcc trece prin mai multe faze de prelucrare a fişierului sursă până la obținerea
executabilului. Aceste faze sunt evidențiate în diagrama de mai jos:

Opţiuni

Implicit, la o invocare a comenzii gcc se obţine din fişierul sursă un executabil. Folosind diverse
opțiuni, putem opri compilarea la una din fazele intermediare astfel:

‐E ­ se realizează doar preprocesarea fişierului sursă
gcc ‐E hello.c – va genera fişierul preprocesat pe care, implicit, îl va afişa la ieşirea
standard.
‐S ­ se realizează inclusiv faza de compilare
gcc ‐S hello.c – va genera fişierul în limbaj de asamblare hello.s
‐c ­ se realizează inclusiv faza de asamblare
gcc ‐c hello.c – va genera fişierul obiect hello.o

Opţiunile de mai sus pot fi combinate cu ‐o pentru a specifica fişierul de ieşire.

Preprocesarea
Preprocesarea presupune înlocuirea directivelor de preprocesare din fişierul sursă C. Directivele de
preprocesare încep cu #. Printre cele mai folosite sunt:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 3/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

#include – pentru includerea fişierelor header într­un alt fișier.
#define și #undef – pentru definirea, respectiv anularea definirii de macrouri.
#if, #ifdef, #ifndef, #else, #elif, #endif, pentru compilarea condiţionată.
utile pentru comentarea bucăților mari de cod. Pentru a comenta toată funcția
do_evil_things de mai jos nu putem folosi comentarii de tip C, ca în exemplul din
dreapta, întrucat limbajul C nu permite comentariile imbricate. În astfel de cazuri se
poate folosi directiva #if <condiţie> ca în exemplul din stânga.

#if 0   /* 
int do_evil_things(context_t *ctx)  int do_evil_things(context_t *ctx)  
{  { 
         int go_drink;           int go_drink; 
     
         /* set student mode ON :) */           /* set student mode ON :) */ 
     
         ctx‐>go_drink = NO;           ctx‐>go_drink = NO;     
}  } 
#endif  */

utile pentru evitarea includerii de mai multe ori a unui fişier header, tehnică numită include
guard [http://en.wikipedia.org/wiki/Include_guard].

În exemplul de mai jos, dacă fişierul <string.h> este inclus, simbolul _STRING_H este deja definit
de la prima includere, iar a doua operaţie de includere nu va avea niciun efect.

#ifndef _STRING_H 
#define _STRING_H       1 
  
__BEGIN_DECLS 
  
/* Get size_t and NULL from <stddef.h>.  */ 
#define __need_size_t 
#define __need_NULL 
  
/* 
  * string related defines  
  */ 
  
#endif /* string.h  */

__FILE__, __LINE__, __func__ sunt înlocuite cu numele fişierului, linia curentă în fișier şi
numele funcției
operatorul # este folosit pentru a înlocui o variabilă transmisă unui macro cu numele acesteia.

#include <stdio.h>    
   so@spook$ gcc ‐o show show.c 
#define show_var(a) printf("Variable %s has value %d\n", #a, a)  so@spook$ ls 
   show  show.c 
int main(void)  so@spook$ ./show 
{  Variable teh_var has value 42  
        int teh_var = 42; 
        show_var(teh_var); 
        return 0; 
}

operatorul ## (token paste) este folosit pentru concatenarea între un argument al
macrodefiniţiei și un alt şir de caractere sau între două argumente ale macrodefiniţiei.

Depanarea folosind directive de preprocesare

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 4/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

De multe ori, un dezvoltator va dori să poată activa sau dezactiva foarte facil afişarea de mesaje
suplimentare (de informare sau de debug) în sursele sale.

Metoda cea mai simplă pentru a realiza acest lucru este prin intermediul unui macro:

#define DEBUG      1 
  
#ifdef DEBUG 
 /* afisare mesaje debug */ 
#endif

Folosirea perechii de directive #ifdef, #endif prezintă dezavantajul încărcării codului. Se poate
încerca modularizarea afişării mesajelor de debug printr­o construcţie de forma:

#ifdef DEBUG 
#define Dprintf(msg)  printf(msg) 
#else 
#define Dprintf(msg)                /* do nothing */ 
#endif

Definiţia aceasta nu permite apelul lui Dprintf cu mai multe argumente şi, implicit, nici afişarea
formatată. O soluţie este dată de implementarea prin intermediul macro­urilor cu număr variabil de
parametri sau variadic macros [http://www.delorie.com/gnu/docs/gcc/cpp_19.html]:

#ifdef DEBUG 
#define Dprintf(msg,...)  printf(msg, __VA_ARGS__) 
#else 
#define Dprintf(msg,...)                /* do nothing */ 
#endif

Singura problemă care poate apărea este folosirea Dprintf exact cu un argument. În acest caz
macroul se expandează la Dprintf(msg,) – expresie nevalidă în C (din cauza virgulei de la sfârșit).
Pentru a elimina acest incovenient se folosește operatorul ##. Dacă acesta este folosit peste un
argument care nu există, atunci virgula se elimină şi expresia devine corectă. Acest lucru nu se
întâmplă în cazul în care argumentul există (altfel spus operatorul ## nu schimbă sensul de până
atunci):

#ifdef DEBUG 
#define Dprintf(msg,...)  printf(msg, ##__VA_ARGS__) 
#else 
#define Dprintf(msg,...)               /* do nothing */ 
#endif

Un ultim retuş este afişarea, dacă se doreşte, a fişierului şi liniei unde s­a apelat macroul:

#ifdef DEBUG 
#define Dprintf(msg,...) printf("[%s]:%d" msg, __FILE__, __LINE__, ##__VA_ARGS__) 
#else 
#define Dprintf(msg,...)                /* do nothing */ 
#endif

Compilarea

Compilarea este faza în care din fişierul preprocesat se obţine un fişier în limbaj de asamblare.

so@spook$ ls 
hello.c 
so@spook$ gcc ‐S hello.c  
so@spook$ ls 
hello.c  hello.s

În exemplul de mai jos sunt prezentate, în stânga, fişierul sursă hello.c, iar în dreapta fişierul în
limbaj de asamblare corespunzător hello.s.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 5/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

#include <stdio.h>      .file  "hello.c" 
       .section        .rodata 
int main(void)  .LC0: 
{      .string "SO, ... hello world!" 
  printf("SO, ... hello world!\n");      .text 
   .globl main 
  return 0;        .type  main, @function
} main:    
    pushl  %ebp 
    movl   %esp, %ebp 
    andl   $‐16, %esp 
    subl   $16, %esp 
    movl   $.LC0, (%esp) 
    call   puts 
    movl   $0, %eax
    leave 
    ret 
    .size  main, .‐main 
    .ident "GCC: (Ubuntu 4.4.1‐4ubuntu9) 4.4.1" 
    .section      .note.GNU‐stack,"",@progbits

Asamblarea
Asamblarea este faza în care codul scris în limbaj de asamblare este tradus în cod mașină
reprezentând codificarea binară a instrucțiunilor programului iniţial. Fişierul obţinut poartă numele de
fişier cod obiect, se obţine folosind opţiunea ‐c a compilatorului şi are extensia .o.

so@spook$ ls 
hello.c 
so@spook$ gcc ‐c hello.c  
so@spook$ ls 
hello.c  hello.o

Editarea de legături

Pentru obținerea unui fişier executabil este necesară rezolvarea diverselor simboluri prezente în fişierul
obiect. Această operaţie poartă denumirea de editare de legături, link­editare, linking sau legare.

 void f(void);   void f(void); 
     
/*   void f(void) 
  * no definition for f here   { 
  */   } 
     
 int main(void)    
 {   int main(void) 
        f();         { 
        return 0;           f(); 
 }          return 0; 
 }

so@spook$ ls  so@spook$ ls 
sample.c  sample.c 
so@spook$ gcc ‐c ‐o sample.o sample.c  so@spook$ gcc ‐c ‐o sample.o sample.c 
so@spook$ ls  so@spook$ ls 
sample.c sample.o  sample.c sample.o 
so@spook$ gcc ‐o sample sample.o  so@spook$ gcc ‐o sample sample.o 
sample.o: In function `main':  so@spook$ ls 
sample.c:(.text+0x5): undefined reference to `f'  sample sample.c  sample.o
collect2: error: ld returned 1 exit status

Observăm că în partea stângă deși am obținut fișierul obiect sample.o, linkerul nu poate genera
fişierul executabil întrucât nu găseşte definiţia funcţiei f. În partea dreaptă totul decurge normal,
definiţia funcţiei f fiind inclusă în fişierul sursă.

Activarea avertismentelor
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 6/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

În mod implicit, o rulare a gcc oferă puține avertismente utilizatorului. Pentru a activa afișarea de
avertismente se folosesc opțiunile de tip ‐W cu sintaxa ‐Woptiune‐avertisment. optiune‐
avertisment poate lua mai multe valori posibile printre care return‐type, switch, unused‐
variable, uninitialized, implicit, all. Folosirea opțiunii ‐Wall înseamnă afișarea tuturor
avertismentelor care pot cauza inconsistențe la rulare.

Considerăm ca fiind indispensabilă folosirea opțiunii ‐Wall pentru a putea detecta încă din momentul
compilării posibilele erori. O cauză importantă a aparițiilor acestor erori o constituie sintaxa foarte
permisivă a limbajului C. Sperăm ca exemplul de mai jos să justifice utilitatea folosirii opțiunii ‐Wall:

middle.c so@spook$ ls 
middle.c 
#include <stdio.h>  so@spook$ gcc ‐o middle middle.c 
   so@spook$ ./middle  
int main(void)  Middle of interval [10, 20] is 10

   int min = 10, max = 20, midpoint; 
so@spook$ gcc ‐Wall ‐o middle middle.c 
  
middle.c: In function ‘main’: 
   /* midpoint = min+(max‐min)/2; */ 
middle.c:8: warning: suggest parentheses around ‘+’ inside ‘>>’
   midpoint = min + (max ‐ min) >> 1; 
  
   printf("The middle of interval \ 
           [%d, %d] is %d\n",     \ 
           min, max, midpoint); 
  
   return 0; 
}

La prima rulare, rezultatul nu e nici pe departe cel așteptat. Eroarea poate fi detectată ușor dacă
includem și opțiunea ‐Wall la compilare. (operatorul + are prioritate în fața operatorului >>)

Opțiuni utile

‐Lcale – instruiește compilatorul să caute și în directorul cale bibliotecile pe care le folosește
programul; opțiunea se poate specifica de mai multe ori, pentru a adãuga mai multe directoare
‐lbiblioteca – instruiește compilatorul cã programul are nevoie de biblioteca biblioteca.
Fișierul ce conține biblioteca trebuie să se numească libbiblioteca.so sau libbiblioteca.a.
‐Icale – instruiește compilatorul sã caute fișierele antet (headere) și în directorul cale;
opțiunea se poate specifica de mai multe ori, pentru a adãuga mai multe directoare
‐Onivel‐optimizări, instuiește compilatorul ce nivel de optimizare trebuie aplicat:
‐O0, va determina compilatorul sã nu optimizeze codul generat;
‐O3, va determina compilatorul sã optimizeze la maxim codul generat;
‐O2, este pragul de unde compilatorul va începe sã insereze direct în cod functiile inline
în loc sã le apeleze;
‐Os, va pune accentul pe optimizările care duc la reducerea dimensiunii codului generat,
și nu a vitezei la execuție.
‐g, dacã se folosește această opțiune compilatorul va genera în fișierele de ieșire informații
care pot fi apoi folosite de un debugger (informații despre fișierele sursã și o mapare între
codul mașinã și liniile de cod ale fișierelor sursã)

Paginile de ajutor ale GCC [http://linux.die.net/man/1/gcc] (man gcc, info gcc) oferă o listă cu toate
opțiunile posibile ale GCC.

Compilarea din mai multe fișiere

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 7/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Exemplele de până acum tratează programe scrise într­un singur fișier sursă. În realitate, aplicațiile
sunt complexe și scrierea întregului cod într­un singur fișier îl face greu de menținut și greu de extins.
În acest sens aplicația este scrisă în mai multe fișiere sursă denumite module. Un modul conține, în
mod obișnuit, funcții care îndeplinesc un rol comun.

Următoarele fișiere sunt folosite ca suport pentru a exemplifica modul de compilare a unui program
provenind din mai multe fișiere sursă:

main.c util.h

#include <stdio.h>  #ifndef UTIL_H 
#include "util.h"  #define UTIL_H   1 
     
int main(void)  void f1 (void); 
{  void f2 (void); 
   f1();    
   f2();    
   return 0;  #endif
}

f1.c f2.c

#include <stdio.h>  #include <stdio.h> 
#include "util.h"  #include "util.h" 
     
void f1(void)  void f2(void) 
{  { 
   printf("Current file name is %s\n", __FILE__);     printf("Current line %d in file %s\n", 
              __LINE__, __FILE__); 
} }

În programul de mai sus se apelează funcțiile f1 și f2 în funcția main pentru a afișa diverse
informații. Pentru compilarea acestora se transmit toate fișierele C ca argumente către gcc:

so@spook$ ls 
f1.c  f2.c  main.c  util.h 
so@spook$ gcc ‐Wall main.c f1.c f2.c ‐o main 
so@spook$ ls 
f1.c  f2.c  main  main.c  util.h 
so@spook$ ./main  
Current file name f1.c 
Current line 8 in file f2.c

Executabilul a fost denumit main; pentru acest lucru s­a folosit opțiunea  ‐o.

Se observă folosirea fișierului header util.h pentru declararea funcțiilor f1 și f2. Declararea unei
funcții se realizează prin precizarea antetului. Fișierul header este inclus în fișierul main.c pentru ca
acesta să aibă cunoștință de formatul de apel al funcțiilor f1 și f2. Funcțiile f1 și f2 sunt definite,
respectiv, în fișierele f1.c și f2.c. Codul acestora este integrat în executabil în momentul link­editării.

În general, pentru obținerea unui executabil din surse multiple se obișnuiește compilarea fiecărei surse
până la modul obiect și apoi link­editarea acestora:

so@spook$ ls 
f1.c  f2.c  main.c  util.h 
so@spook$ gcc ‐Wall ‐c  f1.c 
so@spook$ gcc ‐Wall ‐c  f2.c 
so@spook$ gcc ‐Wall ‐c  main.c 
so@spook$ ls 
f1.c  f1.o  f2.c  f2.o  main.c  main.o  util.h 
so@spook$ gcc ‐o main main.o f1.o f2.o 
so@spook$ ls 
f1.c  f1.o  f2.c  f2.o  main  main.c  main.o  util.h 
so@spook$ ./main  
Current file name f1.c 
Current line 8 in file f2.c

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 8/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Se observă obținerea executabilului main prin legarea modulelor obiect. Această abordare are
avantajul eficienței. Dacă se modifică fișierul sursă f2.c atunci doar acesta va trebui compilat și
refăcută link­editarea. Dacă s­ar fi obținut un executabil direct din surse atunci s­ar fi compilat toate
cele trei fișiere și apoi refăcută link­editarea. Timpul consumat ar fi mult mai mare
[http://xkcd.com/303/], în special în perioada de dezvoltare când fazele de compilare sunt dese și se
dorește compilarea doar a fișierelor sursă modificate.

Scăderea timpului de dezvoltare prin compilarea numai a surselor care au fost modificate este
motivația de bază pentru existența utilitarelor de automatizare precum make sau nmake.

Biblioteci în Linux
O bibliotecă este o colecție de funcții precompilate. În momentul în care un program are nevoie de o
funcție, linker­ul va apela respectiva funcție din bibliotecă. Numele fișierului reprezentând biblioteca
trebuie să aibă prefixul lib:

so@spook$ ls ‐l /usr/lib/libm.* 
‐rw‐r‐‐r‐‐ 1 root root 496218 2010‐01‐03 15:19 /usr/lib/libm.a 
lrwxrwxrwx 1 root root     14 2010‐01‐14 12:17 /usr/lib/libm.so ‐> /lib/libm.so.6

Biblioteca matematică este denumită libm.a sau libm.so. În Linux bibliotecile sunt de două tipuri:

statice ­ au, de obicei, extensia .a
partajate ­ au extensia .so

Legarea se face folosind opțiunea ‐l transmisă comenzii gcc. Astfel, dacă se dorește folosirea unor
funcții din math.h, trebuie legată biblioteca matematică:

cbrt.c   
so@spook$ ls 
#include <stdio.h>  cbrt.c 
#include <math.h>  so@spook$ gcc ‐Wall ‐o cbrt cbrt.c 
   /tmp/ccwvm1zq.o: In function `main': 
int main(void)  cbrt.c:(.text+0x1b): undefined reference to `cbrt' 
{  collect2: ld returned 1 exit status 
   double x = 1000.0;  so@spook$ gcc ‐Wall ‐o cbrt cbrt.c ‐lm 
   printf("Cubic root for %g is %g\n", x, cbrt(x));  so@spook$ ./cbrt  
   return 0;  Cubic root for 1000 is 10
}

Se observă că, în primă fază, nu s­a rezolvat simbolul  cbrt. După legarea bibliotecii matematice,
programul s­a compilat și a rulat fără probleme.

Crearea unei biblioteci statice
Pentru crearea de biblioteci vom folosi fișierele din secțiunea Compilarea din mai multe fișiere. Vom
include modulele obiect rezultate din fișierele sursă f1.c și f2.c într­o bibliotecă pe care o vom folosi
ulterior pentru obținerea executabilului final.

Primul pas constă în obținerea modulelor obiect asociate:

so@spook$ gcc ‐Wall ‐c f1.c 
so@spook$ gcc ‐Wall ‐c f2.c

O bibliotecă statică este o arhivă ce conține fișiere obiect creată cu ajutorul utilitarului ar
[http://linux.die.net/man/1/ar] ( interpretați parametrii rc).

so@spook$ ar rc libintro.a f1.o f2.o  so@spook$ gcc ‐Wall main.c ‐o main ‐lintro ‐L. 
so@spook$ gcc ‐Wall main.c ‐o main ‐lintro  so@spook$ ./main 
/usr/bin/ld: cannot find ‐lintro  Current file name is f1.c 
collect2: ld returned 1 exit status Current line 5 in file f2.c

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 9/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Atenție: ­lintro trebuie să apară după specificarea sursei

Linker­ul returnează eroare precizând că nu găsește biblioteca libintro. Aceasta deoarece linker­ul
nu a fost configurat să caute și în directorul curent. Pentru aceasta se folosește opțiunea ‐L, urmată de
directorul în care trebuie căutată biblioteca (în cazul nostru este vorba de directorul curent).

Dacă biblioteca se numește libnume.a, atunci ea va fi referită cu ‐lnume

Crearea unei biblioteci partajate
Spre deosebire de o bibliotecă statică despre care am văzut că nu este nimic altceva decât o arhivă de
fișiere obiect, o bibliotecă partajată este ea însăși un fișier obiect. Crearea unei biblioteci partajate se
realizează prin intermediul linker­ului. Optiunea ‐shared indică compilatorului să creeze un obiect
partajat și nu un fișier executabil. Este, de asemenea, indicată folosirea opțiunii ‐fPIC la crearea
fișierelor obiect.

so@spook$ gcc ‐fPIC ‐c f1.c 
so@spook$ gcc ‐fPIC ‐c f2.c 
so@spook$ gcc ‐shared f1.o f2.o ‐o libintro_shared.so 
so@spook$ gcc ‐Wall main.c ‐o main ‐lintro_shared ‐L. 
so@spook$ ./main 
./main: error while loading shared libraries: libintro_shared.so: 
     cannot open shared object  file: No such file or directory

La rularea executabilului se poate observa că nu se poate încărca biblioteca partajată. Cauza este
deosebirea dintre bibliotecile statice și bibliotecile partajate. În cazul bibliotecilor statice codul funcției
de bibliotecă este copiat în codul executabil la link­editare. De partea cealaltă, în cazul bibliotecilor
partajate, codul este încărcat în memorie în momentul rulării.

Astfel, în momentul rulării unui program, loader­ul (programul responsabil cu încărcarea programului în
memorie), trebuie să știe unde să caute biblioteca partajată pentru a o încărca în memorie în cazul în
care aceasta nu a fost încărcată deja. Loader­ul folosește câteva căi predefinite (/lib, /usr/lib etc) și de
asemenea locații definite în variabila de mediu LD_LIBRARY_PATH:

so@spook$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. 
so@spook$ ./main 
Current file name is f1.c 
Current line 5 in file f2.c

În exemplul de mai sus variabilei de mediu LD_LIBRARY_PATH i­a fost adăugată calea către directorul
curent rezultând în posibilitatea rulării programului. LD_LIBRARY_PATH va rămâne modificată cât timp
va rula consola curentă. Pentru a face o modificare a unei variabile de mediu doar pentru o instanță a
unui program se face atribuirea noii valori înaintea comenzii de execuție:

so@spook$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.  ./main 
Fisierul curent este f1.c 
Va aflati la linia 5 din fisierul f2.c 
so@spook$ ./main 
./main: error while loading shared libraries: libintro_shared.so: 
     cannot open shared object  file: No such file or directory

GNU Make
Make este un utilitar care permite automatizarea și eficientizarea sarcinilor. În mod particular este
folosit pentru automatizarea compilării programelor. După cum s­a precizat, pentru obținerea unui
executabil provenind din mai multe surse este ineficientă compilarea de fiecare dată a fiecărui fișier și
apoi link­editarea. Se compilează fiecare fișier separat, iar la o modificare se va recompila doar fișierul
modificat.

Exemplu simplu de Makefile

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 10/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Utilitarul make [http://linux.die.net/man/1/make] folosește un fișier de configurare denumit Makefile. Un
astfel de fișier conține reguli și comenzi de automatizare.

Makefile so@spook$ make  so@spook$ make clean 


gcc ‐Wall hello.c ‐o hello rm ‐f hello 
all:  so@spook$ ./hello  so@spook$ make all 
       gcc ‐Wall hello.c ‐o hello  SO, ... hello world! gcc ‐Wall hello.c ‐o hello
clean: 
       rm ‐f hello

Exemplul prezentat mai sus conține două reguli: all și clean. La rularea comenzii make se execută
prima regulă din Makefile (în cazul de față all, nu contează în mod special denumirea). Comanda
executată este gcc ‐Wall hello.c ‐o hello. Se poate preciza explicit ce regulă să se execute
prin transmiterea ca argument comenzii make. (comanda make clean pentru a șterge executabilul
hello și comanda make all pentru a obține din nou acel executabil).
În mod implicit, GNU Make caută, în ordine, fișierele GNUmakefile, Makefile, makefile și le analizează.
Pentru a preciza ce fișier Makefile trebuie analizat, se folosește opțiunea ‐f. Astfel, în exemplul de mai
jos, folosim fișierul Makefile.ex1:

so@spook$ mv Makefile Makefile.ex1 
so@spook$ make 
make: *** No targets specified and no makefile found.  Stop. 
so@spook$ make ‐f Makefile.ex1 
gcc ‐Wall hello.c ‐o hello 
so@spook$ make ‐f Makefile.ex1 clean 
rm ‐f hello

Sintaxa unei reguli

În continuare este prezentată sintaxa unei reguli dintr­un fișier Makefile:

target ­ este, de obicei, fișierul care se va obține prin rularea comenzii command. După cum s­
a observat și din exemplul anterior, poate să fie o țintă virtuală care nu are asociat un fișier.
prerequisites ­ reprezintă dependențele necesare pentru a urmări regula; de obicei sunt fișiere
necesare pentru obținerea țintei.
<tab> ­ reprezintă caracterul tab și trebuie neaparat folosit înaintea precizării comenzii.
command ­ o listă de comenzi (niciuna, una, oricâte) rulate în momentul în care se trece la
obținerea țintei.

Un exemplu indicat pentru un fișier Makefile este:

Makefile.ex2

all: hello 
  
hello: hello.o 
        gcc hello.o ‐o hello 
  

hello.o: hello.c 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 11/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]
hello.o: hello.c 
        gcc ‐Wall ‐c hello.c 
  
clean: 
        rm ‐f *.o *~ hello

Se observă prezența regulii all care va fi executată implicit.

all are ca dependență hello și nu execută nicio comandă;
hello are ca dependență hello.o și realizează link­editarea fișierului hello.o;
hello.o are ca dependență hello.c și realizează compilarea și asamblarea fișierului hello.c.

Pentru obținerea executabilului se folosește comanda:

so@spook$ make ‐f Makefile.ex2 
gcc ‐Wall ‐c hello.c 
gcc hello.o ‐o hello

Funcționarea unui fișier Makefile

Pentru obținerea unui target trebuie satisfăcute dependențele (prerequisites) acestuia. Astfel, pentru
obținerea targetului implicit (primul target), în cazul nostru all:

pentru obținerea target­ului all trebuie obținut target­ul hello, care este un nume de
executabil
pentru obținerea target­ului hello trebuie obținut target­ul hello.o
pentru obținerea target­ului hello.o trebuie obținut hello.c; acest fișier există deja, și cum
acesta nu apare la rândul lui ca target în Makefile, nu mai trebuie obținut
drept urmare se rulează comanda asociată obținerii hello.o; aceasta este gcc ‐Wall ‐c
hello.c
rularea comenzii duce la obținerea target­ului hello.o, care este folosit ca dependență pentru
hello
se rulează comanda gcc hello.o ‐o hello pentru obținerea executabilului hello
hello este folosit ca dependență pentru all; acesta nu are asociată nicio comandă deci este
automat obținut.

De remarcat este faptul că un target nu trebuie să aibă neapărat numele fișierului care se obține. Se
recomandă, însă, acest lucru pentru înțelegerea mai ușoară a fișierului Makefile, și pentru a beneficia
de faptul că make utilizează timpul de modificare al fișierelor pentru a decide când nu trebuie să facă
nimic.

Acest format al fișierului Makefile are avantajul eficientizării procesului de compilare. Astfel, după ce
s­a obținut executabilul hello conform fișierului  Makefile anterior, o nouă rulare a make nu va genera
nimic:

so@spook$ make ‐f Makefile.ex2 
make: Nothing to be done for 'all'.

Folosirea variabilelor

Un fișier Makefile permite folosirea de variabile. Astfel, un exemplu uzual de fișier Makefile este:

Makefile.ex3

CC = gcc 
CFLAGS = ‐Wall ‐g 
  
all: hello 
  
hello: hello.o 
        $(CC) $^ ‐o $@ 
  
hello.o: hello.c 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 12/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]
hello.o: hello.c 
        $(CC) $(CFLAGS) ‐c $< 
  
.PHONY: clean 
clean: 
        rm ‐f *.o *~ hello

În exemplul de mai sus au fost definite variabilele CC și CFLAGS. Variabila CC reprezintă compilatorul
folosit, iar variabila CFLAGS reprezintă opțiunile (flag­urile) de compilare utilizate; în cazul de față sunt
afișarea avertismentelor și compilarea cu suport de depanare. Referirea unei variabile se realizează
prin intermediul construcției $(VAR_NAME). Astfel, $(CC) se înlocuiește cu gcc, iar $(CFLAGS) se
înlocuiește cu ‐Wall ‐g.

Variabile predefinite folositoare sunt:

$@ se expandează la numele target­ului.
$^ se expandează la lista de cerințe.
$< se expandează la prima cerință.

Pentru mai multe detalii despre variabile consultați pagina info [1] sau manualul online [2] (sau folosiți
această pagină [https://www.gnu.org/software/make/manual/make.html#Automatic­Variables]).

Folosirea regulilor implicite
De foarte multe ori nu este nevoie să se precizeze comanda care trebuie rulată; aceasta poate fi
detectată implicit. Astfel, fișierul Makefile.ex2 de mai sus poate fi simplificat, folosind reguli implicite,
ca mai jos:

Makefile.ex4 Makefile.ex5

CC = gcc  CC = gcc 
CFLAGS = ‐Wall ‐g  CFLAGS = ‐Wall ‐g 
     
all: hello  all: hello 
     
hello: hello.o  hello: hello.o 
     
hello.o: hello.c    
     
.PHONY: clean  .PHONY: clean 
     
clean:  clean: 
       rm ‐f *.o *~ hello         rm ‐f *.o *~ hello

so@spook$ make ‐f Makefile.ex4  so@spook$ make ‐f Makefile.ex5 
gcc ‐Wall ‐g   ‐c ‐o hello.o hello.c  gcc ‐Wall ‐g   ‐c ‐o hello.o hello.c 
gcc  hello.o   ‐o hello gcc   hello.o   ‐o hello

De remarcat faptul că dacă avem un singur fișier sursă nici nu trebuie să existe un fișier Makefile
pentru a obține executabilul dorit.

so@spook$ls  
hello.c  
so@spook$ make hello 
cc hello.c ‐o hello

Pentru mai multe detalii despre reguli implicite consultați pagina info [3] sau manualul online [4].

Folosind toate facilitațile de până acum, ne propunem compilarea unui executabil client și a unui
executabil server.

Fișierele folosite sunt:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 13/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

executabilul server depinde de fișierele C ­ server.c, sock.c, cli_handler.c, log.c,
sock.h, cli_handler.h, log.h;
executabilul client depinde de fișierele C ­ client.c, sock.c, user.c, log.c, sock.h,
user.h, log.h;
Dorim, așadar, obținerea executabilelor client și server pentru rularea celor două entități. Structura
fișierului Makefile este prezentată mai jos:

Makefile.ex6

CC = gcc                        # compilatorul folosit 
CFLAGS = ‐Wall ‐g               # optiunile pentru compilare 
LDLIBS = ‐lefence               # optiunile pentru linking 
  
# creeaza executabilele client si server 
all: client server 
  
# leaga modulele client.o user.o sock.o in executabilul client 
client: client.o user.o sock.o log.o 
  
# leaga modulele server.o cli_handler.o sock.o in executabilul server 
server: server.o cli_handler.o sock.o log.o 
  
# compileaza fisierul client.c in modulul obiect client.o 
client.o: client.c sock.h user.h log.h 
  
# compileaza fisierul user.c in modulul obiect user.o 
user.o: user.c user.h 
  
# compileaza fisierul sock.c in modulul obiect sock.o 
sock.o: sock.c sock.h 
  
# compileaza fisierul server.c in modulul obiect server.o 
server.o: server.c cli_handler.h sock.h log.h 
  
# compileaza fisierul cli_handler.c in modulul obiect cli_handler.o
cli_handler.o: cli_handler.c cli_handler.h 
  
# compileaza fisierul log.c in modulul obiect log.o 
log.o: log.c log.h 
  
.PHONY: clean 
  
clean: 
        rm ‐fr *~ *.o server client

Pentru obținerea executabilelor server și client se folosește:

so@spook$ make ‐f Makefile.ex6 
gcc ‐Wall ‐g ‐c ‐o client.o client.c 
gcc ‐Wall ‐g ‐c ‐o user.o user.c 
gcc ‐Wall ‐g ‐c ‐o sock.o sock.c 
gcc ‐Wall ‐g ‐c ‐o log.o log.c 
gcc client.o user.o sock.o log.o ‐lefence ‐o client 
gcc ‐Wall ‐g ‐c ‐o server.o server.c 
gcc ‐Wall ‐g ‐c ‐o cli_handler.o cli_handler.c 
gcc server.o cli_handler.o sock.o log.o ‐lefence ‐o server

Regulile implicite intră în vigoare și se obțin, pe rând, fișierele obiect și fișierele executabile. Variabila
LDLIBS este folosită pentru a preciza bibliotecile cu care se face link­editarea pentru obținerea
executabilului.

Depanarea programelor
Există câteva unelte GNU care pot fi folosite atunci când nu reușim să facem un program să ne asculte.
gdb, acronimul de la “Gnu DeBugger” este probabil cel mai util dintre ele, dar există și altele, cum ar
fi ElectricFence, gprof sau mtrace. gdb este prezentat pe scurt aici.

Windows
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 14/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Compilatorul Microsoft cl.exe

Soluția folosită pentru platforma Windows în cadrul acestui laborator este cl.exe, compilatorul
Microsoft pentru C/C++. Recomandăm instalarea Microsoft Visual C++ Express 2010 (10.0) (versiunea
Professional a Visual C++ este disponibilă gratuit în cadrul MSDNAA). Programele C/C++ pot fi
compilate prin intermediul interfeței grafice sau în linie de comandă. În cele ce urmează vom prezenta
compilarea folosind linia de comandă. În Windows fișierele cod obiect au extensia *.obj.

hello.c cl hello.c

#include <stdio.h> 
$ cl /?    /* list of options for compiler */
  
int main(void) 
{  $ link /?  /* list of options for linker */
    printf("Hello, world!\n"); 
    return 0; 
}

Se vor prezenta mai jos o serie de opțiuni uzuale:

/Wall ­ activează toate warning­urile
/LIBPATH:<dir> ­ această opțiune indică linker­ului să caute și în directorul dir bibliotecile pe
care trebuie să le folosească programul; opțiunea se folosește după /link
/I<dir> ­ caută și în acest director fișierele incluse prin directiva include
/c ­ se va face numai compilarea, adică se va omite etapa de link­editare.
/D<define_symbol> ­ definirea unui macro de la compilare

Opțiuni privind optimizarea codului: Setarea numelui pentru diferite fișiere de ieșire:

/O1 minimizează spațiul ocupat /Fo<file> nume fișier obiect
/O2 maximizează viteza /Fa<file> nume fișier în cod de
/Os favorizează spațiul ocupat asamblare
/Ot favorizează viteza /Fp<file> nume fișier header
/Od fără optimizări (implicit) precompilat
/Og activează optimizările globale /Fe<file> nume fișier executabil
 

Exemple:

Creare fișier obiect myobj.obj din sursa mysrc.c:

cl /Fomyobj.obj /c mysrc.c

Creare fișier myasm.asm în cod de asamblare din sursa mysrc.c:

cl /Famyasm.asm /FA /c mysrc.c

Lista completă de opțiuni o puteți găsi aici [https://msdn.microsoft.com/en­us/library/fwkeyyhe.aspx]

Biblioteci în Windows

Crearea unor biblioteci statice

Pentru a crea biblioteci statice se folosește comanda lib

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 15/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

>lib /out:<nume.lib> <lista fișiere obiecte>

Vom considera exemplul folosit pentru crearea de biblioteci în Linux (main.c, util.h, f1.c, f2.c):

# obținem fișierul obiect f1.obj din sursa f1.c 
>cl /c f1.c 
Microsoft (R) 32‐bit C/C++ Optimizing Compiler Version 14.00.50727.42 for 80x86 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
f1.c 
  
#obținem fișierul f2.obj din sursa f2.c 
>cl /c f2.c 
Microsoft (R) 32‐bit C/C++ Optimizing Compiler Version 14.00.50727.42 for 80x86 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
f2.c 
  
>cl /c main.c 
Microsoft (R) 32‐bit C/C++ Optimizing Compiler Version 14.00.50727.42 for 80x86 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
main.c 
  
#obținem biblioteca statică intro.lib din f1.obj și f2.obj 
>lib /out:intro.lib f1.obj f2.obj 
Microsoft (R) Library Manager Version 8.00.50727.42 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
#intro.lib este compilat împreună cu main.obj pentru a obține main.exe 
>cl main.obj intro.lib 
Microsoft (R) 32‐bit C/C++ Optimizing Compiler Version 14.00.50727.42 for 80x86 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
Microsoft (R) Incremental Linker Version 8.00.50727.42 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
/out:main.exe 
main.obj 
intro.lib

Pentru obținerea unei biblioteci statice folosim comanda lib. Argumentul /out: precizează numele
bibliotecii statice de ieșire. Biblioteca are de obicei extensia *.lib. Pentru obținerea executabilului se
folosește cl care primește ca argumente fișierele obiect și bibliotecile care conțin funcțiile dorite.

Crearea unor biblioteci partajate

Bibliotecile partajate din Linux au ca echivalent bibliotecile DLL (Dynamic Link Library) în Windows.
Crearea unei biblioteci partajate pe Windows este mai complicată decât pe Linux. Pe de o parte, pentru
că în afara bibliotecii partajate (dll), mai trebuie creată o bibliotecă de import (lib). Pe de altă
parte, legarea bibliotecii partajate presupune exportarea explicită a simbolurilor (funcții, variabile) care
vor fi folosite.

Pentru precizarea simbolurilor care vor fi exportate de bibliotecă se folosesc identificatori predefiniți:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 16/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

__declspec(dllimport), este folosit pentru a importa o funcție dintr­o bibliotecă.
__declspec(dllexport), este folosit pentru a exporta o funcție dintr­o bibliotecă.

Exemplul de mai jos prezintă trei programe: două dintre ele vor fi legate într­o bibliotecă partajată, iar
celălalt conține codul de utilizare a funcțiilor exportate.

main.c 1 funs.h

#include <stdio.h>  #ifndef FUNS_H 
   #define FUNS_H   1 
#define DLL_IMPORTS    
#include "funs.h"  #ifdef DLL_IMPORTS 
   #define DLL_DECLSPEC __declspec(dllimport) 
int main(void)  #else 
{  #define DLL_DECLSPEC __declspec(dllexport) 
   f1();  #endif 
   f2();    
   DLL_DECLSPEC void f1 (void); 
   return 0;  DLL_DECLSPEC void f2 (void); 
}   
#endif

f1.c f2.c

#include <stdio.h>  #include <stdio.h> 
#include "funs.h"  #include "funs.h" 
     
void f1(void)  void f2(void) 
{  { 
   printf("Current file name is %s\n", __FILE__);     printf("Current line %d in file %s\n", 
              __LINE__, __FILE__); 
} }

Așadar, pentru crearea bibliotecii partajate și utlizarea acesteia de către programul main parcurgem
următorii pași:

f1.c va exporta funcția f1() folosind __declspec(dllexport)
f2.c va exporta funcția f2() folosind __declspec(dllexport)
main.c va importa funcțiile f1() și f2() folosind __declspec(dllimport)
după obținerea fișierelor obiect f1.obj și f2.obj acestea vor fi folosite la crearea bibliotecii
partajate folosind opțiunea /LD a comenzii cl.
în final legăm main.obj cu biblioteca partajată și obținem main.exe

>cl /LD f1.obj f2.obj 
Microsoft (R) 32‐bit C/C++ Optimizing Compiler Version 14.00.50727.42 for 80x86 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
Microsoft (R) Incremental Linker Version 8.00.50727.42 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
/out:f1.dll 
/dll 

/implib:f1.lib 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 17/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]
/implib:f1.lib 
f1.obj 
f2.obj 
   Creating library f1.lib and object f1.exp 
  
>cl main.obj f1.lib 
Microsoft (R) 32‐bit C/C++ Optimizing Compiler Version 14.00.50727.42 for 80x86 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
Microsoft (R) Incremental Linker Version 8.00.50727.42 
Copyright (C) Microsoft Corporation.  All rights reserved. 
  
/out:main.exe 
main.obj 
f1.lib

Alternativ, biblioteca poate fi obținută cu ajutorul comenzii link:

>link /nologo /dll /out:intro.dll /implib:intro.lib f1.obj f2.obj 
  Creating library intro.lib and object intro.exp 
  
>link /nologo /out:main.exe main.obj intro.lib 
  
>main.exe 
Current file name is f1.c 
Current line 6 in file f2.c

Nmake
Nmake este utilitarul folosit pentru compilare incrementală pe Windows. Nmake are o sintaxă foarte
asemănătoare cu Make. Un exemplu simplu de makefile este cel atașat parser­ului de la tema 2:

Makefile

OBJ_LIST = parser.tab.obj parser.yy.obj 
CFLAGS   = /nologo /W4 /EHsc /Za 
  
EXE_NAMES = CUseParser.exe UseParser.exe DisplayStructure.exe 
  
all : $(EXE_NAMES) 
  
CUseParser.exe : CUseParser.obj $(OBJ_LIST) 
  $(CPP) $(CFLAGS) /Fe$@ $** 
  
UseParser.exe : UseParser.obj $(OBJ_LIST) 
  $(CPP) $(CFLAGS) /Fe$@ $** 
  
DisplayStructure.exe : DisplayStructure.obj $(OBJ_LIST) 
  $(CPP) $(CFLAGS) /Fe$@ $** 
  
clean : exe_clean obj_clean 
  
obj_clean : 
  del *.obj 
  
exe_clean : 
  del $(EXE_NAMES)

Nmake oferă următoarele variabile speciale:

Macro Semnificație
$@ numele țintei curente
$* numele țintei curente mai puțin extensia
$** toate dependențele unei ținte
$? toate dependențele mai vechi decât ținta

Exerciții
Este recomandat ca înainte de a începe laboratorul, să dezarhivați și porniți mașina virtuală de
Windows.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 18/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

arhiva se găsește în /home/student/vm
porniți VMPlayer și instalați modulele de kernel.
porniți mașina virtuală
nu folosiți VMPlayer în modul full­screen (se blochează)

Exercițiul 1 ­ Joc interactiv (2p)

Punctaj: 2 puncte
Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

În rezolvarea laboratorului folosiți arhiva de sarcini lab01­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab01­tasks.zip]

Windows
Pentru a parcurge laboratorul mai ușor, recomandăm deschiderea unui browser în interiorul mașinii
virtuale de Windows. Descărcați arhiva de laborator [http://elf.cs.pub.ro/so/res/laboratoare/lab01­tasks.zip] și
în cadrul mașinii virtuale.

Exercițiul 2 ­ Utilizare Visual Studio (3p)

Punctaj total exercițiu: 3 puncte

2a. Compilare și rulare (1p)

Punctaj: 1 punct

Pentru acest pas vom folosi proiectul aflat în directorul win/VS Tutorial. Deschideți proiectul
folosind una dintre următoarele trei metode:

click dreapta pe fișierul *.sln → Open with → Microsoft Visual C++ 2010 Express;
deschideți Visual Studio și apoi File → Open → Project/Solution și selectați fișierul *.sln
corespunzător;
dublu click pe fișierul *.sln.

Dacă Solution Explorer View nu este vizibil (în stânga), îl putecți activa selectând View → Solution
Explorer (sau Ctrl+Alt+L).

Pentru a compila proiectul selectați Build → Build Solution sau apăsați tasta F7. În fereastra Output se
poate observa output­ul procesului de compilare. În acest caz, compilarea se va efectua cu succes.

Pentru a rula proiectul selectați Debug → Start Without Debugging sau tastați Ctrl+F5.

Similar cu mediul Linux, executabilele pot fi rulate și din linia de comandă. PowerShell se poate
deschide astfel:

selectând Tools → PowerShell Command Prompt din Visual Studio;
folosind link­ul Windows PowerShell aflat pe Desktop.

În consolă, navigați până când ajungeți în folderul win/VS Tutorial/Debug. Rulați comanda:
.\Hello World.exe. Se poate folosi tasta TAB pentru autocomplete, ca în Linux.

2b. Creare proiect nou (1p)

Punctaj: 1 punct

Pentru a crea un proiect nou selectați File → New → Project. Pe ecran o să apară o fereastră nouă.
Selectați Win32 Console Application. În partea de jos a ferestrei, specificați un nume proiectului și
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 19/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

apăsați butonul OK.

Se va deschide un nou wizard. Apăsați butonul Next pentru a începe etapa de configurare. Selectați
următoarele proprietări:

Application type == Console Application;
bifați opțiunea Empty Project din secțiunea Additional options.

Apoi puteți apăsa butonul Finish.

Vom adăuga un fișier (deja existent) la proiect. În fereastra Solution Explorer (din stânga) selectați
Source files. Dați click dreapta → Add → Existing Item. O să apară o nouă fereastră din care vom
selecta fișierul win/VS Tutorial/debug.c.

Compilați.

Pentru a vedea prima eroare, apăsați tasta F8. Cu F8 și Shift+F8 se poate naviga între erorile de
compilare.

Modificați antetul funcției f astfel încât să întoarcă int.

Compilați din nou și rulați. Programul va afișa pe ecran un mesaj după care o să crape.

2c. Debugging (1p)

Punctaj: 1 punct

Programul anterior ar trebui să afișeze valoarea salvată în variabila bug. După cum am observat,
programul crapă înainte de a face acest lucru.

Vom adăuga un breakpoint la funcția f.

click pe linia cu definiția funcției (linia 6) și apoi apăsăm tasta F9.
observați cerculețul roșu

Rulați programul în mediul de debug apăsând tasta F5. Programul a început execuția și s­a oprit în
primul breakpoint întâlnit (cel adăugat anterior).

Pentru a continua execuția step­by­step, selectați Debug → Step Over sau apăsați tasta F10. Observați
faptul că săgeata galbenă a înaintat.

Pentru a urmări valorile diverselor variabile, vom seta watch­uri pentru variabilele a, b, c și bug.

selectați Debug → Windows → Watch → Watch1;
adăugați pe rând numele variabilelor.

Vom continua rularea programului step­by­step (F10) și vom observa cum se schimbă valoarea
variabilei bug, cât și mesajele afișate în fereastra Output.

Remediați problema și rulați din nou programul.

Mai multe informații utile despre Visual Studio găsiți aici.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 20/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

Exercițiul 3 ­ Makefiles (2p)

Acest set de exerciții se rulează din command­shell­ul Windows PowerShell (nu cmd.exe).

Găsiți link la acesta pe Desktop sau accesând Tools → PowerShell Command Prompt.

3a. Compilarea unui singur fișier (1p)

Intrați în directorul win/1‐hello. Folosind cl obțineți și rulați executabilul hello.

cl hello.c 
.\hello.exe

Rămâneți în directorul curent și analizați fișierul Makefile (folosiți comanda cat). Folosind nmake
obțineți și rulați executabilul hello.

nmake  
.\hello.exe

3b. Compilarea din mai multe surse (1p)

Intrați în directorul win/2‐debug. Analizați fișierele add.c și main.c. Folosiți comanda cat.

Completați fișierul Makefile.ndbg astfel încât

să obțineți obiecte din sursele main.c și add.c.
să obțineți executabilul main.exe din obiectele creat.

Completați fișierul Makefile.dbg astfel încât:

să compilați cu simbolul DEBUG__ definit.
să obțineți obiecte din sursele main.c și add.c și executabilul main.exe (ca la subpunctul
precedent)

Hint: Revedeți secțiunea cl.

Linux

Exercițiul 4 ­ Fișiere make (4p)

4a. Compilarea unui singur fișier (1p)

Intrați în directorul lin/1‐hello/ și analizați conținutul fișierului hello.c. Compilați folosind gcc și
obțineți și rulați executabilul a.out.

$ gcc hello.c 
$ ./a.out 

Pentru a specifica numele executabilului, folosiți opțiunea ‐o.

$ gcc ‐o hello hello.c 
$ ./hello

4b. Creare biblioteci statice (1.5p)

Intrați în directorul lin/2‐lib/ și completați fișierul Makefile_static astfel încât:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 21/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

La rularea comenzii make libhexdump_static.a să creeze biblioteca statică
libhexdump_static.a Biblioteca va conține fișierele obiect asociate fișierelor hexdump.c și
sample.c
La rularea comenzii make să creeze executabilul main_static obținut din legarea fișierului
obiect corespunzător lui main.c cu biblioteca libhexdump_static.a.

Revedeți secțiunea crearea unei biblioteci statice.

4c. Creare biblioteci dinamice (1.5p)

Rămâneți în directorul lin/2‐lib/ și completați fișierul Makefile_dynamic reguli astfel încât:

La rularea comenzii make libhexdump_dynamic.so să creeze biblioteca dinamică
libhexdump_dynamic.so. Biblioteca va conține fișierele obiect asociate fișierelor hexdump.c
și sample.c
La rularea comenzii make pe lângă biblioteca dinamică libhexdump_dynamic.so obținută
anterior să se creeze și executabilul main_dynamic obținut din legarea fișierului obiect
corespunzător lui main.c cu biblioteca partajată libhexdump_dynamic.so.

Revedeți secțiunea despre crearea unei biblioteci dinamice.

BONUS

1 so karma ­ Compilare din mai multe surse, opțiuni la compilare

Intrați în directorul lin/3‐ops/ și analizați fișierele ops.c, mul.c și add.c. Fișierul ops.c, se
folosește de funcțiile definite în mul.c și add.c pentru a realiza operații de adunare și înmulțire
simple.

Creați fișierul Makefile, astfel încât să obțineți din surse fișierele obiect mul.o, add.o și ops.o, iar
apoi să obțineți executabilul ops din obiectele create. Observați rezultatul obținut pentru sumă și
înmulțire. Este corect? Rezolvați. Revedeți secțiunea despre compilarea mai multor fișiere.

Rămâneți în directorul lin/3‐ops/ și folosiți opțiunea ‐D pentru a defini simbolul HAVE_MATH la
compilarea fișierului ops.c. Obțineți și rulați executabilul ops. Pentru a folosi funcția pow trebuie să
includeți fișierul math.h și să legați biblioteca libm, folosindu­vă de opțiunea  ‐l.

1 so karma ­ Utilizare gdb

Intrați în directorul lin/4‐gdb/ și analizați fișierul fault.c. Completați fișierul Makefile astfel
încât la rularea comenzii make să se obțină fișierul executabil fault. Compilați.

Folosiți gdb pentru a determina cauza erorilor din fișierul fault.c Citiți secțiunea GDB. Folosiți
opțiunea ‐g pentru a compila sursa cu simbolurile de debug incluse. Folosiți comanda print pentru a
printa valorile variabilelor când faceți depanarea.

1 so karma ­ Editare de legături

Intrați în directorul lin/5‐linker/ și analizați fișierele main.c și str.c. Compilați. De ce nu
obținem o eroare de compilare? Rulați programul main și explicați rezultatele.

EXTRA

JNI [http://en.wikipedia.org/wiki/Java_Native_Interface]

Soluții
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 22/23
6/11/2017 Laborator 01 ­ Introducere [CS Open CourseWare]

lab01­sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab01­sol.zip]

Resurse utile

Linux

1. GCC online documentation [https://gcc.gnu.org/onlinedocs/]
2. Tech Talk: Preprocesorul C [http://www.youtube.com/watch?
v=IHoWmi5GFRU&context=C364d09dADOEgsToPDskLpLFav1qDpuI0xUTn1fIcF]
3. Linking, Loading and Library Management under Linux [http://techblog.rosedu.org/library­
management.html]
4. The GNU C Library [http://www.gnu.org/software/libc/manual/]
5. Program Library HOWTO [http://tldp.org/HOWTO/Program­Library­HOWTO/]
6. GNU make manual [http://www.gnu.org/software/make/manual/make.html]
7. GDB documentation [http://sourceware.org/gdb/documentation/]

Windows

1. Visual C++ Express [http://www.microsoft.com/express/Windows/]
2. Nmake tool [http://msdn.microsoft.com/en­us/library/ms930369.aspx]
3. Nmake Macros [http://msdn.microsoft.com/en­us/library/ms933742.aspx]
4. Dynamic link library [http://en.wikipedia.org/wiki/Dynamic­link_library]
5. Creating and using DDL's [http://www.flipcode.com/archives/Creating_And_Using_DLLs.shtml]
6. Dynamic libraries [http://herbert.the­little­red­haired­girl.org/en/c­tips/windows/index.html]

so/laboratoare/laborator­01.txt · Last modified: 2017/02/27 17:10 by elena.sandulescu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­01 23/23
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Laborator 02 ­ Operații I/O simple

Materiale ajutătoare

lab02­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab02­slides.pdf]
lab02­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab02­refcard.pdf]
Video Operații IO [http://elf.cs.pub.ro/so/res/tutorial/lab­02­operatii­io/]

Nice to read

TLPI ­ Chapter 4, File I/O: The Universal I/O model
WSP4 ­ Chapter 2, Using the Windows File System

Fișiere. Sisteme de fișiere
Fișierul este una dintre abstractizările fundamentale în domeniul sistemelor de operare; cealaltă
abstractizare este procesul. Dacă procesul abstractizează execuția unei anumite sarcini pe procesor,
fișierul abstractizează informația persistentă a unui sistem de operare. Un fișier este folosit pentru a
stoca informațiile necesare funcționării sistemului de operare și interacțiunii cu utilizatorul.

Un sistem de fișiere este un mod de organizare a fișierelor și prezentare a acestora utilizatorului. Din
punctul de vedere al utilizatorului, un sistem de fișiere are o structură ierarhică de fișiere și directoare,
începând cu un director rădăcină. Localizarea unei intrări (fișier sau director) se realizează cu ajutorul
unei căi în care sunt prezentate toate intrările de până atunci. Astfel, pentru calea
/usr/local/file.txt directorul rădăcină '/' are un subdirector usr care include subdirectorul
local ce conține un fișier file.txt.
Fiecare fișier are asociat, așadar, un nume cu ajutorul căruia se face identificarea, un set de drepturi
de acces și zone conținând informația utilă.

Sistemele de fișiere suportate de sistemele de operare de tip Unix și Windows sunt ierarhice. Sistemele
Linux/Unix sunt case­sensitive (Data este diferit de data), iar sistemele Windows sunt case­
insensitive.

Ierarhia sistemului de fișiere Unix are un singur director cunoscut sub numele de root și notat '/',
prin care se localizează orice fișier (a nu se confunda cu directorul /root, care este home­ul
utilizatorului privilegiat, root). Notația Unix pentru căile fișierelor este un șir de nume de directoare
despărțite prin '/', urmat de numele fișierului. Există și căi relative la directorul curent '.' sau la
directorul părinte '..'.

În Unix nu se face nicio deosebire între fișierele aflate pe partițiile discului local, pe CD sau pe o
mașină din rețea. Toate aceste fișiere vor face parte din ierarhia unică a directorului root. Acest lucru
se realizează prin montare: sistemele de fișiere vor fi montate într­unul dintre directoarele sistemului
de fișiere rădăcină.

În Windows există mai multe ierarhii, câte una pentru fiecare partiție și pentru fiecare loc din rețea.
Spre deosebire de Unix, delimitatorul între numele directoarelor dintr­o cale este '\', și pentru căile
absolute trebuie specificat numele ierarhiei în forma C:\, E:\ sau \\FILESERVER\myFile (pentru
rețea). Ca și Unix, Windows folosește '.' pentru directorul curent și '..' pentru directorul părinte.

Operații pe fișiere
În Unix, un descriptor de fișier este un întreg care indexează o tabelă cu pointeri spre structuri care
descriu fișierele deschise de un proces. În cazul în care un program rulează într­un shell Unix, procesul

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 1/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

părinte (shell­ul) deschide pentru procesul copil (programul respectiv) 3 fișiere standard având
descriptori de fișiere cu valori speciale:

standard input (0) ­ citirea de la intrarea
standard (tastatură)
standard output (1) ­ afișarea la ieşirea
standard (consolă)
standard error (2) ­ afișarea la ieşirea
standard de eroare (consolă)

În Windows, noțiunea de bază pentru managementul
fișierelor este handle­ul, o valoare din care se obține
un pointer spre o structură descriptivă a fișierului.
Aceleași 3 fișiere standard sunt deschise de fiecare
proces.

În continuare, pentru descrierea comportamentului operațiilor de intrare­ieșire pe Windows, s­a ales ca
toate apelurile să facă parte din API­ul Win32, care este cel mai aproape de kernelul Windows.
Sistemul oferă ca alternativă apeluri standard (POSIX, de exemplu, compatibile între Windows și
Linux), dar acestea se implementează în Windows prin apelurile Win32 și formează un nivel de
abstractizare aflat mai departe de kernel.

Un fișier are asociat cursorul de fișier (file pointer) care indică poziția curentă în cadrul fișierului.
Cursorul de fișier este un întreg care reprezintă deplasamentul (offset­ul) față de începutul fișierului.

Operațiile specifice pentru lucrul cu fișiere:

deschiderea/crearea unui fișier ­ înseamnă asocierea unui descriptor de fișier sau a unui
1)
handle cu un fișier identificat prin numele său  . ( Linux, Windows )
închiderea unui fișier ­ înseamnă eliberarea structurilor de fișier asociate procesului și a
descriptorului (handle­ului) acelui fișier ­ doar dacă nu mai există nici o intrare în tabela file
2)
descriptorilor care să puncteze spre acea structură  . ( Linux, Windows )
citirea dintr­un fișier ­ înseamnă copierea unui bloc de date într­un buffer; după ce se
3)
realizează citirea se actualizează cursorul de fișier  . ( Linux, Windows )
scrierea într­un fișier ­ înseamnă copierea unui bloc de date dintr­un buffer în fișier;
4)
efectuarea scrierii înseamnă și actualizarea cursorului de fișier  . ( Linux, Windows )
poziționarea într­un fișier ­ înseamnă schimbarea valorii cursorului de fișier; citirile sau
5)
scrierile ulterioare vor porni din locul indicat de acest cursor de fișier  . ( Linux, Windows )
6)
schimbarea atributelor unui fișier ­ înseamnă stabilirea unor parametri pentru fișier  . (
Linux)

Operații pe fișiere în Linux

Crearea, deschiderea și închiderea fișierelor

open

Pentru deschiderea/crearea unui fișier se folosește funcția open [http://linux.die.net/man/2/open].

int open(const char *pathname, int flags);               /* deschidere */ 
int open(const char *pathname, int flags, mode_t mode);  /* creare */

creat
Pentru crearea de fișiere se poate utiliza și creat [http://linux.die.net/man/2/creat]:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 2/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

int creat(const char *pathname, mode_t mode);

Funcția este echivalentă cu apelul open unde flag­ul O_CREAT e setat și fișierul nu există deja:

open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);

close
Închiderea de fișiere se realizează cu close [http://linux.die.net/man/2/close]:

int close(int fd)

O greșeală frecventă de programare este neverificarea codului de eroare întors la close
[http://linux.die.net/man/2/close], pentru că se poate întâmpla ca o eroare la scriere (EIO) să fie întoarsă
utilizatorului abia la close.

unlink

Ștergerea efectivă a unui fișier de pe disk se realizează cu funcția unlink
[http://linux.die.net/man/2/unlink]:

int unlink(const char *pathname);

Exemplu

Dacă, spre exemplu, dorim să deschidem fișierul in.txt pentru citire și scriere, cu eventuala creare a
acestuia, iar fișierul out.txt pentru scriere, cu trunchiere putem folosi următoarea secvență de cod:

io­01.c

#include <sys/types.h>  /* open */ 
#include <sys/stat.h>  /* open */ 
#include <fcntl.h>  /* O_RDWR, O_CREAT, O_TRUNC, O_WRONLY */ 
#include <unistd.h>  /* close */ 
  
#include "utils.h" 
  
int main(void) 

  int rc; 
  int fd1, fd2; 
  
  fd1 = open("in.txt", O_RDWR | O_CREAT, 0644); 
  DIE(fd1 < 0, "open in.txt"); 
  
  /* will fail if out.txt does not exist */ 
  fd2 = open("out.txt", O_WRONLY | O_TRUNC); 
  DIE(fd2 < 0, "open out.txt"); 
  
  rc = close(fd1); 
        DIE(rc < 0, "close fd1"); 
  
        rc = close(fd2); 
        DIE(rc < 0, "close fd2"); 
  
  return 0; 
}

Atenție! O greșeală frecventă este omiterea drepturilor de creare a fișierului (0644 în exemplul de mai
sus) când se apelează open cu flag­ul O_CREAT setat.

Scrierea și citirea

read
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 3/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Funcția read [http://linux.die.net/man/2/read] e folosită pentru citirea din fișier a maxim count octeți:

ssize_t read(int fd, void *buf, size_t count);

Funcția read [http://linux.die.net/man/2/read] întoarce numărul de octeți efectiv citiți, cel mult count.
Valoarea minimă este de 1 octet, iar când se ajunge la sfârșitul de fișier se va întoarce 0.

write

Funcția write [http://linux.die.net/man/2/write] e folosită pentru scrierea în fișier a maxim count octeți:

ssize_t write(int fd, const void *buf, size_t count);

Valoarea întoarsă este numărul de octeți ce au fost efectiv scriși, cel mult count. În mod implicit nu se
garantează că la revenirea din write [http://linux.die.net/man/2/write] scrierea în fișier s­a terminat.
Pentru a forța actualizarea se poate folosi fsync [http://linux.die.net/man/2/fsync] sau fișierul se poate
deschide folosind flagul O_FSYNC, caz în care se garantează că după fiecare write fișierul a fost
actualizat.

Observație: Pentru read [http://linux.die.net/man/2/read]/write [http://linux.die.net/man/2/write] există
versiunile pread [http://linux.die.net/man/2/pread]/pwrite [http://linux.die.net/man/2/pwrite], care permit
specificarea unui offset în fișier de la care să se efectueaze operația de citire/scriere. (De asemenea,
există și versiunile pread64/pwrite64 care folosesc offset­uri de 64 de biți ­ pentru a putea specifica
offset­uri mai mari decât 4GB).

Poziționarea în fișier (lseek)

lseek
Funcția lseek [http://linux.die.net/man/2/lseek] permite mutarea cursorului unui fișier la o poziție absolută
sau relativă.

 off_t lseek(int fd, off_t offset, int whence)

Parametrul whence reprezintă poziția relativă de la care se face deplasarea:

SEEK_SET ­ față de poziția de început
SEEK_CUR ­ față de poziția curentă
SEEK_END ­ față de poziția de sfârșit
Observație lseek [http://linux.die.net/man/2/lseek] permite și poziționări după sfârșitul fișierului.
Scrierile care se fac în astfel de zone nu se pierd, ceea ce se obține fiind un fișier cu goluri, o zonă
care este sărită ­ nu este alocată pe disc.

Pentru această funcție există și o versiune lseek64 [http://linux.die.net/man/3/lseek64] la care offset­ul
este pe 64 de biți.

Trunchierea fișierelor

Pe lângă trunchierea la 0 care se poate face prin apelul open cu flag­ul O_TRUNC, se poate specifica
trunchierea unui fișier la o dimensiune specificată, prin apelurile de sistem ftruncate
[http://linux.die.net/man/2/ftruncate] și truncate [http://linux.die.net/man/2/ftruncate]: 

int ftruncate(int fd, off_t length);         
int truncate(const char *path, off_t length);

În cazul ftruncate [http://linux.die.net/man/2/ftruncate], parametrul fd este file descriptorul obținut cu un
apel open care a asigurat drept de scriere. În cazul truncate [http://linux.die.net/man/2/ftruncate], fișierul
reprezentat prin path trebuie să aibă drept de scriere.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 4/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Exemplu utilizare operații I/O
io­2.c

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
  
#include <sys/types.h>  /* open */ 
#include <sys/stat.h>  /* open */ 
#include <fcntl.h>  /* O_CREAT, O_RDONLY */ 
#include <unistd.h>  /* close, lseek, read, write */ 
  
#include "utils.h" 
  
/* Print the last 100 bytes from a file */ 
  
int main (void) 

  int fd, rc; 
  char *buf; 
  ssize_t bytes_read; 
  
  /* alocate space for the read buffer */ 
  buf = malloc(101); 
  DIE(buf == NULL, "malloc"); 
  
  /* open file */ 
  fd = open("file.txt", O_RDONLY); 
  DIE(fd < 0, "open"); 
  
  /* set file pointer at 100 characters 
   _before_ the end of the file */ 
  rc = lseek(fd, ‐100, SEEK_END); 
  DIE(rc < 0, "lseek"); 
  
  /* read the last 100 characthers */ 
  bytes_read = read(fd, buf, 100); 
  DIE(bytes_read < 0, "read"); 
  
  /* set '\0' at end of buffer for printing purposes*/ 
  buf[bytes_read] = '\0'; 
  
  printf("the last %ld bytes: \n%s\n", bytes_read, buf); 
  
  /* close file */ 
  rc = close(fd); 
  DIE(rc < 0, "close"); 
  
  /* cleanup */ 
  free(buf); 
  
  return 0; 
}

Redirectări
În Linux redirectările se realizează cu ajutorul funcțiilor de duplicare a descriptorilor de fișiere dup
[http://linux.die.net/man/2/dup] și dup2 [http://linux.die.net/man/2/dup2] (observați diferența dintre cele 2 în
link­urile anterioare):

int dup(int oldfd); 
int dup2(int oldfd, int newfd);

De exemplu, pentru redirectarea ieșirii în fișierul output.txt, sunt necesare două linii de cod:

fd = open("output.txt", O_RDWR|O_CREAT|O_TRUNC, 0600); 
dup2(fd, STDOUT_FILENO);

Operații speciale
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 5/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Funcția fcntl [http://linux.die.net/man/2/fcntl] permite efectuarea unor operații speciale asupra
descriptorilor de fișier.

int fcntl(int fd, int cmd); 
int fcntl(int fd, int cmd, long arg); 
int fcntl(int fd, int cmd, struct flock *lock);

cmd efect

F_DUPFD duplicarea unui file descriptor

F_GETFD citește flag­urile pentru fd

F_SETFD setează flag­urile pentru fd la valoarea specificată de arg

F_GETFL citește flag­urile de stare pentru fd

F_SETFL setează flag­urile de stare pentru fd la valoarea specificată de arg

F_GETLK obținerea informațiilor despre un lock pe fișier

F_SETLK obținerea / eliberarea unui lock pe fișier

F_SETLKW similar cu F_SETLK dar se așteaptă terminarea operației
F_GETOWN obținerea PID­ului procesului care primește semnalul SIGIO
F_SETOWN stabilirea procesului care va primi semnalul SIGIO

Operații pe fișiere în Windows

Crearea, deschiderea și închiderea

CreateFile
Pentru a crea un handle asociat cu un fișier, director sau altă resursă abstractizată sub forma unui fișier
(port COM, pipe, modem etc.) se folosește funcția CreateFile [http://msdn.microsoft.com/en­
us/library/aa363858%28VS.85%29.aspx]. Funcția se ocupă atât de crearea, cât și de deschiderea unui
fișier (și întoarce în ambele cazuri un handle asociat cu fișierul):

HANDLE CreateFile(   handle1 = CreateFile( 
   LPCTSTR lpFileName,       "out.txt", 
   DWORD dwDesiredAccess,       GENERIC_READ,     /* access mode */ 
   DWORD dwShareMode,       FILE_SHARE_READ,     /* sharing option */ 
   LPSECURITY_ATTRIBUTES lpSecAttributes,       NULL,       /* security attributes */ 
   DWORD dwCreationDisposition,      OPEN_EXISTING,     /* open only if it exists */ 
   DWORD dwFlagsAndAttributes,       FILE_ATTRIBUTE_NORMAL,/* file attributes */ 
   HANDLE hTemplateFile       NULL 
); ); 

Atenție! Explicațiile complete se găsesc pe pagina de manual pentru CreateFile
[http://msdn.microsoft.com/en­us/library/aa363858%28VS.85%29.aspx]. În continuare vom prezenta cele
mai importante proprietăți.

Drepturile de acces cerute la deschiderea fișierului sunt specificate în dwDesiredAccess:

GENERIC_WRITE
GENERIC_READ

Lista completă aici [http://msdn.microsoft.com/en­us/library/aa363874%28v=vs.85%29.aspx]

Parametrul dwCreationDisposition precizează modul în care apelul acționează în cazul în care
fișierul există sau nu; poate avea valori de forma:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 6/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

CREATE_ALWAYS ­ creează un fișier nou; dacă fișierul există, apelul îl suprascrie, ștergând
atributele existente;
CREATE_NEW ­ creează un fișier nou; apelul eșuează dacă fișierul există deja;
OPEN_ALWAYS ­ deschide fișierul, dacă acesta există; altfel, se comportă ca și CREATE_NEW;
OPEN_EXISTING ­ deschide fișierul; dacă nu există, apelul eșuează;
TRUNCATE_EXISTING ­ deschide fișierul (cu drept de acces GENERIC_WRITE) și îl trunchiază la
dimensiunea zero; dacă fișierul nu există, apelul eșuează.

Dacă fișierul există deja și dwCreationDisposition este CREATE_ALWAYS sau OPEN_ALWAYS, apelul
NU eșuează, dar GetLastError returnează ERROR_ALREADY_EXISTS.

La deschiderea unui fișier se poate preciza prin parametrul lpSecurityAttributes [in] modul în
care handle­ul returnat de apel poate fi moștenit de procesele fii ale procesului apelant. Mai multe
detalii în laboratorul de procese.

Un fișier poate fi deschis de mai multe ori (de procese diferite, sau de același proces). În acest caz, la
prima deschidere, parametrul dwShareMode [in] va avea una dintre valorile:

FILE_SHARE_DELETE permite unor operații de deschidere ulterioare să capete acces de tip
delete.
FILE_SHARE_READ permite unor operații de deschidere ulterioare să capete acces de tip read.
FILE_SHARE_WRITE permite unor operații de deschidere ulterioare să capete acces de tip
write.

Un set de flaguri și atribute suplimentare (valabile numai în cazul fișierelor) pot fi precizate în
dwFlagsAndAttributes [in]. Valori uzuale sunt:

FILE_ATTRIBUTE_NORMAL fișierul nu are alte atribute setate (folosit numai singur)
FILE_ATTRIBUTE_READONLY fișierul va fi read only pentru toate procesele

Pentru copierea și mutarea fișierelor există apelurile CopyFile [http://msdn.microsoft.com/en­
us/library/aa363851(VS.85).aspx], MoveFile [http://msdn.microsoft.com/en­us/library/aa365239(VS.85).aspx] și
ReplaceFile [http://msdn.microsoft.com/en­us/library/aa365512(VS.85).aspx]. Un exemplu de schimbare a
atributelor găsiți aici [http://msdn.microsoft.com/en­us/library/aa365522(v=VS.85).aspx].

CloseHandle
Când fișierul nu mai este folosit, fișierul este închis cu apelul generic pentru orice tip de handle­uri
CloseHandle [http://msdn.microsoft.com/en­us/library/ms724211%28VS.85%29.aspx]

BOOL CloseHandle(HANDLE hObject);

DeleteFile
Ștergerea se face prin închiderea fișierului și folosirea apelului de sistem DeleteFile
[http://msdn.microsoft.com/en­us/library/aa363915%28VS.85%29.aspx]

CloseHandle(hFile); 
DeleteFile("myfile.txt");

unde DeleteFile [http://msdn.microsoft.com/en­us/library/aa363915%28VS.85%29.aspx] are signatura

BOOL DeleteFile(LPCTSTR lpFileName);

Citirea și scrierea

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 7/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

ReadFile

ReadFile [http://msdn.microsoft.com/en­us/library/aa365467%28VS.85%29.aspx] operează asupra unui fișier
care are drepturi de acces cel puțin pentru citire, copiind un număr de octeți (începând cu poziția
curentă a cursorului de fișier) într­un buffer și întoarce într­o variabilă numărul de octeți citiți.

BOOL ReadFile(  bRet = ReadFile( 
      HANDLE hFile,     hFile,        /* open file handle */ 
      LPVOID lpBuffer,     lpBuffer,     /* where to put data */ 
      DWORD nNumberOfBytesToRead,     dwBytesToRead,/* number of bytes to read */ 
      LPDWORD lpNumberOfBytesRead,     &dwBytesRead, /* number of bytes that were read */ 
      LPOVERLAPPED lpOverlapped     NULL          /* no overlapped structure */ 
); );

ReadFile [http://msdn.microsoft.com/en­us/library/aa365467%28VS.85%29.aspx] primește un handle de fișier
hFile, creat anterior cu drepturi cel puțin de citire. Rezultatul citirii este copiat în lpBuffer, iar
numărul de octeți efectiv citiți este întors în variabila pointată de lpNumberOfBytesRead. Numărul de
octeți efectiv citiți poate fi mai mic decât numărul de octeți care se doresc a fi citiți ­
nNumberOfBytesToRead.
În mod normal, după acest apel, cursorul de fișier este actualizat cu numărul de octeți citiți. Singura
excepție este cazul în care fișierul este deschis pentru operații de I/O de tip OVERLAPPED ­ asincrone,
caz în care conceptul de cursor de fișier nu mai este folositor (și deci nu mai este actualizat). Mai
multe detalii despre operațiile asincrone în Laborator 10 ­ Operatii IO avansate ­ Windows.

ReadFile [http://msdn.microsoft.com/en­us/library/aa365467%28VS.85%29.aspx] returnează o valoare
diferită de zero în caz de succes, și zero altfel. Dacă se returnează o valoare diferită de zero, dar
numărul de octeți citiți este zero, atunci s­a ajuns la sfârșitul de fișier.

WriteFile

Apelul WriteFile [http://msdn.microsoft.com/en­us/library/aa365747%28VS.85%29.aspx] copiază în mod
sincron sau asincron un număr specificat de octeți dintr­un buffer în conținutul unui fișier și returnează
într­o variabilă numărul efectiv de octeți copiați. Scrierea în fișier se face în general începând din
poziția curentă a cursorului și după terminarea operației, poziția cursorului fișierului este actualizată
(rămân valabile observațiile anterioare despre operații OVERLAPPED).

BOOL WriteFile(  bRet = WriteFile(  
      HANDLE hFile,     hFile,          /* open file handle */ 
      LPCVOID lpBuffer,     lpBuffer,       /* start of data to write */ 
      DWORD nNumberOfBytesToWrite,     dwBytesToWrite, /* number of bytes to write */ 
      LPDWORD lpNumberOfBytesWritten,     &dwBytesWritten,/* number of bytes that were written */ 
      LPOVERLAPPED lpOverlapped     NULL            /* no overlapped structure */ 
); );

Handle­ul de fișier în care se scrie hFile [in] trebuie să fi fost creat cu drepturi de acces
GENERIC_WRITE. Parametrii WriteFile [http://msdn.microsoft.com/en­
us/library/aa365747%28VS.85%29.aspx] au aceleași semnificații cu parametrii ReadFile
[http://msdn.microsoft.com/en­us/library/aa365467%28VS.85%29.aspx], adaptate pentru operații de scriere.

Poziționarea în fișier

SetFilePointer

Fiecare fișier deschis are asociat un cursor (memorat pe 64 de biți) care reprezintă poziția curentă de
citire/scriere. Un proces poziționează cursorul la un offset specificat cu SetFilePointer
[http://msdn.microsoft.com/en­us/library/aa365541(VS.85).aspx]:

DWORD SetFilePointer(  /* Example: How to get current position */ 
         HANDLE hFile, 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 8/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]
         HANDLE hFile,  currentPos = SetFilePointer(  
         LONG lDistanceToMove,                 myFileHandle, 
         PLONG lpDistanceToMoveHigh,                 0,           /* offset 0 */ 
         DWORD dwMoveMethod                 NULL,        /* no 64bytes offset */ 
);                FILE_CURRENT  
);

Deplasarea se face asupra unui fișier reprezentat prin handle­ul hFile deschis în prealabil, creat cu
unul din drepturile de acces GENERIC_READ sau GENERIC_WRITE. O valoare pozitivă înseamnă o
deplasare înainte, iar una negativă, înapoi.

Numărul de octeți cu care se mută cursorul este specificat de lDistanceToMove [in] și
lpDistanceToMoveHigh; cele două câmpuri de 32 de biți formează o valoare de 64 de biți. Uzual cel
de­al doilea câmp este NULL.

Parametrul dwMoveMethod specifică punctul de start pentru mutarea cursorului, și poate avea una
dintre valorile:

FILE_BEGIN ­ punctul de start este începutul fișierului; lDistanceToMove este considerat
unsigned
FILE_CURRENT ­ punctul de start este valoarea curentă a cursorului
FILE_END ­ punctul de start este valoarea curentă a sfârșitului de fișier

Apelul returnează noua valoare a cursorului, dacă lpDistanceToMoveHigh este NULL; altfel, se
returnează jumătatea low a valorii, jumătatea high luând locul lpDistanceToMoveHigh.

Varianta extinsă SetFilePointerEx [http://msdn.microsoft.com/en­us/library/aa365542(VS.85).aspx] a apelului
SetFilePointer [http://msdn.microsoft.com/en­us/library/aa365541(VS.85).aspx] memorează valoarea
cursorului într­un singur câmp, în loc de două câmpuri separate, apelul extins făcând lucrul cu valorile
cursorului mai ușor.

Trunchierea fișierelor

SetEndOfFile

Un fișier poate fi trunchiat sau extins folosind apelul SetEndOfFile [http://msdn.microsoft.com/en­
us/library/aa365531(VS.85).aspx], care face poziția sfârșitului de fișier EOF egală cu poziția curentă a
cursorului fișierului. În cazul extinderii fișierului peste limita sa, conținutul adăugat este nedefinit.

 BOOL SetEndOfFile(HANDLE hFile);

Exemplu
win_io.c

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
#include <windows.h> 
  
#include "utils.h" 
  
#define BUF_SIZE  100 
  
int main (void) 

  HANDLE hFile; 
  DWORD dwBytesRead, dwPos, dwBytesToRead = BUF_SIZE, dwRet; 
  BOOL bRet; 
  CHAR outBuffer[BUF_SIZE+1]; 
  
  /* deschidem fisierul */ 
  hFile = CreateFile( 
      "file.txt", 

      GENERIC_READ, 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 9/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]
      GENERIC_READ, 
      FILE_SHARE_READ, 
      NULL,  /* no security attributes */ 
      OPEN_EXISTING, 
      FILE_ATTRIBUTE_NORMAL, 
      NULL  /* no pattern */ 
    ); 
  DIE(hFile == INVALID_HANDLE_VALUE, "CreateFile"); 
  
  /* set file pointer at 100 bytes 
  _before_ the end of file */ 
  dwPos = SetFilePointer(
      hFile, 
      ‐100, 
      NULL,  /* used only for offsets on 64bytes */ 
      FILE_END 
  ); 
  DIE(dwPos == INVALID_SET_FILE_POINTER, "SetFilePointer"); 
  
  /* read last 100 bytes into buffer */ 
  dwRet = ReadFile( 
      hFile, 
      outBuffer, 
      dwBytesToRead, 
      &dwBytesRead, 
      NULL);  /* do nothing asynchronous */ 
  DIE(dwRet == FALSE, "ReadFile"); 
  
  /* print buffer */ 
  outBuffer[dwBytesRead] = '\0'; 
  printf("last %ld bytes: \n%s\n", dwBytesRead, outBuffer); 
  fflush(stdout); 
  
  /* close file */ 
  bRet = CloseHandle (hFile); 
  DIE(bRet == FALSE, "CloseHandle"); 
  
  return 0; 
}

Wrapper­e

În domeniul sistemelor de operare, prin wrapper înțelegem un layer software subțire (care nu aduce
un overhead prea mare) peste sistemul de operare, cu scopul de a abstractiza serviciile oferite de
acesta, adaptându­le la o interfață comună. Interfața comună este definită astfel încât să se
potrivească cu mai multe sisteme de operare. Programele pe care le scriem ulterior nu vor folosi
direct apelurile de sistem specifice fiecărui sistem de operare, ci interfața comună.

Un wrapper este folositor atunci când dorim să scriem software portabil pe mai multe platforme (spre
exemplu, temele de la Sisteme de Operare) cu un “overhead” minim de portare și fără a plăti un cost
de performanță prea scump (există și alte soluții pentru această problemă, de exemplu, mașina
virtuală Java ­ JVM).

Una din metodele posibile pentru realizarea unui wrapper este folosirea preprocesorului. Să
presupunem că încercăm să abstractizăm conceptul de fișier și operațiile disponibile cu el. Vom
exemplifica doar operațiile de read/write.

io­wrapper.h

#ifdef __linux__ 
  
#include <sys/types.h> 
#include <sys/stat.h> 
#include <unistd.h> 
#include <fcntl.h> 
  
typedef int os_handle; 
typedef size_t os_size;  
typedef ssize_t os_ssize;  
  
#elif defined(_WIN32) 
  
#include <windows.h> 
  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 10/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]
  
typedef HANDLE os_handle; 
typedef DWORD os_size; 
typedef DWORD os_ssize; 
  
#else  
#error "Unknown OS!" 
#endif 
  
os_ssize os_read(os_handle fd, void *buffer, os_size count); 
os_ssize os_write(os_handle fd, const void *buffer, os_size count);

Se observă că în funcție de sistemul de operare definit, diferă:

fișierele header incluse
definițiile tipurilor cu care lucrează wrapper­ul

De asemenea, se observă că semnăturile funcțiilor definite sunt identice pentru ambele sisteme de
operare. Iată un exemplu de implementare a lor:

io­wrapper.c

#include "io‐wrapper.h" 
  
#ifdef __linux__ 
  
os_ssize os_read(os_handle fd, void *buffer, os_size count) 

    return read(fd, buffer, count); 

  
os_ssize os_write(os_handle fd, const void *buffer, os_size count) 

    return write(fd, buffer, count); 

  
#elif defined(_WIN32) 
  
os_ssize os_read(os_handle fd, void *buffer, os_size count) 

    os_ssize result = ‐1; 
    ReadFile(fd, buffer, count, &result, NULL); 
    return result; 

  
os_ssize os_write(os_handle fd, void *buffer, os_size count) 

    os_ssize result = ‐1; 
    WriteFile(fd, buffer, count, &result, NULL); 
    return result; 

  
#endif

Acum putem genera fișiere executabile compatibile cu o platformă Linux sau Windows, în funcție de un
singur macro, definit automat de către compilator.

Se observă că folosind această tehnică putem să convertim inclusiv între procedură și funcție (funcțiile
de pe Windows primesc ca parametru transmis prin referință numărul de octeți citiți/scriși, iar cele de
pe Linux îl întorc direct). Desigur, abordarea de mai sus este incompletă, pentru că ar fi trebuit
convertite și codurile de eroare într­un format comun.

Odată scris acest wrapper, putem folosi în continuare funcțiile os_read și os_write pentru a citi / scrie
din fișiere, fară a ne preocupa de sistemul de operare pe care rulează programul nostru. Acesta este
însă un caz fericit, pentru că așa după cum veți observa la laboratorul de procese, nu toate serviciile
oferite de sisteme de operare diferite se pot “unifica” atât de ușor (este vorba de fork() + exec() vs.
CreateProcess).

Exerciții

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 11/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

În rezolvarea laboratorului folosiți arhiva de sarcini lab02­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab02­tasks.zip]

Observații: Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din
arhivă există un fișier utils.h cu funcții utile.

Folosiți man/MSDN pentru informații despre apelurile de sistem

Verificați valorile de retur a apelurilor de sistem
Puteți folosi macro­ul DIE [https://ocw.cs.pub.ro/courses/so/laboratoare/resurse/die](valoare_retur ==
eroare, “mesaj eroare”);

Exercițiul ­1 ­ GSOC (0p)
Google Summer of Code este un program de vară în care studenții (indiferent de anul de studiu) sunt
implicați în proiecte Open Source pentru a își dezvolta skill­urile de programare, fiind răsplătiți cu o
bursă a cărei valoare depinde de țară [https://developers.google.com/open­source/gsoc/help/student­stipends]
(pagină principală GSOC [https://developers.google.com/open­source/gsoc]).

UPB se află în top ca număr de studenți acceptați; în fiecare an fiind undeva la aprox. 30­40 de studenți
acceptați. Vă încurajăm să aplicați! Există și un grup de fb cu foști participanți unde puteti să îi
contactați pentru sfaturi facebook page [https://www.facebook.com/groups/240794072931431/]

Exercițiul 0 ­ Joc interactiv (2p)

Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (5p)

Exercițiul 1 ­ redirect (1p)

Intrați în directorul 1‐redirect și urmăriți conținutul fișierului redirect.c.

Compilați fișierul (folosiți make). Rulați programul obținut folosind comanda ./redirect.

Deschideți alt terminal și rulați comanda:

 watch ‐d lsof ‐p $(pidof redirect) 

lsof [http://linux.die.net/man/8/lsof] este un utilitar care afișează informații despre fișierele deschise (ce
fișiere sunt deschise în sistem, ce fișiere a deschis un anumit user etc). Căutați în manual (man 8
lsof) pentru a identifica semnificația coloanei FD și a coloanei TYPE.
Folosiți comanda ENTER pentru a continua programul. În paralel urmăriți cum se modifică tabela de
file­descriptori.

În cod, observați parametrii cu care s­a realizat redirectarea cu ajutorul funcțieidup2
[http://linux.die.net/man/2/dup2] (dup2(fd2, STDERR_FILENO)). Observați ce se întamplă dacă parametrii
sunt în ordine inversă.

revedeți secțiunea de redirectări

Exercițiul 2 ­ lseek (1p)

Intrați în directorul 2‐lseek și urmăriți codul sursă din lseek.c. Ce valoare va întoarce al doilea apel
al funcției lseek? Decomentați linia de afișare, compilați și rulați pentru verificare.

Sursa închide doar file descriptorul fd1. Este nevoie să se închidă și file descriptorul fd2? De ce?

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 12/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Exercițiul 3 ­ mcat (3p)

Intrați în directorul 3‐mcat.

3a. Similitudine cat (1p)

Completați fișierul astfel încât programul rezultat mcat să aibă funcționalitate similară cu a utilitarului
cat (urmăriți comentariile cu TODO 1)

Programul mcat va primi ca argument în linia de comandă numele unui fișier al cărui conținut îl va
afișa la ieșirea standard. Nu aveți voie să citiți tot fișierul în memorie. Puteți citi doar bucăți de
dimensiune maximum BUFSIZE.

Verificați codul de eroare întors de apelurile de sistem. Puteți folosi macro­ul DIE
[http://elf.cs.pub.ro/so/wiki/laboratoare/resurse/die]. Revedeți secțiunile Crearea, deschiderea și închiderea
fișierelor și Scrierea și citirea fișierelor.

Testați cu o comandă de genul:

./mcat Makefile 

3b. Similitudine cp (1p)

Extindeți funcționalitatea astfel încât output­ul să fie redirectat într­un fișier primit ca al doilea
argument ­ funcționalitate similară cu a utilitarului cp. (urmăriți comentariile cu TODO 2)

Revedeți secțiunea de redirectări.

Testați funcționalitatea:

./mcat Makefile out ; ./mcat out 

3c. /dev/nasty (1p)

Inițializați fișierul /dev/nasty:

./set_nasty.sh

Încercați funcționalitatea de copiere pe fișierul /dev/nasty:

./mcat /dev/nasty 
./mcat /dev/nasty out ; ./mcat out 

Dacă apar diferențe, fiți atenți la ce întorc funcțiile read și write (eventual afișați aceste valori) și
reparați problema.

Testați scrierea cu:

./mcat Makefile /dev/nasty ; cat /dev/nasty

În cazul în care ultima comandă nu produce rezultatul așteptat, cel mai probabil nu ați tratat corect
cazurile în care read/write întorc o valoare mai mică decât al treilea parametru.

Windows (4p)

Executabilele sunt generate în directorul win/Debug (în directorul Debug al soluției, nu al fiecărui
proiect în parte).

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 13/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Exercițiul 1 ­ cat (0.5p)

Deschideți folderul win din arhiva laboratorului 2 și intrați în proiectul 1‐cat, iar apoi urmăriți sursa
cat.c
Compilați și testați executabilul cat.exe folosind command prompt­ul de Visual Studio: Tools → Visual
Studio Command Prompt

Exercițiul 2 ­ CRC (3.5p)
Exercițiul are ca scop realizarea unui utilitar care, pentru un fișier dat, calculeaza CRC­ul pentru fiecare
bucată de 512 bytes din fișier și o salvează într­un fișier de output.

2a. Generare (1.5p)

Deschideți fișierul crc.c din proiectul 2‐crc și completați funcția GenerateCrc.

Funcția primește ca prim argument fișierul pentru care trebuie calculat CRC­ul, iar ca al doilea
argument fișierul în care se salvează CRC­urile pentru fiecare bucată de câte 512 bytes. La ultima
bucată se va face padding.

Revedeți secțiunile Crearea, deschiderea și închiderea fișierelor, cât și Citirea și scrierea fișierelor.

Urmăriți comentariile cu TODO 1.

2b. Comparare (2p)
Odată calculat fișierul cu CRC, vrem să vedem dacă două fișiere de CRC sunt egale. Extindeți
funcționalitatea programului anterior astfel încât să compare 2 fișiere. Vom lucra în funcția
CompareFiles.
Inițial comparați dimensiunile fișierelor astfel:

Completați funcția GetSize pentru calcularea dimensiunii unui fișier, urmărind comentariile din
TODO 2
Folosiți doar funcția SetFilePointer [http://msdn.microsoft.com/en‐
us/library/aa365541%28VS.85%29.aspx]

Dacă dimensiunile sunt egale, comparați cele 2 fișiere bucată cu bucată (nu citiți tot fișierul în
memorie), urmărind comentariile cu TODO 3.

BONUS ­ Linux

1 so karma ­ Troubleshooting

Intrați în directorul 4‐trouble. Compilați și rulați programul trouble.

Programul ar trebui să afișeze în fișierul tmp1.txt mesajul din msg. Afișați fișierul tmp1.txt.

Ce observați? Identificați și remediați problema. Revedeți secțiunea: Crearea, deschiderea și închiderea
fișierelor.

1 so karma ­ File lock

Vrem să ne asigurăm că doar o instanță a unui program rulează la un moment dat. Pentru asta se
creează un fișier temporar pe care se încearcă obținerea unui lock folosind apelul flock
[http://linux.die.net/man/2/flock].

Intrați în directorul 5‐singular și completați sursa singular.c (urmăriți comentariile cu TODO ).

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 14/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Hint: man 2 flock, nonblocking

Testați rulând executabilul din două terminale diferite, sau cu comanda:

./singular & sleep 3 ; ./singular

Găsiți o metodă prin care ne putem asigura că programul nostru are doar o singură instanță, folosind
mai puține apeluri de sistem.

BONUS ­ Windows

Utilitar echivalent cu ls ‐a ‐R.

1 so karma ­ Creare utilitar ls

Deschideți din arhiva laboratorului 2 proiectul 3‐ls. Completați fișierul ls.c pentru ca programul 3‐
ls.exe să se comporte ca utilitarul ls.
Afișarea fișierelor dintr­un director se face în doi pași:

se obține un handle la o primă intrare din lista de fișiere a directorului cu funcția: FindFirstFile
[http://msdn.microsoft.com/en­us/library/aa364418%28VS.85%29.aspx]
se iterează această listă folosind funcția: FindNextFile [http://msdn.microsoft.com/en­
us/library/aa364428%28VS.85%29.aspx]

Pentru rezolvare, urmăriți comentariile marcate cu TODO 1. Pentru testare folosiți dintr­un prompt
Visual Studio:

ls.exe ..

1 so karma ­ Afișare detalii pentru parametrul ­a

Pentru fișiere afișați numele, dimensiunea și data la care au fost modificate ultima oară. Pentru
directoare afișați numele și un indicator de director (ex: <DIR> nume ).

Atributele unui fișier sunt definite într­o structură de forma: WIN32_FIND_DATA
[http://msdn.microsoft.com/en­us/library/aa365740%28VS.85%29.aspx]. Pentru a verifica dacă un fișier e
director, trebuie să aibă bitul “FILE_ATTRIBUTE_DIRECTORY” din câmpul “dwFileAttributes” ( vezi File
Attributes [http://msdn.microsoft.com/en­us/library/ee332330%28v=VS.85%29.aspx]).

Urmăriți comentariile marcate cu TODO 2

1 so karma ­ Afișare detalii pentru parametrul ­R

Realizați parcurgerea recursivă a directoarelor prin apelarea recursivă a funcției ListFile.

Pentru rezolvare, urmăriți comentariile marcate cu TODO 3. Aveți grijă să concatenați numele noului
director la calea deja existentă.

1 so karma ­ Troubleshooting

Deschideți din arhiva laboratorului 2 proiectul 4‐trouble. Programul ar trebui să creeze un fișier cu
mesajul “Testing 123”.

Compilați și rulați programul trouble. Identificați și remediați problema.

Revedeți secțiunea: Crearea, deschiderea și închiderea fișierelor.

EXTRA

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 15/16
6/11/2017 Laborator 02 ­ Operații I/O simple [CS Open CourseWare]

Operații cu fișiere în Python
Studiați exemplele din arhivă, citiți documentația și observați diferențele între API­uri

Soluții
lab02­sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab02­sol.zip]

Resurse utile

1. Low level I/O [http://www.gnu.org/software/libc/manual/html_node/Low_002dLevel­I_002fO.html] (info
libc “Low­Level I/O”)
2. Duplicating descriptors [http://www.gnu.org/software/libc/manual/html_node/Duplicating­
Descriptors.html] (info libc “Duplicating Descriptors”)
3. Low level I/O [http://www.advancedlinuxprogramming.com/alp­folder/alp­apB­low­level­io.pdf] (Advanced
Linux Programming)
4. File management functions [http://msdn.microsoft.com/en­us/library/aa364232%28VS.85%29.aspx]

1)
 fopen (ISO C), open, creat (POSIX), CreateFile (Win32 API)
2)
 fclose (ISO C), close (POSIX), CloseHandle (Win32 API)
3)
 fread (ISO C), read (POSIX), ReadFile (Win32 API)
4)
 fwrite (ISO C), write (POSIX), WriteFile (Win32 API)
5)
 fseek (ISO C), lseek (POSIX), SetFilePointer (Win32 API)
6)
 fcntl (POSIX), SetFileAttributes (Win32 API)
so/laboratoare/laborator­02.txt · Last modified: 2017/03/06 17:44 by elena.sandulescu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­02 16/16
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

Laborator 03 ­ Procese

Materiale ajutătoare

lab03­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab03­slides.pdf]
lab03­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab03­refcard.pdf]
Video Procese [http://elf.cs.pub.ro/so/res/tutorial/lab­03­procese/]

Nice to read

TLPI ­ Chapter 6, Processes, Chapter 26 Monitoring Child Processes
WSP4 ­ Chapter 6, Process Management

Prezentare concepte
Un proces este un program în execuție. Procesele sunt unitatea primitivă prin care sistemul de
operare alocă resurse utilizatorilor. Orice proces are un spațiu de adrese și unul sau mai multe fire de
execuție. Putem avea mai multe procese ce execută același program, dar oricare două procese sunt
complet independente.

Informațiile despre procese sunt ținute într­o structură numită Process Control Block (PCB
[http://en.wikipedia.org/wiki/Process_control_block]), câte una pentru fiecare proces existent în sistem.
Printre cele mai importante informații conținute de PCB regăsim:

PID ­ identificatorul procesului
spațiu de adresă
registre generale, PC (contor program), SP (indicator stivă)
tabela de fișiere deschise
informații referitoare la semnale
lista de semnale blocate, ignorate sau care așteaptă să fie trimise procesului
handler­ele de semnale
informațiile referitoare la sistemele de fișiere (directorul rădăcină, directorul curent)

În momentul lansării în execuție a unui program, în sistemul de operare se va crea un proces pentru
alocarea resurselor necesare rulării programului respectiv. Fiecare sistem de operare pune la dispoziție
apeluri de sistem pentru lucrul cu procese: creare, terminare, așteptarea terminării. Totodată există
apeluri pentru duplicarea descriptorilor de resurse între procese, ori închiderea acestor descriptori.

Procesele pot avea o organizare:

ierarhică ­ de exemplu pe Linux ­ există o structură arborescentă în care rădăcina este procesul
init (pid = 1).
neierarhică ­ de exemplu pe Windows.

În general, un proces rulează într­un mediu specificat printr­un set de variabile de mediu. O variabilă
de mediu este o pereche NUME = valoare. Un proces poate să verifice sau să seteze valoarea unei
variabile de mediu printr­o serie de apeluri de bibliotecă (Linux, Windows).

Pipe­urile (canalele de comunicație) sunt mecanisme primitive de comunicare între procese. Un pipe
poate conține o cantitate limitată de date. Accesul la aceste date este de tip FIFO (datele se scriu la un
capăt al pipe­ului pentru a fi citite de la celălalt capăt). Sistemul de operare garantează sincronizarea
între operațiile de citire și scriere la cele două capete (Linux, Windows).

Există două tipuri de pipe­uri:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 1/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

pipe­uri anonime: pot fi folosite doar de procese înrudite (un proces părinte și un copil sau doi
copii), deoarece sunt accesibile doar prin moștenire. Aceste pipe­uri nu mai există după ce
procesele și­au terminat execuția.
pipe­uri cu nume: au suport fizic ­ există ca fișiere cu drepturi de acces. Prin urmare, ele vor
exista independent de procesul care le creează și pot fi folosite de procese neînrudite.

Procese în Linux
Lansarea în execuție a unui program presupune următorii pași:

Se creează un nou proces cu fork ­ procesul copil are o copie a resurselor procesului părinte.
Dacă se dorește înlocuirea imaginii procesului copil aceasta poate fi schimbată prin apelarea
unei funcții din familia exec*.

Crearea unui proces
În UNIX un proces se creează folosind apelul de sistem fork [http://linux.die.net/man/2/fork]:

pid_t fork(void);

Efectul este crearea unui nou proces (procesul copil), copie a celui care a apelat fork (procesul
părinte). Procesul copil primește un nou process id (PID) de la sistemul de operare.

Această funcție este apelată o dată și se întoarce (în caz de succes) de două ori:

În părinte va întoarce pid­ul procesului nou creat (copil).
În procesul copil apelul va întoarce 0.

Pentru aflarea PID­ului procesului curent și al
procesului părinte se vor apela funcțiile de mai jos.

Funcția getpid [http://linux.die.net/man/3/getpid] întoarce
PID­ul procesului apelant:

pid_t getpid(void);

Funcția getppid [http://linux.die.net/man/3/getppid]
întoarce PID­ul procesului părinte al procesului apelant:

pid_t getppid(void);

Înlocuirea imaginii unui proces
Familia de funcții exec [http://linux.die.net/man/3/exec] va
executa un nou program, înlocuind imaginea procesului
curent, cu cea dintr­un fișier (executabil). Acest lucru înseamnă:

Spațiul de adrese al procesului va fi înlocuit cu unul nou, creat special pentru execuția fișierului.
Registrele PC (contorul program), SP (indicatorul stivă) și registrele generale vor fi
reinițializate.
Măștile de semnale ignorate și blocate sunt setate la valorile implicite, ca și handler­ele
semnalelor.
PID­ul și descriptorii de fișier care nu au setat flag­ul CLOSE_ON_EXEC rămân neschimbați
(implicit, flag­ul CLOSE_ON_EXEC nu este setat).

int execl(const char *path, const char *arg, ...);  
int execv(const char *path, char *const argv[]); 
int execlp(const char *file, const char *arg, ...);
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 2/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]
int execlp(const char *file, const char *arg, ...);

Exemplu de folosire a funcțiilor de mai sus:

execl("/bin/ls", "ls", "‐la", NULL); 
  
char *const argvec[] = {"ls", "‐la", NULL}; 
execv("/bin/ls", argvec); 
  
execlp("ls", "ls", "‐la", NULL);

Primul argument este numele programului. Ultimul argument al listei de parametri trebuie să fie NULL,
indiferent dacă lista este sub formă de vector (execv*) sau sub formă de argumente variabile (execl*).

execl și execv nu caută programul dat ca parametru în PATH, astfel că acesta trebuie însoțit de calea
completă. Versiunile execlp și execvp caută programul și în PATH.

Toate funcțiile exec* sunt implementate prin apelul de sistem execve [http://linux.die.net/man/2/execve].

Așteptarea terminării unui proces
Familia de funcții wait [http://linux.die.net/man/3/waitpid] suspendă execuția procesului apelant până când
procesul (procesele) specificat în argumente fie s­a terminat, fie a fost oprit (SIGSTOP).

pid_t waitpid(pid_t pid, int *status, int options);

Starea procesului interogat se poate afla examinând status cu macrodefiniții precum WEXITSTATUS
[http://linux.die.net/man/3/waitpid], care întoarce codul de eroare cu care s­a încheiat procesul așteptat,
evaluând cei mai nesemnificativi 8 biți.

Există o variantă simplificată, care așteaptă orice proces copil să se termine. Următoarele secvențe de
cod sunt echivalente:

wait(&status);                   |  waitpid(‐1, &status, 0);

În caz că se dorește doar așteptarea terminării procesului copil, nu și examinarea statusului, se poate
folosi:

wait(NULL);

Terminarea unui proces

Pentru terminarea procesului curent, Linux pune la dispoziție apelul de sistem exit.

void exit(int status);

Dintr­un program C există trei moduri de invocare a acestui apel de sistem:

1. apelul _exit [http://linux.die.net/man/2/exit] (POSIX.1­2001 [http://linux.die.net/man/7/standards]):

void _exit(int status);

2. apelul _Exit [http://linux.die.net/man/2/exit] din biblioteca standard C (conform C99
[http://en.wikipedia.org/wiki/C99]):

void _Exit(int status);

3. apelul exit [http://linux.die.net/man/3/exit] din biblioteca standard C (conform C89, C99), cel prezentat
mai sus.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 3/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

_exit(2) și _Exit(2) sunt funcțional echivalente (doar că sunt definite de standarde diferite):
procesul apelant se va termina imediat
toți descriptorii de fișier ai procesului sunt închiși
copiii procesului sunt “înfiați” de init
un semnal SIGCHLD va fi trimis către părintele procesului. Tot acestuia îi va fi întoarsă valoarea
status, ca rezultat al unei funcții de așteptare (wait sau waitpid).

În plus, exit(3):

va șterge toate fișierele create cu tmpfile()
va scrie bufferele streamurilor deschise și le va închide

Conform ISO C, un program care se termină cu return x din main() va avea același comportament
ca unul care apelează exit(x).

Un proces al cărui părinte s­a terminat poartă numele de proces orfan. Acest proces este adoptat
automat de către procesul init, dar poartă denumirea de orfan în continuare deoarece procesul care
l­a creat inițial nu mai există.

Un proces finalizat al cărui părinte nu a citit (încă) statusul terminării acestuia poartă numele de
proces zombie. Procesul intră într­o stare de terminare, iar informația continuă să existe în tabela de
procese astfel încât să ofere părintelui posibilitatea de a verifica codul cu care s­a finalizat procesul. În
momentul în care părintele apelează funcția wait, informația despre proces dispare. Orice proces copil
o să treacă prin starea de proces zombie la terminare.

Pentru terminarea unui alt proces din sistem, se va trimite un semnal către procesul respectiv prin
intermediul apelului de sistem kill [http://linux.die.net/man/2/kill]. Mai multe detalii despre kill și
semnale în laboratorul de semnale.

Exemplu (my_system)
my_system.c

#include <stdlib.h> 
#include <stdio.h> 
  
#include <sys/types.h> 
#include <sys/wait.h> 
#include <unistd.h> 
  
int my_system(const char *command) 

  pid_t pid; 
  int status; 
  const char *argv[] = {command, NULL};
  
  pid = fork(); 
  switch (pid) { 
  case ‐1: 
    /* error forking */ 
    return EXIT_FAILURE; 
  case 0: 
    /* child process */ 
    execvp(command, (char *const *) argv); 
  
    /* only if exec failed */ 
    exit(EXIT_FAILURE); 
  default: 
    /* parent process */ 
    break; 
  } 
  
  /* only parent process gets here */ 
  waitpid(pid, &status, 0); 
  if (WIFEXITED(status)) 
    printf("Child %d terminated normally, with code %d\n", 

      pid, WEXITSTATUS(status)); 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 4/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]
      pid, WEXITSTATUS(status)); 
  
  return status; 

  
int main(void) { 
  my_system("ls"); 
  return 0; 
}

Copierea descriptorilor de fișier

dup [http://linux.die.net/man/2/dup] duplică descriptorul de fișier oldfd și întoarce noul descriptor de
fișier, sau ‐1 în caz de eroare:

int dup(int oldfd);

dup2 [http://linux.die.net/man/2/dup] duplică descriptorul de fișier oldfd în descriptorul de fișier newfd;
dacă newfd există, mai întâi va fi închis. Întoarce noul descriptor de fișier, sau ‐1 în caz de eroare:

int dup2(int oldfd, int newfd);

Descriptorii de fișier sunt, de fapt, indecși în tabela de fișiere deschise. Tabela este populată cu pointeri
către structuri cu informațiile despre fișiere. Duplicarea unui descriptor de fișier înseamnă duplicarea
intrării din tabela de fișiere deschise (adică 2 pointeri de la poziții diferite din tabelă vor indica spre
aceeași structură din sistem, asociată fișierului). Din acest motiv, toate informațiile asociate unui fișier
(lock­uri, cursor, flag­uri) sunt partajate de cei doi file descriptori. Aceasta înseamnă că operațiile ce
modifică aceste informații pe unul dintre file descriptori (de ex. lseek) sunt vizibile și pentru celălalt
file descriptor (duplicat).

Flag­ul CLOSE_ON_EXEC nu este partajat (acest flag nu este ținut în structura menționată mai sus).

Moștenirea descriptorilor de fișier după operații fork/exec

Descriptorii de fișier ai procesului părinte se moștenesc în procesul copil în urma apelului fork. După
un apel exec, descriptorii de fișier sunt păstrați, excepție făcând cei care au flag­ul CLOSE_ON_EXEC
setat.

fcntl

Pentru a seta flag­ul CLOSE_ON_EXEC se folosește funcția fcntl [http://linux.die.net/man/3/fcntl], cu un
apel de forma:

fcntl(file_descriptor, F_SETFD, FD_CLOEXEC);

O_CLOEXEC

fcntl poate activa flag­ul FD_CLOEXEC doar pentru descriptori de fișier deja existenți. În aplicații cu
mai multe fire de execuție, între crearea unui descriptor de fișier și un apel fcntl se poate interpune
un apel exec pe un alt fir de execuție.

/ * THREAD 1 */                   |/ * THREAD 2 */     
fd = op_creare_fd()               | 
                                  | exec(...) 
fcntl(fd, F_SETFD, FD_CLOEXEC);   |

Cum, implicit, descriptorii de fișiere sunt moșteniți după un apel exec, deși programatorul a dorit ca
acesta să nu poată fi accesat după exec, nu poate preveni apariția unui apel exec între creare și
fcntl.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 5/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

Pentru a rezolva această condiție de cursă, s­au introdus în Linux 2.6.27 (2008) versiuni noi ale unor
apeluri de sistem:

int dup3(int oldfd, int newfd, int flags); 
int pipe2(int pipefd[2], int flags); 
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

Aceste variante ale apelurilor de sistem adaugă câmpul flags, prin care se poate specifica
O_CLOEXEC, pentru a crea și activa CLOSE_ON_EXEC în mod atomic. Numărul din numele apelului de
sistem, specifică numărul de parametri ai apelului.

Apelurile de sistem care creează descriptori de fișiere care primeau deja un parametru flags (e.g.
open) au fost doar extinse să accepte și O_CLOEXEC.

Variabile de mediu în Linux
În cadrul unui program se pot accesa variabilele de mediu, prin evidențierea celui de­al treilea
parametru (opțional) al funcției main, ca în exemplul următor:

int main(int argc, char **argv, char **environ)

Acesta desemnează un vector de pointeri la șiruri de caractere, ce conțin variabilele de mediu și
valorile lor. Șirurile de caractere sunt de forma VARIABILA=VALOARE. Vectorul e terminat cu NULL.

getenv [http://linux.die.net/man/3/getenv] întoarce valoarea variabilei de mediu denumite name, sau NULL
dacă nu există o variabilă de mediu denumită astfel:

char* getenv(const char *name);

setenv [http://linux.die.net/man/3/setenv] adaugă în mediu variabila cu numele name (dacă nu există
deja) și îi setează valoarea la value. Dacă variabila există și replace e 0, acțiunea de setare a
valorii variabilei e ignorată; dacă replace e diferit de 0, valoarea variabilei devine value:

int setenv(const char *name, const char *value, int replace);

unsetenv [http://linux.die.net/man/3/unsetenv] șterge din mediu variabila denumită name:

int unsetenv(const char *name);

Pipe­uri în Linux

Pipe­uri anonime în Linux
Pipe­ul este un mecanism de comunicare unidirecțională între două procese. În majoritatea
implementărilor de UNIX, un pipe apare ca o zonă de memorie de o anumită dimensiune în spațiul
nucleului. Procesele care comunică printr­un pipe anonim trebuie să aibă un grad de rudenie; de obicei,
un proces care creează un pipe va apela după aceea fork, iar pipe­ul se va folosi pentru comunicarea
între părinte și fiu. În orice caz, procesele care comunică prin pipe­uri anonime nu pot fi create de
utilizatori diferiți ai sistemului.

Apelul de sistem pentru creare este pipe [http://linux.die.net/man/2/pipe]:

int pipe(int filedes[2]);

Vectorul filedes conține după execuția
funcției 2 descriptori de fișier:

filedes[0], deschis pentru citire;
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 6/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

filedes[1], deschis pentru scriere;

Mnemotehnică: STDIN_FILENO este 0 (citire), STDOUT_FILENO este 1 (scriere).

Observații:

1)
citirea/scrierea din/în pipe­uri este atomică dacă nu se citesc/scriu mai mult de PIPE_BUF
octeți.
citirea/scrierea din/în pipe­uri se realizează cu ajutorul funcțiilor read/write.

Majoritatea aplicațiilor care folosesc pipe­uri închid în fiecare dintre procese capătul de pipe neutilizat
în comunicarea unidirecțională. Dacă unul dintre descriptori este închis se aplică regulile:

o citire dintr­un pipe pentru care descriptorul de scriere a fost închis, după ce toate datele au
fost citite, va returna 0, ceea ce indică sfârșitul fișierului. Descriptorul de scriere poate fi
duplicat astfel încât mai multe procese să poată scrie în pipe. De regulă, în cazul pipe­urilor
anonime există doar două procese, unul care scrie și altul care citește, pe când în cazul
fișierelor pipe cu nume (FIFO) pot exista mai multe procese care scriu date.
o scriere într­un pipe pentru care descriptorul de citire a fost închis cauzează generarea
semnalului SIGPIPE. Dacă semnalul este captat și se revine din rutina de tratare, funcția de
sistem write returnează eroare și variabila errno are valoarea EPIPE.

Cea mai frecventă greșeală, relativ la lucrul cu pipe­urile, provine din neglijarea faptului că nu se
trimite EOF prin pipe (citirea din pipe nu se termină) decât dacă sunt închise TOATE capetele de
scriere din TOATE procesele care au deschis descriptorul de scriere în pipe (în cazul unui fork, nu
uitați să închideți capetele pipe­ului în procesul părinte).

Alte funcții utile: popen [http://linux.die.net/man/3/popen], pclose [http://linux.die.net/man/3/pclose].

Pipe­uri cu nume în Linux
Elimină necesitatea ca procesele care comunică să fie înrudite. Astfel, fiecare proces își poate deschide
pentru citire sau scriere fișierul pipe cu nume (FIFO), un tip de fișier special, care păstrează în spate
caracteristicile unui pipe. Comunicația se face într­un sens sau în ambele sensuri. Fișierele de tip FIFO
pot fi identificate prin litera p în primul câmp al drepturilor de acces (ls ‐l).

Apelul de bibliotecă pentru crearea pipe­urilor de tip FIFO este mkfifo [http://linux.die.net/man/3/mkfifo]:

int mkfifo(const char *pathname, mode_t mode);

După ce pipe­ul FIFO a fost creat, acestuia i se pot aplica toate funcțiile pentru operații obișnuite
pentru lucrul cu fișiere: open, close, read, write.

Modul de comportare al unui pipe FIFO după deschidere este afectat de flagul O_NONBLOCK:

dacă O_NONBLOCK nu este specificat (cazul normal), atunci un open pentru citire se va bloca
până când un alt proces deschide același FIFO pentru scriere. Analog, dacă deschiderea este
pentru scriere, se poate produce blocare până când un alt proces efectuează deschiderea pentru
citire.
dacă se specifică O_NONBLOCK, atunci deschiderea pentru citire revine imediat, dar o
deschidere pentru scriere poate returna eroare cu errno având valoarea ENXIO, dacă nu există
un alt proces care a deschis același FIFO pentru citire.

Atunci când se închide ultimul descriptor de fișier al capătului de scriere pentru un FIFO, se generează
un „sfârșit de fișier” – EOF – pentru procesul care citește din FIFO.

Depanarea unui proces
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 7/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

Informații suplimentare legate de depanarea unui proces se găsesc aici

Procese în Windows

Crearea unui proces
În Windows, atât crearea unui nou proces, cât și înlocuirea imaginii lui cu cea dintr­un program
executabil se realizează prin apelul funcției CreateProcess [http://msdn.microsoft.com/en­
us/library/ms682425.aspx].

BOOL CreateProcess(  BOOL bRes = CreateProcess( 
     LPCTSTR               lpApplicationName,       NULL,          // No module name 
     LPTSTR                lpCommandLine,       "notepad.exe"  // Command line 
     LPSECURITY_ATTRIBUTES lpProcessAttributes,       NULL,          // Process handle not inheritable 
     LPSECURITY_ATTRIBUTES lpThreadAttributes,       NULL,          // Thread handle not inheritable 
     BOOL                  bInheritHandles,       FALSE,         // Set handle inheritance to false 
     DWORD                 dwCreationFlags,       0,             // No creation flags 
     LPVOID                lpEnvironment,       NULL,          // Use parent's environment block 
     LPCTSTR               lpCurrentDirectory,       NULL,          // Use parent's starting directory 
     LPSTARTUPINFO         lpStartupInfo,       &si,           // Pointer to STARTUPINFO structure 
     LPPROCESS_INFORMATION lpProcessInformation       &pi            // Pointer to PROCESS_INFORMATION 
); );                  // structure

API­ul Windows mai pune la dispoziție câteva funcții înrudite precum CreateProcessAsUser
[http://msdn.microsoft.com/en­us/library/ms682429%28VS.85%29.aspx], CreateProcessWithLogonW
[http://msdn.microsoft.com/en­us/library/ms682431%28VS.85%29.aspx] ori CreateProcessWithTokenW
[http://msdn.microsoft.com/en­us/library/ms682434%28VS.85%29.aspx], care permit crearea unui proces
într­un context de securitate diferit de cel al utilizatorului curent.

Pentru a se obține un handle al unui proces, cunoscându­se PID­ul procesului respectiv, se va apela
funcția OpenProcess [http://msdn.microsoft.com/en­us/library/ms684320(VS.85).aspx]:

HANDLE OpenProcess( 
        DWORD dwDesiredAccess, 
        BOOL  bInheritHandle, 
        DWORD dwProcessId 
);

iar pentru a obține un handle al procesului curent se va apela GetCurrentProcess
[http://msdn.microsoft.com/en­us/library/ms683179(VS.85).aspx]:

HANDLE GetCurrentProcess(void);

Pentru a obține PID­ul procesului curent se va apela GetCurrentProcessId [http://msdn.microsoft.com/en­
us/library/ms683180(VS.85).aspx]:

DWORD GetCurrentProcessId(void);

Spre deosebire de Linux, în Windows nu se impune o ierarhie a proceselor în sistem. Teoretic există o
ierarhie implicită din modul cum sunt create procesele. Un proces deține handle­uri ale proceselor
create de el, însă handle­urile pot fi duplicate între procese ceea ce duce la situația în care un proces
deține handle­uri ale unor procese care nu sunt create de el, deci ierarhia implicită dispare.

Deoarece funcția CreateProcess [http://msdn.microsoft.com/en­us/library/ms682425.aspx] se întoarce
imediat, fără a aștepta ca procesul nou creat să­și termine inițializările, este nevoie de un mecanism
prin care procesul părinte să se sincronizeze cu procesul copil înainte de a încerca să comunice cu
acesta. Windows pune la dispoziție funcția de așteptare WaitForInputIdle [http://msdn.microsoft.com/en­
us/library/ms687022.aspx].

DWORD WaitForInputIdle( 
        HANDLE hProcess, 

        DWORD  dwMilliseconds 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 8/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]
        DWORD  dwMilliseconds 
);

Funcția va cauza blocarea firului de execuție apelant până în momentul în care procesul hProcess și­a
terminat inițializarea și așteaptă date de intrare. Funcția poate fi folosită oricând pentru a aștepta ca
procesul hProcess să treacă în starea în care așteaptă date de intrare, nu doar la momentul creării
sale. Funcției i se poate specifica o durată de așteptare prin intermediul parametrului
dwMilliseconds.

Așteptarea terminării unui proces
Pentru a suspenda execuția procesului curent până când unul sau mai multe alte procese se termină, se
va folosi una din funcțiile de așteptare WaitForSingleObject [http://msdn.microsoft.com/en­
us/library/ms687032.aspx] ori WaitForMultipleObjects [http://msdn.microsoft.com/en­
us/library/ms687025.aspx].

DWORD WaitForSingleObject(HANDLE hHandle, DWORD  dwMilliseconds);

Exemplul următor așteaptă nedefinit terminarea procesului reprezentat de hProcess.

DWORD dwRes = WaitForSingleObject(hProcess, INFINITE); 
if (dwRes == WAIT_FAILED) 
        // handle error

Funcțiile de așteptare sunt folosite în cadrul mai general al mecanismelor de sincronizare între procese.
Mai multe detalii pot fi găsite aici.

Aflarea codului de terminare a procesului așteptat
Pentru a determina codul de eroare cu care s­a terminat un anumit proces, se va apela funcția
GetExitCodeProcess [http://msdn.microsoft.com/en­us/library/ms683189.aspx]:

BOOL GetExitCodeProcess(HANDLE  hProcess, LPDWORD lpExitCode);

Dacă procesul hProcess nu s­a terminat încă, funcția va întoarce în  lpExitCode codul de terminare
STILL_ACTIVE. Dacă procesul s­a terminat, se va întoarce codul său de terminare care poate fi:

parametrul transmis uneia din funcțiile ExitProcess [http://msdn.microsoft.com/en­
us/library/ms682658(VS.85).aspx] sau TerminateProcess [http://msdn.microsoft.com/en­
us/library/ms686714(VS.85).aspx] (exit din libc)
valoarea returnată de funcția main sau WinMain a procesului
codul de eroare al unei excepții netratate care a cauzat terminarea procesului
dacă procesul se termină cu succes valoarea întoarsă în lpExitCode va fi 0 sau 1 în caz de
eroare.

Terminarea unui proces
Pentru terminarea procesului curent, Windows API pune la dispoziție funcția ExitProcess
[http://msdn.microsoft.com/en­us/library/ms682658(VS.85).aspx].

void ExitProcess(UINT uExitCode);

Consecinţele funcţiei ExitProcess sunt:

Procesul apelant și toate firele sale de execuție se vor termina imediat.
Toate DLL­urile de care era atașat procesul sunt notificate și se apelează metode de distrugere a
resurselor alocate de acestea în spațiul de adresă al procesului.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 9/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

Toți descriptorii de resurse (handle) ai procesului sunt închiși.

ExitProcess nu se ocupă de eliberarea resurselor bibliotecii standard C. Pentru a asigura o finalizare
corectă a programului trebuie apelat exit.

Pentru terminarea unui alt proces din sistem se va apela funcția TerminateProcess
[http://msdn.microsoft.com/en­us/library/ms686714(VS.85).aspx].

BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);

Funcţia TerminateProcess se ocupă de:

A iniția terminarea procesului hProcess și a tuturor firelor sale de execuție. Se vor revoca
operațiile de intrare/ieșire neterminate după care funcția TerminateProcess
[http://msdn.microsoft.com/en­us/library/ms686714(VS.85).aspx] va întoarce imediat.
A închide toți descriptorii de resurse (handle) ai procesului.

Funcția TerminateProcess [http://msdn.microsoft.com/en­us/library/ms686714(VS.85).aspx] este periculoasă
și se recomandă folosirea ei doar în cazuri extreme, deoarece ea nu notifică DLL­urile de care este
atașat procesul hProcess asupra detașării acestuia, lăsând astfel alocate eventualele date rezervate
de DLL în spațiul de adrese al procesului.

Terminarea unui proces nu implică terminarea proceselor create de acesta.

Exemplu
exec.c

#include <windows.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
  
#include "utils.h" 
  
void CloseProcess(LPPROCESS_INFORMATION lppi) { 
    CloseHandle(lppi‐>hThread); 
    CloseHandle(lppi‐>hProcess); 

  
int main(void) 

    STARTUPINFO si; 
    PROCESS_INFORMATION pi; 
    DWORD dwRes; 
    BOOL bRes; 
    CHAR cmdLine[] = "mspaint"; 
  
    ZeroMemory(&si, sizeof(si)); 
    si.cb = sizeof(si); 
    ZeroMemory(&pi, sizeof(pi)); 
  
    /* Start child process */ 
    bRes =  CreateProcess(  
                NULL,           /* No module name (use command line) */ 
    cmdLine,        /* Command line */ 
    NULL,           /* Process handle not inheritable */ 
    NULL,           /* Thread handle not inheritable */ 
    FALSE,          /* Set handle inheritance to FALSE */ 
    0,              /* No creation flags */ 
    NULL,           /* Use parent's environment block */ 
    NULL,           /* Use parent's starting directory */  
    &si,            /* Pointer to STARTUPINFO structure */ 
    &pi             /* Pointer to PROCESS_INFORMATION structure */ 
  );  
    DIE(bRes == FALSE, "CreateProcess"); 
  
    /* Wait for the child to finish */ 
    dwRes = WaitForSingleObject(pi.hProcess, INFINITE); 
    DIE(dwRes == WAIT_FAILED, "WaitForSingleObject"); 
  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 10/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]
  
    bRes = GetExitCodeProcess(pi.hProcess, &dwRes); 
    DIE(bRes == FALSE, "GetExitCode");
  
    return 0; 
}

Moștenirea handle­urilor la CreateProcess
După un apel CreateProcess [http://msdn.microsoft.com/en­us/library/ms682425%28VS.85%29.aspx], handle­
urile din procesul părinte pot fi moștenite în procesul copil.

Pentru ca un handle să poată fi moștenit în procesul creat, trebuie îndeplinite 2 condiții:

membrul bInheritHandle, al structurii SECURITY_ATTRIBUTES, transmise lui CreateFile
[http://msdn.microsoft.com/en­us/library/aa363858%28VS.85%29.aspx], trebuie să fie TRUE
parametrul bInheritHandles, al lui CreateProcess [http://msdn.microsoft.com/en­
us/library/ms682425%28VS.85%29.aspx], trebuie să fie TRUE.
atunci când se doreşte moştenierea handler­elor în procesul copil, trebuie să ne asigurăm că
acestea sunt valide deorece în procesul copil nu se fac validări suplimentare. Transmiterea unor
handlere invalide poate duce la un comportament nedefinit în procesul copil.

Handle­urile moștenite sunt valide doar în contextul procesului copil.

Cei 3 descriptori speciali de fișier pot fi obținuți apelând funcția GetStdHandle
[http://msdn.microsoft.com/en­us/library/ms683231(VS.85).aspx]:

HANDLE GetStdHandle(DWORD nStdHandle);

cu unul din parametrii:

STD_INPUT_HANDLE
STD_OUTPUT_HANDLE
STD_ERROR_HANDLE

Pentru redirectarea handle­urilor standard în procesul copil puteți folosi membrii hStdInput,
hStdOutput, hStdError ai structurii STARTUPINFO [http://msdn.microsoft.com/en­
us/library/ms686331(v=VS.85).aspx], transmise lui CreateProcess [http://msdn.microsoft.com/en­
us/library/ms682425%28VS.85%29.aspx]. În acest caz, membrul dwFlags al aceleiași structuri trebuie
setat la STARTF_USESTDHANDLES. Dacă se dorește ca anumite handle­uri să rămână implicite, li se
poate atribui handle­ul întors de GetStdHandle [http://msdn.microsoft.com/en­
us/library/ms683231(VS.85).aspx].

STARTUPINFO si; 
... 
/* initialize process startup info structure */ 
ZeroMemory(&si, sizeof(si)); 
si.cb = sizeof(si); 
  
/* setup flags to allow handle inheritence (redirection) */ 
si.dwFlags |= STARTF_USESTDHANDLES;

Pentru a realiza redirectarea in mod corespunzător, câmpurile hStdInput, hStdOutput, hStdError
din structura STARTUPINFO trebuie inițializate.

Alte proprietăți ale procesului părinte care pot fi moștenite sunt variabilele de mediu și directorul
curent. Nu vor fi moștenite handle­uri ale unor zone de memorie alocate de procesul părinte și nici
pseudo­descriptori precum cei întorși de funcția GetCurrentProcess [http://msdn.microsoft.com/en­
us/library/ms683179%28VS.85%29.aspx].

Handle­ul din procesul părinte și cel moștenit în procesul copil vor referi același obiect, exact ca în
cazul duplicării. De asemenea, handle­ul moștenit în procesul copil are aceeași valoare și aceleași
drepturi de acces ca și handle­ul din procesul părinte. Pentru a folosi handle­ul moștenit, procesul copil
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 11/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

va trebui să­i cunoască valoarea și ce obiect referă. Aceste informații trebuie să fie pasate de părinte
printr­un mecanism extern (IPC etc).

Variabile de mediu în Windows
Pentru a afla valoarea unei variabile de mediu se va apela funcția GetEnvironmentVariable
[http://msdn.microsoft.com/en­us/library/ms683188(VS.85).aspx]:

DWORD GetEnvironmentVariable( 
        LPCTSTR lpName, 
        LPTSTR  lpBuffer, 
        DWORD   nSize 
);

care va umple lpBuffer, de dimensiune nSize, cu valoarea variabilei lpName.

Pentru a seta o variabilă de mediu se va apela SetEnvironmentVariable [http://msdn.microsoft.com/en­
us/library/ms686206(VS.85).aspx]:

BOOL SetEnvironmentVariable( 
        LPCTSTR lpName, 
        LPCTSTR lpValue 
);

care va seta variabila lpName la valoarea specificată de lpValue. Funcția se va folosi și pentru
ștergerea unei variabile de mediu prin transmiterea unui parametru lpValue = NULL.
SetEnvironmentVariable [http://msdn.microsoft.com/en­us/library/ms686206(VS.85).aspx] are efect doar
asupra variabilelor de mediu ale utilizatorului și nu poate modifica variabile de mediu globale.

În Windows există un set de variabile de mediu globale, valabile pentru toți utilizatorii. În plus, fiecare
utilizator în parte are asociat un set propriu de variabile de mediu. Împreună, cele două seturi
formează Environment Block­ul utilizatorului respectiv. Acest Environment Block este similar cu
variabila environ, din Linux.

Un utilizator are acces la propriul Environment Block prin apelul funcției GetEnvironmentStrings
[http://msdn.microsoft.com/en­us/library/ms683187(VS.85).aspx]:

LPTCH GetEnvironmentStrings(void);

care îi va întoarce un pointer spre acesta, pe care îl poate elibera cu FreeEnvironmentStrings
[http://msdn.microsoft.com/en­us/library/ms683151(VS.85).aspx]:

BOOL FreeEnvironmentStrings( 
        LPTSTR lpszEnvironmentBlock 
);

Un proces copil va moșteni Environment Block­ul părintelui dacă acesta apelează CreateProcess,
cu parametrul lpEnvironment = NULL.

Se poate obține Environment Block­ul unui alt utilizator prin intermediul funcției
CreateEnvironmentBlock [http://msdn.microsoft.com/en­us/library/bb762270(VS.85).aspx]:

BOOL CreateEnvironmentBlock( 
        LPVOID* lpEnvironment, 
        HANDLE  hToken, 
        BOOL    bInherit 
);

Trebuie să pasăm hToken, token­ul asociat utilizatorului al cărui bloc vrem să­l aflăm, pe care putem
să­l obținem prin apelarea funcției LogonUser [http://msdn.microsoft.com/en­us/library/aa378184.aspx]:

BOOL LogonUser( 
        LPTSTR  lpszUsername, 
        LPTSTR  lpszDomain, 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 12/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]
        LPTSTR  lpszDomain, 
        LPTSTR  lpszPassword, 
        DWORD   dwLogonType, 
        DWORD   dwLogonProvider, 
        PHANDLE phToken 
);

Și, bineînțeles, trebuie cunoscută parola utilizatorului respectiv.

Environment Block­ul, obținut prin CreateEnvironmentBlock, poate fi transmis ca parametru
funcției CreateProcessAsUser [http://msdn.microsoft.com/en­us/library/ms682429(VS.85).aspx], și se va
distruge prin apelul funcției DestroyEnvironmentBlock [http://msdn.microsoft.com/en­
us/library/bb762274(VS.85).aspx]:

BOOL DestroyEnvironmentBlock( 
        LPVOID lpEnvironment 
);

Pipe­uri in Windows

Pipe­uri anonime în Windows
Ca și pe Linux, pipe­urile anonime de pe Windows sunt unidirecționale. Fiecare pipe are două capete
reprezentate de câte un handle: un handle de citire și un handle de scriere. Funcția de creare a unui
pipe este CreatePipe [http://msdn.microsoft.com/en­us/library/aa365152(VS.85).aspx]:

BOOL CreatePipe(  CreatePipe( 
     PHANDLE               hReadPipe,      &hReadPipe,  
     PHANDLE               hWritePipe,      &hWritePipe, 
     LPSECURITY_ATTRIBUTES lpPipeAttributes,      &sa,        //pentru moștenire sa.bInheritHandle=TRUE 
     DWORD                 nSize      0           //dimensiunea default pentru pipe 
); );

Pentru a moșteni un pipe anonim, este nevoie ca parametrul bInheritHandle din structura
LPSECURITY_ATTRIBUTES [http://msdn.microsoft.com/en­us/library/aa379560%28v=VS.85%29.aspx] să fie
setat pe TRUE.

CreatePipe creează atât pipe­ul, cât și handler­urile folosite pentru scriere/citire din/în pipe cu
ajutorul funcțiilor ReadFile [http://msdn.microsoft.com/en­us/library/aa914377.aspx] și WriteFile
[http://msdn.microsoft.com/en­us/library/aa910675.aspx].

ReadFile [http://msdn.microsoft.com/en­us/library/aa914377.aspx] se termină în unul din cazurile:

o operație de scriere a luat sfârșit la capătul de scriere în pipe
numărul de octeți cerut a fost citit
a apărut o eroare.

WriteFile [http://msdn.microsoft.com/en­us/library/aa910675.aspx] se termină atunci când toți octeții au fost
scriși. Dacă bufferul pipe­ului este plin înainte ca toți octeții să fie scriși, WriteFile
[http://msdn.microsoft.com/en­us/library/aa910675.aspx] rămâne blocat până când alt proces sau thread
folosește ReadFile [http://msdn.microsoft.com/en­us/library/aa914377.aspx] pentru a face loc în buffer.

Pipe­urile anonime sunt implementate folosind un pipe cu nume unic. De aceea se poate pasa un handle
al unui pipe anonim unei funcții care cere un handle al unui pipe cu nume.

Pipe­uri cu nume în Windows
În Windows, un pipe cu nume este un pipe unidirecțional (inbound ori outbound) sau bidirecțional ce
realizează comunicația între un server pipe și unul sau mai mulți clienți pipe. Se numește server pipe
procesul care creează un pipe cu nume și client pipe procesul care se conectează la pipe. Pentru a face

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 13/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

posibilă comunicarea între server și mai mulți clienți prin același pipe, se folosesc instanțe ale pipe­
ului. O instanță a unui pipe folosește același nume, dar are propriile handle­uri și buffere.

Pipe­urile cu nume au următoarele caracteristici care le diferențiază de cele anonime:

sunt orientate pe mesaje ­ se pot transmite mesaje de lungime variabilă (nu numai byte
stream);
sunt bidirecționale ­ două procese pot schimba mesaje pe același pipe;
pot exista mai multe instanțe ale aceluiași pipe
poate fi accesat din rețea ­ comunicația între două procese aflate pe mașini diferite este aceeași
cu cea între procese aflate pe aceeași mașină.

Mod de lucru ­ Server Pipe

Serverul creează un pipe cu funcția CreateNamedPipe [http://msdn.microsoft.com/en­
us/library/aa365150%28VS.85%29.aspx].

HANDLE CreateNamedPipe(  HANDLE hNamedPipe = CreateNamedPipe( 
   LPCTSTR               lpName,     "\\\\.\\pipe\\mypipe",     // name 
   DWORD                 dwOpenMode,     PIPE_ACCESS_DUPLEX,        // read/write access 
   DWORD                 dwPipeMode,     PIPE_TYPE_BYTE | PIPE_WAIT,// byte stream 
   DWORD                 nMaxInstances,     PIPE_UNLIMITED_INSTANCES,  // max. instances   
   DWORD                 nOutBufferSize,     BUFSIZE,                   // output buffer size  
   DWORD                 nInBufferSize,     BUFSIZE,                   // input buffer size  
   DWORD                 nDefaultTimeOut,     0,                         // default time out 
   LPSECURITY_ATTRIBUTES lpSecurityAttributes     NULL                       // default security  
); );                            // attribute 

Funcția returnează un handle către capătul serverului la pipe. Acest handle poate fi transmis funcției
ConnectNamedPipe [http://msdn.microsoft.com/en­us/library/aa365146(VS.85).aspx] pentru a aștepta
conectarea unui proces client la o instanță a unui pipe.

BOOL ConnectNamedPipe(HANDLE hNamedPipe, LPOVERLAPPED lpOverlapped);

Mod de lucru ­ Client Pipe

Un client se conectează transmițând numele pipe­ului la una din funcțiile CreateFile
[http://msdn.microsoft.com/en­us/library/aa363858%28VS.85%29.aspx] sau CallNamedPipe
[http://msdn.microsoft.com/en­us/library/aa365144(VS.85).aspx] ­ ultima funcţie este mai utilă pentru
transmiterea de mesaje.

Un exemplu funcțional folosind pipe­uri cu nume se află aici [http://msdn.microsoft.com/en­
us/library/aa365588%28v=VS.85%29.aspx]

Mai multe detalii despre moștenirea pipe­urilor se pot găsi aici [http://msdn.microsoft.com/en­
us/library/windows/desktop/aa365782(v=vs.85).aspx].

Moduri de comunicare

Comunicația prin pipe­urile cu nume poate fi de tip:

mesaj
se scriu/citesc date sub formă de mesaje;
este necesară cunoașterea lungimii mesajului;
se scriu/citesc doar mesaje complete;
creat cu PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE.
flux de octeți
nu există nicio garanție asupra numărului de octeți care sunt citiți/scriși în orice
moment;

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 14/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

se pot transmite date fără să se țină seama de conținut, pe când, prin pipe­urile de tip
mesaj, comunicația are loc în unități discrete (mesaje);
creat cu PIPE_TYPE_BYTE | PIPE_READMODE_BYTE (implicit).

Numire

Crearea unui pipe cu nume se poate face numai pe mașina locală (reprezentată prin primul .) cu un
string de forma:

\\.\pipe\[path]pipename

Accesarea unui pipe cu nume se poate face folosind ca parametru un string de forma:

\\servername\pipe\[path]pipename

Funcții utile

GetNamedPipeHandleState [http://msdn.microsoft.com/en­us/library/aa365443%28VS.85%29.aspx] ­ întoarce
informații despre anumite atribute cum ar fi: tipul de comunicare (mesaj sau byte­stream), numărul
de instanțe, dacă a fost deschisă în mod blocant sau neblocant.

SetNamedPipeHandleState [http://msdn.microsoft.com/en­us/library/aa365787%28VS.85%29.aspx] ­ permite
modificarea atributelor unui pipe

Exerciții
În rezolvarea laboratorului folosiți arhiva de sarcini lab03­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab03­tasks.zip]

Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din arhivă există un
fișier utils.h cu funcții utile.

Exercițiul ­1 ­ GSOC (0p)
Google Summer of Code este un program de vară în care studenții (indiferent de anul de studiu) sunt
implicați în proiecte Open Source pentru a își dezvolta skill­urile de programare, fiind răsplătiți cu o
bursă a cărei valoare depinde de țară [https://developers.google.com/open­source/gsoc/help/student­stipends]
(pagină principală GSOC [https://developers.google.com/open­source/gsoc]).

UPB se află în top ca număr de studenți acceptați; în fiecare an fiind undeva la aprox. 30­40 de studenți
acceptați. Vă încurajăm să aplicați! Există și un grup de fb cu foști participanți unde puteti să îi
contactați pentru sfaturi facebook page [https://www.facebook.com/groups/240794072931431/]

Exercițiul 0 ­ Joc interactiv (2p)

Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (5p)

Exercițiul 1 ­ system (1.5p)

Intrați în directorul 1‐system. Programul my_system.c execută o comandă transmisă ca parametru,
folosind funcția de bibliotecă system [http://linux.die.net/man/3/system]. Modul de funcționare al system
[http://linux.die.net/man/3/system] este următorul:

se creează un nou proces cu fork [http://linux.die.net/man/2/fork]

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 15/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

procesul copil execută, folosind execve [http://linux.die.net/man/2/execve], programul sh cu
argumentele ­c “comanda”, timp în care procesul părinte așteaptă terminarea procesului copil.

Compilați (folosind make) și rulați programul dând ca parametru o comandă.

Exemplu:

 ./my_system pwd 

Cum procedați pentru a trimite mai mulți parametri unei comenzi? (ex: ls ‐la)

Pentru a vedea câte apeluri de sistem execve [http://linux.die.net/man/2/execve] se realizează, rulați:

 strace ‐e execve,clone ‐ff ‐o output ./my_system ls 

atenție! nu este spațiu după virgulă în argumentul execve,clone
argumentul ‐ff însoțit de ‐o output generează câte un fișier de output pentru fiecare proces.
cițiți pagina de manual strace [http://linux.die.net/man/1/strace]

Revedeți secțiunea Înlocuirea imaginii unui proces și pagina de manual pentru execve
[http://linux.die.net/man/2/execve].

Exercițiul 2 ­ orphan (1p)

Intrați în directorul 2‐orphan și inspectați sursa orphan.c.

Compilați programul (make) și apoi rulați­l folosind comanda:

 ./ 
orphan 

Deschideți alt terminal și rulați comanda:

 watch ps ‐al 

Observați că pentru procesul indicat de executabilul  
orphan (coloana  
CMD ),  pid­ul procesului părinte
(coloana  
PPID ) devine 1, întrucât procesul este adoptat de  
init după terminarea procesului său
părinte.

Exercițiul 3 ­ Tiny­Shell (2.5p)

Intrați în directorul 3‐tiny.

Următoarele subpuncte au ca scop implementarea unui shell minimal, care oferă suport pentru execuția
unei singure comenzi externe cu argumente multiple și redirectări. Shell­ul trebuie să ofere suport
pentru folosirea și setarea variabilelor de mediu.

Observație: Pentru a ieși din tiny shell folosiți exit sau CTRL+D.

3a. Execuția unei comenzi simple (0.5p)
Creați un nou proces care să execute o comandă simplă.

Funcția simple_cmd primește ca argument un vector de șiruri ce conține comanda și parametrii
acesteia.

Citiți exemplul my_system și urmăriți în cod comentariile cu TODO 1. Pentru testare puteți folosi
comenzile:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 16/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

 ./tiny 
> pwd 
> ls ‐al 
> exit 

3b. Adăugare suport pentru setarea și expandarea variabilelor de mediu (1p)

Trebuie să completați funcțiile set_var și expand; acestea sunt apelate deja atunci când se face
parsarea liniei de comandă. Verificarea erorilor trebuie făcută în aceaste funcții.

Urmăriți în cod comentariile cu TODO 2.
Citiți secțiunea Variabile de mediu in Linux.
Pentru testare puteți folosi comenzile:

 ./tiny
> echo $HOME 
> name=Makefile  
> echo $name  

3c. Redirectarea ieșirii standard (1p)

Completați funcția do_redirect astfel încât tiny­shell trebuie să suporte redirectarea output­ului unei
comenzi (stdout) într­un fișier.

Dacă fișierul indicat de filename nu există, va fi creat. Dacă există, trebuie trunchiat.

Citiți secțiunea Copierea descriptorilor de fișier și urmăriți în cod comentariile cu TODO 3. Pentru
testare puteți folosi comenzile:

 ./tiny  
> ls ‐al > out 
> cat out

Windows (4p)
Pentru exerciţiul Tiny­Shell on Windows compilarea se va realiza din Visual Studio sau din
command­prompt­ul de Visual Studio, iar rularea executabilului tiny.exe se va realiza din Cygwin.

Pentru a ajunge din Cygwin pe Desktop (atenție la folosirea apostrofurilor):

$ cd 'C:\Users\Student\Desktop'

Exercițiul 1 ­ Bomb (0.5p)

Deschideți proiectul (fișierul .sln) și compilați primul subproiect: 1‐bomb.

Inspectați sursa 1‐bomb.c. Ce credeți că face? Fork Bomb [https://en.wikipedia.org/wiki/Fork_bomb]

NU executați 1­bomb.exe

Exercițiul 2 ­ Tiny­Shell on Windows (3.5p)
Ne propunem să continuăm implementarea de Tiny­Shell.

Compilarea se va realiza din Visual Studio sau din command­prompt­ul de Visual Studio, iar rularea
executabilului tiny.exe se va realiza din Cygwin.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 17/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

Pentru a ajunge din Cygwin pe Desktop (atenție la folosirea apostrofurilor):

$ cd 'C:\Users\Student\Desktop'

2a. Execuția unei comenzi simple (0.5p)

Partea de execuție a unei comenzi simple și a variabilelor de mediu este deja implementată.

Deschideți fișierul tiny.c din subproiectul 1­tiny. Urmăriți în sursă funcția RunSimpleCommand. Testați
funcționalitatea prin comenzi de tipul:

 ./tiny 
> ls ‐al  
> exit

2b. Redirectare (1.5p)
Realizați redirectarea tuturor HANDLE­relor.

Completați funcția RedirectHandle.

Atenție! Trebuie inițializate toate handle­rele structurii STARTUPINFO.
Urmăriți în cod comentariile cu TODO 1.
Revedeți secțiunea Moștenirea handle­urilor.
Atenție la metoda de moștenire a handle­relor

Pentru testare puteți folosi comenzile:

 ./tiny  
> ls ‐al > out 
> cat out 
> exit

2c. Implementarea unei comenzi cu pipe­uri (1.5p)

Shell­ul vostru trebuie să ofere suport pentru o comandă de forma ' comanda_simpla |
comanda_simpla '.

Urmăriți în cod comentariile cu TODO 2.

Completați funcția PipeCommands.
zeroizaţi structura SECURITY_ATTRIBUTES sa, respectiv structurile PROCESS_INFO pi1,
pi2;
Atenție! În procesul părinte, trebuie închise capetele pipe­urilor.
Pentru redirectari, folosiți­vă de funcția  RedirectHandle.
Revedeți secțiunea despre Pipe­uri anonime în Windows.

Pentru testare puteți folosi comenzile:

 ./tiny  
> cat Makefile | grep tiny 
> exit

BONUS

1 so karma ­ Pipe­uri cu nume (Linux/Windows)

Realizați două programe, denumite server și client, care interacționează printr­un pipe cu nume.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 18/19
6/11/2017 Laborator 03 ­ Procese [CS Open CourseWare]

FIFO−ul se numește myfifo. Dacă nu există, este creat de server.

Serverul trebuie rulat înaintea clientului.
Clientul citește de la intrarea standard un mesaj care va fi transmis serverului.
Serverul va afișa mesajul primit la ieșirea standard.

Linux:

Citiți secțiunea Pipe­uri cu nume în Linux.

Windows:

Citiți secțiunea Pipe­uri cu nume în Windows.
Puteți porni de la exemplul [http://msdn.microsoft.com/en­us/library/aa365588.aspx] din
documentația CreateNamedPipe.
Atenție: Dacă ReadFile întoarce FALSE, iar mesajul de eroare (ce întoarce GetLastError())
este ERROR_BROKEN_PIPE, înseamnă că au fost închise toate capetele de scriere.

1 so karma ­ Magic

Intrați în directorul lin/5‐magic și deschideți sursa magic.c

Completați doar condiția instrucțiunii if pentru a obține la rulare mesajul “Hello World”. Încercați
forțarea afișarea cuvântului “World” înainte de “Hello”. Nu sunt permise alte modificări în funcția
main.

Soluții
lab03­sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab03­sol.zip]

Resurse utile

1. Fork ­ Wikipedia [http://en.wikipedia.org/wiki/Fork_(operating_system)]
2. About Fork and Exec [http://www­h.eng.cam.ac.uk/help/tpl/unix/fork.html]
3. Fork, Exec and Process Control ­ YoLinux Tutorial
[http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html]
4. Windows Handles and Data Types ­ Wikipedia
[http://en.wikibooks.org/wiki/Windows_Programming/Handles_and_Data_Types]
5. MSDN: Processes and Threads [http://msdn.microsoft.com/library/default.asp?url=/library/en­
us/dllproc/base/processes_and_threads.asp]
6. C++ CreateProcess example
[http://www.goffconcepts.com/techarticles/development/cpp/createprocess.html]
7. Windows XP and 2003 Server Boot.ini options [http://support.microsoft.com/kb/833721]

1)
 limită globală setată implicit pe Linux la 4096 bytes
so/laboratoare/laborator­03.txt · Last modified: 2017/03/14 12:51 by ioana_elena.ciornei

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­03 19/19
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Laborator 04 ­ Semnale

Materiale ajutătoare

lab04­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab04­slides.pdf]
lab04­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab04­refcard.pdf]

Nice to read

TLPI ­ Chapter 20, Signals: Fundamental Concepts
TLPI ­ Chapter 21: Signals: Signal Handlers

Semnale în Linux
În lumea reală, un proces poate cunoaște o multitudine de situații neprevăzute, care­i afectează cursul
normal de execuție. Dacă procesul nu le poate trata, ele sunt pasate, mai departe, sistemului de
operare. Cum sistemul de operare nu poate ști dacă procesul își poate continua execuția în mod
normal, fără efecte secundare nedorite, este obligat să termine procesul în mod forțat. O rezolvare a
acestei probleme o reprezintă semnalele.

Un semnal este o întrerupere software, în fluxul normal de execuție a unui proces.

Semnalele sunt un concept specific sistemelor de operare UNIX. Sistemul de operare le folosește
pentru a semnala procesului apariția unor situații excepționale oferindu­i procesului posibilitatea de a
reacționa. Fiecare semnal este asociat cu o clasă de evenimente care pot apărea și care respectă
anumite criterii.

Procesele pot trata, bloca, ignora sau lăsa sistemul de operare să efectueze acțiunea implicită la
primirea unui semnal:

De obicei acțiunea implicită este terminarea procesului.
Dacă un proces dorește să ignore un semnal, sistemul de operare nu va mai trimite acel
semnal procesului.
Dacă un proces specifică faptul că dorește să blocheze un semnal, sistemul de operare nu va
mai trimite semnalele de acel tip spre procesul în cauză, dar va salva numai primul semnal de
acel tip, restul pierzându­se. Când procesul hotărăște că vrea să primească, din nou, semnale
de acel tip, dacă există vreun semnal în așteptare, acesta va fi trimis.

Mulțimea tipurilor de semnale este finită; sistemul de operare ține, pentru fiecare proces, o tabelă cu
acțiunile alese de acesta, pentru fiecare tip de semnal. La fiecare moment de timp aceste acțiuni sunt
bine determinate. La pornirea procesului tabela de acțiuni este inițializată cu valorile implicite. Modul
de tratare a semnalului nu este decis la primirea semnalului de către proces, ci se alege, în mod
automat, din tabelă.

Semnalele sunt sincrone/asincrone cu fluxul de execuție al procesului care primește semnalul dacă
evenimentul care cauzează trimiterea semnalului este sincron/asincron cu fluxul de execuție al
procesului.

Un eveniment este sincron cu fluxul de execuție al procesului dacă apare de fiecare dată la
rularea programului, în același punct al fluxului de execuție. Exemple în acest sens sunt
încercarea de accesare a unei locații de memorie nevalide sau nepermise, împărțire la zero etc.

Un eveniment este asincron dacă nu este sincron. Exemple de evenimente asincrone: un
semnal trimis de un alt proces (semnalul de terminare unui proces copil), sau o cerere de
terminare externă (utilizatorul dorește să reseteze calculatorul).

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 1/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Un semnal primit de un proces poate fi generat:

fie direct de sistemul de operare ­ în cazul în care acesta raportează diferite erori;
fie de un proces ­ care­și poate trimite și singur semnale (semnalul va trece tot prin sistemul
de operare).

Dacă două semnale sunt prea apropiate în timp ele se pot confunda într­unul singur. Astfel, în mod
normal, nu există niciun mecanism care să garanteze celui care trimite semnalul că acesta a ajuns la
destinație.

În anumite cazuri, există nevoia de a ști, în mod sigur, că un semnal trimis a ajuns la destinație și,
implicit, că procesul va răspunde la el (efectuând una din acțiunile posibile). Sistemul de operare oferă
un alt mod de a trimite un semnal, prin care se garantează fie că semnalul a ajuns la destinație, fie
că această acțiune a eșuat. Acest lucru este realizat prin crearea unei stive de semnale, de o anumită
capacitate (ea trebuie să fie finită, pentru a nu produce situații de overflow). La trimiterea unui semnal,
sistemul de operare verifică dacă stiva este plină. În acest caz, cererea eșuează, altfel semnalul este
pus în stivă și operația se termină cu succes. Modul clasic de a trimite semnale este analog cu acesta
(stiva are dimensiunea 1) cu excepția faptului că nu se oferă informații despre ajungerea la destinație
a unui semnal.

Noțiunea de semnal este folosită pentru a indica alternativ fie un anumit tip de semnal, fie efectiv
obiectele de acest tip.

Generarea semnalelor
În general, evenimentele care generează semnale se încadrează în trei categorii majore:

O eroare indică faptul că un program a făcut o operație nepermisă și nu­și poate continua
execuția. Însă, nu toate tipurile de erori generează semnale (de fapt, cele mai multe nu o fac).
De exemplu, deschiderea unui fișier inexistent este o eroare, dar nu generează un semnal; în
schimb, apelul de sistem open returnează ­1, indicând că apelul s­a terminat cu eroare. În
general, erorile asociate cu anumite biblioteci sunt raportate prin întoarcerea unei valori
speciale. Erorile care generează semnale sunt cele care pot apărea oriunde în program, nu doar
în apelurile din biblioteci. Ele includ împărțirea cu zero și accesarea nevalidă a memoriei.

Un eveniment extern este, în general, legat de I/O și de alte procese. Exemple: apariția de
noi date de intrare, expirarea unui timer, terminarea execuției unui proces copil.

O cerere explicită indică utilizarea unui apel de sistem, cum ar fi kill, pentru a genera un
semnal.

Semnalele pot fi generate sincron sau asincron:

Un semnal sincron se raportează la o acțiune specifică din program, și este trimis (dacă nu
este blocat) în timpul acelei acțiuni. Cele mai multe erori generează semnale în mod sincron. De
asemenea, semnalele pot fi generate în mod sincron și prin anumite cereri explicite trimise de
un proces lui însuși. Pe anumite mașini, anumite tipuri de erori hardware (de obicei, excepțiile
în virgulă mobilă) nu sunt raportate complet sincron, și pot ajunge câteva instrucțiuni mai târziu.

Semnalele asincrone sunt generate de evenimente necontrolabile de către procesul care le
primește. Aceste semnale ajung la momente de timp impredictibile. Evenimentele externe
generează semnale în mod asincron, la fel ca și cererile explicite trimise de alte procese.

Un tip de semnal dat este fie sincron, fie asincron. De exemplu, semnalele pentru erori sunt, în
general, sincrone deoarece erorile generează semnale în mod sincron. Însă, orice tip de semnal poate
fi generat sincron sau asincron cu o cerere explicită.

Transmiterea și primirea semnalelor

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 2/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Când un semnal este generat, el intră într­o stare de așteptare (pending). În mod normal, el rămâne în
această stare pentru o perioadă de timp foarte mică și apoi este trimis procesului destinație. Însă, dacă
acel tip de semnal este, în momentul de față, blocat, el ar putea rămâne în starea de așteptare
nedefinit, până când semnalele de acel tip sunt deblocate. O dată deblocat acel tip de semnal, el va fi
trimis imediat.

Când semnalul a fost primit, fie imediat, fie cu întârziere, acțiunea specificată pentru acel semnal este
executată. Pentru anumite semnale, cum ar fi SIGKILL și SIGSTOP, acțiunea este fixată (procesul
este terminat), dar, pentru majoritatea semnalelor, programul poate alege să:

ignore semnalul
specifice o funcție de tip handler
accepte acțiunea implicită pentru acel tip de semnal.

Programul își specifică alegerea utilizând funcții precum signal [http://linux.die.net/man/2/signal] sau
sigaction [http://linux.die.net/man/2/sigaction]. În timp ce handler­ul rulează, acel tip de semnal este în
mod normal blocat (deblocarea se va face printr­o cerere explicită în handler­ul care tratează
semnalul).

În codul de mai jos ne propunem să capturăm semnalele SIGINT și SIGUSR1 și să facem o acțiune în
cazul în care le recepționăm. SIGINT e recepționat atât folosind comanda kill ‐SIGINT
<program>, cât și prin trimiterea combinației de taste CTRL+c programului.

#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h> 
#include <unistd.h> 
  
pid_t child1, child2; 
int child1_pid; 
  
  
void signal_handler(int signum) 

  
    switch(signum) { 
        case SIGINT: 
            printf("CTRL+C received in %d Exiting\n", getpid()); 
            exit(EXIT_SUCCESS);
        case SIGUSR1: 
            printf("SIGUSR1 received. Continuing execution\n"); 
    } 

  
int main(void) 

  
    printf("Process %d started\n", getpid()); 
  
    /* Semnale ca SIGKILL sau SIGSTOP nu pot fi prinse */ 
    if (signal(SIGKILL, signal_handler) == SIG_ERR) 
        printf("\nYou shall not catch SIGKILL\n"); 
  
    if(signal(SIGINT, signal_handler) == SIG_ERR) { 
        printf("Unable to catch SIGINT"); 
        exit(EXIT_FAILURE); 
    } 
  
    if(signal(SIGUSR1, signal_handler) == SIG_ERR) { 
        printf("Unable to catch SIGUSR1"); 
        exit(EXIT_FAILURE); 
    } 
  
  
    printf("Press CTRL+C to stop us\n"); 
  
    while(1) { 
        sleep(1); 
    } 
  
    return 0; 
}

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 3/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Observați că semnalul SIGKILL nu poate fi handle­uit (kill ‐9 <program> sau kill ‐SIGKILL
<program>).

Dacă acțiunea specificată pentru un tip de semnal este să îl ignore, atunci orice semnal de acest tip,
care este generat pentru procesul în cauză, este ignorat. Același lucru se întâmplă dacă semnalul este
blocat în acel moment. Un semnal neglijat în acest mod nu va fi primit niciodată, nici dacă programul
specifică ulterior o acțiune diferită pentru acel tip de semnal și apoi îl deblochează.

Dacă este primit un semnal pentru care nu s­a specificat niciun tip de acțiune, se execută acțiunea
implicită. Fiecare tip de semnal are propria lui acțiune implicită. Pentru majoritatea semnalelor
acțiunea implicită este terminarea procesului. Pentru anumite tipuri de procese, care reprezintă
evenimente fără consecințe majore, acțiunea implicită este să nu se facă nimic.

Când un semnal forțează terminarea unui proces, părintele procesului poate determina cauza terminării
examinând codul de terminare raportat de funcțiile wait și waitpid. Informațiile pe care le poate
obține includ faptul că terminarea procesului a fost cauzată de un semnal, precum și tipul semnalului.
Dacă un program pe care îl rulați din linia de comandă este terminat de un semnal, shell­ul afișează,
de obicei, niște mesaje de eroare.

Semnalele care în mod normal reprezintă erori de program au o proprietate specială: când unul din
aceste semnale termină procesul, el scrie și un fișier core dump care înregistrează starea procesului
în momentul terminării. Puteți examina fișierul cu un debugger, pentru a afla ce anume a cauzat
eroarea.

Dacă generați un semnal, care reprezintă o eroare de program, printr­o cerere explicită, și acesta
termină procesul, fișierul este generat ca și cum semnalul ar fi fost generat de o eroare.

În cazul în care un semnal este trimis procesului, în timp ce acesta execută un apel de sistem blocant,
procesul va suspenda apelul, va executa handler­ul de tratare a semnalului definit folosind signal și
apoi fie operația va eșua (cu errno setat pe EINTR), fie se va reporni operația. Sistemele System V se
comportă ca în primul caz, cele BSD ca în cel de­al doilea. De la glibc v2 incoace, comportamentul este
acelasi ca si pe BSD, totul depinzand de definitia macrou­ului _BSD_SOURCE. Comportamentul poate fi
controlat de catre programator folosind sigaction cu flag­ul SA_RESTART.

Tipuri standard de semnale
Această secțiune prezintă numele pentru diferite tipuri standard de semnale și descrie ce fel de
evenimente indică.

Fiecare nume de semnal este o macrodefiniție care reprezintă, de fapt, un număr întreg pozitiv
(numărul pentru acel tip de semnal).

Un program nu ar trebui să facă niciodată presupuneri despre codul numeric al unui tip particular de
semnal, ci, mai degrabă, să le refere, întotdeauna, prin nume. Acest lucru este din cauza faptului că un
număr pentru un tip de semnal poate varia de la un sistem la altul, dar numele lor sunt standard.
Pentru lista completă de semnale suportate de un sistem se poate rula în linia de comandă:

$ kill ‐l 
  
     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL 
     5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE 
     9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2 
    13) SIGPIPE     14) SIGALRM     15) SIGTERM     17) SIGCHLD 
    18) SIGCONT     19) SIGSTOP     20) SIGTSTP     21) SIGTTIN 
    22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ 
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO 
    30) SIGPWR      31) SIGSYS      33) SIGRTMIN    34) SIGRTMIN+1 
    35) SIGRTMIN+2  36) SIGRTMIN+3  37) SIGRTMIN+4  38) SIGRTMIN+5 
    39) SIGRTMIN+6  40) SIGRTMIN+7  41) SIGRTMIN+8  42) SIGRTMIN+9 
    43) SIGRTMIN+10 44) SIGRTMIN+11 45) SIGRTMIN+12 46) SIGRTMIN+13 
    47) SIGRTMIN+14 48) SIGRTMIN+15 49) SIGRTMAX‐15 50) SIGRTMAX‐14 
    51) SIGRTMAX‐13 52) SIGRTMAX‐12 53) SIGRTMAX‐11 54) SIGRTMAX‐10 
    55) SIGRTMAX‐9  56) SIGRTMAX‐8  57) SIGRTMAX‐7  58) SIGRTMAX‐6 
    59) SIGRTMAX‐5  60) SIGRTMAX‐4  61) SIGRTMAX‐3  62) SIGRTMAX‐2 
    63) SIGRTMAX‐1  64) SIGRTMAX

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 4/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Numele de semnale sunt definite în header­ul signal.h. În general, semnalele au roluri predefinite,
dar acestea pot fi suprascrise de programator.

Cele mai cunoscute sunt următoarele semnale:

SIGINT ­ transmis la apăsarea combinației CTRL+C;
SIGQUIT ­ transmis la apăsarea combinației de taste CTRL+\;
SIGSEGV ­ transmis în momentul accesării unei locații nevalide de memorie, etc;
SIGKILL ­ nu poate fi ignorat sau suprascris. Transmiterea acestui semnal are ca efect
terminarea procesului, indiferent de context.

Mesaje pentru descrierea semnalelor
Cel mai bun mod de a afișa un mesaj de descriere a unui semnal este utilizarea funcțiilor strsignal
[http://www.kernel.org/doc/man­pages/online/pages/man3/strsignal.3.html] și psignal
[http://www.kernel.org/doc/man­pages/online/pages/man3/psignal.3.html]. Aceste funcții folosesc un număr de
semnal pentru a specifica tipul de semnal care trebuie descris. Mai jos este prezentat un exemplu de
folosire a acestor funcții:

msg_signal.c

#include <stdio.h> 
#include <stdlib.h> 
  
#define __USE_GNU 
#include <string.h> 
  
#include <signal.h> 
  
int main(void) { 
    char *sig_p = strsignal(SIGKILL); 
  
    printf("signal %d is %s\n", SIGKILL, sig_p); 
  
    psignal(SIGKILL, "death and decay"); 
  
    return 0; 
}

Pentru compilare și rulare secvența este:

so@spook$ gcc ‐Wall ‐g ‐o msg_signal msg_signal.c 
so@spook$ ./msg_signal  
signal 9 is Killed 
death and decay: Killed

Măști de semnale. Blocarea semnalelor
Pentru a putea efectua operații de blocare/deblocare semnale avem nevoie să știm, la fiecare pas din
fluxul de execuție, starea fiecărui semnal. Sistemul de operare are, de asemenea, nevoie de același
lucru pentru a putea lua o decizie asupra unui semnal care trebuie trimis unui proces (el are nevoie de
acest gen de informație pentru fiecare proces în parte). În acest scop se folosește o mască de semnale
proprie fiecărui proces.

O mască de semnale are fiecare bit asociat unui tip de semnal.

Masca de biți este folosită de mai multe funcții, printre care și funcția sigprocmask
[http://www.kernel.org/doc/man­pages/online/pages/man2/sigprocmask.2.html], folosită pentru schimbarea
măștii de semnale a procesului curent.

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

Tipul de date folosit de sistemele UNIX pentru a reprezenta măștile de semnale este sigset_t.
Variabilele de acest tip sunt neinițializate. Operațiile pe acest tip de date sunt:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 5/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

de inițializare cu biți de 0;
de inițializare cu biți de 1;
de blocare a unui semnal;
de deblocare a unui semnal;
de detectare a blocării unui semnal.

Funcțiile următoare sunt folosite pentru a manipula masca de biți. Ele nu decid acțiunea de blocare sau
deblocare a unui semnal, ci doar setează semnalul respectiv în masca de biți (pentru adăugare se pune
bitul corespunzător semnalului pe 1, iar pentru ștergere pe 0), pentru ca apoi să se folosească
sigprocmask pentru a seta acțiunea de blocare/deblocare efectivă. Mai multe detalii despre aceste
funcții găsiți aici [http://linux.die.net/man/3/sigemptyset].

int sigemptyset(sigset_t *set); 
int sigfillset(sigset_t *set); 
int sigaddset(sigset_t *set, int signo); 
int sigdelset(sigset_t *set, int signo); 
int sigismember(sigset_t *set, int signo);

Înainte de a folosi funcțiile sigaddset, sigdelset și sigismember asupra unui sigset_t, acest tip
trebuie inițializat folosind sigemptyset sau sigfillset. Comportamentul este nedefinit în caz
contrar.

Secvența de mai jos constituie un caz de utilizare a funcțiilor de lucru cu masca de semnale, în care, la
fiecare 5 secunde, se blochează/deblochează semnalul SIGINT:

sigset_t set; 
  
sigemptyset(&set); 
sigaddset(&set, SIGINT); 
  
while (1) { 
    sleep(5); 
    sigprocmask(SIG_BLOCK, &set, NULL); 
    sleep(5); 
    sigprocmask(SIG_UNBLOCK, &set, NULL); 
}

O altă valoare pe care o poate lua primul parametru al funcției sigprocmask
[http://www.kernel.org/doc/man­pages/online/pages/man2/sigprocmask.2.html] este SIG_SETMASK, care
specifică pur și simplu că vechea mască (al treilea parametru) e înlocuită cu cel de­al doilea parametru
(noua mască). Un exemplu de folosire a acesteia puteți găsi la această adresă
[https://support.sas.com/documentation/onlinedoc/sasc/doc750/html/lr1/zlocking.htm].

Tratarea semnalelor
Tratarea semnalelor se realizează prin asocierea unei funcții (handler) unui semnal. Funcția va fi
apelată în momentul în care procesul recepționează semnalul respectiv.

În mod tradițional, funcția folosită pentru asocierea de handler­e pentru tratarea unui semnal era signal
[http://www.kernel.org/doc/man­pages/online/pages/man2/signal.2.html]. Pentru a preîntâmpina deficiențele
acestei funcții, standardul POSIX a definit funcția sigaction [http://www.kernel.org/doc/man­
pages/online/pages/man2/sigaction.2.html] pentru asocierea unui handler cu un semnal. sigaction oferă
mai mult control, cu prețul unui grad de complexitate mai mare.

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

Componenta importantă a funcției sigaction este structura cu același nume, descrisă în pagina de
manual a funcției:

 struct sigaction { 
               void     (*sa_handler)(int); 
               void     (*sa_sigaction)(int, siginfo_t *, void *); 
               sigset_t   sa_mask; 
               int        sa_flags; 
};
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 6/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Dacă în câmpul sa_flags se precizează flag­ul SA_SIGINFO, handler­ul folosit este cel specificat de
sa_sigaction. Altfel, handler­ul folosit este sa_handler. Masca de semnale care ar trebui blocate în
timpul execuției handler­ului este reprezentată de sa_mask.

Un exemplu de asociere a unui handler de tratare a unui semnal este prezentat mai jos:

#include <signal.h> 
... 
  
/* SIGUSR2 handler */ 
static void usr2_handler(int signum) {
    /* actions that should be taken when the signal signum is received */ 
    ... 

  
int main(void) { 
    struct sigaction sa; 
  
    memset(&sa, 0, sizeof(sa)); 
  
    sa.sa_flags   = SA_RESETHAND;   /* restore handler to previous state */ 
    sa.sa_handler = usr2_handler; 
    sigaction(SIGUSR2, &sa, NULL); 
  
    return 0; 
}

Se poate opta pentru configurarea unui handler propriu sau se poate folosi unul predefinit. Se poate
folosi SIG_IGN pentru ignorarea semnalului sau SIG_DFL pentru rularea acțiunii implicite (terminarea
procesului, ignorarea semnalului etc).

Structura siginfo_t

Dacă flag­ul SA_SIGINFO este setat, se folosește câmpul sa_sigaction al structurii sigaction
pentru a specifica handler­ul asociat semnalului. Handler­ul folosit primește în acest caz trei parametri
și poate fi folosit pentru a transmite o informație utilă, o dată cu procesul. Al treilea argument (de
tipul void*) este rar utilizat. Al doilea argument, de tipul siginfo_t definește o structură ce conține
informații utile despre contextul apariției semnalului și alte informații pe care le poate furniza
programatorul. Definiția structurii se găsește în pagina de manual a funcției sigaction
[http://www.kernel.org/doc/man­pages/online/pages/man2/sigaction.2.html].

 siginfo_t { 
               int      si_signo;    /* Signal number */ 
               int      si_errno;    /* An errno value */ 
               int      si_code;     /* Signal code */ 
               int      si_trapno;   /* Trap number that caused 
                                        hardware‐generated signal 
                                        (unused on most architectures) */ 
               pid_t    si_pid;      /* Sending process ID */ 
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */ 
               clock_t  si_utime;    /* User time consumed */ 
               clock_t  si_stime;    /* System time consumed */ 
               sigval_t si_value;    /* Signal value */ 
               int      si_int;      /* POSIX.1b signal */ 
               void    *si_ptr;      /* POSIX.1b signal */ 
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */ 
               int      si_timerid;  /* Timer ID; POSIX.1b timers */ 
               void    *si_addr;     /* Memory location which caused fault */ 
               long     si_band;     /* Band event (was int in 
                                        glibc 2.3.2 and earlier) */
               int      si_fd;       /* File descriptor */ 
               short    si_addr_lsb; /* Least significant bit of address 
                                        (since kernel 2.6.32) */ 
}

Membrii structurii sunt inițializați numai atunci când valorile lor sunt utile. Membrii si_signo,
si_errno și si_code sunt întotdeauna definiți pentru toate semnalele. Restul structurii poate fi o
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 7/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

uniune, așa că ar trebui citite numai câmpurile care au sens pentru semnalul primit. Spre exemplu,
apelul de sistem kill, semnalele POSIX.1b și SIGCHLD completează si_pid și si_uid, iar SIGILL,
SIGFPE, SIGSEGV și SIGBUS completează si_addr cu adresa care a provocat eroarea.

Semnalarea proceselor
Pentru transmiterea unui semnal, se poate folosi funcția kill [http://www.kernel.org/doc/man­
pages/online/pages/man2/kill.2.html] sau funcția sigqueue [http://www.kernel.org/doc/man­
pages/online/pages/man2/sigqueue.2.html]. Funcția kill [http://www.kernel.org/doc/man­
pages/online/pages/man2/kill.2.html] are dezavantajul că nu garantează recepționarea semnalului de
procesul destinație. Dacă este nevoie să se trimită un semnal unui proces și să se știe sigur că a ajuns
se recomandă folosirea funcției sigqueue [http://www.kernel.org/doc/man­
pages/online/pages/man2/sigqueue.2.html]:

int sigqueue(pid_t pid, int signo, const union sigval value);

Funcția trimite semnalul signo, cu parametrii specificați de value, procesului cu identificatorul pid.
Dacă semnalul este zero, se fac verificări pentru cazurile de eroare posibile, dar nu se trimite niciun
semnal. Semnalul nul poate fi folosit pentru a verifica faptul că pid­ul este valid.

Valoarea ce poate fi trimisă odată cu semnalul este un union:

union sigval { 
     int   sival_int; 
     void *sival_ptr; 
};

Un parametru trimis astfel apare în câmpul si_value al structurii siginfo_t, primite de handler­ul
de semnal. În mod evident, nu are sens transmiterea de pointeri dintr­un proces în altul.

Condițiile cerute pentru ca un proces să aibă permisiunea de a trimite un semnal altui proces sunt
aceleași ca și în cazul lui kill. Dacă semnalul specificat este blocat în acel moment, funcția va ieși
imediat și dacă flagul SA_SIGINFO este setat și există resurse necesare, semnalul va fi pus în coadă în
starea pending (un proces poate avea în coadă maxim SIGQUEUE_MAX semnale). De asemenea, când
semnalul este primit, câmpul si_code, pasat structurii siginfo, va fi setat la SI_QUEUE, și
si_value va fi setat la value.

Dacă flagul SA_SIGINFO nu este setat, atunci signo, dar nu în mod necesar și value, vor fi trimise,
cel puțin o dată, procesului care trebuie să primească semnalul.

Așteptarea unui semnal

În cazul în care se utilizează semnalele pentru comunicare și/sau sincronizare, există, deseori, nevoie
să se aștepte ca un anumit tip de semnal să­i sosească procesului în cauză. Un mod simplu de a
realiza acest lucru este o buclă, a cărei condiție de ieșire ar fi setarea corespunzătoare a unei variabile
(variabila trebuie să fie de tipul sig_atomic_t). De exemplu:

while (!signal_has_arrived);

Principalul dezavantaj al abordării de mai sus (de tip busy­waiting) este timpul de procesor pe care
procesul considerat îl pierde în mod inutil. O variantă ar fi folosirea funcției sleep
[http://www.kernel.org/doc/man­pages/online/pages/man3/sleep.3.html]:

while (!signal_has_arrived) { 
    sleep(1); 
}

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 8/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

O astfel de abordare nu ar mai ocupa timp inutil de procesor, dar timpul de răspuns în cazul sosirii
unui semnal este destul de mare. O altă soluție a problemei este funcția pause
[http://www.kernel.org/doc/man­pages/online/pages/man2/pause.2.html] (care blochează fluxul de execuție
până când procesul curent este întrerupt de un semnal). Deși această abordare pare foarte simplă, ea
introduce, adeseori, deadlock­uri, care blochează programul nedefinit. Un exemplu în acest sens este
pseudosoluția de mai jos, la problema așteptării unui semnal:

while (!signal_has_arrived) { 
    pause(); 
}

Bucla este necesară pentru prevenirea situației în care procesul este întrerupt de alte semnale decât
cel așteptat. Se poate întâmpla ca semnalul să ajungă după testarea variabilei și înainte de apelul
funcției pause. În acest caz, procesul se blochează și, dacă nu apare un alt semnal care să cauzeze
ieșirea din pause [http://www.kernel.org/doc/man­pages/online/pages/man2/pause.2.html], el va rămâne
blocat nedefinit.

Soluția cea mai bună pentru a aștepta un semnal se poate realiza prin utilizarea funcției sigsuspend
[http://www.kernel.org/doc/man­pages/online/pages/man2/sigsuspend.2.html]:

int sigsuspend(const sigset_t *set);

Funcția înlocuiește masca de semnale blocate a procesului, cu set, și suspendă procesul până când
este primit un semnal care nu este blocat de noua mască. La ieșire, funcția restaurează vechea mască
de semnale.

În secvența de mai jos, funcția sigsuspend [http://www.kernel.org/doc/man­
pages/online/pages/man2/sigsuspend.2.html] este folosită pentru a întrerupe procesul curent până la
recepționarea semnalului SIGINT. Semnalele SIGKILL și SIGSTOP, deși prezente în masca de
semnale, nu vor fi blocate:

sigset_t set; 
  
/* block all signals except SIGINT */ 
sigfillset(&set); 
sigdelset(&set, SIGINT); 
  
/* wait for SIGINT */ 
sigsuspend(&set);

Considerente privind utilizarea unui handler de semnal

Un obiect de tip semnal este atașat unui obiect de tip proces. Dacă procesul nu rulează în acel
moment, sistemul de operare poate atașa unui proces numai un singur semnal care va rămâne în
starea pending. Dacă procesul rula în acel moment, semnalul primit este deservit imediat și va
întrerupe fluxul normal de execuție, permițându­se primirea, fără pierdere, a unui semnal de același
tip.

Pentru că numărul de semnale de un anumit tip care poate fi primit de un proces într­un anumit timp
este limitat și pentru a evita pierderea de semnale, un handler trebuie să se execute cât mai repede.

Fluxul de execuție al unui proces este văzut de către sistemul de operare ca o înșiruire de instrucțiuni
pe care platforma le suportă. Unele operații din limbajele de programare de nivel înalt nu sunt
atomice și indivizibile, fiind nevoie de mai multe instrucțiuni în cod mașină pentru a se efectua
respectiva operație. Un exemplu simplu este atribuirea între variabile:

a = b;

Majoritatea platformelor actuale nu permit instrucțiuni în care ambii operanzi să fie în memorie. Pe
astfel de platforme, o implementare standard pentru această operație ar fi încărcarea valorii lui b într­
un registru, după care ar urma încărcarea la adresa lui a a valorii salvate în registru :

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 9/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

load registru_1, b 
store a, registru_1

De aceea, este nevoie de atenție suplimentară atunci când un semnal folosește variabile care nu sunt
locale funcției, deoarece semnalele pot întrerupe fluxul de execuție în orice punct al său, lăsând astfel
unele variabile într­o stare inconsistentă. Pentru a fi siguri că o variabilă nu are valori inconsistente
se recomandă folosirea tipului sig_atomic_t pentru variabilele din fluxul de execuție care
interacționează cu handlerele de semnale. Acest tip este unul din tipurile întregi disponibile, putând
varia de la o platformă la alta. Așadar operațiile ce se pot efectua cu acest tip, sunt aceleași cu cele
ale unui întreg.

Timere în Linux
În Linux, folosirea timer­elor este legată de folosirea semnalelor. Acest lucru se întâmplă întrucât cea
mai mare parte a funcțiilor de tip timer folosesc semnale.

Un timer este, de obicei, un întreg a cărui valoare este decrementată în timp. În momentul în care
întregul ajunge la 0, timer­ul expiră. În Linux, expirarea timer­ului are drept rezultat, în general,
transmiterea unui semnal. Definirea unui “timer handler” (rutină apelată în momentul expirării timer­
ului) este, astfel, echivalentă cu definirea unui handler pentru semnalul asociat.

Înregistrarea unui timer, în Linux, înseamnă specificarea unui interval după care un timer expiră și
configurarea handler­ului care va rula. Configurarea handler­ului se poate realiza atât prin intermediul
funcției sigaction (în momentul in care timer­ul expiră se generază un semnal, care la rândul lui
generează rularea handler­ului asociat), sau direct prin intermediul parametrilor funcției timer_create
[http://www.kernel.org/doc/man­pages/online/pages/man2/timer_create.2.html].

Utilizarea unui timer presupune mai mulți pași:

crearea unui timer ­ folosind funcția timer_create [http://www.kernel.org/doc/man­
pages/online/pages/man2/timer_create.2.html]:

 int timer_create(clockid_t clockid, struct sigevent *evp, timer_t *timerid) 

Timer­ul creat se identifică prin timerid. Prin intermediul structurii sigevent se setează modul în
care va interacționa timer­ul cu procesul/thread­ul care l­a lansat. Exemplu de folosire:

timer_t timerid; 
struct sigevent sev; 
  
sev.sigev_notify = SIGEV_SIGNAL;             /* notification method */ 
sev.sigev_signo = SIGRTMIN;                  /* Timer expiration signal */ 
sev.sigev_value.sival_ptr = &timerid;         
timer_create(CLOCK_REALTIME, &sev, &timerid);

Prin intermediul primului argument se poate măsura timpul real al sistemului, timpul de rulare al
procesului sau timpul de rulare al procesului în user­space și kernel­space. La timeout timer­ul va livra
semnalul salvat în sev.sigev_signo

armarea unui timer ­ folosind funcția timer_settime [http://www.kernel.org/doc/man­
pages/online/pages/man2/timer_settime.2.html]:

int timer_settime(timer_t timerid, int flags,  
                          const struct itimerspec *new_value, 
                          struct itimerspec * old_value); 

Armarea timer­ului presupune completarea structurii itimerspec în care specifică timpul de pornire al
timer­ului, cât și intervalul de expirare al timeout­ului (intervalele sunt măsurate în secunde și
nanosecunde). Exemplu de folosire:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 10/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

its.it_value.tv_sec = freq_nanosecs / 1000000000; /* Initial expiration in secs*/
its.it_value.tv_nsec = freq_nanosecs % 1000000000;/* Initial expiration in nsecs*/ 
its.it_interval.tv_sec = its.it_value.tv_sec;     /* Timer interval in secs */ 
its.it_interval.tv_nsec = its.it_value.tv_nsec;   /* Timer interval in nanosecs */  
  
timer_settime(timerid, 0, &its, NULL);

ștergerea unui timer ­ folosind funcția timer_delete [http://www.kernel.org/doc/man­
pages/online/pages/man2/timer_delete.2.html]

int timer_delete(timer_t timerid); 

Pentru a folosi funcțiile de mai sus programul trebuie compilat cu ‐lrt

Una dintre formele de utilizare a timer­elor este implementarea funcțiilor de așteptare de tipul sleep
[http://www.kernel.org/doc/man­pages/online/pages/man3/sleep.3.html] sau nanosleep
[http://www.kernel.org/doc/man­pages/online/pages/man2/nanosleep.2.html]. Avantajul folosirii funcției sleep
[http://www.kernel.org/doc/man­pages/online/pages/man3/sleep.3.html] este simplitatea. Dezavantajele sunt
rezoluția scăzută (secunde) și posibila interacțiune cu semnale (în special SIGALRM). nanosleep
[http://www.kernel.org/doc/man­pages/online/pages/man2/nanosleep.2.html] are un apel mai complex, dar
oferă rezoluție până la ordinul nanosecundelor și este signal­safe (nu interacționează cu semnale).

Timere sub Windows
În Windows există mai multe mecanisme de notificare a proceselor: evenimente
[http://msdn.microsoft.com/en­us/library/ms682655(VS.85).aspx], APC­uri [http://msdn.microsoft.com/en­
us/library/ms681951(VS.85).aspx], evenimente de consolă [http://msdn.microsoft.com/en­
us/library/ms682073%28VS.85%29.aspx], timere [http://msdn.microsoft.com/en­
us/library/ms687012(VS.85).aspx]. Evenimentele sunt folosite pentru sincronizarea între thread­
uri/procese. APC­urile sunt mecanisme de execuție asincronă în contextul unui proces/thread. Pot fi
folosite pentru rularea unei secvențe de cod în combinație cu un timer.

Waitable Timer Objects
Un obiect de tipul waitable timer este un obiect de sincronizare a cărui stare este semnalizată
(signaled) atunci când timpul specificat se scurge. Există două tipuri de obiecte waitable timer ce pot
fi create:

timer cu resetare manuală: un timer a cărui stare rămâne semnalizată până când un nou apel
al funcției SetWaitableTimer [http://msdn.microsoft.com/en­us/library/ms686289(VS.85).aspx] setează
un nou timp de așteptare;
timer cu resetare automată: un timer a cărui stare rămâne signaled până când un thread
efectuează o operație de așteptare pe acel obiect.

Oricare dintre cele două tipuri de timer poate fi configurat ca un timer periodic. Un timer periodic este
reactivat de fiecare dată când perioada de timp specificată expiră, până când timer­ul este setat din
nou sau anulat. Operațiile care se pot efectua cu un timer sunt:

crearea unui timer ­ se realizează prin intermediul funcției CreateWaitableTimer
[http://msdn.microsoft.com/en­us/library/ms682492(VS.85).aspx]
setarea unui timer ­ se poate face cu funcția SetWaitableTimer [http://msdn.microsoft.com/en­
us/library/ms686289(VS.85).aspx]
anularea ­ se realizează cu ajutorul funcției CancelWaitableTimer [http://msdn.microsoft.com/en­
us/library/ms681985.aspx]
așteptarea ­ folosind funcția WaitForSingleObject [http://msdn.microsoft.com/en­
us/library/ms687032.aspx]

În secvența de cod de mai jos, se folosește un timer pentru afișarea unui mesaj după 5 secunde:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 11/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

#define _WIN32_WINNT    0x0500 
#include <windows.h> 
... 
  
int main(void) { 
    HANDLE timerHandle; 
    LARGE_INTEGER dueTime; 
  
    timerHandle = CreateWaitableTimer(NULL, FALSE, NULL); 
    if (timerHandle == NULL) { 
        fprintf(stderr, "CreateWaitableTimer failed (%d)\n", GetLastError()); 
        exit(‐1); 
    } 
  
    /* configure to expire in 5 seconds; base unit is 100ns */ 
    dueTime.QuadPart = ‐50000000LL; 
    if (SetWaitableTimer(timerHandle, &dueTime, 0, NULL, NULL, 0) == FALSE) { 
        fprintf(stderr, "SetWaitableTimer failed (%d)\n", GetLastError()); 
        exit(‐1); 
    } 
  
    if (WaitForSingleObject(timerHandle, INFINITE) != WAIT_OBJECT_0) { 
        fprintf("WaitForSingleObject failed (%d)\n", GetLastError()); 
        exit(‐1); 
    } 
  
    printf("5 seconds timer expired\n"); 
  
    return 0; 
}

Exerciții
În rezolvarea laboratorului folosiți arhiva de sarcini lab04­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab04­tasks.zip].

Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din arhivă există un
fișier utils.h cu funcții utile.

Exercițiul ­1 ­ GSOC (0p)
Google Summer of Code este un program de vară în care studenții (indiferent de anul de studiu) sunt
implicați în proiecte Open Source pentru a își dezvolta skill­urile de programare, fiind răsplătiți cu o
bursă a cărei valoare depinde de țară [https://developers.google.com/open­source/gsoc/help/student­stipends]
(pagină principală GSOC [https://developers.google.com/open­source/gsoc]).

UPB se află în top ca număr de studenți acceptați; în fiecare an fiind undeva la aprox. 30­40 de studenți
acceptați. Vă încurajăm să aplicați! Există și un grup de fb cu foști participanți unde puteti să îi
contactați pentru sfaturi facebook page [https://www.facebook.com/groups/240794072931431/]

Exercițiul 0 ­ Joc interactiv (2p)

Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (7p)

La folosirea sigaction, veți inițializa, în general, câmpul sa_flags al structurii struct sigaction
la 0.

Exercițiul 1 ­ hitme (1p)

Intrați în directorul 1‐hitme/ și analizați conținutul fișierului hitme.c. Compilați și rulați programul.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 12/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Folosiți comanda kill ‐l pentru a lista toate semnalele disponibile. Ce valoare are semnalul
SIGKILL? Într­o altă consolă trimiteți programului hitme semnale cu valori cuprinse între 20 și 25
astfel:

kill ‐20 $(pidof hitme) 
kill ‐21 $(pidof hitme) 
kill ‐22 $(pidof hitme) 
kill ‐23 $(pidof hitme) 
kill ‐24 $(pidof hitme) 
kill ‐25 $(pidof hitme)

Încercați să trimiteți același semnal de două ori și explicați comportamentul. Ce ar trebui schimbat ca
să se poată trimite același semnal de două ori?

Analizați cu atenție ce este setat pe signals.sa_flags.
Puteți reveni la secțiunea Tratarea semnalelor sau investigați structura sigaction
[http://www.kernel.org/doc/man­pages/online/pages/man2/sigaction.2.html]

Observați că eliminarea flagului SA_RESETHAND nu mai reface handlerul la valoarea implicită după
primirea primului semnal.

Exercițiul 2 ­ Normal signals vs Real­Time signals (1p)

Intrați în directorul 2‐signals și urmăriți conținutul fișierului signals.c. Programul numără de câte
ori se apelează handlerul de semnal în cazul trimiterii semnalelor SIGINT și SIGRTMIN

Porniți într­o consolă programul signals:

./signals

Pentru cazul semnalelor normale, într­o altă consolă rulați scriptul send_normal.sh:

./send_normal.sh

Pentru semnalele real‐time, într­o altă consolă rulați scriptul send_rt.sh:

./send_rt.sh

Pentru a închide executabilul signals este trimis semnalul SIGQUIT. De unde apare diferența? Citiți
din pagina de manual man 7 signal secțiunea “Real­time signals” și revedeți secțiunea Tipuri
standard de semnale.

Diferența între numărul semnalelor primite se datorează faptului că semnalele cu indecșii între
SIGRTMIN și SIGRTMAX sunt semnale real time, prin urmare se garantează că ele ajung la destinație.
Vezi link [http://www.linuxprogrammingblog.com/all­about­linux­signals?page=9].

Exercițiul 3 ­ askexit (1p)

Intrați în directorul 3‐askexit și urmăriți codul sursă. Programul face busy waiting, afișând la consolă
numere consecutive.

Trebuie să completați programul pentru a intercepta semnalele generate de CTRL+\, CTRL+C și
SIGUSR1 (folosiți comanda kill). Handler­ul asociat cu fiecare din semnale va fi ask_handler.
Pentru fiecare semnal primit, utilizatorul va fi întrebat dacă dorește să încheie execuția sau nu.

Testați funcționalitatea programului.

Observați că folosirea funcțiilor printf, scanf în handlere de semnale poate fi problematică deoarece
aceste funcții nu sunt signal­safe.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 13/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Consultați secțiunea Tratarea semnalelor.

Exercițiul 4 ­ nohup (2p)

Intrați în directorul 4‐nohup și realizați un program, denumit mynohup, care simulează comanda
nohup [http://linux.die.net/man/1/nohup]. Programul primește, ca prim parametru, numele unei comenzi
de executat. Restul parametrilor reprezintă argumentele cu care trebuie invocată comanda respectivă;
lista de argumente poate fi nulă.

Programul executat de mynohup trebuie să nu fie înștiințat de închiderea terminalului la care era
conectat. Va trebui să ignorați semnalul SIGHUP, livrat de shell procesului, în momentul încheierii
sesiunii curente.

Revedeți secțiunea despre Tratarea semnalelor.

Dacă fișierul standard de ieșire era legat la un terminal acesta trebuie redirectat într­un fișier definit
prin macro­ul NOHUP_OUT_FILE.

Folosiți apelul isatty [http://www.kernel.org/doc/man­pages/online/pages/man3/isatty.3.html].

Pentru testare, rulați

./mynohup sleep 120 &

După rulare închideți sesiunea de shell curentă: fie trimițând un semnal SIGHUP, fie folosind iconița 'X'
din partea dreaptă a ferestrei.

Dintr­o altă consolă rulați respectiv

ps ‐ef | grep sleep 

Cine este noul părinte al procesului?

Consultați secțiunea Tratarea semnalelor și secțiunile Înlocuirea imaginii unui proces și
Redirectări din laboratoarele precedente.

Folosirea comenzii exit, fie combinația de taste Ctrl‐d nu va trimite un semnal SIGHUP procesului
de sleep; puteți testa utilizând sleep 120 &, inchideți shell­ul curent utilizând una din cele 2 metode,
iar după verificați că procesul înca rulează.

Exercițiul 5 ­ zombie (2p)

Intrați în directorul 5‐zombie și urmăriți conținutul fișierelor mkzombie.c și nozombie.c. Fiecare
program va crea câte un proces copil nou, care doar va apela exit.

Implementați mkzombie fără a aștepta copilul creat să se termine. Procesul părinte va aștepta
TIMEOUT secunde și va ieși (urmăriți TODO­urile).
Din altă consolă rulați:

ps ‐eF | grep zombie

Observați faptul că procesul copil, deși nu mai rulează, apare în lista de procese ca <defunct> și are
un pid (unic în sistem la acel moment). De asemenea, observați că, după moartea procesului părinte,
dispare și procesul zombie.

Implementați nozombie fără a folosi funcțiile de așteptare de tipul wait, astfel încât procesul copil să
nu treacă în starea de zombie. nozombie va aștepta TIMEOUT secunde și va ieși. Folosiți semnalul
SIGCHLD (informații găsiți în sigaction(2) [http://linux.die.net/man/2/sigaction] și wait(2)

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 14/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

[http://linux.die.net/man/3/wait]). Consultați, de asemenea, secțiunile Tratarea semnalelor și Crearea unui
proces.

Dacă părintele ignoră în mod explicit semnalul SIGCHLD prin setarea handler­ului la SIG_IGN (în loc să
ignore semnalul în mod implicit) informația despre exit status al copiilor va fi aruncată iar copiii nu vor
deveni procese zombie.

Windows (2p)

Exercițiul 1 ­ timer (2p)

Intrați în directorul 1‐timer și urmăriți conținutul fișierului mytimer.c. Realizați un program care
afișează data curentă la fiecare TIMEOUT secunde.

Folosiți componenta QuadPart a tipului LARGE_INTEGER pentru specificarea timeout­ului. Timeout­ul
este negativ și expiră la atingerea valorii 0. Al treilea argument al SetWaitableTimerObject
[http://msdn.microsoft.com/en­us/library/ms686289(VS.85).aspx] este timpul (în milisecunde) după care se
va livra primul semnal de timer.

Folosiți un handler APC pentru tratarea timer­ului și afișarea mesajului.

folosiți exemplul de utilizare a timer­elor cu APC­uri din documentația MSDN
[http://msdn.microsoft.com/en­us/library/ms686898(VS.85).aspx].
ignorați warning­urile de compilare.
completați funcțiile din fișierul sursă.
folosiți ctime [http://www.kernel.org/doc/man­pages/online/pages/man3/ctime.3.html] și time
[http://www.kernel.org/doc/man­pages/online/pages/man2/time.2.html] pentru afișarea timpului curent.
ctime adaugă un caracter new­line (\n) la sfârșitul șirului întors.
folosiți SleepEx [http://msdn.microsoft.com/en­us/library/ms686307(VS.85).aspx] și argumentele
INFINITE, pentru a aștepta nedefinit, și TRUE, pentru a forța intrarea procesului într­o stare
alertabilă (care să declanșeze rularea APC­ului).
urmăriți secțiunea Waitable Timer Objects.

BONUS ­ Linux

2 so karma ­ timer

Intrați în directorul 6‐timer și urmăriți conținutul fișierului mytimer.c.

Exercițiul urmărește afișarea timpului curent la fiecare TIMEOUT secunde. Pentru a nu consuma inutil
timpul de procesor, se va suspenda procesul curent până la apariția semnalului generat de timer.

Urmăriți secțiunile cu TODO din fișierul sursă. Folosiți ctime [http://linux.die.net/man/3/ctime] și time
[http://linux.die.net/man/2/time] pentru afișarea timpului curent. Puteți vedea un exemplu de folosire aici
[http://www.cplusplus.com/reference/clibrary/ctime/ctime/]

Folosiți timer_create [http://www.kernel.org/doc/man­pages/online/pages/man2/timer_create.2.html] și
timer_settime [http://www.kernel.org/doc/man­pages/online/pages/man2/timer_settime.2.html] pentru a crea
și arma timer­ul. Semnalul generat de timer va fi  SIGALRM. Urmăriți secțiunea Timere în Linux

Folosiți sigsuspend [http://www.kernel.org/doc/man­pages/online/pages/man2/sigsuspend.2.html] pentru a
aștepta declanșarea semnalului generat de timer. Obțineți, folosind sigprocmask
[http://www.kernel.org/doc/man­pages/online/pages/man2/sigprocmask.2.html], masca procesului curent și
transmiteți­o ca argument către sigsuspend [http://www.kernel.org/doc/man­
pages/online/pages/man2/sigsuspend.2.html]. Urmăriți secțiunile Timere în Linux și Așteptarea unui semnal

1 so karma ­ timer
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 15/16
6/11/2017 Laborator 04 ­ Semnale [CS Open CourseWare]

Ramâneți în directorul 6‐timer. Modificați sursa de la exercițiul anterior astfel încât să configurați
funcția de handler direct din parametrii funcției timer_create() [http://www.kernel.org/doc/man­
pages/online/pages/man2/timer_create.2.html]. Urmăriți conținutul structurii sigevent. Un exemplu găsiți
aici [http://nicku.org/ossi/lab/processes/programming­posix­threads/sigev_thread.c].

Soluții
Soluții exerciții laborator 4 [http://elf.cs.pub.ro/so/res/laboratoare/lab04­sol.zip]

Resurse utile
TLPI ­ Chapter 22: Signals ­ Advanced features
TLPI ­ Chapter 23: Timers and Sleeping
WSP ­ Chapter 14: Asynchronous IO ­ Waitable Timers
so/laboratoare/laborator­04.txt · Last modified: 2017/03/30 13:10 by theodor.stoican

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­04 16/16
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Laborator 05 ­ Gestiunea memoriei

Materiale ajutătoare

lab05­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab05­slides.pdf]
lab05­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab05­refcard.pdf]

Nice to read

TLPI ­ Chapter 7, Memory Allocation

Gestiunea memoriei
Subsistemul de gestiune a memoriei din cadrul unui sistem de operare este folosit de toate celelalte
subsisteme: planificator, I/O, sistemul de fișiere, gestiunea proceselor, networking. Memoria este o
resursă importantă, de aceea sunt necesari algoritmi eficienți de utilizare și gestiune a acesteia.

Rolul subsistemului de gestiune a memoriei este de:

a ține evidența zonelor de memorie fizică (ocupate sau libere)
a oferi proceselor sau celorlalte subsisteme acces la memorie
a mapa paginile de memorie virtuală ale unui proces (pages) peste paginile fizice (frames).

Nucleul sistemului de operare oferă un set de interfețe (apeluri de sistem) care permit
alocarea/dealocarea de memorie, maparea unor regiuni de memorie virtuală peste fișiere, partajarea
zonelor de memorie.

Din păcate, nivelul limitat de înțelegere a acestor interfețe și a acțiunilor ce se petrec în spate conduc
la o serie de probleme foarte des întâlnite în aplicațiile software: memory leak­uri, accese nevalide,
suprascrieri, buffer overflow, corupere de zone de memorie.

Este, în consecință, fundamentală cunoașterea contextului în care acționează subsistemul de gestiune a
memoriei și înțelegerea interfeței puse la dispoziție programatorului de către sistemul de operare.

Spațiul de adresă al unui proces
Spațiul de adresă al unui proces, sau, mai bine spus, spațiul virtual de adresă al unui proces reprezintă
zona de memorie virtuală utilizabilă de un proces. Fiecare proces are un spațiu de adresă propriu.
Chiar în situațiile în care două procese partajează o zonă de memorie, spațiul virtual este distinct, dar
se mapează peste aceeași zonă de memorie fizică.

În figura alăturată este prezentat un spațiu de adresă tipic pentru un proces. În sistemele de operare
moderne, în spațiul virtual al fiecărui proces se mapează memoria nucleului, aceasta poate fi mapată
fie la început, fie la sfârșitul spațiului de adresă. În continuare, ne vom referi numai la spațiul de
adresă din user­space pentru un proces.

Cele 4 zone importante din spațiul de adresă al unui proces sunt zona de date, zona de cod, stiva și
heap­ul. După cum se observă și din figură, stiva și heap­ul sunt zonele care pot crește. De fapt,
aceste două zone sunt dinamice și au sens doar în contextul unui proces. De partea cealaltă,
informațiile din zona de date și din zona de cod sunt descrise în executabil.

Zona de cod

Segmentul de cod (denumit și text segment) reprezintă instrucțiunile în limbaj mașină ale
programului. Registrul de tip instruction pointer (IP) va referi adrese din zona de cod. Se citește
instrucțiunea indicată de către IP, se decodifică și se interpretează, după care se incrementează
contorul programului și se trece la următoarea instrucțiune.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 1/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

contorul programului și se trece la următoarea instrucțiune.
Zona de cod este, de obicei, o zonă read­only pentru ca
procesul să nu poată modifica propriile instrucțiuni prin
folosirea greșită a unui pointer. Zona de cod este partajată
între toate procesele care rulează același program. Astfel, o
singură copie a codului este mapată în spațiul de adresă virtual
al tuturor proceselor.

Zone de date
Zonele de date conțin variabilele globale definite într­un
program și variabilele de tipul read­only. În funcție de tipul de
date există mai multe subtipuri de zone de date.

.data

Zona .data conține variabilele globale și variabilele statice
inițializate la valori nenule ale unui program. De exemplu:

static int a = 3; 
char b = 'a';

.bss

Zona .bss conține variabilele globale și variabilele statice neinițializate ale unui program. Înainte de
execuția codului, acest segment este inițializat cu 0. De exemplu:

static int a; 
char b;

În general aceste variabile nu vor fi prealocate în executabil, ci în momentul creării procesului.
Alocarea zonei .bss se face peste pagini fizice zero (zeroed frames).

.rodata

Zona .rodata conține informație care poate fi doar citită, nu și modificată. Aici sunt stocate constantele:

const int a; 
const char *ptr;

și literalii:

"Hello, World!" 
"En Taro Adun!"

Stiva
Stiva este o regiune dinamică în cadrul unui proces, fiind gestionată automat de compilator.

Stiva este folosită pentru a stoca “stack frame­uri”. Pentru fiecare apel de funcție se va crea un nou
“stack frame”.

Un “stack frame” conține:

variabile locale
argumentele funcției
adresa de retur

Pe marea majoritate a arhitecturilor moderne stiva crește în jos (de la adrese mari la adrese mici) și
heap­ul crește în sus. Stiva crește la fiecare apel de funcție și scade la fiecare revenire din funcție.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 2/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

În figura de mai jos este prezentată o vedere conceptuală asupra stivei in momentul apelului unei
funcții.

Heap­ul
Heap­ul este zona de memorie dedicată alocării dinamice a memoriei. Heap­ul este folosit pentru
alocarea de regiuni de memorie a căror dimensiune este determinată la runtime.

La fel ca și stiva, heap­ul este o regiune dinamică care își modifică dimensiunea. Spre deosebire de
stivă, însă, heap­ul nu este gestionat de compilator. Este de datoria programatorului să știe câtă
memorie trebuie să aloce și să rețină cât a alocat și când trebuie să dealoce. Problemele frecvente în
majoritatea programelor țin de pierderea referințelor la zonele alocate (memory leaks) sau referirea
de zone nealocate sau insuficient alocate (accese nevalide).

În limbaje precum Java, Lisp etc. unde nu există “pointer freedom”, eliberarea spațiului alocat se face
automat prin intermediul unui garbage collector. Pe aceste sisteme se previne problema pierderii
referințelor, dar încă rămâne activă problema referirii zonelor nealocate.

Alocarea/Dealocarea memoriei
Alocarea memoriei este realizată static de compilator sau dinamic, în timpul execuției. Alocarea
statică e realizată în segmentele de date pentru variabilele globale sau pentru literali.

În timpul execuției, variabilele se alocă pe stivă sau în heap. Alocarea pe stivă se realizează automat
de compilator pentru variabilele locale unei funcții (mai puțin variabilele locale prefixate de
identificatorul static).

Alocarea dinamică se realizează în heap. Alocarea dinamică are loc atunci când nu se știe, în momentul
compilării, câtă memorie va fi necesară pentru o variabilă, o structură, un vector. Dacă se știe din
momentul compilării cât spațiu va ocupa o variabilă, se recomandă alocarea ei statică, pentru a
preveni erorile frecvent apărute în contextul alocării dinamice.

Pentru a fragmenta cât mai puțin spațiul de adrese al procesului, ca urmare a alocărilor și dealocărilor
unor zone de dimensiuni variate, alocatorul de memorie va organiza segmentul de date alocate dinamic
sub formă de heap, de unde și numele segmentului.

Dealocarea memoriei înseamnă eliberarea zonei de memorie (este marcată ca fiind liberă) alocate
dinamic anterior.

Dacă se omite dealocarea unei zone de memorie, aceasta va rămâne alocată pe întreaga durata de
rulare a procesului. Ori de câte ori nu mai este nevoie de o zonă de memorie, aceasta trebuie
dealocată pentru eficiența utilizării spațiului de memorie.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 3/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Nu trebuie neapărat realizată dealocarea diverselor zone înainte de un apel exit
[http://linux.die.net/man/3/exit] sau înainte de încheierea programului pentru că acestea sunt automat
eliberate de sistemul de operare.

Atenție! Pot apărea probleme și dacă se încearcă dealocarea aceleiași regiuni de memorie de două sau
mai multe ori și se corup datele interne de management al zonelor alocate dintr­un heap.

Alocarea memoriei în Linux
În Linux, alocarea memoriei pentru procesele utilizator se realizează prin intermediul funcțiilor de
bibliotecă malloc [http://linux.die.net/man/3/malloc], calloc [http://linux.die.net/man/3/calloc] și realloc
[http://linux.die.net/man/3/realloc], iar dealocarea ei prin intermediul funcției free
[http://linux.die.net/man/3/free]. Aceste funcții reprezintă apeluri de bibliotecă și rezolvă cererile de
alocare și dealocare de memorie, pe cât posibil, în user space.

Implementarea funcției malloc [http://linux.die.net/man/3/malloc] depinde de sistemul de operare.

Există implementări care țin niște tabele care specifică zonele de memorie alocate în heap. Dacă
există zone libere pe heap, un apel malloc [http://linux.die.net/man/3/malloc] care cere o zonă de
memorie care poate fi încadrată într­o zonă liberă din heap va fi satisfăcut imediat, marcând în tabel
zona respectivă ca fiind alocată și întorcând programului apelant un pointer spre ea.

Dacă, în schimb, se cere o zonă care nu încape în nici o zonă liberă din heap, malloc
[http://linux.die.net/man/3/malloc] va încerca extinderea heap­ului prin apelul de sistem brk
[http://linux.die.net/man/2/brk] sau mmap [http://linux.die.net/man/3/mmap].

Există implementări care pentru fiecare zonă de memorie cerută cu malloc
[http://linux.die.net/man/3/malloc] adaugă un header în care sunt trecute informații utile ­ dimensiunea
unei zone, pointer la următoarea zonă, dacă zonă a fost eliberată sau nu.

Mai multe detalii despre malloc și modul de organizare al heap­ului aici
[http://academicearth.org/lectures/heap­management] și aici [https://docs.google.com/viewer?url=http://wiki­
prog.kh405.net/images/0/04/Malloc_tutorial.pdf].

void *malloc(size_t size); 
void *calloc(size_t nmemb, size_t size); 

void *realloc(void *ptr, size_t size); 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 4/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]
void *realloc(void *ptr, size_t size); 
void free(void *ptr);

Întotdeauna eliberați (free [http://linux.die.net/man/3/free]) memoria alocată. Memoria alocată de proces
este eliberată automat la terminarea procesului, însă, de exemplu în cazul unui proces server care
rulează foarte mult timp și nu eliberează memoria alocată acesta va ajunge să ocupe toată memoria
disponibilă în sistem, având astfel consecințe nefaste.

Atenție! Nu eliberați de două ori aceeași zonă de memorie întrucât acest lucru va avea drept urmare
coruperea tabelelor ținute de malloc [http://linux.die.net/man/3/malloc] ceea ce va avea din nou
consecințe nefaste. Întrucât funcția free [http://linux.die.net/man/3/free] se întoarce imediat dacă
primește ca parametru un pointer NULL, este recomandat ca după un apel free
[http://linux.die.net/man/3/free], pointer­ul să fie resetat la NULL.

În continuare, sunt prezentate câteva exemple de alocare a memoriei folosind malloc
[http://linux.die.net/man/3/malloc]:

int n = atoi(argv[1]); 
char *str; 
  
/* usually malloc receives the size argument like: 
   num_elements * size_of_element */ 
str = malloc((n + 1) * sizeof(char)); 
if (NULL == str) { 
  perror("malloc"); 
  exit(EXIT_FAILURE); 

  
[...] 
  
free(str); 
str = NULL;

/* Creating an array of references to the arguments received by a program */ 
char **argv_no_exec; 
  
/* allocate space for the array */ 
argv_no_exec = malloc((argc ‐ 1) * sizeof(char*)); 
if (NULL == argv_no_exec) { 
  perror("malloc"); 
  exit(EXIT_FAILURE); 

  
/* set references to the program arguments */ 
for (i = 1; i < argc; i++) 
  argv_no_exec[i‐1] = argv[i]; 
  
[...] 
  
free(argv_no_exec); 
argv_no_exec = NULL;

Apelul realloc [http://linux.die.net/man/3/realloc] este folosit pentru modificarea spațiului de memorie
alocat cu un apel malloc [http://linux.die.net/man/3/malloc] sau calloc [http://linux.die.net/man/3/calloc]:

int *p; 
  
p = malloc(n * sizeof(int)); 
if (NULL == p) { 
  perror("malloc"); 
  exit(EXIT_FAILURE); 

  
[...] 
  
p = realloc(p, (n + extra) * sizeof(int)); 
  
[...] 
  
free(p); 
p = NULL;

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 5/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Apelul calloc [http://linux.die.net/man/3/calloc] este folosit pentru alocarea de zone de memorie al căror
conținut este nul (plin de valori de zero). Spre deosebire de malloc [http://linux.die.net/man/3/malloc],
apelul va primi două argumente: numărul de elemente și dimensiunea unui element.

list_t *list_v; /* list_t could be any C type ( except void ) */ 
  
list_v = calloc(n, sizeof(list_t)); 
if (NULL == list_v) { 
  perror("calloc"); 
  exit(EXIT_FAILURE); 

  
[...] 
  
free(list_v); 
list_v = NULL;

Atenție Conform standardului C, este redundant (și considerat bad practice) să faceți cast la valoarea
întoarsă de malloc [http://linux.die.net/man/3/malloc].

int *p = (int *)malloc(10 * sizeof(int));

malloc întoarce void * care în C este automat convertit la tipul dorit. Mai mult, dacă se face cast, iar
headerul stdlib.h necesar pentru funcția malloc nu este inclus, nu se va genera eroare! Pe anumite
arhitecturi, acest caz poate conduce la un comportament nedefinit. Spre deosebire de C, în C++ este
nevoie de cast. Mai multe detalii despre această problemă: aici [http://www.cprogramming.com/faq/cgi­
bin/smartfaq.cgi?answer=1047673478&id=1043284351]

Mai multe informații despre funcțiile de alocare găsiți în manualul bibliotecii standard C
[http://www.gnu.org/software/libc/manual/html_node/Unconstrained­Allocation.html#Unconstrained­Allocationl] și
în pagina de manual man malloc.

Alocarea memoriei în Windows

În Windows, un proces poate să­și creeze mai multe obiecte Heap pe lângă Heap­ul implicit al
procesulului. Acest lucru este foarte util în momentul în care o aplicație alocă și dealocă foarte multe
zone de memorie cu câteva dimensiuni fixe. Aplicația poate să­și creeze câte un Heap pentru fiecare
dimensiune și, în cadrul fiecărui Heap, să aloce zone de aceeași dimensiune reducând astfel la maxim
fragmentarea heap­ului.

Pentru crearea, respectiv distrugerea unui Heap se vor folosi funcțiile HeapCreate
[http://msdn.microsoft.com/en­us/library/aa366599%28VS.85%29.aspx] și HeapDestroy
[http://msdn.microsoft.com/en­us/library/aa366700%28VS.85%29.aspx]:

HANDLE HeapCreate( 
        DWORD flOptions, 
        SIZE_T dwInitialSize, 
        SIZE_T dwMaximumSize 
); 
  
BOOL HeapDestroy( 
        HANDLE hHeap 
);

Pentru a obține un descriptor al heap­ului implicit al procesului (în cazul în care nu dorim crearea altor
heapuri) se va apela funcția GetProcessHeap [http://msdn2.microsoft.com/en­us/library/aa366569.aspx].
Pentru a obține descriptorii tuturor heap­urilor procesului se va apela GetProcessHeaps
[http://msdn2.microsoft.com/en­us/library/aa366571.aspx].

Există, de asemenea, funcții care enumeră toate blocurile alocate într­un heap, validează unul sau
toate blocurile alocate într­un heap sau întorc dimensiunea unui bloc pe baza descriptorului de heap și a
adresei blocului: HeapWalk [http://msdn.microsoft.com/en­us/library/aa366710%28VS.85%29.aspx],
HeapValidate [http://msdn.microsoft.com/en­us/library/aa366708%28VS.85%29.aspx], HeapSize
[http://msdn.microsoft.com/en­us/library/aa366706%28VS.85%29.aspx].

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 6/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Pentru alocarea, dealocarea, redimensionarea unui bloc de memorie din Heap, Windows pune la
dispoziția programatorului funcțiile HeapAlloc [http://msdn.microsoft.com/en­
us/library/aa366597%28VS.85%29.aspx], HeapFree [http://msdn.microsoft.com/en­
us/library/aa366701%28VS.85%29.aspx], respectiv HeapReAlloc [http://msdn.microsoft.com/en­
us/library/aa366704%28VS.85%29.aspx], cu signaturile de mai jos:

LPVOID HeapAlloc( 
        HANDLE hHeap, 
        DWORD dwFlags, 
        SIZE_T dwBytes 
); 
  
BOOL HeapFree( 
        HANDLE hHeap, 
        DWORD dwFlags, 
        LPVOID lpMem 
); 
  
LPVOID HeapReAlloc( 
        HANDLE hHeap, 
        DWORD dwFlags, 
        LPVOID lpMem, 
        SIZE_T dwBytes 
);

În continuare, este prezentat un exemplu de folosire al acestor funcții:

#include <windows.h> 
#include "utils.h" 
  
/* Example of matrix allocation */ 
  
int main(void) 
{      
  HANDLE processHeap; 
  DWORD **mat; 
  DWORD i, j, m = 10, n = 10; 
  
  processHeap = GetProcessHeap(); 
  DIE (processHeap == NULL, "GetProcessHeap"); 
  
  mat = HeapAlloc(processHeap, 0, m * sizeof(*mat)); 
  DIE (mat == NULL, "HeapAlloc"); 
  
  for (i = 0; i < n; i++) { 
    mat[i] = HeapAlloc(processHeap, 0, n * sizeof(**mat)); 
    if (mat[i] == NULL) { 
      PrintLastError("HeapAlloc failed"); 
      goto freeMem; /* free previously allocated memory */ 
    } 
  } 
  
  /* do work */ 
  
freeMem: 
  for (j = 0; j < i; j++) 
    HeapFree(processHeap, 0, mat[j]); 
  HeapFree(processHeap, 0, mat); 
  
  return 0; 
}

Pe sistemele Windows se pot folosi și funcțiile bibliotecii standard C pentru gestiunea memoriei: malloc
[http://linux.die.net/man/3/malloc], realloc [http://linux.die.net/man/3/realloc], calloc
[http://linux.die.net/man/3/calloc], free [http://linux.die.net/man/3/free], dar apelurile de sistem specifice
Windows oferă funcționalități suplimentare și nu implică legarea bibliotecii standard C în executabil.

Lucru cu memoria ­ Probleme
Lucrul cu heap­ul este una dintre cauzele principale ale aparițiilor problemelor de programare. Lucrul
cu pointerii, necesitatea folosirii unor apeluri de sistem/bibliotecă pentru alocare/dealocare, pot
conduce la o serie de probleme care afectează (de multe ori fatal) funcționarea unui program.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 7/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Problemele cele mai des întâlnite în lucrul cu memoria sunt:

accesul nevalid la memorie ­ ce prespune accesarea unor zone care nu au fost alocate sau au
fost eliberate.
leak­urile de memorie ­ situațiile în care se pierde referința la o zonă alocată anterior. Acea
zonă va rămâne ocupată până la încheierea procesului.

Ambele probleme și utilitarele care pot fi folosite pentru combaterea acestora vor fi prezentate în
continuare.

Acces nevalid
De obicei, accesarea unei zone de memorie nevalide rezultă într­o eroare de pagină (page fault) și
terminarea procesului (în Unix înseamnă trimiterea semnalului SIGSEGV → afișarea mesajului
'Segmentation fault'). Totuși, dacă eroarea apare la o adresă nevalidă, dar într­o pagină validă,
hardware­ul și sistemul de operare nu vor putea sesiza acțiunea ca fiind nevalidă. Acest lucru este din
cauza faptului că alocarea memoriei se face la nivel de pagină. Spre exemplu, pot exista situații în care
să fie folosită doar jumătate din pagină. Deși cealaltă jumătate conține adrese nevalide, sistemul de
operare nu va putea detecta accesele nevalide la acea zonă. Mai multe detalii în laboratorul de
Memorie virtuală

Asemenea accese pot duce la coruperea heap­ului și la pierderea consistenței memoriei alocate. După
cum se va vedea în continuare, există utilitare care ajută la detectarea acestor situații.

Un tip special de acces nevalid este buffer overflow [http://en.wikipedia.org/wiki/Buffer_overflow]. Acest tip
de atac presupune referirea unor regiuni valide din spațiul de adresă al unui proces prin intermediul
unei variabile care nu ar trebui să poată referenția aceste adrese. De obicei, un atac de tip buffer
overflow rezultă în rularea de cod nesigur. Protejarea împotriva atacurilor de tip buffer overflow se
realizează prin verificarea limitelor unui buffer/vector fie la compilare, fie la rulare.

GDB ­ Detectarea zonei de acces nevalid de tip page fault

O comandă foarte utilă atunci când se depanează programe complexe este backtrace. Această
comandă afișează toate apelurile de funcții în curs de execuție.

#include <stdio.h> 
#include <stdlib.h> 
  
static int fibonacci(int no) 

  if (1 == no || 2 == no) 
    return 1; 
  return fibonacci(no‐1) + fibonacci(no‐2); 

  
int main(void) 

  short int numar, baza=10; 
  char sir[1]; 
  
  scanf("%s", sir); 
  numar=strtol(sir, NULL, baza);
  printf("fibonacci(%d)=%d\n", numar, fibonacci(numar)); 
  return 0; 
}

Pe exemplul de mai sus, vom demonstra utilitatea comenzii backtrace:

 so@spook$ gcc ‐Wall exemplul‐7.c ‐g 
 so@spook$ gdb a.out 
 (gdb) break 8 
 Breakpoint 1 at 0x8048482: file exemplul‐7.c, line 8. 
 (gdb) run 
 Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 
 7 
  
 Breakpoint 1, fibonacci (no=2) at exemplul‐7.c:8 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 8/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]
 Breakpoint 1, fibonacci (no=2) at exemplul‐7.c:8 
 8                       return 1; 
 (gdb) bt 
 #0  fibonacci (no=2) at exemplul‐7.c:8 
 #1  0x0804849d in fibonacci (no=3) at exemplul‐7.c:9 
 #2  0x0804849d in fibonacci (no=4) at exemplul‐7.c:9 
 #3  0x0804849d in fibonacci (no=5) at exemplul‐7.c:9 
 #4  0x0804849d in fibonacci (no=6) at exemplul‐7.c:9 
 #5  0x0804849d in fibonacci (no=7) at exemplul‐7.c:9 
 #6  0x0804851c in main () at exemplul‐7.c:20 
 #7  0x4003d280 in __libc_start_main () from /lib/libc.so.6 
 (gdb) 

Se observă că la afișarea apelurilor de funcții se listează și parametrii cu care a fost apelată funcția.
Acest lucru este posibil datorită faptului că atât variabilele locale, cât și parametrii acesteia sunt
păstrați pe stivă până la ieșirea din funcție. (pentru detalii, revedeți secțiunea despre stivă)

Fiecare funcție are alocată pe stivă un frame, în care sunt plasate variabilele locale funcției, parametrii
funcției și adresa de revenire din funcție. În momentul în care o funcție este apelată, se creează un nou
frame prin alocarea de spațiu pe stivă de către funcția apelată. Astfel, dacă avem apeluri de funcții
imbricate, atunci stiva va conține toate frame­urile tuturor funcțiilor apelate imbricat.

GDB dă posibilitatea utilizatorului să examineze frame­urile prezente în stivă. Astfel, utilizatorul poate
alege oricare din frame­urile prezente folosind comanda frame. După cum s­a observat, exemplul
anterior are un bug ce se manifestă atunci când numărul introdus de la tastatură depășește
dimensiunea buffer­ului alocat (static). Acest tip de eroare poartă denumirea de buffer overflow și este
extrem de gravă. Cele mai multe atacuri de la distanță pe un sistem sunt cauzate de acest tip de erori.
Din păcate, acest tip de eroare nu este ușor de detectat, pentru că în procesul de buffer overrun se pot
suprascrie alte variabile, ceea ce duce la detectarea erorii nu imediat când s­a făcut suprascrierea, ci
mai târziu, când se va folosi variabila afectatã.

 so@spook$ gdb a.out 
 (gdb) run 
 Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 
 10 
  
 Program received signal SIGSEGV, Segmentation fault. 
 0x08048497 in fibonacci (no=‐299522) at exemplul‐7.c:9 
 9               return fibonacci(no‐1) + fibonacci(no‐2); 
 (gdb) bt ‐5 
  
 #299520 0x0804849d in fibonacci (no=‐2) at exemplul‐7.c:9 
 #299521 0x0804849d in fibonacci (no=‐1) at exemplul‐7.c:9 
 #299522 0x0804849d in fibonacci (no=0) at exemplul‐7.c:9 
 #299523 0x0804851c in main () at exemplul‐7.c:20 
 #299524 0x4003e280 in __libc_start_main () from /lib/libc.so.6

Din analiza de mai sus se observă că funcția fibonacci a fost apelată cu valoarea 0. Cum funcția nu
testează ca parametrul să fie valid, se va apela recursiv de un număr suficient de ori pentru a cauza
umplerea stivei programului. Se pune problema cum s­a apelat funcția cu valoarea 0, când trebuia
apelată cu valoarea 10.

 so@spook$ gdb a.out 
 (gdb) run 
 Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 
 10 
  
 Program received signal SIGSEGV, Segmentation fault. 
 0x08048497 in fibonacci (no=‐299515) at exemplul‐7.c:9 
 9               return fibonacci(no‐1) + fibonacci(no‐2); 
 (gdb) bt ‐2 
 #299516 0x0804851c in main () at exemplul‐7.c:20 
 #299517 0x4003d280 in __libc_start_main () from /lib/libc.so.6 
 (gdb) fr 299516 
 #299516 0x0804851c in main () at exemplul‐7.c:20 
 20              printf("fibonacci(%d)=%d\n", numar, fibonacci(numar)); 
 (gdb) print numar 
 $1 = 0 
 (gdb) print baza 
 $2 = 48 
 (gdb) 

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 9/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Se observă că problema este cauzată de faptul că variabila baza a fost alterată. Pentru a determina
când s­a întâmplat acest lucru, se poate folosi comanda  watch. Această comandă primește ca
parametru o expresie și va opri execuția programului de fiecare dată când valoarea expresiei se
schimbă.

 (gdb) quit 
 so@spook$ gdb a.out 
 (gdb) break main 
 Breakpoint 1 at 0x80484d6: file exemplul‐7.c, line 15. 
 (gdb) run 
 Starting program: /home/tavi/cursuri/so/lab/draft/intro/a.out 
  
 Breakpoint 1, main () at exemplul‐7.c:15 
 15              short int numar, baza=10; 
 (gdb) n 
 18              scanf("%s", sir); 
 (gdb) watch baza 
 Hardware watchpoint 2: baza 
 (gdb) continue 
 Continuing. 
 10 
 Hardware watchpoint 2: baza 
  
 Old value = 10 
 New value = 48 
 0x40086b41 in _IO_vfscanf () from /lib/libc.so.6 
 (gdb) bt 
 #0  0x40086b41 in _IO_vfscanf () from /lib/libc.so.6 
 #1  0x40087259 in scanf () from /lib/libc.so.6 
 #2  0x080484ed in main () at exemplul‐7.c:18 
 #3  0x4003d280 in __libc_start_main () from /lib/libc.so.6 
 (gdb) 

Din analiza de mai sus se observă că valoarea variabilei este modificată în funcția _IO_vfscanf, care
la rândul ei este apelată de către functia scanf. Dacă se analizează apoi parametrii pasați functiei
scanf, se observă imediat cauza erorii.
Pentru mai multe informații despre GDB consultați documentația online
[http://sourceware.org/gdb/download/onlinedocs/] (alternativ pagina info ­ info gdb) sau folosiți comanda
help din cadrul GDB.

mcheck ­ verificarea consistenței heap­ului

glibc permite verificarea consistenței heap­ului prin intermediul apelului mcheck
[http://www.gnu.org/software/libc/manual/html_node/Heap­Consistency­Checking.html#Heap­Consistency­
Checking] definit în mcheck.h. Apelul mcheck [http://www.gnu.org/software/libc/manual/html_node/Heap­
Consistency­Checking.html#Heap­Consistency­Checking] forțează malloc să execute diverse verificări de
consistență precum scrierea peste un bloc alocat cu malloc.

Alternativ, se poate folosi opțiunea ‐lmcheck la legarea programului fără a afecta sursa acestuia.

Varianta cea mai simplă este folosirea variabilei de mediu MALLOC_CHECK_. Dacă un program va fi
lansat în execuție cu variabila MALLOC_CHECK_ configurată, atunci vor fi afișate mesaje de eroare
(eventual programul va fi terminat forțat ­ aborted).

În continuare, este prezentat un exemplu de cod cu probleme în alocarea și folosirea heap­ului:

mcheck_test.c

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
  
int main(void) 

  int *v1; 
  
  v1 = malloc(5 * sizeof(*v1)); 
  if (NULL == v1) { 
    perror("malloc"); 
    exit (EXIT_FAILURE); 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 10/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]
    exit (EXIT_FAILURE); 
  } 
  
  /* overflow */ 
  v1[6] = 100; 
  
  free(v1); 
  
  /* write after free */ 
  v1[6] = 100; 
  
  /* reallocate v1 */ 
  v1 = malloc(10 * sizeof(int));
  if (NULL == v1) { 
    perror("malloc"); 
    exit (EXIT_FAILURE); 
  } 
  
  return 0; 
}

Mai jos se poate vedea cum programul este compilat și rulat. Mai întâi este rulat fără opțiuni de
mcheck, după care se definește variabila de mediu MALLOC_CHECK_ la rularea programului. Se
observă că deși se depășește spațiul alocat pentru vectorul v1 și se referă vectorul după eliberarea
spațiului, o rulare simplă nu rezultă în afișarea nici unei erori.

Totuși, dacă definim variabila de mediu MALLOC_CHECK_, se detectează cele două erori. De observat
că o eroare este detectată doar în momentul unui nou apel de memorie interceptat de mcheck.

 so@spook$ make 
 cc ‐Wall ‐g    mcheck_test.c   ‐o mcheck_test 
 so@spook$ ./mcheck_test   
 so@spook$ MALLOC_CHECK_=1 ./mcheck_test 
 malloc: using debugging hooks 
  *** glibc detected *** ./mcheck_test: free(): invalid pointer: 0x0000000000601010 *** 
  *** glibc detected *** ./mcheck_test: malloc: top chunk is corrupt: 0x0000000000601020 ***

mcheck nu este o soluție completă și nu detectează toate erorile ce pot apărea în lucrul cu memoria.
Detectează, totuși, un număr important de erori și reprezintă o facilitate importantă a glibc.

O descriere completă găsiți în pagina asociată [http://www.gnu.org/software/libc/manual/html_node/Heap­
Consistency­Checking.html#Heap­Consistency­Checking] din manualul glibc
[http://www.gnu.org/software/libc/manual].

Leak­uri de memorie
Un leak de memorie [http://en.wikipedia.org/wiki/Memory_leak] apare în două situații:

un program omite să elibereze o zonă de memorie
un program pierde referința la o zonă de memorie alocată și, drept consecință, nu o poate
elibera

Memory leak­urile au ca efect reducerea cantității de memorie existentă în sistem. Se poate ajunge, în
situații extreme, la consumarea întregii memorii a sistemului și la imposibilitatea de funcționare a
diverselor aplicații ale acestuia.

Ca și în cazul problemei accesului nevalid la memorie, utilitarul Valgrind este foarte util în detectarea
leak­urilor de memorie ale unui program.

Valgrind
Valgrind reprezintă o suită de utilitare folosite pentru operații de debugging și profiling. Cel mai popular
este Memcheck [http://valgrind.org/docs/manual/mc­manual.html], un utilitar care permite detectarea de
erori de lucru cu memoria (accese nevalide, memory leak­uri etc.). Alte utilitare din suita Valgrind sunt
Cachegrind, Callgrind utile pentru profiling sau Helgrind, util pentru depanarea programelor
multithreaded.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 11/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

În continuare, ne vom referi doar la utilitarul Memcheck [http://valgrind.org/docs/manual/mc­manual.html]
de detectare a erorilor de lucru cu memoria. Mai precis, acest utilitar detectează următoarele tipuri de
erori:

folosirea de memorie neinițializată
citirea/scrierea din/în memorie după ce regiunea respectivă a fost eliberată
citirea/scrierea dincolo de sfârșitul zonei alocate
citirea/scrierea pe stivă în zone necorespunzătoare
memory leak­uri
folosirea necorespunzătore de apeluri malloc/new și free/delete

Valgrind nu necesită adaptarea codului unui program, ci folosește direct executabilul (binarul) asociat
unui program. La o rulare obișnuită Valgrind va primi argumentul ‐‐tool pentru a preciza utilitarul
folosit și programul care va fi verificat de erori de lucru cu memoria.

În exemplul de rulare, de mai jos, se folosește programul prezentat la secțiunea ''mcheck'':

 so@spook$ valgrind ‐‐tool=memcheck ./mcheck_test 
 ==17870== Memcheck, a memory error detector. 
 ==17870== Copyright (C) 2002‐2007, and GNU GPL'd, by Julian Seward et al. 
 ==17870== Using LibVEX rev 1804, a library for dynamic binary translation. 
 ==17870== Copyright (C) 2004‐2007, and GNU GPL'd, by OpenWorks LLP. 
 ==17870== Using valgrind‐3.3.0‐Debian, a dynamic binary instrumentation framework. 
 ==17870== Copyright (C) 2000‐2007, and GNU GPL'd, by Julian Seward et al. 
 ==17870== For more details, rerun with: ‐v 
 ==17870==  
 ==17870== Invalid write of size 4 
 ==17870==    at 0x4005B1: main (mcheck_test.c:17) 
 ==17870==  Address 0x5184048 is 4 bytes after a block of size 20 alloc'd 
 ==17870==    at 0x4C21FAB: malloc (vg_replace_malloc.c:207) 
 ==17870==    by 0x400589: main (mcheck_test.c:10) 
 ==17870==  
 ==17870== Invalid write of size 4 
 ==17870==    at 0x4005C8: main (mcheck_test.c:22) 
 ==17870==  Address 0x5184048 is 4 bytes after a block of size 20 free'd 
 ==17870==    at 0x4C21B2E: free (vg_replace_malloc.c:323) 
 ==17870==    by 0x4005BF: main (mcheck_test.c:19) 
 ==17870==  
 ==17870== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 8 from 1) 
 ==17870== malloc/free: in use at exit: 40 bytes in 1 blocks. 
 ==17870== malloc/free: 2 allocs, 1 frees, 60 bytes allocated. 
 ==17870== For counts of detected errors, rerun with: ‐v 
 ==17870== searching for pointers to 1 not‐freed blocks. 
 ==17870== checked 76,408 bytes. 
 ==17870==  
 ==17870== LEAK SUMMARY: 
 ==17870==    definitely lost: 40 bytes in 1 blocks. 
 ==17870==      possibly lost: 0 bytes in 0 blocks. 
 ==17870==    still reachable: 0 bytes in 0 blocks. 
 ==17870==         suppressed: 0 bytes in 0 blocks. 
 ==17870== Rerun with ‐‐leak‐check=full to see details of leaked memory.

S­a folosit utilitarul Memcheck [http://valgrind.org/docs/manual/mc­manual.html] pentru obținerea
informațiilor de acces la memorie.

Se recomandă folosirea opțiunii ‐g la compilarea programului pentru a include în executabil informații
de depanare. În rularea de mai sus, Valgrind a identificat două erori: una apare la linia 17 de cod și
este corelată cu linia 10 (malloc), iar cealaltă apare la linia 22 și este corelată cu linia 19 (free):

       v1 = (int *) malloc (5 * sizeof(*v1));
       if (NULL == v1) { 
              perror ("malloc"); 
              exit (EXIT_FAILURE); 
       } 
  
       /* overflow */ 
       v1[6] = 100; 
  
       free(v1); 
  
       /* write after free */ 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 12/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]
       /* write after free */ 
       v1[6] = 100;

Exemplul următor reprezintă un program cu o gamă variată de erori de alocare a memoriei:

#include <stdlib.h> 
#include <string.h> 
  
int main(void) 

  char buf[10]; 
  char *p; 
  
  /* no init */ 
  strcat(buf, "al"); 
  
  /* overflow */ 
  buf[11] = 'a'; 
  
  p = malloc(70); 
  p[10] = 5; 
  free(p); 
  
  /* write after free */ 
  p[1] = 'a'; 
  p = malloc(10); 
  
  /* memory leak */ 
  p = malloc(10); 
  
  /* underrun */ 
  p‐‐; 
  *p = 'a'; 
  
  return 0; 
}

În continuare, se prezintă comportamentul executabilului obținut la o rulare obișnuită și la o rulare sub
Valgrind:

 so@spook$ make 
 cc ‐Wall ‐g    valgrind_test.c   ‐o valgrind_test 
 so@spook$ ./valgrind_test  
 so@spook$ valgrind ‐‐tool=memcheck ./valgrind_test 
 ==18663== Memcheck, a memory error detector. 
 ==18663== Copyright (C) 2002‐2007, and GNU GPL'd, by Julian Seward et al. 
 ==18663== Using LibVEX rev 1804, a library for dynamic binary translation. 
 ==18663== Copyright (C) 2004‐2007, and GNU GPL'd, by OpenWorks LLP. 
 ==18663== Using valgrind‐3.3.0‐Debian, a dynamic binary instrumentation framework. 
 ==18663== Copyright (C) 2000‐2007, and GNU GPL'd, by Julian Seward et al. 
 ==18663== For more details, rerun with: ‐v 
 ==18663==  
 ==18663== Conditional jump or move depends on uninitialised value(s) 
 ==18663==    at 0x40050D: main (valgrind_test.c:10) 
 ==18663==  
 ==18663== Invalid write of size 1 
 ==18663==    at 0x400554: main (valgrind_test.c:20) 
 ==18663==  Address 0x5184031 is 1 bytes inside a block of size 70 free'd 
 ==18663==    at 0x4C21B2E: free (vg_replace_malloc.c:323) 
 ==18663==    by 0x40054B: main (valgrind_test.c:17) 
 ==18663==  
 ==18663== Invalid write of size 1 
 ==18663==    at 0x40057C: main (valgrind_test.c:28) 
 ==18663==  Address 0x51840e7 is 1 bytes before a block of size 10 alloc'd 
 ==18663==    at 0x4C21FAB: malloc (vg_replace_malloc.c:207) 
 ==18663==    by 0x40056E: main (valgrind_test.c:24) 
 ==18663==  
 ==18663== ERROR SUMMARY: 6 errors from 3 contexts (suppressed: 8 from 1) 
 ==18663== malloc/free: in use at exit: 20 bytes in 2 blocks. 
 ==18663== malloc/free: 3 allocs, 1 frees, 90 bytes allocated. 
 ==18663== For counts of detected errors, rerun with: ‐v 
 ==18663== searching for pointers to 2 not‐freed blocks. 
 ==18663== checked 76,408 bytes. 
 ==18663==  
 ==18663== LEAK SUMMARY: 

 ==18663==    definitely lost: 20 bytes in 2 blocks. 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 13/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]
 ==18663==    definitely lost: 20 bytes in 2 blocks. 
 ==18663==      possibly lost: 0 bytes in 0 blocks. 
 ==18663==    still reachable: 0 bytes in 0 blocks. 
 ==18663==         suppressed: 0 bytes in 0 blocks. 
 ==18663== Rerun with ‐‐leak‐check=full to see details of leaked memory.

Se poate observa că, la o rulare obișnuită, programul nu generează nici un fel de eroare. Totuși, la
rularea cu Valgrind, apar erori în 3 contexte:

1. la apelul strcat (linia 10) șirul nu a fost inițializat
2. se scrie în memorie după free (linia 20: p[1] = 'a')
3. underrun (linia 28)

În plus, există leak­uri de memorie datorită noului apel malloc care asociază o nouă valoare lui p
(linia 24).

Valgrind este un utilitar de bază în depanarea programelor. Este facil de folosit (nu este intrusiv, nu
necesită modificarea surselor) și permite detectarea unui număr important de erori de programare
apărute ca urmare a gestiunii defectuoase a memoriei.

Informații complete despre modul de utilizare a Valgrind și a utilitarelor asociate se găsesc în paginile
de documentație [http://valgrind.org/docs/manual/index.html] Valgrind.

mtrace

Un alt utilitar care poate fi folosit la depanarea erorilor de lucru cu memoria este mtrace
[http://en.wikipedia.org/wiki/Mtrace]. Acest utilitar ajută la identificarea leak­urilor de memorie ale unui
program.

Utilitarul mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracing­malloc.html#Tracing­malloc] se
folosește cu apelurile mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracing­
malloc.html#Tracing­malloc] și muntrace [http://www.gnu.org/software/libc/manual/html_node/Tracing­
malloc.html#Tracing­malloc] implementate în biblioteca standard C:

void mtrace(void); 
void muntrace(void);

Utilitarul mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracing­malloc.html#Tracing­malloc]
introduce handlere pentru apelurile de biblioteca pentru lucrul cu memoria (malloc, realloc, free).
Apelurile mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracing­malloc.html#Tracing­malloc] și
muntrace [http://www.gnu.org/software/libc/manual/html_node/Tracing­malloc.html#Tracing­malloc] activează,
respectiv dezactivează monitorizarea apelurilor de bibliotecă de lucru cu memoria.

Jurnalizarea operațiilor efectuate se realizează în fișierul definit de variabila de mediu MALLOC_TRACE.
După ce apelurile au fost înregistrate în fișierul specificat, utilizatorul poate să folosească utilitarul
mtrace pentru analiza acestora.
În exemplul de mai jos este prezentată o situație în care se alocă memorie fără a fi eliberată:

mtrace_test.c

#include <stdlib.h> 
#include <mcheck.h> 
  
int main(void) 

  /* start memcall monitoring */ 
  mtrace(); 
  
  malloc(10); 
  malloc(20); 
  malloc(30); 
  
  /* stop memcall monitoring */ 
  muntrace(); 
  
  return 0; 
}

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 14/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

În secvența de comenzi ce urmează se compilează fișierul de mai sus, se stabilește fișierul de
jurnalizare și se rulează comanda mtrace pentru a detecta problemele din codul de mai sus.

 so@spook$ gcc ‐Wall ‐g mtrace_test.c ‐o mtrace_test 
 so@spook$ export MALLOC_TRACE=./mtrace.log 
 so@spook$ ./mtrace_test  
 so@spook$ cat mtrace.log  
 = Start 
 @ ./mtrace_test:[0x40054b] + 0x601460 0xa 
 @ ./mtrace_test:[0x400555] + 0x601480 0x14 
 @ ./mtrace_test:[0x40055f] + 0x6014a0 0x1e 
 = End 
 so@spook$ mtrace mtrace_test mtrace.log  
  
 Memory not freed: 
 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 
        Address     Size     Caller 
 0x0000000000601460      0xa  at /home/razvan/school/so/labs/lab4/samples/mtrace.c:11 
 0x0000000000601480     0x14  at /home/razvan/school/so/labs/lab4/samples/mtrace.c:12 
 0x00000000006014a0     0x1e  at /home/razvan/school/so/labs/lab4/samples/mtrace.c:15

Mai multe informații despre detectarea problemelor de alocare folosind mtrace găsiți în pagina asociată
[http://www.gnu.org/software/libc/manual/html_node/Allocation­Debugging.html#Allocation­Debugging] din
manualul glibc [http://www.gnu.org/software/libc/manual].

Dublă dealocare
Denumirea de “dublă dealocare” oferă o bună intuiție asupra cauzei: eliberarea de două ori a aceluiași
spațiu de memorie. Dubla dealocare poate avea efecte negative deoarece afectează structurile interne
folosite pentru a gestiona memoria ocupată.

În ultimele versiuni ale bibliotecii standard C, se detectează automat cazurile de dublă dealocare. Fie
exemplul de mai jos:

dubla_dealocare.c

#include <stdlib.h> 
  
int main(void) 

  char *p; 
  
  p = malloc(10); 
  free(p); 
  free(p); 
  
  return 0; 
}

Rularea executabilului obținut din programul de mai sus duce la afișarea unui mesaj specific al glibc de
eliberare dublă a unei regiuni de memorie și terminare a programului:

 so@spook$ make 
 cc ‐Wall ‐g    dfree.c   ‐o dfree 
 so@spook$ ./dfree  
  *** glibc detected *** ./dfree: double free or corruption (fasttop): 0x0000000000601010 *** 
 ======= Backtrace: ========= 
 /lib/libc.so.6[0x2b675fdd502a] 
 /lib/libc.so.6(cfree+0x8c)[0x2b675fdd8bbc] 
 ./dfree[0x400510] 
 /lib/libc.so.6(__libc_start_main+0xf4)[0x2b675fd7f1c4] 
 ./dfree[0x400459]

Situațiile de dublă dealocare sunt, de asemenea, detectate de Valgrind.

Alte utilitare pentru depanarea problemelor de lucru cu memoria
Utilitarele prezentate mai sus nu sunt singurele folosite pentru detectarea problemelor apărute in lucrul
cu memoria [http://en.wikipedia.org/wiki/Category:Memory_management_software]. Alte utilitare sunt:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 15/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

dmalloc [http://dmalloc.com/]
mpatrol [http://mpatrol.sourceforge.net/]
DUMA [http://duma.sourceforge.net]
Electric Fence [http://perens.com/works/software/ElectricFence/], prezentat în laboratorul de Memorie
virtuală [http://elf.cs.pub.ro/so/wiki/laboratoare/laborator­07#electricfence]

Exerciții

Exercițiul 0 ­ Joc interactiv (2p)
Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (9p)
În rezolvarea laboratorului folosiți arhiva de sarcini lab05­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab05­tasks.zip]

Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din arhivă există un
fișier utils.h cu funcții utile.

Exercițiul 1 ­ Zone de stocare a variabilelor (0.5p)

Intrați în directorul 1‐counter și implementați funcția inc() care întoarce de fiecare dată un întreg
reprezentând numărul de apeluri până în momentul respectiv al funcției inc (nu aveți voie să folosiți
variabile globale).

Exercițiul 2 ­ Spațiul de adresă al unui proces (1p)

Intrați în directorul 2‐adr_space și deschideți sursa adr_space.c. În alt terminal compilați și rulați
programul. Observați zonele de memorie din executabil în care sunt salvate variabilele, folosind
comanda:

objdump ‐t adr_space | grep var

Observați că unele variabile apar in tabela de simboluri (variabilele globale și cele locale statice ­ așa
cum arată și flagurile l și g din dreptul acestora; `man objdump` ), iar altele nu. Variabilele care nu
apar in tabelă se află pe stivă.

Afișați conținutul zonei '.rodata' folosind utilitarul readelf

Hint: Trebuie să afișați hex dump­ul secțiunii .rodata a executabilului adr_space. Consultați pagina
de manual a readelf după parametrul potrivit.

Nu uitați să adăugați și numele fișierului executabil ca parametru al comenzii readelf.

Exercițiul 3 ­ Alocarea, realocarea și dezalocarea memoriei (1p)

Intrați în directorul 3‐alloc, compilați și rulați programul alloc.

Folosiți valgrind pentru a detecta eventualele probleme de lucru cu memoria și corectați­le.

Observați că se generează leak­uri de memorie din cauză că memoria alocată nu a fost eliberată
corespunzător atunci când zona respectivă nu a mai fost necesară în program.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 16/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Revedeți secțiunile Valgrind și Alocarea memoriei în Linux din laborator.

Exercițiul 4 ­ Rezolvarea unei probleme de tip Segmentation Fault (1p)

Intrați în directorul 4‐gdb și inspectați sursa. Programul ar trebui să citescă un mesaj de la stdin și
să­l afișeze. Compilați și rulați sursa. Rulați încă o dată programul din gdb (revedeți rularea unui
program din gdb).

Pentru a identifica exact unde crapă programul folosiți comanda backtrace
[http://inside.mines.edu/fs_home/lwiencke/elab/gdb/gdb_42.html]. Pentru detalii despre comenzile din gdb
folosiți comanda help:

(gdb) help

Schimbați frame­ul curent cu frame­ul funcției main (revedeți detectarea unui acces nevalid de tip
page fault):

(gdb) frame main

Inspectați valoarea variabilei buf:

(gdb) print buf

Acum dorim să vedem de ce este buf = NULL, urmărind pașii:

Omorâți actualul proces:

(gdb) kill

Puneți un breakpoint la începutul funcției main:

(gdb) break main

Rulați programul și inspectați valoarea lui buf înainte și după apelul funcției malloc (folosiți
next pentru a trece la instrucțiunea următoare, fără a urmări apelul de funcție).
Explicați sursa erorii, apoi rezolvați­o.

Exercițiul 5 ­ Lucru cu memoria ­ Valgrind (1p)

Intrați în directorul 5‐struct și completați fișierul struct.c conform comentariilor marcate cu TODO.

În funcția allocate_flowers alocați memorie pentru no elemente de tip flower_info, iar în
funcția free_flowers eliberați memoria alocată în funcția allocate_flowers.

Observați dacă programul se execută cu succes. Corectați eventualele greșeli având în vedere
următoarele aspecte:

Folosiți opțiunea ‐‐tool=memcheck pentru valgrind.
Revedeți secțiunea Valgrind din laborator.

Exercițiul 6 ­ Stack overflow (2p)
Intrați în directorul 6­stack și inspectați sursa și completați problemele marcate cu TODO1 astfel:

în funcția show_snapshot iterați pe toată lungimea stivei și afișați adresa și valoarea de la
adresa curentă

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 17/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

în funcția take_snapshot salvați în structura de date ce reține imaginea stivei câmpurile
adresă și valoare.

Ce reține structura stack_elements?

Funcția f2 pune pe stivă un vector de 3 întregi. În ce ordine sunt puse elementele vectorului pe stivă?

Compilați și rulați programul, iar pe urmă identificați care este adresa de revenire din funcția f2.

Dezasamblați executabilul. Observați că înainte de call f2 se pune pe stivă instruction pointer‐
ul(eip) care este adresa primului byte de după call. La intrarea în funcție controlul s­a transmis de la
caller la callee. Acesta din urmă salvează vechiul base pointer(ebp) iar ebp va conține adresa
vârfului stivei.

In funcția f2 bufferul v se află pe stivă sub adresa de return a funcției (IP­ul la care se întoarce
programul dupa ce execută f2). Scriind în bufferul v mai multe elemente decat are acesta alocate pe
stivă, vom putea suprascrie adresa de return a lui f2 cu o alta adresă (aici, adresa funcției
show_message). Atenție, după adresa de return este salvat pe stivă base pointerul și abia apoi găsim
și bufferul v.

Folosindu­vă de vectorul  v fortați execuția funcției show_message fără a o apela explicit. Astfel,
după apelul funcției f2, fluxul programului nu se va mai întoarce în funcția f1, ci va executa
show_message. Urmăriți comentariile marcate cu TODO2 (revedeți partea din laborator referitoare la
stivă )

Calling convention­ul pe baza căruia se construiește stack frame­ul la apelul unei funcții poate să difere
de la un sistem la altul. Astfel, poziția parametrilor și a variabilelor locale pe stivă pe un sistem Linux
pe 64 de biți (x86­64 [https://aaronbloomfield.github.io/pdr/book/x86­64bit­ccc­chapter.pdf]) nu o să fie
aceeași cu cea de pe un sistem pe 32 de biți.

Exercițiul 7 ­ Detectare probleme de lucru cu memoria ­ mcheck (1p)

În directorul 7‐trim analizați programul trim.c, compilați și rulați executabilul trim.

Încercați să detectați problema folosind gdb (revedeți tehnicile folosite la exercițiul 3). După aceea,
folosiți mcheck pentru a detecta problema și corectați­o (citiți secțiunea mcheck din laborator). Rularea
cu mcheck se face astfel:

MALLOC_CHECK_=1 ./trim

Exercițiul 8 ­ Endianess (1p)

Intrați în directorul 8‐endian și inspectați sursa endian.c. Folosindu­vă de variabila  w afișați
numărul n=0xDEADBEEF.

Ce tip de arhitectură se folosește? (big­endian sau little­endian, vezi aici
[http://en.wikipedia.org/wiki/Endianness] pentru detalii). Gândiți­vă la  n ca la un vector de caractere.

Exercițiul 9 ­ Lucrul cu stiva (0.5p)

Intrați în directorul 9‐bad_stack și analizați fișierul bad_stack.c. Compilați și rulați programul.

Se observă că în funcția main, prima oară se afișează valoarea din str, iar a doua oară nu. Observați
că după ieșirea din funcția myfun() variabila lab_so nu mai este accesibilă deoarece se iese din stack
frame­ul funcției myfun după return. Variabila va fi suprascrisă in cazul altor apeluri de funcții. Funcția
myfun nu returneaza o adresă (așa cum se cere explicit) ci registrul eax conține valoarea 0x0 după
return.

Care sunt tipurile de variabile care nu se află pe stivă?
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 18/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Modificați sursa, mai exact funcția myfun(), astfel încât variabila lab_so să fie accesibilă după return.

Indicație: Mutați variabila lab_so din funcția my_fun() într­o altă zonă

Exerciții BONUS (3 SO Karma)

BONUS Windows

1 so karma ­ Realizarea unui wrapper pentru funcțiile malloc și free

Deschideți proiectul Visual Studio din directorul malloc‐wrapper și inspectați cele două fișiere
existente: xmalloc.c și xmalloc.h.

Completați fișierul xmalloc.c cu definiția funcției xmalloc și fișierul xmalloc.h cu macrodefiniția
xfree după cum urmează:

în cazul xmalloc se alocă spațiu folosind HeapAlloc (trebuie să verificați dacă alocarea are
succes sau nu)
xfree este un macro care primește ca argument pointer­ul de eliberat (se apelează HeapFree
și pointer­ul este resetat la NULL)

De ce este mai dificil să se realizeze o funcție xfree care să realizeze aceleași operații?

1 so karma ­ Program de test pentru wrapperul xmalloc

Analizați fișierul test.c și implementați funcțiile tensor_alloc, respectiv tensor_free care
alocă/dealocă un vector tridimensional (tensor). Folosiți funcțiile xmalloc și xfree implementate în
cadrul exercițiului anterior (urmăriți comentariile marcate cu TODO).

BONUS Linux

1 so karma ­ Realizarea unei implementări sumare a funcției malloc

Urmăriți în man specificarea apelurilor brk [http://linux.die.net/man/2/brk] și sbrk
[http://linux.die.net/man/2/sbrk]. Folosind acest apel de sistem, completați implementarea funcției malloc
[http://linux.die.net/man/3/malloc] din sursa my_malloc.c. Va trebui întâi să extindeți limita curentă a
heap­ului (program break) cu valoarea cerută pentru alocare.

Compilați și testați rulând programul de test:

./test

Pentru rularea programului de test, nu uitați să exportați LD_LIBRARY_PATH (revedeți secțiunea de
biblioteci partajate din laboratorul 1)

Soluții
lab05­sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab05­sol.zip]

Resurse utile

Linux System Programming ­ Chapter 8 ­ Memory Management

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 19/20
6/11/2017 Laborator 05 ­ Gestiunea memoriei [CS Open CourseWare]

Windows System Programming ­ Chapter 5 ­ Memory Management (Win32 and Win64 Memory
Management Architecture, Heaps, Managing Heap Memory
Linux Application Programming ­ Chapter 7 ­ Memory Debugging Tools
Windows Memory Management [http://msdn2.microsoft.com/en­us/library/aa366779(VS.85).aspx]
Memory Allocation and Paging
[http://www.gnu.org/software/libc/manual/html_node/Memory.html#Memory Virtual]
Valgrind Home [http://www.valgrind.org/]
Using Valgrind to Find Memory Leaks [http://www.cprogramming.com/debugging/valgrind.html]
The Memory Management Reference [http://www.memorymanagement.org/]
Using Purify [http://www.ibm.com/developerworks/rational/library/06/0822_satish­giridhar/]
Memory Management Software [http://en.wikipedia.org/wiki/Category:Memory_management_software]
Smashing the Stack for Fun and Profit [http://insecure.org/stf/smashstack.html]
Guide to Faster, Less Frustrating Debugging
[http://heather.cs.ucdavis.edu/~matloff/UnixAndC/CLanguage/Debug.html]
GDB tutorial [http://individual.utoronto.ca/n_hoa/www/Misc/gdb.html]
BUG of the month [http://www.gimpel.com/html/newbugs/bug620.htm]

so/laboratoare/laborator­05.txt · Last modified: 2017/03/29 12:11 by adrian.stanciu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05 20/20
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Laborator 06 ­ Memoria virtuală

Materiale ajutătoare

lab06­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab06­slides.pdf]
lab06­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab06­refcard.pdf]

Nice to read

TLPI ­ Chapter 49, Memory mappings
TLPI ­ Chapter 50, Virtual memory operations

Memoria virtuală
Mecanismul de memorie virtuală este folosit de către nucleul sistemului de operare pentru a
implementa o politică eficientă de gestiune a memoriei. Astfel, cu toate că aplicațiile folosesc în mod
curent memoria virtuală, ele nu fac acest lucru în mod explicit. Există însă câteva cazuri în care
aplicațiile folosesc memoria virtuală în mod explicit.

Sistemul de operare oferă primitive de mapare a fișierelor, a memoriei sau a dispozitivelor în spațiul
de adresă al unui proces.

Maparea fișierelor în memorie este folosită în unele sisteme de operare pentru a implementa
mecanisme de memorie partajată. De asemenea, acest mecanism face posibilă implementarea
paginării la cerere și a bibliotecilor partajate.
Maparea memoriei în spațiul de adresă este folositoare atunci când un proces dorește să aloce
o cantitate mare de memorie.
Maparea dispozitivelor este folositoare atunci când un proces dorește să folosească direct
memoria unui dispozitiv (precum placa video).

Concepte teoretice
Dimensiunea spațiului de adresă virtual al unui proces depinde de dimensiunea registrelor procesorului.
Astfel, pe un sistem de 32 biți un proces va putea accesa 2^32 = 4GB spațiu de memorie (pe de altă
parte, pe un sistem de 64 biți va accesa teoretic 2^64 B). Spațiul de memorie al procesului este
împărțit în spațiu rezervat pentru adresele virtuale de kernel ­ acest spațiu este comun tuturor
proceselor ­ și spațiul virtual (propriu) de adrese al procesului. De cele mai multe ori, împărțirea între
cele două este de 3/1 (3GB user space vs 1GB kernel space).

Memoria fizică (RAM) este împărțită între procesele active în momentul respectiv și sistemul de
operare. Astfel că, în funcție de câtă memorie avem pe mașina fizică, este posibil să epuizăm toate
resursele și să nu mai putem porni un proces nou. Pentru a evita acest scenariu s­a introdus
mecanismul de memorie virtuală. În felul acesta, chiar dacă spațiul virtual (compus din segmentul de
text, data, heap, stivă) al unui proces este mai mare decât memoria fizică disponibilă pe sistem,
procesul va putea rula încărcându­și în memorie doar paginile de care are nevoie în timpul execuției
(on demand paging).

Spațiul virtual de adrese este împărțit în pagini virtuale (page). Corespondentul pentru memoria fizică
este pagina fizică (frame). Dimensiunea unei pagini virtuale este egală cu cea a unei pagini fizice.
Dimensiunea este dată de hardware (în majoritatea cazurilor o pagină are 4KB pe un sistem de 32 biți
sau 64 biți).

Atât timp cât un proces în timpul rulării accesează numai pagini rezidente în memorie, se execută ca și
când ar avea tot spațiul mapat în memoria fizică. În momentul în care un proces va dori să acceseze o
anumită pagină virtuală, care nu este mapată în memorie, se va genera un page fault, iar în urma
acestui page fault pagina virtuală va fi mapată la o pagină fizică. Două procese diferite au spațiu virtual
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 1/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

diferit, însă anumite pagini virtuale din aceste procese se pot mapa la aceeași pagină fizică. Astfel că,
două procese diferite pot partaja o aceeași pagină fizică, dar nu partajează pagini virtuale.

malloc

Așa cum am aflat la laboratorul de gestiunea memoriei
[http://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05#alocareadealocarea_memoriei], malloc alocă
memorie pe heap, deci în spațiul virtual al procesului. Funcția malloc poate fi implementată fie
folosind apeluri de sistem brk, fie apeluri mmap (mai multe detalii găsiți aici
[http://ocw.cs.pub.ro/courses/so/cursuri/curs­06#alocarea_de_memorie_virtuala]). Despre funcția mmap vom
vorbi în următoarele paragrafe din laboratorul curent.

Alocarea memoriei virtuale se face la nivel de pagină, astfel că malloc va aloca de fapt cel mai mic
număr de pagini virtuale ce cuprinde spațiul de memorie cerut. Fie următorul cod:

char *p = malloc(4150); 
DIE(p == NULL, "malloc failed");

Considerând că o pagină virtuală are 4KB = 4096 octeți, atunci apelul malloc va aloca 4096 octeți +
54 octeți = 4KB + 54 octeți, spațiu care nu este cuprins într­o singură pagină virtuală, astfel că se vor
aloca 2 pagini virtuale. În momentul alocării cu malloc nu se vor aloca (tot timpul) și pagini fizice;
acestea vor fi alocate doar atunci când sunt accesate datele din zona de memorie alocată cu malloc.
De exemplu, în momentul accesării unui element din p se va genera un page fault, iar pagina virtuală
ce cuprinde acel element va fi mapată la o pagină fizică.

În general, la apelul malloc de dimensiuni mici (când se apelează în spate apelul de sistem brk)
biblioteca standard C parcurge paginile alocate, se generează page fault­uri, iar la revenirea din apel
paginile fizice vor fi deja alocate. Putem spune că pentru dimensiuni mici, apelul malloc, așa cum este
văzut el din aplicație (din afara bibliotecii standard C), alocă și pagini fizice și pagini virtuale.

Mai mult, alocarea efectivă de pagini virtuale și fizice are loc în momentul apelului de sistem brk.
Acesta prealocă un spațiu mai mare, iar viitoarele apeluri malloc vor folosi acest spațiu. În acest fel,
următoarele apeluri malloc vor fi eficiente: nu vor face apel de sistem, nu vor face alocare efectivă
de spațiu virtual sau fizic, nu vor genera page fault­uri.

Apelul malloc este mai eficient decât apelul calloc pentru că nu parcurge spațiul alocat pentru a­l
umple cu zero­uri. Acest lucru înseamnă că malloc va întoarce zona alocată cu informațiile de acolo;
în anumite situații, acest lucru poate fi un risc de securitate ­ dacă datele de acolo sunt private.

Linux
Funcțiile cu ajutorul cărora se pot face cereri explicite asupra memoriei virtuale sunt funcțiile din
familia mmap(2). Funcțiile folosesc ca unitate minimă de alocare pagina (adică se poate aloca numai
un număr întreg de pagini, iar adresele trebuie să fie aliniate corespunzător).

Maparea fișierelor
În urma mapării unui fișier în spațiul de adresă al unui proces, accesul la acest fișier se poate face
similar cu accesarea datelor dintr­un vector. Eficiența metodei vine din faptul că zona de memorie este
gestionată similar cu memoria virtuală, supunându­se regulilor de evacuare pe disc atunci când
memoria devine insuficientă (în felul acesta se poate lucra cu mapări care depășesc dimensiunea
efectivă a memoriei fizice). Mai mult, partea de I/O este realizată de către kernel, programatorul
scriind cod care doar preia/stochează valori din/în regiunea mapată. Astfel nu se mai apelează read,
write, lseek ­ ceea ce adesea simplifică scrierea codului.
Nu orice descriptor de fișier poate fi mapat în memorie. Socket­urile, pipe­urile, dispozitivele care nu
permit decât accesul secvențial (ex. char device) sunt incompatibile cu conceptele de mapare. Există
cazuri în care fișiere obișnuite nu pot fi mapate (spre exemplu, dacă nu au fost deschise pentru a putea
fi citite; pentru mai multe informații: man mmap).

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 2/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

mmap

Prototipul funcției mmap [http://linux.die.net/man/2/mmap] ce permite maparea unui fișier în spațiul de
adresă al unui proces este următorul:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

Funcția va întoarce în caz de eroare MAP_FAILED. Dacă maparea s­a făcut cu succes, va întoarce un
pointer spre o zonă de memorie din spațiul de adresă al procesului, zonă în care a fost mapat fișierul
descris de descriptorul fd, începând cu offset­ul offset. Folosirea parametrului start permite
propunerea unei anumite zone de memorie la care să se facă maparea. Folosirea valorii NULL pentru
parametrul start indică lipsa vreunei preferințe în ceea ce privește zona în care se va face alocarea.
Adresa precizată prin parametrul start trebuie să fie multiplu de dimensiunea unei pagini. Dacă
sistemul de operare nu poate să mapeze fișierul la adresa cerută, atunci îl va mapa la o adresă
apropiată și multiplu de dimensiunea unei pagini. Adresa la care se mapează fișierul este întoarsă de
funcție.

Parametrul prot specifică tipul de acces care se dorește; poate fi PROT_READ (citire), PROT_WRITE
(scriere), PROT_EXEC (execuție) sau PROT_NONE; dacă zona e folosită altfel decât s­a declarat se va
genera un semnal SIGSEGV.

Parametrul flags permite stabilirea tipului de mapare ce se dorește; poate lua următoarele valori
(combinate prin SAU pe biți; trebuie să existe cel puțin MAP_PRIVATE sau MAP_SHARED):

MAP_PRIVATE ­ se folosește o politică de tip copy­on­write; zona va conține inițial o copie a
fișierului, dar scrierile nu sunt făcute în fișier; modificările nu vor fi vizibile în alte procese dacă
există mai multe procese care au făcut mmap pe aceeași zonă din același fișier
MAP_SHARED ­ scrierile sunt actualizate imediat în toate mapările existente (în acest fel toate
procesele care au realizat mapări vor vedea modificările); modificările vor fie vizibile și pentru
un proces ce utilizează read/write deoarece mapările MAP_SHARED se fac peste paginile fizice
din page cache iar apelurile read/write folosesc paginile fizice din page cache pentru a reduce
numărul de citiri/scrieri de pe disc; în schimb, actualizările pe disc vor avea loc la un moment
de timp ulterior, nespecificat
MAP_FIXED ­ dacă nu se poate face alocarea la adresa specificată de start, apelul va eșua
MAP_LOCKED ­ se va bloca paginarea pe această zonă, în maniera mlock
[http://linux.die.net/man/2/mlock]
MAP_ANONYMOUS ­ se mapează memorie (argumentele fd și offset sunt ignorate)

Este de remarcat că folosirea MAP_SHARED permite partajarea memoriei între procese care nu sunt
înrudite. În acest caz, conținutul fișierului devine conținutul inițial al memoriei partajate și orice
modificare făcută de procese în această zonă este copiată apoi în fișier, asigurând persistență prin
sistemul de fișiere.

msync
Pentru a declanșa în mod explicit sincronizarea fișierului cu maparea din memorie este disponibilă
funcția msync [http://linux.die.net/man/2/msync]:

int msync(void *start, size_t length, int flags);

unde flags poate fi:

MS_SYNC ­ datele vor fi scrise în fișier și după aceea funcția se va termina.
MS_ASYNC ­ este inițiată secvența de salvare, dar nu se așteaptă terminarea ei.
MS_INVALIDATE ­ se invalidează mapările zonei din alte procese, pentru a forța recitirea paginii
în toate celelalte procese la următorul acces.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 3/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Apelul msync este util pentru a face scrierea paginilor modificate din page cache pe disc, cu scopul de
a evita pierderea modificărilor în cazul unei căderi a sistemului.

Alocare de memorie în spațiul de adresă al procesului
În UNIX, tradițional, pentru alocarea memoriei dinamice, se folosește apelul de sistem brk
[http://linux.die.net/man/2/brk]. Acest apel crește sau descrește zona de heap asociată procesului. Odată
cu oferirea către aplicații a unor apeluri de sistem de gestiune a memoriei virtuale (mmap
[http://linux.die.net/man/2/mmap]), a existat posibilitatea ca procesele să aloce memorie folosind aceste
noi apeluri de sistem. Practic, procesele pot mapa memorie în spațiul de adresă, nu fișiere.

Procesele pot cere alocarea unei zone de memorie de la o anumită adresă din spațiul de adresare,
chiar și cu o anumită politică de acces (citire, scriere sau execuție). În UNIX, acest lucru se face tot
prin intermediul funcției mmap [http://linux.die.net/man/2/mmap]. Pentru acest lucru parametrul flags
trebuie să conțină flag­ul MAP_ANONYMOUS.

Maparea dispozitivelor
Există chiar și posibilitatea ca aplicațiile să mapeze în spațiul de adresă al unui proces un dispozitiv de
intrare­ieșire. Acest lucru este util, de exemplu, pentru plăcile video: o aplicație poate mapa în spațiul
de adresă memoria fizica a plăcii video. În UNIX, dispozitivele fiind reprezentate prin fișiere, pentru a
realiza acest lucru nu trebuie decât să deschidem fișierul asociat dispozitivului și să­l folosim într­un
apel mmap.

Nu toate dispozitivele pot fi mapate în memorie, însă atunci când pot fi mapate, semnificația acestei
mapări depinde strict de dispozitiv.

Un alt exemplu de dispozitiv care poate fi mapat este chiar memoria. În Linux se poate folosi fișierul
/dev/zero pentru a face mapări de memorie, ca și când s­ar folosi flag­ul MAP_ANONYMOUS.

Demaparea unei zone din spațiul de adresă
Dacă se dorește demaparea unei zone din spațiul de adresă al procesului se poate folosi funcția
munmap [http://linux.die.net/man/3/munmap]:

 int munmap(void *start, size_t length);

start reprezintă adresa primei pagini ce va fi demapată (trebuie să fie multiplu de dimensiunea unei
pagini). Dacă length nu este o dimensiune care reprezintă un număr întreg de pagini, va fi rotunjit
superior. Zona poate să conțină bucăți deja demapate. Se pot astfel demapa mai multe zone în același
timp.

Redimensionarea unei zone mapate
Pentru a executa operații de redimensionare a zonei mapate se poate utiliza funcția mremap
[http://linux.die.net/man/2/mremap]:

void *mremap(void *old_address, size_t old_size, size_t new_size, unsigned long flags);

Zona pe care old_address și old_size o descriu trebuie să aparțină unei singure mapări. O singură
opțiune este disponibilă pentru flags: MREMAP_MAYMOVE care arată că este în regulă ca pentru
obținerea noii mapări să se realizeze o nouă mapare într­o altă zonă de memorie (vechea zona fiind
dealocată).

Schimbarea protecției unei zone mapate
Uneori este nevoie ca modul (drepturile de acces) în care a fost mapată o zonă să fie schimbat. Pentru
acest lucru se poate folosi funcția mprotect [http://linux.die.net/man/2/mprotect]:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 4/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

int mprotect(const void *addr, size_t len, int prot);

Funcția primește ca parametri intervalul de adrese [addr, addr + len ­ 1] și noile drepturi de access
(PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE). Ca și la munmap
[http://linux.die.net/man/2/munmap], addr trebuie să fie multiplu de dimensiunea unei pagini. Funcția va
schimba protecția pentru toate paginile care conțin cel puțin un octet în intervalul specificat.

Exemplu

int fd = open("fisier", O_RDWR); 
void *p = mmap(NULL, 2*getpagesize(), PROT_NONE, MAP_SHARED, fd, 0); 
// *(char*)p = 'a'; // segv fault 
mprotect(p, 2*getpagesize(), PROT_WRITE); 
*(char*)p = 'a'; 
munmap(p, 2*getpagesize());

Apelul getpagesize va returna dimensiunea unei pagini in bytes.

Optimizări
Pentru ca sistemul de operare să poată implementa cât mai eficient accesele la o zona de memorie
mapată, programatorul poate să informeze kernel­ul (prin apelul de sistem madvise
[http://linux.die.net/man/2/madvise]) despre modul în care zona va fi folosită.

madvise [http://linux.die.net/man/2/madvise] e utilă mai ales atunci când în spatele memoriei virtuale se
află un dispozitiv fizic (de ex., când se mapează fișiere de pe hard­disk, kernel­ul poate citi în avans
pagini de pe disc, reducând latența datorată poziționării capului de citire). Prototipul funcției este
următorul:

int madvise(void *start, size_t length, int advice);

unde valorile acceptate pentru advice sunt:

MADV_NORMAL ­ regiunea este una obișnuită și nu are nevoie de un tratament special.
MADV_RANDOM ­ regiunea va fi accesată în mod aleator; sistemul de operare nu va citi în avans
pagini.
MADV_SEQUENTIAL ­ regiunea va fi accesată în mod secvențial; sistemul de operare ar putea
citi în avans pagini.
MADV_WILLNEED ­ regiunea va fi utilizată undeva în viitorul apropiat (nucleul poate decide să
preîncarce paginile în memorie).
MADV_DONTNEED ­ regiunea nu va mai fi utilizată; nucleul poate să elibereze zona alocată din
memorie, dar zona nu este demapată; nu se garantează păstrarea datelor la accesări
ulterioare.

Blocarea paginării
Paginarea se referă la evacuarea paginilor pe disc (swap out) si restaurarea lor (swap in) atunci când
sunt folosite. Există o categorie de procese care trebuie să execute anumite acțiuni la momente de
timp bine determinate, pentru a se păstra calitatea execuției. Pentru exemplificare, putem considera un
player audio/video sau un program ce controlează mersul unui robot biped. Problema cu acest gen de
procese este dată de faptul că dacă o anumită pagină nu este prezentă în memorie, va dura un timp
până ce ea va fi adusă de pe disc. Pentru a contracara aceste probleme, sistemele UNIX pun la
dispoziție apelurile mlock [http://linux.die.net/man/2/mlock] și mlockall [http://linux.die.net/man/2/mlockall].

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 5/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

int mlock(const void *addr, size_t len); 
int mlockall(int flags);

Funcția mlock [http://linux.die.net/man/2/mlock] va bloca paginarea (nu se va mai face swap out)
paginilor incluse în intervalul [addr, addr + len ­ 1]. Funcția mlockall
[http://linux.die.net/man/2/mlockall] va bloca paginarea tuturor paginilor procesului, în funcție de flag­uri:

MCL_CURRENT ­ se va bloca paginarea tuturor paginilor mapate în spațiul de adresă al
procesului la momentul apelului
MCL_FUTURE ­ se va bloca paginarea noilor pagini mapate în spațiul de adresă al procesului
(noi mapări realizate cu funcția mmap, dar și paginile de stivă mapate automat de sistem)

Notă:

Flag­ul MCL_FUTURE nu garantează faptul că paginile de stivă vor fi automat mapate în sistem. Dacă
procesul depășește limita de memorie impusă de sistem, va primi semnalul SIGSEGV. Pentru a nu se
ajunge în astfel de situații, programul trebuie să folosească mlockall(MCL_CURRENT |
MCL_FUTURE) și apoi să aloce dimensiunea maximă a stivei pe care urmează să o folosească (prin
declararea unei variabile locale, un vector de exemplu, și accesarea completă a acesteia).

Există, bineînțeles, și funcții ce readuc lucrurile la normal:

int munlock(const void *addr, size_t len); 
int munlockall(void);

Astfel, funcția munlock [http://linux.die.net/man/2/munlock] va reporni mecanismul de paginare al tuturor
paginilor din intervalul [addr, addr + len ­ 1], iar funcția munlockall
[http://linux.die.net/man/2/munlockall] face același lucru pentru toate paginile procesului, atât curente, cât
și viitoare. Trebuie notat faptul că, dacă s­au efectuat mai multe apeluri mlock
[http://linux.die.net/man/2/mlock] sau mlockall [http://linux.die.net/man/2/mlockall], este suficient un singur
apel munlock [http://linux.die.net/man/2/munlock] sau munlockall [http://linux.die.net/man/2/munlockall]
pentru a reactiva paginarea.

Excepții
Atunci când se detectează o încălcare a protecției la accesul la memorie, se va trimite semnalul
SIGSEGV sau SIGBUS procesului. După cum am văzut atunci când am discutat despre semnale,
semnalul poate fi tratat cu două tipuri de funcții: sa_handler și sa_sigaction. Funcția de tip
sa_sigaction va primi ca parametru o structură siginfo_t. În cazul semnalelor ce tratează
excepții cauzate de un acces incorect la memorie, următoarele câmpuri din această structură sunt
setate:

si_signo ­ setat la SIGSEGV sau SIGBUS
si_code ­ pentru SIGSEGV poate fi SEGV_MAPPER pentru a arăta că zona accesată nu este
mapată în spațiul de adresă al procesului, sau SEGV_ACCERR pentru a arăta că zona este
mapată dar a fost accesată necorespunzător; pentru SIGBUS poate fi BUS_ADRALN pentru a
arăta că s­a făcut un acces nealiniat la memorie,  BUS_ADRERR pentru a arăta că s­a încercat
accesarea unei adrese fizice inexistente sau BUS_OBJERR pentru a indica o eroare hardware
si_addr ­ adresa care a generat excepția

ElectricFence
ElectricFence [http://linux.die.net/man/3/efence] este un pachet ce ajută programatorii la depanarea
problemelor de tipul buffer overrun. Aceste probleme sunt cauzate de faptul că anumite date sunt
suprascrise fiindcă nu se fac verificări când se modifică date adiacente. Soluția folosită de Electric
Fence [http://linux.die.net/man/3/efence] este înlocuirea apelurilor standard malloc și free cu
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 6/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

implementări proprii. Electric Fence [http://linux.die.net/man/3/efence] va plasa zona de memorie alocată
în spațiul de adrese al procesului, astfel încât ea să fie mărginită de pagini neaccesibile (protejate la
scriere și citire).

Din păcate, sistemul de operare și arhitectura procesorului limitează dimensiunea paginii la cel puțin 1­
4KB, astfel încât dacă zona de memorie alocată nu este multiplu de această dimensiune, există
posibilitatea ca programul să poată citi sau scrie și în zone în care nu ar trebui, fără ca sistemul de
operare să oprească executia programului. Pentru a preveni situații de acestă natură, Electric Fence
[http://linux.die.net/man/3/efence] alocă zonele de memorie la limita superioară a unei pagini, mapând o
pagină neaccesibilă după aceasta. Această abordare nu previne buffer underrun­ul, în care datele sunt
citite sau scrise sub limita inferioară.

Pentru a putea verifica și astfel de situații, utilizatorul trebuie să definescă variabila de mediu
EF_PROTECT_BELOW înainte de rula programul. În acest caz, Electric Fence
[http://linux.die.net/man/3/efence] va plasa zona de memorie alocată la începutul unei pagini, pagină care
la rândul ei este plasată după o pagină inaccesibilă procesului.

De ce este importantă detectarea situațiilor de buffer overrun? Așa cum am explicat și în secțiunea
precedentă, astfel de situații vor produce în cele din urmă erori, dar la momente de timp ulterioare,
când va fi mai greu să se determine cauza erorilor cu mijloace de depanare obișnuite. În plus, în
situațiile de buffer overrun se pot suprascrie nu numai variabile, ci și alte date importante pentru
stabilitatea programului cum ar fi datele de control folosite de rutinele malloc și free. Biblioteca
Electric Fence [http://linux.die.net/man/3/efence] poate determina erorile de buffer overrun doar dacă
acestea apar în memoria alocată dinamic (adică în zona heap) cu rutinele malloc și free. Pentru a
folosi Electric Fence [http://linux.die.net/man/3/efence] utilizatorul trebuie să folosească la link­editare
biblioteca libefence. Pentru a vedea utilitatea acestui pachet, să analizăm programul de mai jos:

ef_example.c

#include <stdio.h> 
#include <malloc.h> 
  
int main(void) 

  int i; 
  int *data_1, *data_2; 
  
  data_1 = malloc(11 * sizeof(int)); 
  
  for (i = 0; i <= 11; i++) 
    data_1[i] = i; 
  
  data_2 = malloc(11 * sizeof(int)); 
  
  for (i = 0; i <= 11; i++) 
    data_2[i] = 11 ‐ i; 
  
  for (i = 0; i <= 11; i++) 
    printf("%d %d\n", data_1[i], data_2[i]); 
  
  free(data_1);  
  free(data_2); 
  
  return 0; 
}

Aparent totul pare în regulă. La execuția programului însă obținem următorul output:

 so@spook$ gcc ‐Wall ‐g ef_example.c 
 so@spook$ ./a.out 
ff: malloc.c:3074: sYSMALLOc: Assertion `(old_top == (((mbinptr) (((char *)  
&((av)‐>bins[((1) ‐ 1) * 2])) ‐ __builtin_offsetof (struct malloc_chunk, fd))))  
&& old_size == 0) || ((unsigned long)(old_size) >= (unsigned long) 
((((__builtin_offsetof (struct malloc_chunk, fd_nextsize))+((2 * (sizeof(size_t))) 
 ‐ 1)) & ~((2 * (sizeof(size_t))) ‐ 1))) && ((old_top)‐>size & 0x1) &&  
((unsigned long)old_end & pagemask) == 0)' failed.

Ceva este clar în neregulă. Dacă folosim biblioteca libefence și GDB eroarea va fi vizibilă imediat:

 so@spook$ gcc ‐Wall ‐g ef_example.c ‐lefence 
 so@spook$ gdb ./a.out  
 Reading symbols from /home/so/a.out...done. 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 7/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]
 Reading symbols from /home/so/a.out...done. 
 (gdb) run 
 Starting program: /home/so/a.out  
 [Thread debugging using libthread_db enabled] 
  
   Electric Fence 2.1 Copyright (C) 1987‐1998 Bruce Perens. 
  
 Program received signal SIGSEGV, Segmentation fault. 
 0x08048536 in main () at ef.c:12 
 12      data_1[i] = i; 
 (gdb) print i 
 $1 = 11 
 (gdb)

Se observă că eroarea apare în momentul în care încercăm să inițializăm al 12­lea element al
vectorului, deși vectorul nu are decât 11 elemente.

Pentru mai multe informații despre Electric Fence [http://linux.die.net/man/3/efence] consultați pagina de
manual (man efence).

Windows
În Windows funcțiile de control al memoriei virtuale sau mai bine zis al spațiului de adresă al unui
proces nu mai sunt grupate, ca în cazul Unix, într­o singură primitivă oferită de sistemul de operare.
Avem funcții pentru maparea fișierelor în memorie și funcții pentru alocarea de memorie fizică în
spațiul de adresă al unui proces.

Maparea fișierelor
Pentru a mapa un fișier în spațiul de adresă al unui proces trebuie mai întâi creat un handle către un
obiect de tipul FileMapping [http://msdn.microsoft.com/en­us/library/aa366556%28VS.85%29.aspx] și apoi
realizată efectiv maparea.

Pentru a crea un obiect de tip FileMapping se folosește funcția CreateFileMapping
[http://msdn.microsoft.com/en­us/library/aa366537%28v=VS.85%29.aspx]:

HANDLE CreateFileMapping( 
   HANDLE hFile, 
   LPSECURITY_ATTRIBUTES lpAttributes, 
   DWORD flProtect, 
   DWORD dwMaximumSizeHigh, 
   DWORD dwMaximumSizeLow, 
   LPCTSTR lpName 
 );

Funcția primește ca parametri handle­ul fișierului care se dorește a fi mapat, atribute de securitate
care controlează accesul la handle­ul obiectului FileMapping creat, tipul mapării (PAGE_READONLY,
PAGE_READWRITE, PAGE_WRITECOPY pentru copy­on­write) și dimensiunea maximă care poate fi
mapată cu ajutorul funcției MapViewOfFile. Opțional se poate specifica și un șir care să identifice
obiectul FileMapping creat. Dacă mai există un obiect de acest tip, funcția CreateFileMapping nu
va crea unul nou, ci îl va folosi pe cel existent. Atenție însă, obiectul trebuie să fi fost creat cu drepturi
care să permită procesului apelant să îl deschidă.

Pentru deschiderea unui obiect de tip FileMapping deja creat se mai poate folosi funcția
OpenFileMapping [http://msdn.microsoft.com/en­us/library/aa366791%28VS.85%29.aspx]:

HANDLE OpenFileMapping( 
  DWORD dwDesiredAccess, 
  BOOL bInheritHandle, 
  LPCTSTR lpName 
);

Maparea în spațiul de adrese al procesului se face folosind funcția MapViewOfFile
[http://msdn.microsoft.com/en­us/library/aa366761%28VS.85%29.aspx]:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 8/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

LPVOID MapViewOfFile( 
   HANDLE hFileMappingObject, 
   DWORD dwDesiredAccess, 
   DWORD dwFileOffsetHigh, 
   DWORD dwFileOffsetLow, 
   SIZE_T dwNumberOfBytesToMap 
 );

Funcția primește ca parametri un handle către un obiect de tip FileMapping, modul de acces la zona
mapată (FILE_MAP_READ, FILE_MAP_WRITE, FILE_MAP_COPY pentru copy­on­write), offset­ul în
fișier de unde începe maparea și numărul de octeți de mapat. Funcția va întoarce un pointer în spațiul
de adresă al procesului, la zona mapată.

Puteți urmări o prezentare mai detaliată a funcțiilor CreateFileMapping [http://msdn.microsoft.com/en­
us/library/aa366537%28v=VS.85%29.aspx] și MapViewOfFile [http://msdn.microsoft.com/en­
us/library/aa366761%28VS.85%29.aspx].

Alocare de memorie în spațiul de adresă al procesului
Pentru alocarea de memorie în spațiul de adresă al procesului se pot folosi funcțiile VirtualAlloc
[http://msdn.microsoft.com/en­us/library/aa366887%28VS.85%29.aspx] sau VirtualAllocEx
[http://msdn.microsoft.com/en­us/library/aa366890%28v=VS.85%29.aspx]:

 LPVOID VirtualAlloc( 
   LPVOID lpAddress, 
   SIZE_T dwSize, 
   DWORD flAllocationType, 
   DWORD flProtect 
 );

 LPVOID VirtualAllocEx( 
   HANDLE hProcess, 
   LPVOID lpAddress, 
   SIZE_T dwSize, 
   DWORD flAllocationType, 
   DWORD flProtect 
 );

Cu funcția VirtualAllocEx [http://msdn.microsoft.com/en­us/library/aa366890%28v=VS.85%29.aspx] se poate
aloca memorie în spațiul de adresă al unui proces arbitrar, specificat în parametrul hProcess. Procesul
curent trebuie să aibă drepturi corespunzătoare asupra procesului pe care se încearcă operația
(PROCESS_VM_OPERATION). Funcțiile întorc un pointer către adresa de start, iar parametrii așteptați
de funcții sunt descriși în spoiler:

lpAddress ­ adresa de unde începe alocarea; trebuie să fie multiplu de 4KB pentru alocare și
64KB pentru rezervare; dacă parametrul este NULL, sistemul va furniza automat o adresă
dwSize ­ dimensiunea zonei
fAllocationType ­ specifică tipul operației: rezervare (MEM_RESERVE), alocare
(MEM_COMMIT) sau renunțare la zonă (MEM_RESET); rezervarea unei zone înseamnă de fapt
“punerea deoparte” a unui interval din spațiul de adrese virtuale al procesului, fără a se aloca
însă memorie fizică; dacă se folosește MEM_COMMIT, se alocă efectiv memorie (dar doar dacă
în prealabil zona vizată a fost rezervată); atunci când se renunță la zonă nucleul poate face
discard la paginile din zonă, fără a face însă dezalocarea lor; după această operație datele nu
se păstrează
flProtect ­ specifică modul de acces permis la zona alocată: PAGE_EXECUTE,
PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY,
PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY, PAGE_NOACCESS, PAGE_GUARD,
PAGE_NOCACHE. Modurile _WRITECOPY arată că se va folosi mecanismul copy­on­write. Modul
PAGE_GUARD specifică faptul că la primul acces la o astfel de zonă se va genera o excepție
STATUS_GUARD_PAGE. PAGE_GUARD și PAGE_NOCACHE se pot folosi împreună cu celelalte
moduri.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 9/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Demaparea unei zone din spațiul de adresă
Pentru demaparea unei fișier mapat în memorie se folosește funcția UnmapViewOfFile
[http://msdn.microsoft.com/en­us/library/aa366882%28VS.85%29.aspx]:

BOOL UnmapViewOfFile( 
   LPCVOID lpBaseAddress 
);

Funcția primește adresa de început a zonei.

Pentru dezalocarea unei zone de memorie din spațiul de adresă se folosesc funcțiile VirtualFree
[http://msdn.microsoft.com/en­us/library/aa366892%28VS.85%29.aspx] și VirtualFreeEx
[http://msdn.microsoft.com/en­us/library/aa366894%28v=VS.85%29.aspx]:

BOOL VirtualFree( 
   LPVOID lpAddress, 
   SIZE_T dwSize, 
   DWORD dwFreeType 
);

BOOL VirtualFreeEx( 
   HANDLE hProcess, 
   LPVOID lpAddress, 
   SIZE_T dwSize, 
   DWORD dwFreeType 
);

Funcția VirtualFreeEx [http://msdn.microsoft.com/en­us/library/aa366894%28v=VS.85%29.aspx] va dezaloca
o zonă de memorie din spațiul de adresă al unui proces arbitrar, specificat în parametrul hProcess.
Procesul curent trebuie să aibă drepturi corespunzătoare asupra procesului pe care se încearcă operația
(PROCESS_VM_OPERATION).

Parametrii lpAddress și dwSize identifică zona de dezalocat. dwFreeType specifică tipul operației:
MEM_DECOMMIT, MEM_RELEASE. Prima operație va demapa paginile din spațiul de adresă, dar ele vor
rămâne rezervate. Cea de­a doua operație va anula rezervarea întregii zone „puse deoparte” anterior,
astfel încât adresa de start trebuie să coincidă cu adresa de start a zonei rezervate, iar dimensiunea
trebuie să fie 0.

Schimbarea protecției unei zone mapate
În Windows, schimbarea drepturilor de acces a unei zone mapate se poate face cu ajutorul funcțiilor
VirtualProtect [http://msdn.microsoft.com/en­us/library/aa366898%28VS.85%29.aspx] și VirtualProtectEx
[http://msdn.microsoft.com/en­us/library/aa366899%28v=VS.85%29.aspx]:

BOOL VirtualProtect( 
   LPVOID lpAddress, 
   SIZE_T dwSize, 
   DWORD flNewProtect, 
   PDWORD lpflOldProtect 
);

BOOL VirtualProtectEx( 
   HANDLE hProcess, 
   LPVOID lpAddress, 
   SIZE_T dwSize, 
   DWORD flNewProtect, 
   PDWORD lpflOldProtect 
);

Funcțiile vor schimba protecția paginilor care au măcar un octet în intervalul [lpAddress, lpAddress
+ dwSize ­ 1] la cea specificată în flNewProtect. Vechile drepturi de acces sunt salvate în
lpfOldProtect.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 10/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Toate paginile din intervalul specificat trebuie să fie din aceeași regiune rezervată cu apelul
VirtualAlloc sau VirtualAllocEx folosind MEM_RESERVE. Paginile nu pot fi localizate în regiuni
adiacente rezervate prin apeluri separate ale VirtualAlloc sau VirtualAllocEx folosind
MEM_RESERVE.

Interogarea zonelor mapate
Pentru a afla informații despre o zonă mapată în spațiul de adresă al unui proces se pot folosi funcțiile
VirtualQuery [http://msdn.microsoft.com/en­us/library/aa366902%28VS.85%29.aspx] și VirtualQueryEx
[http://msdn.microsoft.com/en­us/library/aa366907%28v=VS.85%29.aspx]. Ele vor oferi informații
apelantului despre adresa de start a zonei, protecție, dimensiune etc.

DWORD VirtualQuery( 
   LPCVOID lpAddress, 
   PMEMORY_BASIC_INFORMATION lpBuffer, 
   SIZE_T dwLength 
);

DWORD VirtualQueryEx( 
   HANDLE hProcess, 
   LPCVOID lpAddress, 
   PMEMORY_BASIC_INFORMATION lpBuffer, 
   SIZE_T dwLength 
);

Funcțiile primesc ca parametri o adresă din cadrul zonei ce se dorește a fi interogată, un pointer către
un buffer alocat ce va primi informații despre zonă și întorc numărul de octeți scriși în buffer. Dacă
funcția întoarce 0 înseamnă că nicio informație nu a fost furnizată. Acest lucru se întâmplă dacă funcției
îi este pasată o adresă din spațiul kernel.

Informațiile primite vor descrie două zone: zona alocată (cu VirtualAlloc) în care este inclusă
adresa dată, și zona care conține pagini de același fel (cu aceeași protecție și stare) în care este
inclusă adresa dată:

typedef struct _MEMORY_BASIC_INFORMATION { 
   PVOID BaseAddress; 
   PVOID AllocationBase; 
   DWORD AllocationProtect; 
   SIZE_T RegionSize; 
   DWORD State; 
   DWORD Protect; 
   DWORD Type; 
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

Câmpurile AllocationBase și AllocationProtect se referă la zona alocată, iar BaseAddress,
RegionSize, Type și Protect la zona ce conține pagini de același fel. State indică starea paginilor
din zonă: MEM_COMMIT pentru zonă alocată, MEM_RESERVED pentru zonă rezervată și MEM_FREE pentru
zonă nealocată. Type indică dacă în zonă este mapat un fișier (MEM_IMAGE sau MEM_MAPPED) sau nu,
și indică de asemenea dacă zona este partajată (MEM_PRIVATE) sau nu.

Blocarea paginării
Pentru blocarea paginării pentru un set de pagini (nu se va mai face swap out ­ în consecință apelurile
ulterioare nu mai produc page fault), sistemul de operare Windows pune la dispoziția utilizatorilor
funcția VirtualLock [http://msdn.microsoft.com/en­us/library/aa366895%28VS.85%29.aspx]:

BOOL VirtualLock( 
   LPVOID lpAddress, 
   SIZE_T dwSize 
);

Funcția primește prin parametri un interval de pagini (alcătuit din paginile care au măcar un octet în
intervalul [lpAddress, lpAddress + dwSize ­ 1]) pentru care se vrea blocarea paginării.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 11/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Funcția pentru reactivarea paginării este VirtualUnlock [http://msdn.microsoft.com/en­
us/library/aa366910%28v=VS.85%29.aspx]:

BOOL VirtualUnlock( 
   LPVOID lpAddress, 
   SIZE_T dwSize 
);

Excepții
Atunci când sistemul de operare detectează accese incorecte la memorie, va genera o excepție către
procesul care a efectuat accesul. Pentru tratarea excepției se pot folosi construcții __try și __except,
pentru care este necesar suport din partea compilatorului, sau se poate folosi funcția
AddVectoredExceptionHandler [http://msdn.microsoft.com/en­us/library/ms679274%28VS.85%29.aspx].

PVOID AddVectoredExceptionHandler( 
   ULONG FirstHandler, 
   PVECTORED_EXCEPTION_HANDLER VectoredHandler 
);

ULONG RemoveVectoredExceptionHandler( 
   PVOID VectoredHandlerHandle 
);

Funcția AddVectoredExceptionHandler [http://msdn.microsoft.com/en­
us/library/ms679274%28VS.85%29.aspx] va adăuga pe lista funcțiilor de executat atunci când se
generează o excepție, pe cea primită ca parametru în VectoredHandler. Parametrul FirstHandler
indică dacă funcția dorește să fie adăugată la începutul listei sau la sfârșit. Funcția de tratare a
excepțiilor trebuie să aibă următoarea semnătură:

LONG WINAPI VectoredHandler( 
   PEXCEPTION_POINTERS ExceptionInfo 
);

typedef struct _EXCEPTION_POINTERS { 
  PEXCEPTION_RECORD ExceptionRecord; 
  PCONTEXT ContextRecord; 
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

typedef struct _EXCEPTION_RECORD { 
  DWORD ExceptionCode; 
  DWORD ExceptionFlags; 
  struct _EXCEPTION_RECORD* ExceptionRecord; 
  PVOID ExceptionAddress; 
  DWORD NumberParameters; 
  ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; 
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

În cazul unor excepții cauzate de un acces invalid la memorie, ExceptionCode va fi setat la
EXCEPTION_ACCESS_VIOLATION sau EXCEPTION_DATATYPE_MISALIGNMENT, iar
ExceptionAddress la adresa instrucțiunii care a cauzat excepția; NumberParameters va fi setat pe
2, iar prima intrare în ExceptionInformation va fi 0 dacă s­a efectuat o operație de citire sau  1
dacă s­a efectuat o operație de scriere. A doua intrare din  ExceptionInformation va conține adresa
virtuală la care s­a încercat accesarea fără drepturi, fapt care a dus la generarea excepției. Așadar,
corespondentul câmpului si_addr din structura siginfo_t de pe Linux este
ExceptionInformation pe Windows, NU ExceptionAddress.
Funcția de tratare a excepției înregistrată cu AddVectoredExceptionHandler [http://msdn.microsoft.com/en­
us/library/ms679274%28VS.85%29.aspx] trebuie să întoarcă EXCEPTION_CONTINUE_EXECUTION, dacă
excepția a fost tratată și se dorește continuarea execuției, sau EXCEPTION_CONTINUE_SEARCH pentru
a continua parcurgerea listei de funcții de tratare a excepțiilor, în caz că au fost înregistrate mai multe
astfel de funcții.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 12/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Exerciții

Exercițiul 0 ­ Joc interactiv (2p)
Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

În rezolvarea laboratorului, folosiți arhiva de sarcini lab06­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab06­tasks.zip]. Platforma este la alegerea voastră. Punctajul maxim
se poate obține fie pe Linux, fie pe Windows. Lucrați în mașina virtuală

Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din arhivă există un
fișier utils.h cu funcții utile.

Linux (9p)

Exercițiul 1 ­ Investigarea mapărilor folosind pmap (0.5p)

Intrați în directorul 1‐intro și compilați sursa intro.c. Rulați programul intro:

./intro

Într­o altă consolă, folosiți comanda pmap [http://linux.die.net/man/1/pmap].:

 watch ‐d pmap $(pidof intro)

pentru a urmări modificările asupra memoriei procesului.

În prima consolă, folosiți ENTER pentru a continua programul. În cea de­a doua consolă urmăriți
modificările care apar în urma diferitelor tipuri de mapare din cod.

Analizați mapările făcute de procesul init folosind comanda:

sudo pmap 1

Puteți observa că pentru bibliotecile partajate (de exemplu, libc) sunt mapate trei zone: zona de cod
(read­execute), zona .rodata (read­only) și zona .data (read­write).

Exercițiul 2 ­ Scrierea în fișier ­ write vs. mmap (1p)

Intrați în directorul 2‐compare și inspectați sursele write.c și mmap.c, apoi compilați. Obțineți
timpul de execuție al celor două programe folosind comanda time:

time ./write; time ./mmap

Observăm că varianta cu mmap este mai rapidă decât varianta cu write. Vom folosi strace
[http://linux.die.net/man/1/strace] pentru a vedea ce apeluri de sistem se realizează pentru rularea
fiecărui program:

strace ‐c ./write 
strace ‐c ./mmap

Din output­ul strace observăm că programul write face foarte multe (100000) de apeluri write și
din această cauză este mai lent decât programul mmap.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 13/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

În continuare vom analiza cele două moduri de mapare a fișierelor: MAP_SHARED și MAP_PRIVATE.
Observați că fișierul test_mmap (creat de programul mmap cu MAP_SHARED) conține 100000 de linii:

cat test_mmap | wc ‐l

În programul mmap.c schimbați flagul de creare al memoriei partajate din MAP_SHARED în
MAP_PRIVATE, compilați și rulați din nou:

./mmap 
cat test_mmap | wc ‐l

Modificările aduse unei zone de memorie mapată cu MAP_PRIVATE nu vor fi vizible nici altor procese și
nici nu vor ajunge în fișierul mapat de pe disc.

Exercițiul 3 ­ Detectare 'buffer underrun' folosind ElectricFence (1p)

Intrați în directorul 3‐efence și urmăriți sursa bug.c. Compilați și rulați executabilul bug:

make 
./bug

Folosiți ElectricFence pentru a prinde situația de 'buffer underrun' urmărind pașii:

Instalați pachetul electric‐fence în cazul in care biblioteca libefence.so nu se găsește pe
sistem.
Setați în bash variabila de mediu EF_PROTECT_BELOW la 1:

export EF_PROTECT_BELOW=1

Creați și rulați programul ef_bug utilizând makefile­ul Makefile_efence:

make ‐f Makefile_efence 
./ef_bug

Exercițiul 4 ­ Copierea fișierelor folosind mmap (2p)

Intrați în directorul 4‐cp și completați sursa mycp.c astfel încât să realizeze copierea unui fișier primit
ca argument. Pentru aceasta, mapați ambele fișiere în memorie și realizați copierea folosind memcpy.
Urmăriți comentariile cu TODO din sursă și următoarele hint­uri:

Înainte de mapare, aflați dimensiunea fișierului sursă folosind fstat
[http://linux.die.net/man/2/fstat].
Trunchiați fișierul destinație la dimensiunea fișierului sursă.
Folosiți MAP_SHARED pentru mapare pentru a fi transmise schimbările în fișier: rețineți faptul că
apelul mmap folosește una dintre opțiunile MAP_SHARED sau MAP_PRIVATE (una singură)
Pentru fișierul de intrare protecția trebuie să fie PROT_READ: fișierul a fost deschis read­only.
Pentru fișierul de ieșire protecția trebuie să fie PROT_READ | PROT_WRITE; anumite
arhitecturi/implementări se pot plânge dacă folosiți doar PROT_WRITE.
Argumentele funcției memcpy [http://man7.org/linux/man­pages/man3/memcpy.3.html] sunt, în
ordine: destinația, sursa, numărul de octeți care să fie copiați.
Revedeți secțiunea maparea fișierelor.

Puteți testa în felul următor:

./mycp Makefile /tmp/Makefile 
diff Makefile /tmp/Makefile

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 14/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Verificați cum realizează utilitarul cp [http://linux.die.net/man/1/cp] copierea de fișiere (folosind mmap
sau read/write) folosind strace [http://linux.die.net/man/1/strace].

Utilitarul cp folosește read/write pentru a copia fișiere, în special pentru a limita consumul de
memorie în cazul copierii unor fișiere de dimensiuni mari. De asemenea, în cazul mapării fișierului în
memorie cu mmap, scrierea efectivă a datelor pe disc se va face într­un timp mai îndelungat, lucru care
de cele mai multe ori nu este dorit (urmăriți acest link [http://stackoverflow.com/a/27987994]).

Exercițiul 5 ­ Tipuri de acces pentru pagini (3p)

Intrați în directorul 5‐prot și inspectați sursa prot.c.

Creați o zonă de memorie în spațiul de adresă, formată din trei pagini virtuale (folosiți un singur apel
mmap). Prima pagină nu va avea vreun drept, a doua va avea drepturi de citire, iar a treia va avea
drepturi de scriere (folosiți mprotect pentru a configura drepturile fiecărei pagini). Testați
comportamentul programului când se fac accese de citire și scriere în aceste zone. Completați
comentariile cu TODO 1.

Adăugați un handler de tratare a excepțiilor care să remapeze incremental zonele cu protecție de citire
și scriere la generarea excepțiilor. Astfel, dacă pagina nu are vreun drept, la page fault se va remapa
cu drepturi de citire. Dacă pagina are drepturi de citire, la page fault se va remapa cu drepturi de citire
+ drepturi de scriere. Completați comentariile cu TODO 2.

Trebuie să ștergeți prima linie old_action.sa_sigaction(signum, info, context); pentru a
putea rezolva a doua parte a exercițiului.

Exercițiul 6 ­ Page fault­uri (0.5p)

Intrați în directorul 6‐faults și urmăriți conținutul fișierului fork‐faults.c.

Vom folosi utilitarul pidstat ( tutorial pidstat [http://www.cyberciti.biz/open­source/command­line­
hacks/linux­monitor­process­using­pidstat]) din pachetul sysstat pentru a monitoriza page fault­urile
făcute de un proces.

Dacă întâmpinați probleme în instalarea pachetului sysstat, descărcați­l de aici
[http://ro.archive.ubuntu.com/ubuntu/pool/main/s/sysstat/sysstat_11.2.0­1_i386.deb] și instalați­l folosind
comanda dpkg.

student@spook:~$ wget http://ro.archive.ubuntu.com/ubuntu/pool/main/s/sysstat/sysstat_11.2.0‐1_i386.deb
student@spook:~$ sudo dpkg ‐i sysstat_11.2.0‐1_i386.deb 

Rulați programul fork‐faults. Într­o altă consolă executați comanda

pidstat ‐r ‐T ALL ‐p $(pidof fork‐faults) 5

pentru a urmări page fault­urile. Comanda de mai sus vă afișează câte un mesaj la fiecare 5 secunde;
ne interesează valorile minflt‐nr.

Pe rând, apăsați tasta ENTER în consola unde ați rulat programul fork‐faults și observați output­ul
comenzii pidstat. Urmăriți evoluția numărului de page fault­uri pentru cele două procese: părinte și
copil. Page fault­urile care apar în cazul unui copy­on­write în procesul copil vor fi vizibile ulterior și în
procesul părinte (după ce procesul copil își încheie execuția).

Pachetul sysstat mai conține și utilitarul sar prin care puteți colecta și realiza rapoarte despre
activitatea sistemului. Pentru a activa salvarea datelor, trebuie setat flag­ul ENABLED din
/etc/default/sysstat. Cu ajutorul utilitarului sar puteți monitoriza informații precum încărcarea
CPU­ului, utilizarea memoriei și a paginilor, operațiile de I/O, activitatea proceselor. Detalii puteți afla
din tutorial sar [http://www.cyberciti.biz/tips/identifying­linux­bottlenecks­sar­graphs­with­ksar.html].

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 15/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Exercițiul 7 ­ Blocarea paginării (1p)
Vă aflați într­o situație în care trebuie să procesați în timp real datele dintr­un buffer și vreți să evitați
swaparea paginilor. Intrați în directorul 7‐paging și completați TODO‐urile astfel încât paginarea va
fi blocată pentru variabila data pe parcursul lucrului cu aceasta, iar la final va fi deblocată. Deși pe
Linux adresa va fi aliniată automat la dimensiunea unei pagini, acest lucru nu se întâmplă pe toate
sistemele POSIX compliant, prin urmare este o practică bună să o aliniem manual.

Deoarece variabila data este o variabilă locală a funcției main, aceasta va fi alocată pe stivă. Rulați
programul paging și folosiți, într­o altă consolă, comanda

pmap ‐X ‐p $(pidof paging)

după fiecare apăsare a tastei ENTER. Veți observa blocarea/deblocarea paginării pentru paginile
mapate pe stivă ce conțin cel puțin un byte al variabilei data.

Limita maximă pentru care se poate executa cu succes mlock este dată de RLIMIT_MEMLOCK (max
locked memory). Aceasta are de obicei valoarea 64KB și poate fi configurată folosind ulimit.

Bonus Linux

1 so karma ­ Schimbarea tipului de acces pentru pagini din segmentul de cod

Intrați în directorul 8‐hack. Programul apelează funcția foo(). Având determinată pagina în care se
află funcția în spațiul de adresă al procesului, i se schimbă drepturile de acces în
PROT_READ|PROT_WRITE|PROT_EXEC și se modifică valoarea de retur a funcției (se scrie în
segmentul de cod).

Analizați cu atenție programul. Analizați comportamentul cu gdb. Având pid­ul procesului afișat la
stdout, folosiți pmap [http://linux.die.net/man/1/pmap] pentru a observa pagina cu drepturile schimbate.
Observați tipul de acces pentru celelalte pagini din spațiul de adresă al procesului.

Modificați drepturile de acces în PROT_READ|PROT_EXEC, compilați și rulați din nou. Observați că fără
drepturi de scriere execuția programului este încheiată de un semnal SIGSEGV.

Windows (9p)

Exercițiul 1 ­ Maparea memoriei (0.5p)

Deschideți proiectul 1‐intro. Inspectați și compilați sursa intro.c. Rulați proiectul, iar în paralel
urmăriți comportamentul programului intro în Task Manager ­ în special coloanele Memory ‐
Working Set, Memory ‐ Private Working Set și Page Faults. Pentru a vedea o listă completă
cu coloanele care pot fi activate accesați Task Manager(tabul Processes)→View→Select Columns.

Exercițiul 2 ­ Crearea unor rutine în mod dinamic (1p)

Deschideți proiectul 2‐dyn și urmăriți sursa dyn.c. Programul alocă memorie în spațiul de adresă al
procesului pentru a stoca o rutină, de forma dyncode. Rutina va incrementa parametrul primit și va
întoarce această valoare. Urmăriți conțintul lui code. Deși în acest caz conținutul rutinei este definit
direct în program prin code, el ar putea fi primit în orice alt mod (fișier, rețea).

Exercițiul 3 ­ Mapare fișiere în memorie (1.5p)

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 16/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Să se scrie un program care copiază un fișier folosind proiectul 3‐copy. Programul primește ca
argumente numele fișierului sursă și numele fișierului desținație, mapează în memorie cele două fișiere
și copiază conținutul primului fișier folosind memcpy(3). Pentru aflarea lungimii fișierului sursă s­a
folosit GetFileAttributesEx [http://msdn.microsoft.com/en­us/library/aa364946(VS.85).aspx]. Fișierul
destinație trebuie trunchiat la dimensiunea fișierului sursă folosind SetFilePointer
[http://msdn.microsoft.com/en­us/library/aa365541(VS.85).aspx] și SetEndOfFile [http://msdn.microsoft.com/en­
us/library/aa365531(VS.85).aspx].

Exercițiul 4 ­ Tipuri de acces pentru pagini (3p)

Încărcați proiectul 4‐prot și inspectați sursa libvm.c.

Să se creeze o zonă de memorie în spațiul de adresă, formată din trei pagini virtuale (folosiți un singur
apel VirtualAlloc). Prima pagina nu va avea vreun drept, a două va avea drepturi de citire, iar a
treia va avea drepturi de scriere (folosiți VirtualProtect pentru a configura drepturile fiecărei
pagini). Să se testeze comportamentul programului când se fac accese de citire și scriere în aceste
zone. Urmăriți comentariile cu TODO 1.

Adăugați un handler de tratare a excepțiilor care să remapeze incremental zonele cu protecție de citire
și scriere la generarea excepțiilor. Astfel, dacă pagina nu are vreun drept, la page fault se va remapa
cu drepturi de citire. Dacă pagina are drepturi de citire, la page fault se va remapa cu drepturi de citire
+ drepturi de scriere. Urmăriți comentariile cu TODO 2.

Exercițiul 5 ­ Detectare 'buffer overrun' ­ implementare utilitar
asemănător cu Electric Fence (2p)

Încărcați proiectul 5‐ef și inspectați sursa, ignorând pentru moment funcția MyMalloc. Compilați și
rulați proiectul.

Completați funcția MyMalloc astfel încât orice depășire a bufferului alocat să producă eroare (urmăriți
comentariile cu TODO). Alocați cu VirtualAlloc [http://msdn.microsoft.com/en­
us/library/aa366887%28VS.85%29.aspx] memorie de dimensiunea primită ca parametru + încă o pagină
la final (o vom numi guard page). Schimbați dreptul de acces pentru pagina de final în
PAGE_NOACCESS utilizând VirtualProtect [http://msdn.microsoft.com/en‐
us/library/aa366898%28v=VS.85%29.aspx]. Întoarceți un pointer la o zonă de memorie cu
dimensiunea egală cu dimensiunea cerută, dar care se termină fix înainte de guard page).

Testați din nou folosind de data aceasta MyMalloc, atât în cazul în care inițializarea vectorului
depășește dimensiunea alocată, cât și în cazul în care nu depășește.

Exercițiul 6 ­ Blocarea paginării (1p)
Vă aflați într­o situație în care trebuie să procesați în timp real datele dintr­un buffer și vreți să evitați
swaparea paginilor. Intrați în directorul 6‐lock și completați TODO‐urile astfel încât paginarea să fie
blocată pentru variabila data pe parcursul lucrului cu aceasta, iar la final să fie deblocată. Adresa
trebuie aliniată la limita unei pagini.

Extra
Comparați timpii de execuție ai algoritmilor de numărare a liniilor dintr­un fișier, aflați în această
arhivă [http://elf.cs.pub.ro/so/res/laboratoare/lab06­extra.zip]

Cât de performantă este metoda cu mapare a fișierului în memorie în raport cu celelalte
metode?
Care sunt cele mai importante diferențe între metoda mmap
[https://docs.python.org/2/library/mmap.html] din modulul de Python cu același nume și funcția
nativă [http://man7.org/linux/man­pages/man2/mmap.2.html] din Linux?
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 17/18
6/11/2017 Laborator 06 ­ Memoria virtuală [CS Open CourseWare]

Soluții
Soluții exerciții laborator 6 [http://elf.cs.pub.ro/so/res/laboratoare/lab06­sol.zip]

Resurse Utile

Wikipedia: Memory Management [http://en.wikipedia.org/wiki/Memory_management]
Memory Management in Linux [http://tldp.org/LDP/tlk/mm/memory.html]
Opengroup ­ mmap [http://www.opengroup.org/onlinepubs/009695399/functions/mmap.html]
MSDN: Managing Virtual Memory in Win32 [http://msdn.microsoft.com/en­us/library/ms810627.aspx]
MSDN: Managing Memory­Mapped Files in Win32 [http://msdn2.microsoft.com/en­
us/library/ms810613.aspx]
MSDN: Structured Exception Handling [http://msdn2.microsoft.com/en­us/library/ms680657.aspx]
Utilizarea vectorilor de excepție (Windows) [http://msdn.microsoft.com/en­
us/library/windows/desktop/ms681411(v=vs.85).aspx]

so/laboratoare/laborator­06.txt · Last modified: 2017/04/12 15:11 by theodor.stoican

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­06 18/18
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]

Laborator 07 ­ Profiling & Debugging

Materiale ajutătoare

lab07­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab07­slides.pdf]

Nice to Watch

Google I/O 2010 ­ Measure in milliseconds: Meet Speed Tracer [http://www.youtube.com/watch?v=73IyVBMf2uY]
MIT Lecture: Performance Engineering with Profiling Tools [http://ocw.mit.edu/courses/electrical­engineering­and­computer­
science/6­172­performance­engineering­of­software­systems­fall­2010/video­lectures/lecture­5­performance­engineering­with­
profiling­tools/]

Latency Comparison Numbers

Operation Time (ns) Notes


L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 3,000 ns
Send 1K bytes over 1 Gbps network 10,000 ns 0.01 ms
Read 4K randomly from SSD* 150,000 ns 0.15 ms
Read 1 MB sequentially from memory 250,000 ns 0.25 ms
Round trip within same datacenter 500,000 ns 0.5 ms
Read 1 MB sequentially from SSD* 1,000,000 ns 1 ms, 4x memory
Disk seek 10,000,000 ns 10 ms, 20x datacenter roundtrip
Read 1 MB sequentially from disk 20,000,000 ns 20 ms, 80x memory, 20x SSD
Send packet Caracal ­ NY ­ Caracal 150,000,000 ns 150 ms

Credits:

By Jeff Dean: http://research.google.com/people/jeff/ [http://research.google.com/people/jeff/]
Originally by Peter Norvig: http://norvig.com/21­days.html#answers [http://norvig.com/21­days.html#answers]

Profiling
Un profiler este un utilitar de analiză a performanței care ajută programatorul să determine punctele critice – bottleneck
– ale unui program. Acest lucru se realizează prin investigarea comportamentului programului, evaluarea consumului de
memorie și relația dintre modulele acestuia.

Tehnici de profiling

Tehnica de instrumentare
Profiler­ele bazate pe această tehnică necesită de obicei modificări în codul programului: se inserează secțiuni de cod la
începutul și sfârșitul funcției ce se dorește analizată. De asemenea, se rețin și funcțiile apelate. Astfel, se poate estima
timpul total al apelului în sine cât și al apelurilor de subfuncții. Dezavantajul major al acestor profilere este legat de
modificarea codului: în funcții de dimensiune scăzută și des apelate, acest overhead poate duce la o interpretare greșită a
rezultatelor.

Tehnica de eșantionare (sampling)
Profiler­ele bazate pe sampling nu fac schimbări în codul programului, ci verifică periodic procesorul cu scopul de a
determina ce funcție (instrucțiune) se execută la momentul respectiv. Apoi estimează frecvența și timpul de execuție al unei
anumite funcții într­o perioadă de timp.

Suport pentru profiler
Suportul pentru profilere este disponibil la nivel de:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 1/7
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]

bibliotecă C (GNU libc), prin informații legate de timpul de viață al alocărilor de memorie,
compilator, prin modificarea codului în tehnica de instrumentare se poate realiza ușor în procesul de compilare,
compilatorul fiind cel ce inserează secțiunile de cod necesare,
nucleu al sistemului de operare, prin punerea la dispoziție de apeluri de sistem specifice,
hardware, unele procesoare sunt dotate cu contoare de temporizare (Time Stamp Counter ­ TSC
[http://en.wikipedia.org/wiki/Time_Stamp_Counter]) sau contoare de performanță care numără evenimente precum cicluri
de procesor sau TLB miss­uri.

Unelte
În continuare sunt prezentate câteva unelte folosite în profiling.

perfcounters

Majoritatea procesoarelor moderne oferă registre speciale (performance counters) care contorizează diferite tipuri de
evenimente hardware: instrucțiuni executate, cache­miss­uri, instrucțiuni de salt anticipate greșit, fără să afecteze
performanța nucleului sau a aplicațiilor. Aceste registre pot declanșa întreruperi atunci când se acumulează un anumit număr
de evenimente și astfel se pot folosi pentru analiza codului care rulează pe procesorul în cauză.

Subsistemul perfcounters:

se găsește în nucleul Linux începând cu versiunea 2.6.31 [http://lwn.net/Articles/339361/] (CONFIG_PERF_COUNTERS=y
)
este înlocuitorul lui oprofile
oferă suport pentru:
evenimente hardware (instrucțiuni, accese cache, ciclii de magistrală).
evenimente software (page fault, cpu­clock, cpu migrations).
tracepoints (e.g: sys_enter_open, sys_exit_open).

perf

Utilitarul perf este interfața subsistemului perfcounters cu utilizatorul. Oferă o linie de comandă asemănătoare cu git și
nu necesită existența unui daemon.

Un tutorial despre perf găsiți aici [https://perf.wiki.kernel.org/index.php/Tutorial].

Utilizare

$ perf [‐‐version] [‐‐help] COMMAND [ARGS]

Cele mai folosite comenzi sunt:

annotate ­ Citește perf.data și afișează codul cu adnotări
list ­ Listează numele simbolice ale tuturor tipurilor de evenimente ce pot fi urmărite de perf
lock ­ Analizează evenimentele de tip lock
record ­ Rulează o comandă și salvează informațiile de profiling în fișierul perf.data
report ­ Citește perf.data (creat de perf record) și afișează profilul
sched ­ Utilitar pentru măsurarea proprietăților planificatorului (latențe)
stat ­ Rulează o comandă și afișează statisticile înregistrate de subsistemul performance counters
top ­ Generează și afișează informații în timp real despre încărcarea unui sistem

perf list

man perf­list [http://manpages.ubuntu.com/manpages/natty/man1/perf­list.1.html]

Afișează numele simbolice ale tuturor tipurilor de evenimente ce pot fi urmărite de perf.

$ perf list  
List of pre‐defined events (to be used in ‐e): 
  
  cpu‐cycles OR cycles                       [Hardware event] 
  instructions                               [Hardware event] 
  
  cpu‐clock                                  [Software event] 
  page‐faults OR faults                      [Software event] 
  
  L1‐dcache‐loads                            [Hardware cache event] 
  L1‐dcache‐load‐misses                      [Hardware cache event] 
  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 2/7
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]
  
  rNNN                                       [Raw hardware event descriptor] 
  
  mem:<addr>[:access]                        [Hardware breakpoint] 
  
  syscalls:sys_enter_accept                  [Tracepoint event] 
  syscalls:sys_exit_accept                   [Tracepoint event]

Atunci când un eveniment nu este disponibil în forma simbolică, poate fi folosit cu perf în forma procesorului din sistemul
analizat.

perf stat
perf­stat [http://manpages.ubuntu.com/manpages/lucid/man1/perf­stat.1.html]

Rulează o comandă și afișează statisticile înregistrate de subsistemul performance counters.

$ perf stat ls ‐R /usr/src/linux 
 Performance counter stats for 'ls ‐R /usr/src/linux': 
  
         934.512846  task‐clock‐msecs         #      0.114 CPUs  
               1695  context‐switches         #      0.002 M/sec 
                163  CPU‐migrations           #      0.000 M/sec 
                306  page‐faults              #      0.000 M/sec 
          725144010  cycles                   #    775.959 M/sec  
          419392509  instructions             #      0.578 IPC    
           80242637  branches                 #     85.866 M/sec  
            5680112  branch‐misses            #      7.079 %      
          174667968  cache‐references         #    186.908 M/sec  
            4178882  cache‐misses             #      4.472 M/sec  
  
        8.199187316  seconds time elapsed

perf stat oferă posibilitatea colectării datelor în urma rulării de mai multe ori a unui program specificând opțiunea ‐r.

$ perf stat ‐r 6 sleep 1 
 Performance counter stats for 'sleep 1' (6 runs): 
  
           1.757147  task‐clock‐msecs #      0.002 CPUs    ( +‐   3.000% )
                  1  context‐switches #      0.001 M/sec   ( +‐  14.286% )
                  0  CPU‐migrations   #      0.000 M/sec   ( +‐ 100.000% )
                144  page‐faults      #      0.082 M/sec   ( +‐   0.147% )
            1373254  cycles           #    781.525 M/sec   ( +‐   2.856% )
             588831  instructions     #      0.429 IPC     ( +‐   0.667% )
             106846  branches         #     60.806 M/sec   ( +‐   0.324% )
              11312  branch‐misses    #     10.587 %       ( +‐   0.851% )
        1.002619407  seconds time elapsed   ( +‐   0.012% )

Observați mai sus evenimentele cele mai importante contorizate.

perf top
man perf­top [http://manpages.ubuntu.com/manpages/natty/man1/perf­top.1.html]

Generează și afișează informații în timp real despre încărcarea unui sistem.

$ ls ‐R /home 
$ perf top ‐p $(pidof ls) 
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 
   PerfTop:     181 irqs/sec  kernel:72.4% (target_pid: 10421) 
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 
             samples  pcnt function             DSO 
             _______ _____ ____________________ ___________________
  
              270.00 15.8% __d_lookup           [kernel.kallsyms]   
              145.00  8.5% __GI___strcoll_l     /lib/libc‐2.12.1.so 
               99.00  5.8% link_path_walk       [kernel.kallsyms]   
               97.00  5.7% find_inode_fast      [kernel.kallsyms]   
               91.00  5.3% __GI_strncmp         /lib/libc‐2.12.1.so 
               55.00  3.2% move_freepages_block [kernel.kallsyms]   
               44.00  2.6% ext3_dx_find_entry   [kernel.kallsyms]   
               41.00  2.4% ext3_find_entry      [kernel.kallsyms]   
               40.00  2.3% dput                 [kernel.kallsyms]   
               39.00  2.3% ext3_check_dir_entry [kernel.kallsyms]  

Observăm că funcțiile de lucru cu fișiere (parcurgere, căutare) sunt cele care apar cel mai des în outputul lui perf­top
corespunzător rulării comenzii de listare recursivă a directorului home.

perf record

man perf­record [http://manpages.ubuntu.com/manpages/natty/man1/perf­record.1.html]

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 3/7
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]

Rulează o comandă și salvează informațiile de profiling în fișierul perf.data.

$ perf record wget http://elf.cs.pub.ro/so/wiki/laboratoare/laborator‐07 
  
[ perf record: Woken up 1 times to write data ] 
[ perf record: Captured and wrote 0.008 MB perf.data (~334 samples) ] 
  
$ ls 
laborator‐07  perf.data

perf report
man perf­report [http://manpages.ubuntu.com/manpages/natty/man1/perf­report.1.html]

Interpretează datele salvate în perf.data în urma analizei folosind perf record. Astfel pentru exemplul wget de mai sus
avem:

$ perf report  
# Events: 13  cycles 

# Overhead  Command      Shared Object  Symbol 
# ........  .......  .................  ...... 

    86.43%     wget             e8ee21  [.] 0x00000000e8ee21 
    11.03%     wget  [kernel.kallsyms]  [k] prep_new_page 
     2.37%     wget  [kernel.kallsyms]  [k] sock_aio_read 
     0.11%     wget  [kernel.kallsyms]  [k] perf_event_comm 
     0.05%     wget  [kernel.kallsyms]  [k] native_write_msr_safe

Debugging

strace
strace interceptează şi înregistrează apelurile de sistem făcute de un proces şi semnalele pe care acesta le primeşte. În cea
mai simplă formă strace rulează comanda specificată până când procesul asociat se încheie.

$strace cat /proc/cpuinfo 
execve("/bin/cat", ["cat", "/proc/cpuinfo"], [/* 30 vars */]) = 0 
open("/proc/cpuinfo", O_RDONLY)         = 3 
read(3, "processor\t: 0\nvendor_id\t: Genuin"..., 32768) = 3652 
write(1, "processor\t: 0$\nvendor_id\t: Genui"..., 7512) = 7512

Cele mai folosite opțiuni pentru strace sunt:

‐f, cu această opțiune vor fi urmărite şi procesele copil create de procesul curent
‐o filename, în mod implicit strace afişează informațiile la stderr. Cu această opțiune, output­ul va fi pus în
fişierul filename
‐p pid, pid­ul procesului de urmărit.
‐e expresie, modifică apelurile urmărite.

daniel@debian$ strace ‐f ‐e connect,socket,bind ‐p $(pidof iceweasel) 
Process 6429 attached with 30 threads ‐ interrupt to quit 
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 50 
connect(50, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("141.85.227.65")}, 16) = ‐1 EINPROGRESS 
 

Un alt utilitar înrudit cu strace este ltrace [http://linux.die.net/man/1/ltrace]. Acesta urmăreşte apelurile de bibliotecă.

gdb
Scopul unui debugger (de exemplu GDB) este să ne permită să inspectăm ce se întâmplă în interiorul unui program în timp
ce acesta rulează sau în momentul când s­a produs o eroare fatală.

Mai multe detalii în secțiunea de resurse [http://ocw.cs.pub.ro/courses/so/laboratoare/resurse/gdb].

valgrind
Mai multe detalii aici [http://ocw.cs.pub.ro/courses/so/laboratoare/laborator­05#valgrind].

Alte utilitare

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 4/7
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]

Oprofile [http://elf.cs.pub.ro/so/wiki/laboratoare/resurse/oprofile]
Kernrate [http://www.microsoft.com/downloads/details.aspx?familyid=d6e95259­8d9d­4c22­89c4­fad382eddcd1&displaylang=en]
este un echivalent al oprofile pentru Windows.
KCachegrind [http://kcachegrind.sourceforge.net/html/Home.html]
perf­tools [http://code.google.com/p/google­perftools/]
XPerf [http://blogs.msdn.com/ntdebugging/archive/2008/04/03/windows­performance­toolkit­xperf.aspx]
GNU gprof [http://sourceware.org/binutils/docs/gprof]

Exerciții

Exerciții laborator ­ Linux (11p)

Exercițiul 0 ­ Joc interactiv (2p)

Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Folosiți arhiva lab07­tasks.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab07­tasks.zip] aferentă laboratorului.

Întrucât avem nevoie de suport hardware, suport inexistent pe mașina virtuală, lucrați pe sistemul fizic.

Pentru a vedea ce pachet trebuie să instalați, rulați comanda perf fără parametri.

Pentru a putea face exercițiile e nevoie de utilitarul linux‐tools. Puteți verifica asta rulând comanda perf ‐‐help. Dacă
comanda nu e găsită, trebuie să instalați pachetul:

student@so:~$ sudo apt‐get update 
student@so:~$ sudo apt‐get install linux‐tools‐generic

Trebuie descarcate urmatoarele pachete:

student@so:~$ wget http://ro.archive.ubuntu.com/ubuntu/pool/main/l/linux‐lts‐xenial/linux‐lts‐xenial‐tools‐4.4.0‐38_4.4.0‐38.57~14.04.1_amd64.deb
student@so:~$ wget http://ro.archive.ubuntu.com/ubuntu/pool/main/l/linux‐lts‐xenial/linux‐tools‐4.4.0‐38‐generic_4.4.0‐38.57~14.04.1_amd64.deb

Trebuie instalate din Ubuntu Software Center sau direct din consolă:

student@so:~$ sudo dpkg ‐i linux‐lts‐xenial‐tools‐4.4.0‐38_4.4.0‐38.57~14.04.1_amd64.deb 
student@so:~$ sudo dpkg ‐i linux‐tools‐4.4.0‐38‐generic_4.4.0‐38.57~14.04.1_amd64.deb

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 5/7
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]

Exercițiul 1 ­ Custom Profiling (1p)

Perf pune la dispoziție un mod de a extrage datele importante din profiling prin suportul de scripting oferit de perf script.
Acesta funcționează împreună cu perf record care obține lista de samples și o salvează în fișierul perf.data. Cu ajutorul
lui perf script se pot parsa eventurile înregistrate in sampleuri în metoda process_event. Mai multe informații despre
perf script se pot găsi la: man perf­script­python [http://man7.org/linux/man­pages/man1/perf­script­python.1.html] și exemplu
de utilizare [https://lwn.net/Articles/620900/]

Intrați în directorul 1‐custom. Primul pas este să generăm fișierul perf.data care conține sampleurile. Pentru asta
executați :

make 
perf record ‐e cycles:pp ‐c 10000 ‐d ./hash

Folosiți comanda perf script cu opțiunea ‐f (investigați man perf‐script) astfel încât să gasiți numărul total de valori
ale instruction pointer‐ului și apoi pe cele aflate în funcția hash_search_index. Având cele două valori, calculați
procentul valorilor din funcția hash_search_index.

Folosiți wc ‐l pentru a număra liniile outputului și grep pentru a filtra după simbolul hash_search_index. Pentru a face
calcule cu numere raționale folosiți o comandă de tipul: echo 7/2 | bc ‐l.

Verificați rezultatul utilizând comanda perf report.

Exercițiul 2 ­ Row/Column major order (1.5p)

Folosind utilitarul perf_3.2 dorim să determinăm dacă limbajul C este column­major sau row­major (row­major­order
[http://en.wikipedia.org/wiki/Row­major_order]).

Intrați în directorul 2‐major și completați programul row.c astfel încât să incrementeze elementele unei matrice pe linii,
după care completați programul columns.c astfel încât să incrementeze elementele unei matrice pe coloane.

Determinați numărul de cache­miss­uri comparativ cu numărul de accese la cache folosind perf stat pentru a urmări
evenimentul L1‐dcache‐load‐misses. Pentru a vedea evenimentele disponibile folosiți comanda perf list. Folosiți
opțiunea ‐e a utilitarului perf pentru a specifica un anumit eveniment de urmărit (revedeți secțiunea perfcounters).

Exercițiul 3 ­ busy (1p)

Intrați în directorul 3‐busy și inspectați fișierul busy.c. Rulați programul busy și analizați încărcarea sistemului folosind
comanda sudo perf top. Ce funcție pare să încarce sistemul?

Exercițiul 4 ­ Căutare într­un șir de caractere (1.5p)

Intrați în directorul 4‐find‐char/ și analizați conținutul fișierului find‐char.c. Compilați fișierul find‐char.c și rulați
executabilul obținut.

Identificați, folosind perf record și perf report, care este funcția care ocupă cel mai mult timp de procesor și încercați
să îmbunătățiți performanțele programului.

Exercițiul 5 ­ Printing order (1p)

Intrați în directorul 5‐print/ și analizați conținutul fișierului print.c. Folosiți comanda make print pentru a compila
programul print. Există fișierul Makefile?

Care este ordinea în care se fac scrierile la consolă? Explicați output­ul.

Puneți o instrucțiune sleep(5) înainte de return 0; în funcția main și folosiți comanda strace ‐e write ./print
pentru a găsi explicația.

Exercițiul 6 ­ Flowers reloaded (1p)
Intrați în directorul 6‐flowers/ și analizați conținutul fișierului flowers.c. Compilați fișierul flowers.c şi rulați
executabilul flowers. Ce se întâmplă? Folosiți valgrind cu opțiunea ‐‐tool=memcheck. Afișați valoarea celui de­al
treilea element al array­ului flowers, adică flowers[2].

Exercițiul 7 ­ Buffer overflow exploit (1p)
Rezolvați acest exercițiu pe mașina virtuală.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 6/7
6/11/2017 Laborator 07 ­ Profiling & Debugging [CS Open CourseWare]

Intrați în directorul 7‐exploit/ și analizați conținutul fișierului exploit.c. Folosiți comanda make pentru a compila
executabilul exploit. Identificați o problemă în funcția read_name.

Folosiți gdb pentru a investiga stiva înaintea efectuării apelului read.

student@spook:~ gdb ./exploit 
(gdb) break read_name 
(gdb) run

Afișați adresele variabilelor name și access.

(gdb) print/x &access 
(gdb) print/x &name

Observați că diferența între adresa variabilei access și adresa bufferului name este de 0x10 (16) octeți, ceea ce înseamnă
că variabila access se află imediat la sfârșitul datelor din bufferul name.

Folosindu­vă de informațiile obținute, construiți un input convenabil pe care să îl oferiți executabilului  exploit, astfel încât
acesta să vă afișeze stringul “Good job, you hacked me!”.

Pentru a genera caractere neprintabile, puteți folosi interpretorul Python: python ‐c.

student@spook:~ python ‐c 'print "A"*8 + "\x01\x00\x00\x00"' | ./exploit

Comanda de mai sus va genera 8 octeți cu valoarea 'A' (codul ASCII 0x41), un octet cu valoarea 0x01 și încă 3 octeți cu
valoarea 0x00 și îi va oferi la stdin executabilului exploit. Rețineti că datele sunt structurate în memorie în format little
endian, prin urmare, dacă ultimii 4 octeți vor ajunge să suprascrie o adresă, aceasta va fi interpretată ca 0x00000001, NU
0x01000000.

Exercițiul 8 ­ Trace the mystery (1p)

Intrați în directorul 8‐mystery/ unde găsiți executabilul mystery. Investigați și explicați ce face acesta. Revedeți secțiunea
strace.

Soluții
Soluții laborator 7 [http://elf.cs.pub.ro/so/res/laboratoare/lab07­sol.zip]

Resurse utile
GNU grof manual [http://sourceware.org/binutils/docs/gprof/]
linux/tools/perf [http://lxr.linux.no/linux+v2.6.38/tools/perf/]
[Announce] Performance Counters for Linux, v8 [http://lkml.org/lkml/2009/6/6/149]
Profiling tools and techniques [http://www.pixelbeat.org/programming/profiling/]
Is Parallel Programming Hard, And, If So, What Can You Do About It?
[http://kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html]
gprof, Valgrind and gperftools – an evaluation of some tools for application level CPU profiling on Linux
[http://gernotklingler.com/blog/gprof­valgrind­gperftools­evaluation­tools­application­level­cpu­profiling­linux/]
Linux Profiling Tools and Techniques [http://www.pixelbeat.org/programming/profiling/]

so/laboratoare/laborator­07.txt · Last modified: 2017/04/11 08:16 by adrian.stanciu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­07 7/7
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Laborator 08 ­ Thread­uri Linux

Materiale ajutătoare

lab08­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab08­slides.pdf]
lab08­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab08­refcard.pdf]

Nice to read

TLPI ­ Chapter 29, Threads: Introduction
TLPI ­ Chapter 30, Threads: Thread Synchronization
TLPI ­ Chapter 31, Threads: Thread Safety and Per­Thread Storage

Prezentare teoretică
În laboratoarele anterioare a fost prezentat conceptul de proces, acesta fiind unitatea elementară de
alocare a resurselor utilizatorilor. În cadrul acestui laborator este prezentat conceptul de fir de
execuție (sau thread), acesta fiind unitatea elementară de planificare într­un sistem. Ca și procesele,
firele de execuție reprezintă un mecanism prin care un calculator poate sǎ ruleze mai multe task­uri
simultan.

Un fir de execuție există în cadrul unui proces, și reprezintă o unitate de execuție mai fină decât
acesta. În momentul în care un proces este creat, în cadrul lui există un singur fir de execuție, care
execută programul secvențial. Acest fir poate la rândul lui sǎ creeze alte fire de execuție; aceste fire
vor rula porțiuni ale binarului asociat cu procesul curent, posibil aceleași cu firul inițial (care le­a
creat).

Diferențe dintre fire de execuție și procese

procesele nu partajează resurse între ele (decât dacă programatorul folosește un mecanism
special pentru asta ­ shared memory spre exemplu), pe când firele de execuție partajează în
mod implicit majoritatea resurselor unui proces. Modificarea unei astfel de resurse dintr­un fir
este vizibilă instantaneu și din celelalte fire:
segmentele de memorie precum .heap, .data și .bss (deci și variabilele stocate în ele)
descriptorii de fișiere (așadar, închiderea unui fișier este vizibilă imediat pentru toate
firele de execuție), indiferent de tipul fișierului:
sockeți
fișiere normale
pipe­uri
fișiere ce reprezintă dispozitive hardware (de ex. /dev/sda1).
fiecare fir are un context de execuție propriu, format din:
stivă
set de registre (deci și un contor de program ­ registrul (E)IP)

Procesele sunt folosite de SO pentru a grupa și aloca resurse, iar firele de execuție pentru a planifica
execuția de cod care accesează (în mod partajat) aceste resurse.

Avantajele firelor de execuție
Deoarece toate firele de execuție ale unui proces folosesc spațiul de adrese al procesului de care
aparțin, folosirea lor are o serie de avantaje:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 1/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

crearea/distrugerea unui fir de execuție durează mai puțin decât crearea/distrugerea unui
proces
durata context switch­ului între firele de execuție aceluiași proces este foarte mică, întrucât nu e
necesar să se “comute” și spațiul de adrese (pentru mai multe informații, căutați „TLB flush”)
comunicarea între firele de execuție are un overhead mai mic (realizată prin modificarea unor
zone de memorie din spațiul comun de adrese)

Firele de execuție se pot dovedi utile în multe situații, de exemplu, pentru a îmbunătăți timpul de
răspuns al aplicațiilor cu interfețe grafice (GUI), unde prelucrările CPU­intensive se fac de obicei într­
un fir de execuție diferit de cel care afișează interfața.

De asemenea, ele simplifică structura unui program și conduc la utilizarea unui număr mai mic de
resurse (pentru că nu mai este nevoie de diversele forme de IPC pentru a comunica).

Tipuri de fire de execuție
Din punctul de vedere al implementării, există 3 categorii de fire de execuție:

Kernel Level Threads (KLT)
User Level Threads (ULT)
Fire de execuție hibride

Kernel Level Threads

Managementul și planificarea firelor de execuție sunt realizate în kernel; programele creează/distrug
fire de execuție prin apeluri de sistem. Kernel­ul menține informații de context, atât pentru procese,
cât și pentru firele de execuție din cadrul proceselor, iar planificarea execuției se face la nivel de fir.

Avantaje :

dacă avem mai multe procesoare putem lansa în execuție simultană mai multe fire de execuție
ale aceluiași proces;
blocarea unui fir nu înseamnă blocarea întregului proces;
putem scrie cod în kernel care să se bazeze pe fire de execuție.

Dezavantaje :

comutarea contextului este efectuată de kernel (cu o viteză de comutare mai mică):
se trece dintr­un fir de execuție în kernel
kernelul întoarce controlul unui alt fir de execuție.

User Level Threads

Kernel­ul nu este conștient de existența firelor de execuție, iar managementul acestora este realizat
de procesul în care ele există (implementarea managementului firelor de execuție este realizată de
obicei în biblioteci). Schimbarea contextului nu necesită intervenția kernel­ului, iar algoritmul de
planificare depinde de aplicație.

Avantaje :

schimbarea de context nu implică kernelul ⇒ comutare rapidă
planificarea poate fi aleasă de aplicație; aplicația poate folosi acea planificare care favorizează
creșterea performanțelor
firele de execuție pot rula pe orice SO, inclusiv pe SO­uri care nu suportă fire de execuție la
nivel kernel (au nevoie doar de biblioteca care implementează firele de execuție la nivel
utilizator).

Dezavantaje :

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 2/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

kernel­ul nu știe de fire de execuție ⇒ dacă un fir de execuție face un apel blocant toate firele
de execuție planificate de aplicație vor fi blocate. Acest lucru poate fi un impediment întrucât
majoritatea apelurilor de sistem sunt blocante. O soluție este utilizarea unor variante non­
blocante pentru apelurile de sistem.
nu se pot utiliza la maximum resursele hardware: kernelul planifică firele de execuție de care
știe, câte unul pe fiecare procesor. Kernelul nu este conștient de existența firelor de execuție
user­level ⇒ el va vedea un singur fir de execuție ⇒ va planifica procesul respectiv pe
maximum un procesor, chiar dacă aplicația ar avea mai multe fire de execuție planificabile în
același timp.

Fire de execuție hibride

Aceste fire încearcă să combine avantajele firelor de execuție user­level cu cele ale firelor de execuție
kernel­level. O modalitate de a face acest lucru este de a utiliza fire kernel­level pe care să fie
multiplexate fire user­level. KLT sunt unitățile elementare care pot fi distribuite pe procesoare. De
regulă, crearea firelor de execuție se face în user space și tot aici se face aproape toată planificarea și
sincronizarea. Kernel­ul știe doar de KLT­urile pe care sunt multiplexate ULT, și doar pe acestea le
planifică. Programatorul poate schimba eventual numărul de KLT alocate unui proces.

Suport POSIX
În ceea ce privește firele de execuție, POSIX nu specifică dacă acestea trebuie implementate în user­
space sau kernel­space. Linux le implementează în kernel­space, dar nu diferențiază firele de execuție
de procese decât prin faptul că firele de execuție partajează spațiul de adresă (atât firele de execuție,
cât și procesele, sunt un caz particular de “task”). Pentru folosirea firelor de execuție în Linux trebuie
să includem header­ul pthread.h (unde se găsesc declarațiile funcțiilor și tipurilor de date necesare)
și să utilizăm biblioteca libpthread.

Crearea firelor de execuție
Un fir de execuție este creat folosind pthread_create [http://linux.die.net/man/3/pthread_create]:

int pthread_create(pthread_t *tid, const pthread_attr_t *tattr,  
                   void*(*start_routine)(void *), void *arg);

Noul fir creat va avea identificatorul tid și va rula concurent cu firul de execuție din care a fost creat.
Acesta va executa codul specificat de funcția start_routine căreia i se va pasa argumentul arg.
Dacă funcția de executat are nevoie de mai mulți parametri, aceștia pot fi agregați într­o structură, în
câmpul arg punându­se un pointer către acea structură.

Prin parametrul tattr se stabilesc atributele noului fir de execuție. Dacă transmitem valoarea NULL
firul de execuție va fi creat cu atributele implicite.

Pentru a determina identificatorul firului de execuție curent se poate folosi funcția pthread_self
[http://linux.die.net/man/3/pthread_self]:

pthread_t pthread_self(void);

Așteptarea firelor de execuție
Firele de execuție se așteaptă folosind funcția pthread_join [http://linux.die.net/man/3/pthread_join]:

int pthread_join(pthread_t th, void **thread_return);

Primul parametru specifică identificatorul firului de execuție așteptat, iar al doilea parametru specifică
unde se va plasa valoarea întoarsă de funcția copil (printr­un pthread_exit
[http://linux.die.net/man/3/pthread_exit] sau printr­un return din rutina utilizată la pthread_create
[http://linux.die.net/man/3/pthread_create]).

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 3/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Firele de execuție se împart în două categorii: unificabile și detașabile.

unificabile :
permit unificarea cu alte fire de execuție care apelează pthread_join
[http://linux.die.net/man/3/pthread_join].
resursele ocupate de fir nu sunt eliberate imediat după terminarea firului, ci sunt
păstrate până când un alt fir de execuție va executa pthread_join
[http://linux.die.net/man/3/pthread_join] (analog proceselor zombie)
implicit firele de execuție sunt unificabile

detașabile
un fir de execuție este detașabil dacă :
a fost creat detașabil.
i s­a schimbat acest atribut în timpul execuției prin apelul pthread_detach
[http://linux.die.net/man/3/pthread_detach].
nu se poate executa un pthread_join [http://linux.die.net/man/3/pthread_join] pe ele
vor elibera resursele imediat ce se vor termina (analog cu ignorarea semnalului
SIGCHLD în părinte la încheierea execuției proceselor copil)

Terminarea firelor de execuție
Un fir de execuție își încheie execuția:

la un apel al funcției pthread_exit [http://linux.die.net/man/3/pthread_exit]:

void pthread_exit(void *retval);

în mod automat, la sfârșitul codului firului de execuție.

Prin parametrul retval se comunică părintelui un mesaj despre modul de terminare al copilului.
Această valoare va fi preluată de funcția pthread_join [http://linux.die.net/man/3/pthread_join].

Metodele ca un fir de execuție să termine un alt fir sunt:

stabilirea unui protocol de terminare (spre exemplu, firul master setează o variabilă globală, pe
care firul slave o verifică periodic).
mecanismul de “thread cancellation”, pus la dispozitie de libpthread. Totuși, această
metodă nu este recomandată, pentru că este greoaie, și pune probleme foarte delicate la
cleanup. Pentru mai multe detalii: Terminarea thread­urilor

Thread Specific Data (TSD)
Uneori este util ca o variabilă să fie specifică unui fir de execuție (invizibilă pentru celelalte fire). Linux
permite memorarea de perechi (cheie, valoare) într­o zonă special desemnată din stiva fiecărui fir de
execuție al procesului curent. Cheia are același rol pe care îl are numele unei variabile: desemnează
locația de memorie la care se află valoarea.

Fiecare fir de execuție va avea propria copie a unei “variabile” corespunzătoare unei chei k, pe care o
poate modifica, fără ca acest lucru să fie observat de celelalte fire, sau să necesite sincronizare. De
aceea, TSD este folosită uneori pentru a optimiza operațiile care necesită multă sincronizare între fire
de execuție: fiecare fir calculează informația specifică, și există un singur pas de sincronizare la
sfârșit, necesar pentru reunirea rezultatelor tuturor firelor de execuție.

Cheile sunt de tipul pthread_key_t, iar valorile asociate cu ele, de tipul generic void * (pointeri
către locația de pe stivă unde este memorată variabila respectivă). Descriem în continuare operațiile
disponibile cu variabilele din TSD:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 4/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Crearea și ștergerea unei variabile
O variabilă se creează folosind pthread_key_create [http://linux.die.net/man/3/pthread_key_create]:

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *));

Al doilea parametru reprezintă o funcție de cleanup. Acesta poate avea una din valorile:

NULL și este ignorat
pointer către o funcție de cleanup care se execută la terminarea firului de execuție

Pentru ștergerea unei variabile se apelează pthread_key_delete
[http://linux.die.net/man/3/pthread_key_delete]:

int pthread_key_delete(pthread_key_t key);

Funcția nu apelează funcția de cleanup asociată variabilei.

Modificarea și citirea unei variabile
După crearea cheii, fiecare fir de execuție poate modifica propria copie a variabilei asociate folosind
funcția pthread_setspecific [http://linux.die.net/man/3/pthread_setspecific]:

int pthread_setspecific(pthread_key_t key, const void *pointer);

Pentru a determina valoarea unei variabile de tip TSD se folosește funcția pthread_getspecific
[http://linux.die.net/man/3/pthread_getspecific]:

void* pthread_getspecific(pthread_key_t key);

Funcții pentru cleanup
Funcțiile de cleanup asociate TSD­urilor pot fi foarte utile pentru a asigura faptul că resursele sunt
eliberate atunci când un fir se termină singur sau este terminat de către un alt fir. Uneori poate fi util
să se poată specifica astfel de funcții fără a crea neapărat un TSD. Pentru acest scop există funcțiile de
cleanup.

O astfel de funcție de cleanup este o funcție care este apelată când un fir de execuție se termină. Ea
primește un singur parametru de tipul void * care este specificat la înregistrarea funcției.

O funcție de cleanup este folosită pentru a elibera o resursă numai în cazul în care un fir de execuție
apelează pthread_exit [http://linux.die.net/man/3/pthread_exit] sau este terminat de un alt fir folosind
pthread_cancel [http://linux.die.net/man/3/pthread_cancel]. În circumstanțe normale, atunci când un fir nu
se termină în mod forțat, resursa trebuie eliberată explicit, iar funcția de cleanup nu trebuie să fie
apelată.

Pentru a înregistra o astfel de funcție de cleanup se folosește :

void pthread_cleanup_push(void (*routine) (void *), void *arg);

Aceasta funcție primește ca parametri un pointer la funcția care este înregistrată și valoarea
argumentului care va fi transmis acesteia. Funcția routine va fi apelată cu argumentul arg atunci
când firul este terminat forțat. Daca sunt înregistrate mai multe funcții de cleanup, ele vor fi apelate în
ordine LIFO (cea mai recent instalată va fi prima apelată).

Pentru fiecare apel pthread_cleanup_push [http://linux.die.net/man/3/pthread_cleanup_push] trebuie să
existe și apelul corespunzător pthread_cleanup_pop [http://linux.die.net/man/3/pthread_cleanup_pop] care
deînregistrează o funcție de cleanup:

void pthread_cleanup_pop(int execute);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 5/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Această funcție va deînregistra cea mai recent instalată funcție de cleanup, și dacă parametrul
execute este nenul o va și executa.
Atentie! Un apel pthread_cleanup_push [http://linux.die.net/man/3/pthread_cleanup_push] trebuie să aibă
un apel corespunzător pthread_cleanup_pop [http://linux.die.net/man/3/pthread_cleanup_pop] în aceeași
funcție și la același nivel de imbricare.

Un mic exemplu de folosire a funcțiilor de cleanup :

th_cleanup.c

void *alocare_buffer(int size) 

  return malloc(size); 

  
void dealocare_buffer(void *buffer) 

  free(buffer); 

  
/* functia apelata de un fir de execuție */ 
  
void functie() 

  void *buffer = alocare_buffer(512); 
  
  /* inregistrarea functiei de cleanup */ 
  pthread_cleanup_push(dealocare_buffer, buffer); 
  
  /* aici au loc prelucrari, si se poate apela pthread_exit 
        sau firul poate fi terminat de un alt fir */ 
  
  /* deinregistrarea functiei de cleanup si executia ei  
        (parametrul dat este nenul) */ 
  
        pthread_cleanup_pop(1);
}

Atributele unui fir de execuție
Atributele reprezintă o modalitate de specificare a unui comportament diferit de comportamentul
implicit. Atunci când un fir de execuție este creat cu pthread_create se pot specifica atributele
pentru respectivul fir de execuție. Atributele implicite sunt suficiente pentru marea majoritate a
aplicațiilor. Cu ajutorul unui atribut se pot schimba:

starea: unificabil sau detașabil
politica de alocare a procesorului pentru firul de execuție respectiv (round robin, FIFO, sau
system default)
prioritatea (cele cu prioritate mai mare vor fi planificate, în medie, mai des)
dimensiunea și adresa de start a stivei

Mai multe detalii puteți găsi în secțiunea suplimentară dedicată.

Cedarea procesorului
Un fir de execuție cedează dreptul de execuție unui alt fir, în urma unuia din următoarele evenimente:

efectuează un apel blocant (cerere de I/O, sincronizare cu un alt fir de execuție) și kernel­ul
decide că este rentabil să facă un context switch
i­a expirat cuanta de timp alocată de către kernel
cedează voluntar dreptul, folosind funcția sched_yield [http://linux.die.net/man/2/sched_yield]:

int sched_yield(void);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 6/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Dacă există alte procese interesate de procesor, unul dintre procese va acapara procesorul, iar dacă nu
există niciun alt proces în așteptare pentru procesor, firul curent își continuă execuția.

Alte operații
Dacă dorim să fim siguri că un cod de inițializare se execută o singură dată putem folosi funcția:

pthread_once_t once_control = PTHREAD_ONCE_INIT; 
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));

Scopul funcției pthread_once este de a asigura că o bucată de cod (de obicei folosită pentru
inițializări) se execută o singură dată. Argumentul once_control este un pointer la o variabilă
inițializată cu PTHREAD_ONCE_INIT. Prima oară când această funcție este apelată ea va apela funcția
init_routine și va schimba valoarea variabilei once_control pentru a ține minte că inițializarea a
avut loc. Următoarele apeluri ale acestei funcții cu același once_control nu vor face nimic.

Funcția pthread_once întoarce 0 în caz de succes sau cod de eroare în caz de eșec.

Pentru a determina dacă doi identificatori se referă la același fir de execuție se poate folosi:

int pthread_equal(pthread_t thread1, pthread_t thread2);

Pentru aflarea/modificarea priorităților sunt disponibile următoarele apeluri:

int pthread_setschedparam(pthread_t target_thread, int policy, const struct sched_param *param); 
int pthread_getschedparam(pthread_t target_thread, int *policy, struct sched_param *param);

Compilare

La compilare trebuie specificată și biblioteca libpthread (deci se va folosi argumentul ‐lpthread).

Nu legați un program single­threaded cu această bibliotecă. Anumite apeluri din bibliotecile standard
pot avea implementări mai ineficiente sau mai greu de depanat când se utilizează această bibliotecă.

Exemplu
În continuare, este prezentat un exemplu simplu în care sunt create 2 fire de execuție, fiecare afișând
un caracter de un anumit număr de ori pe ecran.

exemplu.c

#include <pthread.h> 
#include <stdio.h> 
  
/* parameter structure for every thread */ 
struct parameter { 
  char character; /* printed character */ 
  int number;     /* how many times */ 
}; 
  
/* the function performed by every thread */ 
void* print_character(void *params) 

  struct parameter *p = (struct parameter *) params; 
  int i; 
  
  for (i = 0; i < p‐>number; i++) 
    printf("%c", p‐>character); 
  printf("\n"); 
  
  return NULL; 

  
int main() 

  pthread_t fir1, fir2; 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 7/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]
  pthread_t fir1, fir2; 
  struct parameter fir1_args, fir2_args; 
  
  /* create one thread that will print 'x' 11 times */ 
  fir1_args.character = 'x'; 
  fir1_args.number = 11; 
  if (pthread_create(&fir1, NULL, &print_character, &fir1_args)) { 
    perror("pthread_create"); 
    exit(1); 
  } 
  
  /* create one thread that will print 'y' 13 times */ 
  fir2_args.character = 'y'; 
  fir2_args.number = 13; 
  if (pthread_create(&fir2, NULL, &print_character, &fir2_args)) { 
    perror("pthread_create"); 
    exit(1); 
  } 
  
  /* wait for completion */ 
  if (pthread_join(fir1, NULL)) 
    perror("pthread_join"); 
  if (pthread_join(fir2, NULL)) 
    perror("pthread_join"); 
  
  return 0; 
}

Comanda utilizată pentru a compila acest exemplu va fi:

gcc ‐o exemplu exemplu.c ‐lpthread

Sincronizarea firelor de execuție
Pentru sincronizarea firelor de execuție, avem la dispoziție:

mutex
semafoare
variabile de condiție
bariere

Mutex
Mutex­urile (mutual exclusion locks) sunt obiecte de sincronizare utilizate pentru a asigura accesul
exclusiv într­o secțiune de cod în care se utilizează date partajate între două sau mai multe fire de
execuție. Un mutex are două stări posibile: ocupat și liber. Un mutex poate fi ocupat de un singur fir
de execuție la un moment dat. Atunci când un mutex este ocupat de un fir de execuție, el nu mai poate
fi ocupat de niciun alt fir. În acest caz, o cerere de ocupare venită din partea unui alt fir, în general, va
bloca firul până în momentul în care mutex­ul devine liber.

Inițializarea/distrugerea unui mutex
Un mutex poate fi inițializat/distrus în mai multe moduri:

folosind o macrodefiniție

// inițializare statică a unui mutex, cu atribute implicite 
// NB: mutex‐ul nu este eliberat, durata de viață a mutex‐ului 
//     este durata de viață a programului. 
pthread_mutex_t mutex_static = PTHREAD_MUTEX_INITIALIZER;

inițializare cu atribute implicite (pthread_mutex_init
[http://linux.die.net/man/3/pthread_mutex_init], pthread_mutex_destroy

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 8/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

[http://linux.die.net/man/3/pthread_mutex_destroy])

// semnăturile funcțiilor de inițializare și distrugere de mutex: 
int pthread_mutex_init   (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  
void initializare_mutex_cu_atribute_implicite() { 
    pthread_mutex_t mutex_implicit; 
    pthread_mutex_init(&mutex_implicit, NULL); // atrr = NULL ‐> atribute implicite 
  
    // ... folosirea mutex‐ului ... 
  
    // eliberare mutex 
    pthread_mutex_destroy(&mutex_implicit); 
}

inițializare cu atribute explicite

// NB: funcția pthread_mutexattr_settype și macro‐ul PTHREAD_MUTEX_RECURSIVE  
//     sunt disponibile doar dacă se definește _XOPEN_SOURCE la o valoare >= 500   
//     **ÎNAINTE** de a include <pthread.h>.   
//     Pentru mai multe detalii consultați feature_test_macros(7). 
  
#define _XOPEN_SOURCE 500 
#include <pthread.h> 
  
void initializare_mutex_recursiv() { 
    // definim atributele, le inițializăm și marcăm tipul ca fiind recursiv. 
    pthread_mutexattr_t attr; 
    pthread_mutexattr_init(&attr); 
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); 
  
    // definim un mutex recursiv, îl inițializăm cu atributele definite anterior 
    pthread_mutex_t mutex_recursiv; 
    pthread_mutex_init(&mutex_recursiv, &attr); 
  
    // eliberăm resursele atributului după crearea mutex‐ului 
    pthread_mutexattr_destroy(&attr); 
  
    // ... folosirea mutex‐ului ... 
  
    // eliberare mutex 
    pthread_mutex_destroy(&mutex_recursiv); 
}

Mutex­ul trebuie să fie liber pentru a putea fi distrus. În caz contrar, funcția va întoarce codul de
eroare EBUSY. Întoarcerea valorii 0 semnifică succesul apelului.

Tipuri de mutex­uri
Folosind atributele de inițializare se pot crea mutex­uri cu proprietăți speciale:

activarea moștenirii de prioritate [http://en.wikipedia.org/wiki/Priority_inheritance]
(priority inheritance) pentru a preveni inversiunea de prioritate
[http://en.wikipedia.org/wiki/Priority_inversion]  (priority inversion). Există trei protocoale de
moștenire a priorității:
PTHREAD_PRIO_NONE – nu se moștenește prioritatea când deținem mutex­ul creat cu
acest atribut
PTHREAD_PRIO_INHERIT – dacă deținem un mutex creat cu acest atribut și dacă există
fire de execuție blocate pe acel mutex, se moștenește prioritatea firului de execuție cu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 9/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

cea mai mare prioritate
PTHREAD_PRIO_PROTECT – dacă firul de execuție curent deține unul sau mai multe
mutex­uri, acesta va executa la maximul priorităților specificate pentru toate mutex­
urile deținute.

#define _XOPEN_SOURCE 500 
#include <pthread.h> 
  
int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *attr, int *protocol); 
int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol);

modul de comportare la preluări recursive ale mutex­ului
PTHREAD_MUTEX_NORMAL – nu se fac verificări, preluarea recursivă duce la deadlock
PTHREAD_MUTEX_ERRORCHECK – se fac verificări, preluarea recursivă duce la
întoarcerea unei erori
PTHREAD_MUTEX_RECURSIVE – mutex­urile pot fi preluate recursiv, dar trebuie
eliberate de același număr de ori.

#define _XOPEN_SOURCE 500 
#include <pthread.h> 
  
pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *protocol); 
pthread_mutexattr_settype(pthread_mutexattr_t *attr, int protocol);

Ocuparea/eliberarea unui mutex
Funcțiile de ocupare blocantă/eliberare a unui mutex (pthread_mutex_lock
[http://linux.die.net/man/3/pthread_mutex_lock], pthread_mutex_unlock
[http://linux.die.net/man/3/pthread_mutex_unlock]):

int pthread_mutex_lock  (pthread_mutex_t *mutex); 
int pthread_mutex_unlock(pthread_mutex_t *mutex);

Dacă mutex­ul este liber în momentul apelului, acesta va fi ocupat de firul apelant și funcția va
întoarce imediat. Dacă mutex­ul este ocupat de un alt fir, apelul va bloca până la eliberarea mutex­
ului. Dacă mutex­ul este deja ocupat de firul curent de execuție (lock recursiv), comportamentul
funcției este dictat de tipul mutex­ului:

Tip mutex Lock recursiv Unlock

PTHREAD_MUTEX_NORMAL deadlock eliberează mutex­ul

PTHREAD_MUTEX_ERRORCHECK returnează eroare eliberează mutex­ul


incrementează contorul de decrementează contorul de ocupări (la zero eliberează
PTHREAD_MUTEX_RECURSIVE
ocupări mutex­ul)
PTHREAD_MUTEX_DEFAULT deadlock eliberează mutex­ul

Nu este garantată o ordine FIFO de ocupare a unui mutex. Oricare din firele aflate în așteptare la
deblocarea unui mutex pot să­l acapareze.

Încercarea neblocantă de ocupare a unui mutex
Pentru a încerca ocuparea unui mutex fără a aștepta eliberarea acestuia în cazul în care este deja
ocupat, se va apela funcția pthread_mutex_trylock [http://linux.die.net/man/3/pthread_mutex_trylock]:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

Exemplu:

int rc = pthread_mutex_trylock(&mutex); 
if (rc == 0) { 

    /* successfully aquired the free mutex */ 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 10/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]
    /* successfully aquired the free mutex */ 
} else if (rc == EBUSY) { 
    /* mutex was held by someone else 
       instead of blocking we return EBUSY */ 
} else { 
    /* some other error occured */ 
}

Exemplu de utilizare a mutex­urilor

Un exemplu de utilizare a unui mutex pentru a serializa accesul la variabila globală global_counter:

#include <stdio.h> 
#include <pthread.h> 
  
#define NUM_THREADS 5 
  
/* global mutex */ 
pthread_mutex_t mutex; 
int global_counter = 0; 
  
void *thread_routine(void *arg)  
{     
    /* acquire global mutex */ 
    pthread_mutex_lock(&mutex);
  
    /* print and modify global_counter */ 
    printf("Thread %d says global_counter=%d\n", (int) arg, global_counter); 
    global_counter++; 
  
    /* release mutex ‐ now other threads can modify global_counter */ 
    pthread_mutex_unlock(&mutex); 
  
    return NULL; 

  
int main(void)  

    int i; 
    pthread_t tids[NUM_THREADS]; 
  
    /* init mutex once, but use it in every thread */ 
    pthread_mutex_init(&mutex, NULL); 
  
    /* all threads execute thread_routine 
       as args to the thread send a thread id  
       represented by a pointer to an integer */ 
    for (i = 0; i < NUM_THREADS; i++) 
        pthread_create(&tids[i], NULL, thread_routine, (void *) i); 
  
    /* wait for all threads to finish */ 
    for (i = 0; i < NUM_THREADS; i++) 
        pthread_join(tids[i], NULL); 
  
    /* dispose mutex */ 
    pthread_mutex_destroy(&mutex); 
  
    return 0; 
}

so@spook$ gcc ‐Wall mutex.c ‐lpthread 
so@spook$ ./a.out  
Thread 1 says global_counter=0 
Thread 2 says global_counter=1 
Thread 3 says global_counter=2 
Thread 4 says global_counter=3 
Thread 0 says global_counter=4

Futex­uri
Mutex­urile din firele de execuție POSIX sunt implementate cu ajutorul futex­urilor, din considerente
de performanță.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 11/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Optimizarea constă în testarea și setarea atomică a valorii mutex­ului (printr­o instrucțiune de tip test­
and­set­lock) în user­space, eliminându­se trap­ul în kernel în cazul în care nu este necesară
blocarea.

Numele de futex vine de la Fast User­space muTEX. Ideea de la care a plecat implementarea futex­
urilor a fost aceea de a optimiza operația de ocupare a unui mutex în cazul în care acesta nu este
deja ocupat. Dacă mutex­ul nu este ocupat, el va fi ocupat fără ca procesul care îl ocupă să se
blocheze. În acest caz, nefiind necesară blocarea, nu este necesar ca procesul să intre în kernel­mode
(pentru a intra într­o stare de așteptare). Optimizarea constă în testarea și setarea atomică a valorii
mutex­ului (printr­o instrucțiune de tip test­and­set­lock) în user­space, eliminându­se trap­ul în
kernel în cazul în care nu este necesară blocarea.

Futex­ul poate fi orice variabilă dintr­o zonă de memorie partajată între mai multe fire de execuție sau
procese. Așadar, operațiile efective cu futex­urile se fac prin intermediul funcției do_futex,
disponibilă prin includerea headerului linux/futex.h. Signatura ei arată astfel:

long do_futex(unsigned long uaddr, int op, 
              int val, unsigned long timeout, unsigned long uaddr2, int val2);

În cazul în care este necesară blocarea, do_futex va face un apel de sistem ­ sys_futex. Futex­
urile pot fi utile (și poate fi necesară utilizarea lor explicită) în cazul sincronizării proceselor, fiind
alocate în variabile din zone de memorie partajată între procesele respective.

Semafor
Semafoarele sunt obiecte de sincronizare ce reprezintă o generalizare a mutex­urilor prin aceea că
salvează numărul de operații de eliberare (incrementare) efectuate asupra lor. Practic, un
semafor reprezintă un întreg care se incrementează/decrementează atomic. Valoarea unui semafor nu
poate scădea sub 0. Dacă semaforul are valoarea 0, operația de decrementare se va bloca până când
valoarea semaforului devine strict pozitivă. Mutex­urile pot fi privite, așadar, ca niște semafoare
binare.

Semafoarele POSIX sunt de 2 tipuri:

cu nume ­ folosite în general pentru sincronizare între procese distincte;
fără nume ­ ce pot fi folosite pentru sincronizarea între firele de execuție ale aceluiași proces,
sau între procese ­ cu condiția ca semaforul să fie într­o zonă de memorie partajată.

Diferențele dintre semafoarele cu nume față şi cele fără nume apar în funcțiile de creare și distrugere,
celelalte funcții fiind identice.

ambele tipuri de semafoare sunt reprezentate în cod prin tipul sem_t.
semafoarele cu nume sunt identificate la nivel de sistem printr­un șir de forma ”/nume”.
fișierele antet necesare sunt <fcntl.h>, <sys/types.h> și <semaphore.h>.

Operațiile care pot fi efectuate asupra semafoarelor POSIX sunt multiple:

Semafoare cu nume ­ Inițializare/deinițializare

/* use named semaphore to synchronize processes */ 
/* open */ 
sem_t* sem_open(const char *name, int oflag);                                  
/* create if oflag has O_CREAT set */ 
sem_t* sem_open(const char *name, int oflag, mode_t mode, unsigned int value); 
  
/* close named semaphore */ 
int sem_close(sem_t *sem); 
  
/* delete a named semaphore from system */ 
int sem_unlink(const char *name); 
 

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 12/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Comportamentul este similar cu cel de la deschiderea fișierelor. Dacă flag­ul O_CREAT este prezent,
trebuie folosită a doua formă a funcției, specificând permisiunile și valoarea inițială.

Singurele posibilități pentru al doilea argument sunt:

0 ­ se deschide semaforul dacă există
O_CREAT ­ se creează semaforul dacă nu există; se deschide dacă există
O_CREAT | O_EXCL ­ se creează semaforul numai dacă nu există; se întoarce eroare dacă
există

Semafoare anonime ­ Inițializare/deinițializare

int sem_init(sem_t *sem, int pshared, unsigned int value); 
  
/* close unnamed semaphore */ 
int sem_destroy(sem_t *sem);

Operații comune pe semafoare

/* increment/release semaphore (V) */ 
int sem_post(sem_t *sem); 
  
/* decrement/acquire semaphore (P) */ 
int sem_wait(sem_t *sem); 
  
/* non‐blocking decrement/acquire */ 
int sem_trywait(sem_t *sem); 
  
/* getting the semaphore count */ 
int sem_getvalue(sem_t *sem, int *pvalue);

Exemplu de utilizare semafor cu nume

#include <fcntl.h>           /* For O_* constants */ 
#include <sys/stat.h>        /* For mode constants */ 
#include <semaphore.h> 
  
#include "utils.h" 
  
#define SEM_NAME  "/my_semaphore" 
  
int main(void) 

  sem_t *my_sem; 
  int rc, pvalue; 
  
  /* create semaphore with initial value of 1 */ 
  my_sem = sem_open(SEM_NAME, O_CREAT, 0644, 1);  
  DIE(my_sem == SEM_FAILED, "sem_open failed"); 
  
  
  /* get the semaphore */
  sem_wait(my_sem); 
  
  /* do important stuff protected by the semaphore */ 
  rc = sem_getvalue(my_sem, &pvalue); 
  DIE(rc == ‐1, "sem_getvalue"); 
  printf("sem is %d\n", pvalue); 
  
  /* release the lock */ 
  sem_post(my_sem); 
  
  rc = sem_close(my_sem); 
  DIE(rc == ‐1, "sem_close"); 
  
  rc = sem_unlink(SEM_NAME); 
  DIE(rc == ‐1, "sem_unlink"); 
  

  return 0; 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 13/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]
  return 0; 
}

Semaforul va fi creat în /dev/shm și va avea numele sem.my_semaphore.

Variabile condiție
Variabilele condiție pun la dispoziție un sistem de notificare pentru fire de execuție, permițându­i unui
fir să se blocheze în așteptarea unui semnal din partea unui alt fir. Folosirea corectă a variabilelor
condiție presupune un protocol cooperativ între firele de execuție.

Mutex­urile și semafoarele permit blocarea altor fire de execuție. Variabilele de condiție se folosesc
pentru a bloca firul curent până la îndeplinirea unei condiții.

Variabilele condiție sunt obiecte de sincronizare care­i permit unui fir de execuție să­și suspende
execuția până când o condiție (predicat logic) devine adevărată. Când un fir de execuție determină că
predicatul a devenit adevărat, va semnala variabila condiție, deblocând astfel unul sau toate firele de
execuție blocate la acea variabilă condiție (în funcție de intenție).

O variabilă condiție trebuie întotdeauna folosită împreună cu un mutex pentru evitarea race­ului care
se produce când un fir se pregătește să aștepte la variabila condiție în urma evaluării predicatului logic,
iar alt fir semnalizează variabila condiție chiar înainte ca primul fir să se blocheze, pierzându­se astfel
semnalul. Așadar, operațiile de semnalizare, testare a condiției logice și blocare la variabila condiție
trebuie efectuate având ocupat mutex­ul asociat variabilei condiție. Condiția logică este testată sub
protecția mutex­ului, iar dacă nu este îndeplinită, firul apelant se blochează la variabila condiție,
eliberând atomic mutex­ul. În momentul deblocării, un fir de execuție va încerca să ocupe mutex­ul
asociat variabilei condiție. De asemenea, testarea predicatului logic trebuie făcută într­o buclă,
deoarece, dacă sunt eliberate mai multe fire deodată, doar unul va reuși să ocupe mutex­ul asociat
condiției. Restul vor aștepta ca acesta să­l elibereze, însă este posibil ca firul care a ocupat mutex­ul
să schimbe valoarea predicatului logic pe durata deținerii mutex­ului. Din acest motiv celelalte fire
trebuie să testeze din nou predicatul pentru că, altfel, și­ar începe execuția presupunând predicatul
adevărat, când el este, de fapt, fals.

Inițializarea/distrugerea unei variabile de condiție
Inițializarea unei variabile de condiție se face folosind macro­ul PTHREAD_COND_INITIALIZER sau
funcția pthread_cond_init [http://linux.die.net/man/3/pthread_cond_init]. Distrugerea unei variabile de
condiție se face prin funcția pthread_cond_destroy [http://linux.die.net/man/3/pthread_cond_destroy].

// inițializare statică a unei variabile de condiție cu atribute implicite 
// NB: variabila de condiție nu este eliberată,  
//     durata de viață a variabilei de condiție este durata de viață a programului. 
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 
  
// semnăturile funcțiilor de inițializare și eliberare de variabile de condiție: 
int pthread_cond_init   (pthread_cond_t *cond, pthread_condattr_t *attr); 
int pthread_cond_destroy(pthread_cond_t *cond);

Ca și la mutex­uri:

dacă parametrul attr este NULL, se folosesc atribute implicite
trebuie să nu existe nici un fir de execuție în așteptare pe variabila de condiție atunci când
aceasta este distrusă, altfel se întoarce EBUSY.

Blocarea la o variabilă condiție
Pentru a­și suspenda execuția și a aștepta la o variabilă condiție, un fir de execuție va apela funcția
pthread_cond_wait [http://linux.die.net/man/3/pthread_cond_wait]:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 14/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Firul de execuție apelant trebuie să fi ocupat deja mutex­ul asociat, în momentul apelului. Funcția
pthread_cond_wait va elibera mutex­ul și se va bloca, așteptând ca variabila condiție să fie
semnalizată de un alt fir de execuție. Cele două operații sunt efectuate atomic. În momentul în care
variabila condiție este semnalizată, se va încerca ocuparea mutex­ului asociat, și după ocuparea
acestuia, apelul funcției va întoarce. Observați că firul de execuție apelant poate fi suspendat, după
deblocare, în așteptarea ocupării mutex­ului asociat, timp în care predicatul logic, adevărat în
momentul deblocării firului, poate fi modificat de alte fire. De aceea, apelul pthread_cond_wait
trebuie efectuat într­o buclă în care se testează valoarea de adevăr a predicatului logic asociat
variabilei condiție, pentru a asigura o serializare corectă a firelor de execuție. Un alt argument pentru
testarea în buclă a predicatului logic este acela că un apel pthread_cond_wait poate fi întrerupt de
un semnal asincron (vezi laboratorul de semnale), înainte ca predicatul logic să devină adevărat. Dacă
firele de execuție care așteptau la variabila condiție nu ar testa din nou predicatul logic, și­ar continua
execuția presupunând greșit că acesta e adevărat.

Blocarea la o variabilă condiție cu timeout
Pentru a­și suspenda execuția și a aștepta la o variabilă condiție, nu mai târziu de un moment
specificat de timp, un fir de execuție va apela pthread_cond_timedwait
[http://linux.die.net/man/3/pthread_cond_timedwait]:

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,  
                           const struct timespec *abstime);

Funcția se comportă la fel ca pthread_cond_wait, cu excepția faptului că, dacă variabila condiție nu
este semnalizată mai devreme de abstime, firul apelant este deblocat, și, după ocuparea mutex­ului
asociat, funcția se întoarce cu eroarea ETIMEDOUT. Parametrul abstime este absolut și reprezintă
numărul de secunde trecute de la 1 ianuarie 1970, ora 00:00.

Deblocarea unui singur fir blocat la o variabilă condiție
Pentru a debloca un singur fir de execuție blocat la o variabilă condiție se va semnaliza variabila
condiție folosind pthread_cond_signal [http://linux.die.net/man/3/pthread_cond_signal]:

int pthread_cond_signal(pthread_cond_t *cond);

Dacă la variabila condiție nu așteaptă niciun fir de execuție, apelul funcției nu are efect și semnalizarea
se va pierde. Dacă la variabila condiție așteaptă mai multe fire de execuție, va fi deblocat doar unul
dintre acestea. Alegerea firului care va fi deblocat este făcută de planificatorul de fire de execuție. Nu
se poate presupune că firele care așteaptă vor fi deblocate în ordinea în care și­au început așteptarea.
Firul de execuție apelant trebuie să dețină mutex­ul asociat variabilei condiție în momentul apelului
acestei funcții.

Exemplu:

pthread_mutex_t count_lock; 
pthread_cond_t  count_nonzero; 
unsigned        count; 
  
void decrement_count() { 
    pthread_mutex_lock(&count_lock); 
    while (count == 0) 
        pthread_cond_wait(&count_nonzero, &count_lock); 
    count = count ‐ 1; 
    pthread_mutex_unlock(&count_lock); 

  
void increment_count() { 
    pthread_mutex_lock(&count_lock); 
    count = count + 1; 
    pthread_cond_signal(&count_nonzero); 
    pthread_mutex_unlock(&count_lock); 
}

Deblocarea tuturor firelor blocate la o variabilă condiție
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 15/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Pentru a debloca toate firele de execuție blocate la o variabilă condiție, se semnalizează variabila
condiție folosind pthread_cond_broadcast [http://linux.die.net/man/3/pthread_cond_broadcast]:

int pthread_cond_broadcast(pthread_cond_t *cond);

Dacă la variabila condiție nu așteaptă niciun fir de execuție, apelul funcției nu are efect și semnalizarea
se va pierde. Dacă la variabila condiție așteaptă fire de execuție, toate acestea vor fi deblocate, dar
vor concura pentru ocuparea mutex­ului asociat variabilei condiție. Firul de execuție apelant trebuie să
dețină mutex­ul asociat variabilei condiție în momentul apelului acestei funcții.

Exemplu de utilizare a variabilelor de condiție
În următorul program se utilizează o barieră pentru a sincroniza firele de execuție ale programului.
Bariera este implementată cu ajutorului unei variabile de condiție.

#include <stdio.h> 
#include <pthread.h> 
  
#define NUM_THREADS 5 
  
// implementarea unei bariere *non‐reutilizabile* cu variabile de condiție 
struct my_barrier_t { 
    // mutex folosit pentru a serializa accesele la datele interne ale barierei 
    pthread_mutex_t lock; 
  
    // variabila de condiție pe care se așteptă sosirea tuturor firelor de execuție 
    pthread_cond_t  cond; 
  
    // număr de fire de execuție care trebuie să mai vină pentru a elibera bariera 
    int nr_still_to_come; 
}; 
  
struct my_barrier_t bar; 
  
void my_barrier_init(struct my_barrier_t *bar, int nr_still_to_come) { 
    pthread_mutex_init(&bar‐>lock, NULL); 
    pthread_cond_init(&bar‐>cond, NULL); 
  
    // câte fire de execuție sunt așteptate la barieră 
    bar‐>nr_still_to_come = nr_still_to_come; 

  
void my_barrier_destroy(struct my_barrier_t *bar) { 
    pthread_cond_destroy(&bar‐>cond); 
    pthread_mutex_destroy(&bar‐>lock);   

  
void *thread_routine(void *arg) { 
    int thd_id = (int) arg; 
  
    // înainte de a lucra cu datele interne ale barierei trebuie să preluam mutex‐ul 
    pthread_mutex_lock(&bar.lock); 
  
    printf("thd %d: before the barrier\n", thd_id); 
  
    // suntem ultimul fir de execuție care a sosit la barieră? 
    int is_last_to_arrive = (bar.nr_still_to_come == 1); 
    // decrementăm numarul de fire de execuție așteptate la barieră
    bar.nr_still_to_come ‐‐; 
  
    // cât timp mai sunt fire de execuție care nu au ajuns la barieră, așteptăm. 
    while (bar.nr_still_to_come != 0) 
        // mutex‐ul se eliberează automat înainte de a incepe așteptarea 
        pthread_cond_wait(&bar.cond, &bar.lock);  
  
    // ultimul fir de execuție ajuns la barieră va semnaliza celelalte fire  
    if (is_last_to_arrive) { 
        printf("    let the flood in\n"); 
        pthread_cond_broadcast(&bar.cond); 
    } 
  
    printf("thd %d: after the barrier\n", thd_id); 
  
    // la ieșirea din funcția de așteptare se preia automat mutex‐ul, care trebuie eliberat 
    pthread_mutex_unlock(&bar.lock); 
  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 16/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]
  
    return NULL; 

  
int main(void) { 
    int i; 
    pthread_t tids[NUM_THREADS]; 
  
    my_barrier_init(&bar, NUM_THREADS); 
  
    for (i = 0; i < NUM_THREADS; i++) 
        pthread_create(&tids[i], NULL, thread_routine, (void *) i); 
  
    for (i = 0; i < NUM_THREADS; i++) 
        pthread_join(tids[i], NULL); 
  
    my_barrier_destroy(&bar); 
  
    return 0; 
}

so@spook$ gcc ‐Wall cond_var.c ‐pthread 
so@spook$ ./a.out  
thd 0: before the barrier 
thd 2: before the barrier 
thd 3: before the barrier 
thd 4: before the barrier 
thd 1: before the barrier 
    let the flood in 
thd 1: after the barrier 
thd 2: after the barrier 
thd 3: after the barrier 
thd 4: after the barrier 
thd 0: after the barrier

Din execuția programului se observă:

ordinea în care sunt planificate firele de execuție nu este neapărat cea a creării lor
ordinea în care sunt trezite firele de execuție ce așteaptă la o variabilă de condiție nu este
neapărat ordinea în care acestea au intrat în așteptare.

Barieră
Standardul POSIX definește și un set de funcții și structuri de date de lucru cu bariere. Aceste funcții
sunt disponibile dacă se definește macro­ul _XOPEN_SOURCE la o valoare >= 600.

Inițializarea/distrugerea unei bariere
Bariera se va inițializa folosind pthread_barrier_init [http://linux.die.net/man/3/pthread_barrier_init] și se
va distruge folosind pthread_barrier_destroy [http://linux.die.net/man/3/pthread_barrier_destroy].

// pentru a folosi funcțiile de lucru cu bariere e nevoie să se definească  
// _XOPEN_SOURCE la o valoare >= 600. Pentru detalii consultați feature_test_macros(7). 
#define _XOPEN_SOURCE 600 
#include <pthread.h> 
  
// attr    ‐> un set de atribute, poate fi NULL (se folosesc atribute implicite) 
// count   ‐> numărul de fire de execuție care trebuie să ajungă 
//            la barieră pentru ca aceasta să fie eliberată 
int pthread_barrier_init(pthread_barrier_t *barrier,  
                         const pthread_barrierattr_t *attr,  
                         unsigned count); 
  
// trebuie să nu existe fire de execuție în așteptare la barieră 
// înainte de a apela funcția _destroy, altfel, se întoarce EBUSY  
// și nu se distruge bariera. 
int pthread_barrier_destroy(pthread_barrier_t *barrier);

Așteptarea la o barieră
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 17/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Așteptarea la barieră se face prin apelul pthread_barrier_wait
[http://linux.die.net/man/3/pthread_barrier_wait]:

#define _XOPEN_SOURCE 600 
#include <pthread.h> 
int pthread_barrier_wait(pthread_barrier_t *barrier);

Dacă bariera a fost creată cu count=N, primele N‐1 fire de execuție care apelează
pthread_barrier_wait se blochează. Când sosește ultimul (al N­lea), va debloca toate cele N‐1
fire de execuție. Funcția pthread_barrier_wait întoarce trei valori:

EINVAL – în cazul în care bariera nu este inițializată (singura eroare definită)
PTHREAD_BARRIER_SERIAL_THREAD – în caz de succes, un singur fir de execuție va întoarce
valoarea aceasta – nu e specificat care este acel fir de execuție (nu e obligatoriu să fie ultimul
ajuns la barieră)
0 – valoare întoarsă în caz de succes de celelalte N‐1 fire de execuție.

Exemplu de utilizare a barierei
Cu bariere POSIX, programul de mai sus poate fi simplificat:

#define _XOPEN_SOURCE 600 
#include <pthread.h> 
#include <stdio.h> 
  
#define NUM_THREADS 5 
  
pthread_barrier_t barrier; 
  
void *thread_routine(void *arg) { 
    int thd_id = (int) arg; 
    int rc; 
  
    printf("thd %d: before the barrier\n", thd_id); 
  
    // toate firele de execuție așteaptă la barieră. 
    rc = pthread_barrier_wait(&barrier); 
    if (rc == PTHREAD_BARRIER_SERIAL_THREAD) { 
        // un singur fir de execuție (posibil ultimul) va întoarce PTHREAD_BARRIER_SERIAL_THREAD 
        // restul firelor de execuție întorc 0 în caz de succes. 
        printf("   let the flood in\n");  
    } 
  
    printf("thd %d: after the barrier\n", thd_id); 
  
    return NULL; 

  
int main(void)  

    int i; 
    pthread_t tids[NUM_THREADS]; 
  
    // bariera este inițializată o singură dată și folosită de toate firele de execuție 
    pthread_barrier_init(&barrier, NULL, NUM_THREADS); 
  
    // firele de execuție vor executa codul funcției 'thread_routine'. 
    // în locul unui pointer la date utile, se trimite în ultimul argument 
    // un întreg ‐ identificatorul firului de execuție 
    for (i = 0; i < NUM_THREADS; i++) 
        pthread_create(&tids[i], NULL, thread_routine, (void *) i); 
  
    // așteptăm ca toate firele de execuție să se termine 
    for (i = 0; i < NUM_THREADS; i++) 
        pthread_join(tids[i], NULL); 
  
    // eliberăm resursele barierei 
    pthread_barrier_destroy(&barrier); 
  
    return 0; 
}

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 18/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

so@spook$ gcc ‐Wall barrier.c ‐lpthread 
so@spook$ ./a.out  
thd 0: before the barrier 
thd 2: before the barrier 
thd 1: before the barrier 
thd 3: before the barrier 
thd 4: before the barrier 
   let the flood in 
thd 4: after the barrier 
thd 2: after the barrier 
thd 3: after the barrier 
thd 0: after the barrier 
thd 1: after the barrier

Exerciţii de laborator

Exercițiul 0 ­ Joc interactiv (2p)
Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (9p)
În rezolvarea laboratorului folosiți arhiva de sarcini lab08­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab08­tasks.zip]

Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din arhivă există un
fișier utils.h cu funcții utile.

Pentru a instala paginile de manual pentru 'pthreads'

sudo apt‐get install manpages‐posix manpages‐posix‐dev

Exercițiul 1 ­ Thread Stack (1p)

Intrați în directorul 1‐th_stack și inspectați sursa, apoi compilați și rulați programul. Urmăriți cu
pmap sau folosind procfs cum se modifică spațiul de adresă al programului:

watch ‐d pmap $(pidof th_stack) 
watch ‐d cat /proc/$(pidof th_stack)/maps

Zonele de memorie cu dimensiunea de 8MB (8192KB) care se creează după fiecare apel
pthread_create reprezintă noile stive alocate de către biblioteca libpthread pentru fiecare thread
în parte. Observați că, în plus, se mai mapează de fiecare dată o pagină (4KB) cu protecția ‐‐‐p
(PROT_NONE, private ­ vizibil în procfs) care are rolul de "pagină de gardă".

Motivul pentru care nu se termină programul este prezența unui  
while(1) în funcția thread­urilor.
Folosiți Ctrl+C pentru a termina programul.

Exercițiul 2 ­ Fire de execuție vs Procese (1p)

Intrați în directorul 2‐th_vs_proc și inspectați sursele. Ambele programe simulează un server care
creează fire de execuție/procese. Compilați și rulați pe rând ambele programe.

În timp ce rulează, afișați, într­o altă consolă, câte fire de execuție/procese sunt create în ambele
situații folosind comanda ps ‐L ‐C <nume_program>.

ps ‐L ‐C threads 
ps ‐L ‐C processes

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 19/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Verificați ce se întâmplă dacă la un moment dat un fir de execuție moare (sau un proces, în funcție de
ce executabil testați). Testați utilizând funcția do_bad_task la fiecare al 4­lea fir de execuție/process.

Exercițiul 3 ­ Thread safety (1p)

Datorită faptului că mașina virtuală spook are un singur core virtual, exercițiul următor trebuie realizat
pe mașina fizică pentru a permite mai multor thread­uri să ruleze în același moment de timp.

Intrați în directorul 3‐safety și inspectați sursa vars.c. Funcțiile thread_function și main NU
sunt thread­safe relativ la variabilele a și b (revedeți semnificația lui thread safety
[http://en.wikipedia.org/wiki/Thread_safety]). Există o condiție de cursă
[https://en.wikipedia.org/wiki/Race_condition] între cele două thread­uri create la incrementarea variabilei
b, declarată în funcția thread_function, și o altă condiție de cursă între toate thread­urile procesului
la incrementarea variabilei globale a. Datorită introducerii artificiale a apelurilor sleep, manifestarea
condițiilor de cursă poate fi diminuată (dar nu eliminată).

Un utilitar foarte folositor este helgrind, care poate detecta automat aceste condiții de cursă. Îl
putem folosi în cazul nostru așa:

valgrind ‐‐tool=helgrind ./vars

Observați ce se întâmplă cu memoria alocată pentru variabila rez după ce se face join. Folosiți
valgrind pentru a investiga:

valgrind ‐‐leak‐check=full ./vars

În fișierul malloc.c se creează NUM_THREADS thread­uri care alocă memorie în NUM_ROUNDS runde.
Sunt șanse mari ca thread­urile să execute apeluri malloc concurente. După compilare și rulare de
mai multe ori, observăm că programul rulează cu succes. Pentru a face verificări suplimentare, rulăm
din nou helgrind:

valgrind ‐‐tool=helgrind ./malloc

Observăm că nici helgrind nu raportează vreo eroare, lucru care conduce la faptul că funcția malloc
ar fi thread­safe. Pentru a putea fi siguri trebuie să consultăm paginile de manual și codul sursă.

Este important de știut că anumite funcții sunt thread­safe iar altele nu. Găsiți o listă cu funcțiile care
nu sunt thread­safe în pagina de manual pthreads(7) [http://man7.org/linux/man­
pages/man7/pthreads.7.html], în secțiunea Thread‐safe functions.

Funcția malloc din implementarea GLIBC este thread­safe, lucru indicat în pagina de manual
malloc(3) [http://man7.org/linux/man­pages/man3/malloc.3.html#NOTES] (al treilea paragraf din secțiunea
NOTES) și vizibil în codul sursă prin prezența câmpului mutex în structura malloc_state
[https://sourceware.org/git/?
p=glibc.git;a=blob;f=malloc/malloc.c;h=f361bad636167cf1680cb75b5098232c9232d771;hb=HEAD#l1672].

Exercițiul 4 ­ Parallel fgrep (1.5p)

Datorită faptului că mașina virtuală spook are un singur core virtual, exercițiul următor trebuie realizat
pe mașina fizică pentru a permite mai multor thread­uri să ruleze în același moment de timp.

Implementați un program similar cu fgrep [http://linux.die.net/man/1/fgrep], care să realizeze numărarea
în paralel a aparițiilor unui string într­un fișier. Porniți de la sursa parallel_fgrep.c din directorul
4‐pfgrep și urmăriți secțiunile TODO. Fiecare fir de execuție va căuta șirul într­o anumită zonă din
fișier și va întoarce numărul de apariții găsite. Firul de execuție principal va colecta rezultatele și va
afișa numărul total de apariții.

Fișierul este mapat înainte de pornirea firelor de execuție. Observați că nu este nevoie de sincronizarea
accesului la citire. Citirile se pot executa în paralel, fără condiții de cursă, atât timp cât nu există

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 20/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

scrieri.

Varianta serială a fgrep este implementată în fișierul serial_fgrep.c. Generați un fișier mare și
comparați timpii de execuție:

cat Makefile{,}{,}{,}{,}{,}{,}{,}{,}{,}{,}{,}{,}{,}{,}{,}{,} > 2^16_Makefiles 
  
time ./serial_fgrep grep 2^16_Makefiles 
time ./parallel_fgrep grep 2^16_Makefiles

În exemplul anterior, se numără aparițiile șirului “grep” în fișierul 2^16_Makefiles, obținut prin
concatenarea conținutului fișierului Makefile de 2^16 ori. Ar trebui să observați un timp de rulare mai
mic pentru implementarea paralelă.

Corectitudinea rezultatului se poate testa cu comanda:

fgrep ‐o grep 2^16_Makefiles | wc ‐l

Exercițiul 5 ­ Blocked (1.5p)

Inspectați fișierul blocked.c din directorul 5‐blocked, compilați și executați binarul (repetați până
detectați blocarea programului). Programul creează două fire de execuție care caută un număr magic,
fiecare în intervalul propriu (nu este neapărat necesar ca numărul să fie găsit). Fiecare fir de execuție,
pentru fiecare valoare din intervalul propriu, verifică dacă este valoarea căutată:

dacă da, marchează un câmp found pentru a înștiința și celălalt fir de execuție că a găsit
numărul căutat.
dacă nu, inspectează câmpul found al structurii celuilalt fir de execuție, pentru a vedea dacă
acesta a găsit deja numărul căutat.

Determinați cauza blocării, reparați programul și explicați soluția. Puteți utiliza helgrind, unul din
tool­urile valgrind, pentru a detecta problema:

$ valgrind ‐‐tool=helgrind ./blocked

Așa cum ne arată și helgrind, problema constă în faptul că cele două thread­uri iau cele două mutex­
uri în ordinea inversă, situație foarte probabilă în a cauza un deadlock
[https://en.wikipedia.org/wiki/Deadlock].

Exercițiul 6 ­ Implementare comportament pthread_once (1p)

Aveți o funcție de inițializare pe care vreți să o apelați o singură dată. Pornind de la sursa once.c din
directorul 6‐once, asigurați­vă că funcția  init_func este apelată o singură dată. Nu aveți voie să
modificați funcția init_func sau să folosiţi pthread_once.

Citiți despre funcționalitatea pthread_once [http://linux.die.net/man/3/pthread_once] și revedeți secțiunea
despre mutex.

Exercițiul 7 ­ Producător ­ Consumator (2p)

Intrați în directorul 7‐prodcons. Completați TODO­urile din cod pentru a implementa sincronizarea
unui producător cu un consumator, ce folosesc în comun un buffer.

Producătorul va pune obiecte în buffer atât timp cât buffer­ul nu este plin și se va bloca atunci când
buffer­ul este plin. Producătorul va fi trezit de consumator după ce buffer­ul nu va mai fi plin.

Consumatorul va scoate obiecte din buffer atât timp cât buffer­ul nu este gol și se va bloca atunci când
buffer­ul este gol. Consumatorul va fi trezit de producător după ce buffer­ul nu va mai fi gol.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 21/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Sincronizați accesul la buffer­ul comun folosind variabile de condiție (revedeți secțiunea despre
variabile de condiție).

BONUS

1 so karma ­ fork vs pthread_create

Vrem să aflăm ce apeluri de sistem sunt realizate în urma apelurilor funcțiilor fork și
pthread_create.

Intrați în directorul 8‐fork_thread și inspectați sursa. Programul creează un fir de execuție și un
proces copil.

Folosiți ltrace [https://linux.die.net/man/1/ltrace] pentru a urmări ce apeluri de bibliotecă se fac și ce
apeluri de sistem sunt folosite mai departe de către apelurile de bibliotecă:

ltrace ‐S ‐n 8 ./ft

Observați că atât fork cât și pthread_create folosesc apelul de sistem clone
[http://linux.die.net/man/2/clone]. Urmăriți argumentele apelurilor de sistem clone folosind strace
[http://linux.die.net/man/1/strace]:

strace ‐e clone ./ft

Observați alocarea unei stive separate pentru noul thread (argumentul child_stack) cât și partajarea
resurselor procesului cu acesta (flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND).

1 so karma ­ Thread Specific Data

Fișierul 9‐tsd/tsd.c conține o aplicație ce împarte un task între mai multe fire de execuție. Fiecare
fir de execuție are un fișier de log în care va înregistra mesaje despre progresul său. Observați
următoarele aspecte:

crearea de fire de execuție
așteptarea terminării acestora
modul în care se creează / folosește / șterge o variabilă specifică unui fir de execuție ­
thread_log_key
utilitatea unei funcții de cleanup ­ close_thread_log

1 so karma ­ Mutex vs Spinlock

Dorim să testăm care varianta este mai eficientă pentru a proteja incrementarea unei variabile.

Intrați în directorul 10‐spin, inspectați și compilați sursa spin.c. În urma compilării vor rezulta două
executabile, unul care folosește un mutex pentru sincronizare, iar altul un spinlock.

Comparați timpii de execuție:

time ./mutex 
time ./spin

Atunci când un fir de execuție găsește mutex­ul ocupat se va bloca. Atunci când un fir de execuție
găsește spinlock­ul ocupat va face busy­waiting.

Soluții
lab08­sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab08­sol.zip]

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 22/23
6/11/2017 Laborator 08 ­ Thread­uri Linux [CS Open CourseWare]

Resurse utile
LinuxTutorialPosixThreads [http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html]

POSIX Threads Programming [https://computing.llnl.gov/tutorials/pthreads/]
so/laboratoare/laborator­08.txt · Last modified: 2017/04/25 14:33 by ioana_elena.ciornei

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08 23/23
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Laborator 09 ­ Thread­uri Windows

Materiale ajutătoare

lab09­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab09­slides.pdf]
lab09­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab09­refcard.pdf]

Nice to read

WSP4 ­ Chapter 7, Threads and Scheduling

Crearea firelor de execuție
Pentru a lansa un nou fir de execuție, există funcțiile CreateThread [http://msdn.microsoft.com/en­
us/library/ms682453%28VS.85%29.aspx] și CreateRemoteThread [http://msdn.microsoft.com/en­
us/library/ms682437%28v=VS.85%29.aspx] (a doua fiind folosită pentru a crea un fir de execuție în cadrul
altui proces decât cel curent).

HANDLE CreateThread (  hthread = CreateThread( 
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,          NULL, 
  SIZE_T dwStackSize,          0, 
  LPTHREAD_START_ROUTINE lpStartAddress,          ThreadFunc,
  LPVOID lpParameter,          &dwThreadParam,                         
  DWORD dwCreationFlags,          0, 
  LPDWORD lpThreadId          &dwThreadId 
); );

Parametrul dwStackSize reprezintă mărimea inițială a stivei (în octeți). Sistemul rotunjește această
valoare la cel mai apropiat multiplu de dimensiunea unei pagini. Dacă parametrul este 0, noul fir de
execuție va folosi mărimea implicită (1 MB). lpStartAddress este un pointer la funcția ce trebuie
executată de către firul de execuție. Această funcție are următorul prototip:

DWORD WINAPI ThreadProc(LPVOID lpParameter);

unde lpParameter reprezintă datele care sunt pasate firului în momentul execuției. La fel ca pe Linux,
se poate transmite un pointer la o structură, care conține toți parametrii necesari. Rezultatul întors
poate fi obținut de un alt fir de execuție folosind funcția GetExitCodeThread [http://msdn.microsoft.com/en­
us/library/ms683190%28VS.85%29.aspx].

Handle și identificator
Firele de execuție pot fi identificate în sistem în 3 moduri:

printr­un HANDLE, obținut la crearea firului de execuție, sau folosind funcția OpenThread
[http://msdn.microsoft.com/en­us/library/ms684335%28VS.85%29.aspx], căreia i se dă ca parametru
identificatorul firului de execuție:

HANDLE OpenThread( 
  DWORD dwDesiredAccess, 
  BOOL bInheritHandle, 
  DWORD dwThreadId 
);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 1/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

printr­un pseudo‐HANDLE, o valoare specială care indică funcțiilor de lucru cu HANDLE­uri că
este vorba de HANDLE­ul asociat cu firul de execuție curent (obținut, de exemplu, apelând
GetCurrentThread [http://msdn.microsoft.com/en­us/library/ms683182%28VS.85%29.aspx]). Pentru a
converti un pseudo‐HANDLE într­un HANDLE veritabil, trebuie folosită funcția DuplicateHandle
[http://msdn.microsoft.com/en­us/library/ms724251%28VS.85%29.aspx]. De asemenea, nu are sens
să facem CloseHandle [http://msdn.microsoft.com/en­us/library/ms724211%28VS.85%29.aspx] pe un
pseudo‐HANDLE. Pe de altă parte, handle­ul obținut cu DuplicateHandle
[http://msdn.microsoft.com/en­us/library/ms724251%28VS.85%29.aspx] trebuie închis dacă nu mai
este nevoie de el.

printr­un identificator al firului de execuție, de tipul DWORD, întors la crearea firului, sau obținut
folosind GetCurrentThreadId [http://msdn.microsoft.com/en­us/library/ms683183%28VS.85%29.aspx].
O diferență dintre identificator și HANDLE este faptul că nu trebuie să ne preocupăm să închidem
un identificator, pe când la HANDLE, pentru a evita leak­urile, trebuie să apelăm CloseHandle
[http://msdn.microsoft.com/en­us/library/ms724211%28VS.85%29.aspx].

Handle­ul obținut la crearea unui fir de execuție are implicit drepturi de acces nelimitate. El poate fi
moștenit (sau nu) de procesele copil ale procesului curent, în funcție de flag­urile specificate la crearea
lui. Prin funcția DuplicateHandle [http://msdn.microsoft.com/en­us/library/ms724251%28VS.85%29.aspx], se
poate crea un nou handle cu mai puține drepturi. Handle­ul este valid până când este închis, chiar dacă
firul de execuție pe care îl reprezintă s­a terminat.

Așteptarea firelor de execuție
Pe Windows, se poate aștepta terminarea unui fir de execuție folosind aceeași funcție ca pentru
așteptarea oricărui obiect de sincronizare WaitForSingleObject [http://msdn.microsoft.com/en­
us/library/ms687032%28VS.85%29.aspx]:

DWORD WINAPI WaitForSingleObject( 
  HANDLE hHandle,  
  DWORD dwMilliseconds 
);

Terminarea firelor de execuție
Un fir de execuție se termină în unul din următoarele cazuri :

el însuși apelează funcția ExitThread [http://msdn.microsoft.com/en­
us/library/ms682659%28VS.85%29.aspx] :

void ExitThread(DWORD dwExitCode);

funcția asociată firului de execuție execută un return.

un fir de execuție ce deține un handle cu dreptul THREAD_TERMINATE asupra firului de execuție,
execută un apel TerminateThread [http://msdn.microsoft.com/en­
us/library/ms686717%28VS.85%29.aspx] pe acest handle :

BOOL TerminateThread( 
  HANDLE hThread,  
  DWORD dwExitCode 
);

sau întregul proces se termină ca urmare a unui apel ExitProcess [http://msdn.microsoft.com/en­
us/library/ms682658%28VS.85%29.aspx] sau TerminateProcess [http://msdn.microsoft.com/en­

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 2/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

us/library/ms686714%28VS.85%29.aspx].

La terminarea ultimului fir de execuție al unui proces se termină și procesul.

Funcțiile TerminateThread [http://msdn.microsoft.com/en­us/library/ms686717%28VS.85%29.aspx] și
TerminateProcess [http://msdn.microsoft.com/en­us/library/ms686714%28VS.85%29.aspx] nu trebuie folosite
decât în cazuri extreme (pentru că nu eliberează resursele folosite de firul de execuție, iar unele
resurse pot fi vitale). Metoda preferată de a termina un fir de execuție este ExitThread
[http://msdn.microsoft.com/en­us/library/ms682659%28VS.85%29.aspx], sau folosirea unui protocol de
oprire între firul de execuție care dorește să închidă un alt fir de execuție și firul care trebuie oprit.

Pentru aflarea codului de terminare a unui fir de execuție, folosim funcția GetExitCodeThread
[http://msdn.microsoft.com/en­us/library/ms683190%28VS.85%29.aspx].

BOOL GetExitCodeThread( 
  HANDLE hThread,  
  LPDWORD lpExitCode 
);

hThread ­ handle al firului de execuție ce trebuie să aibă dreptul de acces
THREAD_QUERY_INFORMATION.
lpExitCode ­ pointer la o variabilă în care va fi plasat codul de terminare al firului. Dacă firul
nu și­a terminat execuția, această valoare va fi  STILL_ACTIVE.

Pot apărea probleme dacă firul de execuție returnează STILL_ACTIVE (259), și anume aplicația care
testează valoarea poate intra într­o buclă infinită.

Suspend, Resume

DWORD SuspendThread(HANDLE hThread); 
DWORD ResumeThread(HANDLE hThread);

Prin intermediul acestor două funcții, un fir de execuție poate suspenda/relua execuția unui alt fir de
execuție.

Un fir de execuție suspendat nu mai este planificat pentru a obține timp pe procesor.

Cele două funcții manipulează un contor de suspendare (prin incrementare, respectiv decrementare ­
în limitele 0 ­ MAXIMUM_SUSPEND_COUNT).

În cazul în care contorul de suspendare este mai mare strict decât 0, firul de execuție este suspendat.

Un fir de execuție poate fi creat în starea suspendat folosind flag­ul CREATE_SUSPENDED.

Aceste funcții nu pot fi folosite pentru sincronizare (pentru că nu controlează punctul în care firul de
execuție își va suspenda execuția), dar sunt utile pentru debug.

Cedarea procesorului

Un fir de execuție poate renunța de bună voie la procesor.

În urma apelului funcției Sleep [http://msdn.microsoft.com/en­us/library/ms686298%28VS.85%29.aspx] un
fir de execuție este suspendat pentru cel puțin o anumită perioadă de timp (dwMilliseconds).

void Sleep(DWORD dwMilliseconds);

Există de asemenea funcția SleepEx [http://msdn.microsoft.com/en­us/library/ms686307%28VS.85%29.aspx]
care este un Sleep [http://msdn.microsoft.com/en­us/library/ms686298%28VS.85%29.aspx] alertabil (ceea
ce înseamnă că se pot prelucra APC­uri ­ Asynchronous Procedure Call ­ pe durata execuției lui
SleepEx [http://msdn.microsoft.com/en­us/library/ms686307%28VS.85%29.aspx]).
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 3/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Funcția SwitchToThread [http://msdn.microsoft.com/en­us/library/ms686352%28VS.85%29.aspx] este
asemănătoare cu Sleep [http://msdn.microsoft.com/en­us/library/ms686298%28VS.85%29.aspx], doar că nu
este specificat intervalul de timp, astfel firul de execuție renunță doar la timpul pe care îl avea pe
procesor în momentul respectiv (time­slice).

BOOL SwitchToThread(void);

Funcția întoarce TRUE dacă procesorul este cedat unui alt fir de execuție și FALSE dacă nu există alte
fire gata de execuție.

Alte funcții utile

HANDLE GetCurrentThread(void);

Rezultatul este un pseudo­handle pentru firul curent ce nu poate fi folosit decât de firul apelant. Acest
handle are maximum de drepturi de acces asupra obiectului pe care îl reprezintă.

DWORD GetCurrentThreadId(void);

Rezultatul este identificatorul firului curent de execuție.

DWORD GetThreadId(HANDLE hThread);

Rezultatul este identificatorul firului ce corespunde handle­ului hThread.

Thread Local Storage
Ca și în Linux, în Windows există un mecanism prin care fiecare fir de execuție să aibă anumite date
specifice. Acest mecanism poartă numele de Thread Local Storage (TLS). În Windows, pentru a
accesa datele din TLS se folosesc indecșii asociați acestora (corespunzători cheilor din Linux).

Pentru a crea un nou TLS, se apelează funcția TlsAlloc [http://msdn.microsoft.com/en­
us/library/ms686801%28v=VS.85%29.aspx]:

DWORD TlsAlloc(void);

Funcția întoarce în caz de succes indexul asociat TLS­ului, prin intermediul căruia fiecare fir de execuție
va putea accesa datele specifice. Valoarea stocată în TLS este inițializată cu 0. În caz de eșec, funcția
întoarce valoarea TLS_OUT_OF_INDEXES.

Pentru a stoca o nouă valoare într­un TLS, se folosește funcția TlsSetValue [http://msdn.microsoft.com/en­
us/library/ms686818%28v=VS.85%29.aspx]:

BOOL TlsSetValue( 
  DWORD dwTlsIndex, 
  LPVOID lpTlsValue 
);

Un fir de execuție poate afla valoarea specifică lui dintr­un TLS apelând funcția TlsGetValue
[http://msdn.microsoft.com/en­us/library/ms686812%28v=VS.85%29.aspx]:

 LPVOID TlsGetValue(DWORD dwTlsIndex);

În caz de succes, funcția întoarce valoarea stocată în TLS, iar în caz de eșec, întoarce 0. Dacă data
stocată în TLS are valoarea 0, atunci valoarea întoarsă este tot 0, dar GetLastError
[http://msdn.microsoft.com/en­us/library/ms679360%28VS.85%29.aspx] va întoarce NO_ERROR. Deci trebuie
verificată eroarea întoarsă de GetLastError [http://msdn.microsoft.com/en­
us/library/ms679360%28VS.85%29.aspx].

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 4/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Pentru a elibera un index asociat unui TLS, se folosește funcția TlsFree [http://msdn.microsoft.com/en­
us/library/ms686804%28VS.85%29.aspx]:

 BOOL TlsFree(DWORD dwTlsIndex);

Dacă firele de execuție au alocat memorie și au stocat în TLS un pointer la memoria alocată, această
funcție nu va face dealocarea memoriei. Memoria trebuie dealocată de către fire înainte de apelul lui
TlsFree [http://msdn.microsoft.com/en­us/library/ms686804%28VS.85%29.aspx].

Exemplu
Exemplul prezintă crearea a 2 fire de execuție ce vor folosi un TLS.

ThreadTLS.c

#include <stdio.h> 
#include <windows.h> 
#include "utils.h"  
  
#define NO_THREADS 2 
  
DWORD dwTlsIndex; 
  
  
VOID TLSUse(VOID) 

  LPVOID lpvData; 
  
  /* get the pointer from TLS for current thread */ 
  lpvData = TlsGetValue(dwTlsIndex); 
  DIE((lpvData == 0) && (GetLastError() != 0), "TlsGetValue"); 
  
  /* use this data */ 
  printf("thread %d: get lpvData=%p\n", GetCurrentThreadId(), lpvData); 
  
  Sleep(5000); 

  
/* function executed by the threads */ 
DWORD WINAPI ThreadFunc(LPVOID lpParameter) 

  LPVOID lpvData; 
  DWORD dwReturn; 
  
  /* TLS init for the current thread */ 
  lpvData = (LPVOID) LocalAlloc(LPTR, 256); 
  DIE(lpvData == NULL, "LocallAloc"); 
  
  dwReturn = TlsSetValue(dwTlsIndex, lpvData); 
  DIE(dwReturn == FALSE, "TlsSetValue"); 
  
  printf("thread %d: set lpvData=%p\n", GetCurrentThreadId(), lpvData); 
  
  TLSUse(); 
  
  /* free dinamic memory */ 
  lpvData = TlsGetValue(dwTlsIndex); 
  DIE((lpvData == 0) && (GetLastError() != 0), "TlsGetValue"); 
  
  LocalFree((HLOCAL) lpvData); 
  
  return 0; 

  
DWORD main(VOID) 

  DWORD IDThread, dwReturn; 
  HANDLE hThread[NO_THREADS]; 
  int i; 
  
  /* allocate TLS index */ 
  dwTlsIndex = TlsAlloc(); 
  DIE(dwTlsIndex == TLS_OUT_OF_INDEXES, "Eroare la TlsAlloc"); 
  
  /* create threads */ 
  for (i = 0; i < NO_THREADS; i++) { 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 5/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]
  for (i = 0; i < NO_THREADS; i++) { 
    hThread[i] = CreateThread(NULL,                    /* default security attributes */ 
            0,                                   /* default stack size */ 
            (LPTHREAD_START_ROUTINE) ThreadFunc, /* routine to execute */ 
            NULL,                                /* no thread parameter */ 
            0,                                   /* immediately run the thread */ 
            &IDThread);                          /* thread id */ 
    DIE(hThread[i] == NULL, "CreateThread"); 
  } 
  
  /* wait for threads completion */ 
  for (i = 0; i < NO_THREADS; i++) { 
    dwReturn = WaitForSingleObject(hThread[i], INFINITE); 
    DIE(dwReturn == WAIT_FAILED, "WaitForSingleObject"); 
  } 
  
  /* free TLS index */ 
  dwReturn = TlsFree(dwTlsIndex); 
  DIE(dwReturn == FALSE, "TlsFree"); 
  
  return 0; 
}

Fibre de execuție
Windows pune la dispoziție și o implementare de User­space Threads, numite fibre. Kernel­ul planifică
un singur Kernel Level Thread (KLT) asociat cu un set de fibre, iar fibrele colaborează pentru a partaja
timpul de procesor oferit acestuia. Deși viteza de execuție este mai bună (pentru context­switch, nu
mai este necesară interacțiunea cu kernel­ul), programele scrise folosind fibre pot deveni complexe.
Mai multe informații puteți găsi în cadrul secțiunii suplimentare dedicate.

Securitate și drepturi de acces

Modelul de securitate Windows NT ne permite să controlăm accesul la obiectele de tip fir de execuție.

Descriptorul de securitate pentru un fir de execuție se poate specifica la apelul uneia dintre funcțiile
CreateProcess [http://msdn.microsoft.com/en­us/library/ms682425%28VS.85%29.aspx],
CreateProcessAsUser [http://msdn.microsoft.com/en­us/library/ms682429%28VS.85%29.aspx],
CreateProcessWithLogonW [http://msdn.microsoft.com/en­us/library/ms682431%28VS.85%29.aspx],
CreateThread [http://msdn.microsoft.com/en­us/library/ms682453%28VS.85%29.aspx] sau
CreateRemoteThread [http://msdn.microsoft.com/en­us/library/ms682437%28v=VS.85%29.aspx].

Dacă în locul acestui descriptor este pasată valoarea NULL, firul de execuție va avea un descriptor de
securitate implicit.

Pentru a obține acest descriptor este folosită funcția GetSecurityInfo [http://msdn.microsoft.com/en­
us/library/aa446654%28VS.85%29.aspx], iar pentru a­l schimba funcția SetSecurityInfo
[http://msdn.microsoft.com/en­us/library/aa379588%28VS.85%29.aspx].

DWORD WINAPI GetSecurityInfo( 
  HANDLE handle, 
  SE_OBJECT_TYPE ObjectType, 
  SECURITY_INFORMATION SecurityInfo, 
  PSID *ppsidOwner, 
  PSID *ppsidGroup, 
  PACL *ppDacl, 
  PACL *ppSacl, 
  PSECURITY_DESCRIPTOR *ppSecurityDescriptor 
);

DWORD WINAPI SetSecurityInfo( 
  HANDLE handle, 
  SE_OBJECT_TYPE ObjectType, 
  SECURITY_INFORMATION SecurityInfo, 
  PSID psidOwner, 
  PSID psidGroup, 
  PACL pDacl, 

  PACL pSacl 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 6/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]
  PACL pSacl 
);

Handle­ul întors de funcția CreateThread [http://msdn.microsoft.com/en­
us/library/ms682453%28VS.85%29.aspx] are THREAD_ALL_ACCESS. La apelul GetCurrentThread
[http://msdn.microsoft.com/en­us/library/ms683182%28VS.85%29.aspx], sistemul întoarce un pseudo­handle
cu maximul de drepturi de acces pe care descriptorul de securitate al firului de execuție îl permite
apelantului.

Drepturile de acces pentru un obiect de tip fir de execuție includ drepturile de acces standard:
DELETE, READ_CONTROL, SYNCHRONIZE, WRITE_DAC și WRITE_OWNER la care se adaugă drepturi
specifice, pe care le puteți găsi pe MSDN [http://msdn.microsoft.com/en­us/library/ms686769(VS.85).aspx].

Sincronizarea firelor de execuție
Pentru sincronizarea firelor de execuție avem la dispoziție:

Mutex: POSIX, Win32
Semafor: POSIX, Win32
Secțiune critică (excludere mutuală în cadrul aceluiași proces): Win32
Variabilă de condiție: POSIX, Win32 (începând cu Vista) [http://msdn.microsoft.com/en­
us/library/ms682052(VS.85).aspx]
Eveniment: Win32 [http://msdn.microsoft.com/en­us/library/ms682655(VS.85).aspx]
Timer: Win32.

Standardul POSIX specifică funcții de sincronizare pentru fiecare tip de obiect de sincronizare. API­ul
Win32, fiind controlat de o singură entitate, permite ca toate obiectele de sincronizare să poată fi
utilizate cu funcțiile standard de sincronizare: WaitForSingleObject [http://msdn.microsoft.com/en­
us/library/ms687032%28VS.85%29.aspx], WaitForMultipleObjects [http://msdn.microsoft.com/en­
us/library/ms687025%28v=VS.85%29.aspx] sau SignalObjectAndWait [http://msdn.microsoft.com/en­
us/library/ms686293%28VS.85%29.aspx].

Obiectele de sincronizare Semaphore [http://msdn.microsoft.com/en­
us/library/ms685129%28VS.85%29.aspx], Mutex [http://msdn.microsoft.com/en­
us/library/ms684266%28VS.85%29.aspx], Event [http://msdn.microsoft.com/en­
us/library/ms682655%28VS.85%29.aspx] și WaitableTimer [http://msdn.microsoft.com/en­
us/library/ms687012%28VS.85%29.aspx] pot fi folosite atât pentru sincronizarea proceselor, cât și a
firelor de execuție.

În Windows mai există un mecanism de sincronizare care este disponibil doar pentru firele de execuție
ale aceluiași proces, și anume CriticalSection [http://msdn.microsoft.com/en­
us/library/ms682530%28VS.85%29.aspx]. Se recomandă folosirea CriticalSection
[http://msdn.microsoft.com/en­us/library/ms682530%28VS.85%29.aspx] pentru excluderea mutuală a firelor
de execuție ale aceluiași proces, fiind mai eficient decât Mutex [http://msdn.microsoft.com/en­
us/library/ms684266%28VS.85%29.aspx] sau Semaphore [http://msdn.microsoft.com/en­
us/library/ms685129%28VS.85%29.aspx].

Win32 API pune la dispoziție un mecanism de acces sincronizat la variabile partajate între fire de
execuție prin intermediul funcțiilor interlocked (Interlocked Variable Access
[http://msdn.microsoft.com/en­us/library/ms684122%28VS.85%29.aspx]), precum și operații atomice de
inserare și ștergere în liste simplu înlănțuite (Interlocked Singly Linked Lists
[http://msdn.microsoft.com/en­us/library/ms684121%28VS.85%29.aspx]).

Mutex Win32
Pe scurt:

/* creează un mutex */ 
HANDLE CreateMutex( 
  LPSECURITY_ATTRIBUTES lpMutexAttributes,  
  BOOL bInitialOwner,  
  LPCTSTR lpName 

); 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 7/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]
); 
  
/* deschide un mutex (identificat prin nume) */ 
HANDLE OpenMutex( 
  DWORD dwDesiredAccess, 
  BOOL bInheritHandle,  
  LPCTSTR lpName 
); 
  
/* eliberează un mutex ocupat */ 
BOOL ReleaseMutex( 
  HANDLE hMutex 
);

Mai multe informaţii puteţi găsi în secţiunea dedicată comunicației inter­proces.

Semafor Win32
Avem următoarele funcţii:

/* creează un semafor */ 
HANDLE CreateSemaphore( 
  LPSECURITY_ATTRIBUTES semattr,  
  LONG initial_count, 
  LONG maximum_count,  
  LPCTSTR name 
); 
  
/* deschide un semafor existent */ 
HANDLE OpenSemaphore( 
  DWORD dwDesiredAccess, 
  BOOL bInheritHandle,  
  LPCTSTR name 
); 
  
/* incrementeare contor semafor cu 'lReleaseCount' */ 
BOOL ReleaseSemaphore( 
  HANDLE hSemaphore,  
  LONG lReleaseCount,  
  LPLONG lpPreviousCount 
);

Secțiune critică
Obiectele CriticalSection [http://msdn.microsoft.com/en­us/library/ms682530%28VS.85%29.aspx] sunt
echivalente mutex­urilor POSIX de tip RECURSIVE. Acestea sunt folosite pentru excluderea mutuală a
accesului firelor de execuție ale aceluiași proces la o secțiune critică de cod care conține operații
asupra unor date partajate. Un singur fir de execuție va fi activ la un moment dat în interiorul secțiunii
critice. Dacă mai multe fire așteaptă să intre, nu este garantată ordinea lor de intrare, totuși sistemul
va fi echitabil față de toate.

Operațiile care se pot efectua asupra unei secțiuni critice sunt: intrarea, intrarea neblocantă, ieșirea din
secțiunea critică, inițializarea și distrugerea.

Pentru serializarea accesului la o secțiune critică, fiecare fir de execuție va trebui să intre într­un
obiect CriticalSection la începutul secțiunii și să­l părăsească la sfârșitul ei. În acest fel, dacă
două fire de execuție încearcă să intre în CriticalSection simultan, doar unul dintre ele va reuși, și
își va continua execuția în interiorul secțiunii critice, iar celălalt se va bloca pînă când obiectul
CriticalSection va fi părăsit de primul fir. Așadar, la sfârșitul secțiunii, primul fir trebuie să
părăsească obiectul CriticalSection, permițându­i celuilalt intrarea.

Pentru excluderea mutuală se pot folosi atât obiecte Mutex [http://msdn.microsoft.com/en­
us/library/ms684266%28VS.85%29.aspx], cât și obiecte CriticalSection [http://msdn.microsoft.com/en­
us/library/ms682530%28VS.85%29.aspx]; dacă sincronizarea trebuie făcută doar între firele de execuție
ale aceluiași proces este recomandată folosirea CriticalSection, fiind un mecanism mai eficient.
Operația de intrare în CriticalSection se traduce într­o singură instrucțiune de asamblare de tip
test­and­set­lock (TSL). CriticalSection este echivalentul futex­ului din Linux.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 8/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Inițializarea/distrugerea unei secțiuni critice

Alocarea memoriei pentru o secțiune critică se face prin declararea unui obiect CRITICAL_SECTION.
Acesta nu va putea fi folosit, totuși, înainte de a fi inițializat (InitializeCriticalSection
[http://msdn.microsoft.com/en­us/library/aa915072.aspx], InitializeCriticalSectionAndSpinCount
[http://msdn.microsoft.com/en­us/library/ms683476(v=vs.85).aspx], SetCriticalSectionSpinCount
[http://msdn.microsoft.com/en­us/library/windows/desktop/ms686197(v=vs.85).aspx], DeleteCriticalSection
[http://msdn.microsoft.com/en­us/library/aa909214.aspx]) .

void InitializeCriticalSection(
  LPCRITICAL_SECTION pcrit_sect 
); 
  
BOOL InitializeCriticalSectionAndSpinCount( 
  LPCRITICAL_SECTION pcrit_sect,  
  DWORD dwSpinCount 
); 
  
DWORD SetCriticalSectionSpinCount( 
  LPCRITICAL_SECTION pcrit_sect,  
  DWORD dwSpinCount 
); 
  
void DeleteCriticalSection( 
  LPCRITICAL_SECTION pcrit_sect 
);

Un obiect CRITICAL_SECTION nu poate fi copiat sau modificat după inițializare. De asemenea, un
obiect CRITICAL_SECTION nu trebuie inițializat de două ori, în caz contrar, comportamentul său fiind
nedefinit.

Contorul de spin (Spin Count) are sens doar pe sistemele multiprocesor (SMP) (este ignorat pe
sisteme uniprocesor). Contorul de spin reprezintă numărul de cicli pe care îl petrece un fir de execuție
pe un procesor în busy­waiting, înainte de a­și suspenda execuția la un semafor asociat secțiunii
critice, în așteptarea eliberării acesteia. Scopul așteptării unui număr de cicli în busy­waiting este
evitarea blocării la semafor în cazul în care secțiunea critică se eliberează în intervalul respectiv,
deoarece blocarea la semafor are impact asupra performanțelor. Folosirea contorului de spin este
recomandată mai ales în cazul unei secțiuni critice scurte, accesate foarte des.

Utilizarea secțiunilor critice

Secțiunile critice Windows au comportamentul mutex­urilor POSIX de tip RECURSIVE. Un fir de
execuție care se află deja în secțiunea critică nu se va bloca dacă apelează din nou EnterCriticalSection
[http://msdn.microsoft.com/en­us/library/ms682608%28VS.85%29.aspx], însă va trebui să părăsească
secțiunea critică de un număr de ori egal cu cel al ocupărilor, pentru a o elibera. Pentru a încerca
intrarea într­o secțiune critică fără a se bloca, un fir de execuție trebuie să apeleze
TryEnterCriticalSection [http://msdn.microsoft.com/en­us/library/aa450959.aspx].

void EnterCriticalSection( 
  LPCRITICAL_SECTION lpCriticalSection 
); 
  
void LeaveCriticalSection( 
  LPCRITICAL_SECTION lpCriticalSection 
); 
  
/* pentru TryEnterCriticalSection _WIN32_WINNT >= 0x0400 înainte de include <windows.h> */ 
  
#define _WIN32_WINNT 0x0400 
#include <windows.h> 
BOOL TryEnterCriticalSection( 
  LPCRITICAL_SECTION lpCriticalSection 
);

În cadrul unui fir de execuție, numărul apelurilor LeaveCriticalSection [http://msdn.microsoft.com/en­
us/library/ms684169%28VS.85%29.aspx] trebuie să fie egal cu numărul apelurilor EnterCriticalSection
[http://msdn.microsoft.com/en­us/library/ms682608%28VS.85%29.aspx], pentru a elibera în final secțiunea
critică. Dacă un fir de execuție care nu a intrat în secțiunea critică apelează LeaveCriticalSection

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 9/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

[http://msdn.microsoft.com/en­us/library/ms684169%28VS.85%29.aspx], se va produce o eroare care va
face ca firele care au apelat EnterCriticalSection [http://msdn.microsoft.com/en­
us/library/ms682608%28VS.85%29.aspx] să aștepte pentru o perioadă nedefinită de timp.

Exemplu secțiuni critice

/* global critical section */ 
CRITICAL_SECTION CriticalSection;  
  
DWORD ThreadProc(LPVOID *param) 

    /* only one thread enters the critical section, the rest are blocked */ 
    EnterCriticalSection(&CriticalSection);  
  
    /* use of protected data */
  
    /* leaves the critical section, allowing another thread to enter */ 
    LeaveCriticalSection(&CriticalSection); 

  
int main() 

    /* initialize only one time */ 
    InitializeCriticalSection(&CriticalSection); 
  
    /* the threads execution  ... */ 
  
    DeleteCriticalSection(&CriticalSection); 
  
    return 0; 
}

Evenimente
Evenimentele reprezintă un mecanism prin care un fir de execuție poate semnaliza unul sau mai multe
fire că o anumită condiție este îndeplintă. Ce e important este faptul că pot fi deblocate mai multe fire
de execuție prin semnalarea unui singur eveniment. Evenimentele sunt de două tipuri, în funcție de
modul în care sunt resetate:

resetare manuală ­ după alertarea mai multor fire de execuție, evenimentul trebuie resetat
resetare automată ­ dupa alertarea unui singur fir de execuție, evenimentul se resetează
automat

Un eveniment este creat folosind funcția CreateEvent [http://msdn.microsoft.com/en­
us/library/ms682396%28VS.85%29.aspx]:

HANDLE WINAPI CreateEvent(  hEvent = CreateEvent( 
            LPSECURITY_ATTRIBUTES lpEventAttributes,            NULL, 
            BOOL bManualReset,            TRUE,  /* Manual Reset */ 
            BOOL bInitialState,           FALSE, /* Non‐signaled state */ 
            LPCTSTR lpName            NULL   /* Private variable */ 
); );

Pentru a controla un eveniment se folosesc funcțiile:

SetEvent [http://msdn.microsoft.com/en­us/library/ms686211%28VS.85%29.aspx#] ­ pentru
semnalizarea evenimentului. Dacă evenimentul este de tip auto­reset, atunci un singur fir de
execuție va fi trezit, iar evenimentul se resetează automat. Dacă evenimentul este de tip
manual­reset, atunci evenimentul rămâne semnalizat până când un fir de execuție apelează
ResetEvent [http://msdn.microsoft.com/en­us/library/ms685081%28VS.85%29.aspx]. Altfel, orice fir de
execuție care încearcă să aștepte pe eveniment va fi automat deblocat.
ResetEvent [http://msdn.microsoft.com/en­us/library/ms685081%28VS.85%29.aspx] ­ asigură trecerea
evenimentului în starea non­signaled. Se utilizează împreună cu un eveniment de tip manual­
reset.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 10/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

PulseEvent [http://msdn.microsoft.com/en­us/library/ms684914%28VS.85%29.aspx] ­ deblochează
toate firele de execuție care așteaptă la un eveniment de tip manual­reset, iar evenimentul este
apoi resetat. Dacă funcția este folosită în conjucție cu un eveniment auto­reset, atunci va
debloca un singur fir de execuție.

Obiectele eveniment de pe Windows sunt diferite de variabilele de conditie de pe Linux.
Daca se face signal pe un eveniment, si nu exista un thread care asteapta la acel eveniment, acest
semnal nu va fi retinut.
In momentul in care vine un thread si asteapta la un eveniment DUPA ce s­a dat un semnal, acesta
ramane blocat pana cand alt thread mai trimite inca un semnal.

Operații atomice cu variabile partajate (Interlocked Variable Access)
Funcțiile interlocked pun la dispoziție un mecanism de sincronizare a accesului la variabile partajate
între mai multe fire de execuție. Funcțiile pot fi apelate de fire de execuție ale unor procese diferite,
pentru variabile aflate într­un spațiu de memorie partajată. Funcțiile interlocked reprezintă cel mai
simplu mod de evitare a race­ului care apare când două fire de execuție modifică aceeași variabilă.

Operațiile atomice asupra variabilelor partajate:

incrementare / decrementare (ambele funcții întorc noua valoare)

LONG InterlockedIncrement( 
  LONG volatile *lpAddend 
); 
  
LONG InterlockedDecrement( 
  LONG volatile *lpDecend 
);

atribuirea atomică a unei valori unei variabile partajate (primele două funcții întorc vechea
valoare)

LONG InterlockedExchange( 
  LONG volatile *Target,  
  LONG Value 
); 
  
LONG InterlockedExchangeAdd( 
  LPLONG volatile Addend,  
  LONG Value 
); 
  
PVOID InterlockedExchangePointer( 
  PVOID volatile *Target,  
  PVOID Value 
);

atribuirea atomică după testarea valorii variabilei partajate

LONG InterlockedCompareExchange( 
  LONG volatile * dest,  
  LONG exchange,  
  LONG comp 
); 
  
PVOID InterlockedCompareExchangePointer( 
  PVOID volatile * dest,  
  PVOID exchange,  
  PVOID comp 
);

InterlockedCompareExchange [http://msdn.microsoft.com/en­us/library/ms683560%28VS.85%29.aspx] va
compara dest cu comp; dacă sunt egale, îi va atribui lui dest valoarea exchange. Testul și atribuirea
vor fi executate într­o singură operație atomică. Pentru variabile de tip pointer se va folosi
InterlockedCompareExchangePointer [http://msdn.microsoft.com/en­
us/library/ms683568%28v=VS.85%29.aspx]. Comportamentul este echivalent cu:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 11/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

atomicly_do {                       // execută atomic tot blocul următor 
    tmp = *dest;                    // copiază valoarea din *dest 
    if (tmp == comp) {              // dacă e egală cu valoarea lui 'comp' 
        *dest = exchange;           // atunci scrie valoarea 'exchange' în *dest 
    } 
}

Windows Thread Pooling
Programele cu un număr mare de fire de execuție pot aduce probleme de performanță dincolo de cele
de locking:

Fiecare fir de execuție are o stivă proprie (default 1MB). Astfel, 1000 de fire vor consuma 1GB
de spațiu virtual.
Context­switch­urile între fire de execuție pot cauza page­fault­uri la accesarea stivei.
Crearea și terminarea firelor de execuție presupun calcule suplimentare.

Pentru a facilita dezvoltarea de aplicații eficiente bazate pe fire de execuție, sistemul de operare
Windows pune la dispoziție mecanismul thread pooling. Utilizarea acestuia este benefică în cazul unei
aplicații bazată pe fire de execuție care au de îndeplinit taskuri relativ scurte. Prin utilizarea thread
pooling, fiecare task de efectuat va fi atribuit unui fir de execuție din pool (un task este o procedură
executată de un fir de execuție din thread pool).

Există două modalități prin care o aplicație poate specifica task­urile pe care le dorește executate de
fire de execuție din thread pool:

se pot adăuga taskuri ce vor fi executate imediat ce se eliberează un fir de execuție din thread
pool
se pot adăuga operații de așteptare care au asociată o funcție callback ce urmează a fi
executată la sfârșitul unui timeout de unul dintre firele de execuție din thread pool. Din această
categorie fac parte operațiile de așteptare a terminării unei intrări/ieșiri asincrone, operațiile de
așteptare a expirării unui Timer‐Queue Timer și funcțiile de așteptare înregistrate.

Dacă vreuna dintre funcțiile executate într­un thread­pool apelează TerminateThread,
comportamentul nu este definit.

Un exemplu practic pentru Windows ThreadPools, ce folosește noul API, se găsește aici
[http://msdn.microsoft.com/en­us/library/ms686980%28v=VS.85%29.aspx].

Adăugarea de taskuri la thread pool

Așteptarea unei operații de intrare/ieșire asincrone

Pentru a adăuga la thread pool un task care se va executa la finalul unei operații de intrare/ieșire
asincrone pe un anumit file handle, se va apela funcția:

// înregistrează o funcție ce va fi chemată când se încheie o  
// operație de IO asincron pe fișierul identificat prin FileHandle.
// Pot fi înregistrate mai multe funcții și vor fi chemate toate 
// când se încheie operația IO asincronă. Ordinea în care sunt apelate 
// nu este specificată. 
BOOL BindIoCompletionCallback( 
  HANDLE FileHandle, 
  LPOVERLAPPED_COMPLETION_ROUTINE Function, 
  ULONG Flags 
); 
  
// semnătura funcției înregistrate să fie executată la încheierea operației AIO 
VOID CALLBACK FileIOCompletionRoutine( 
  DWORD dwErrorCode, 
  DWORD dwNumberOfBytesTransfered, 
  LPOVERLAPPED lpOverlapped 
);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 12/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Adăugarea unui task pentru execuție imediată

Pentru a adăuga la thread pool un task care să fie executat imediat se va apela funcția:

BOOL QueueUserWorkItem( 
  LPTHREAD_START_ROUTINE Function,  // funcția de executat 
  PVOID Context, // pointer ce va fi pasat funcției ca argument 
  ULONG Flags);  // tipul rutinei (IO, NON‐IO, funcția așteaptă mult, etc.) 
  
// Semnătura funcției e identică cu semnătura funcțiilor executate cu CreateThread 
DWORD WINAPI ThreadProc( 
  LPVOID param 
);

Timer Queues
Obiectele TimerQueue reprezintă cozi de timere. Ele conțin obiecte Timer­Queue Timer care au
asociată o funcție callback, ce va fi executată de un fir de execuție din thread pool la expirarea
timerului.

Crearea/distrugerea unei cozi de timere

#define _WIN32_WINNT 0x0500 
#include <windows.h> 
  
HANDLE CreateTimerQueue(void); 
  
// marchează coada pentru ștergere, dar *NU* așteaptă 
// ca toate callbackurile asociate cozii să se termine 
BOOL DeleteTimerQueue( 
  HANDLE TimerQueue 
); 
  
/** 
  * CompletionEvent = NULL ‐ marchează coada pentru ștergere și iese imediat (ca DeleteTimerQueue) 
  * CompletionEvent = INVALID_HANDLE_VALUE ‐ funcția așteaptă să se încheie toate callbackurile. 
  * CompletionEvent = un handle de tip Event ‐ un obiect Event care va fi  
  *                   trecut în starea SIGNALED când se încheie toate callbackurile. 
  */ 
BOOL DeleteTimerQueueEx( 
  HANDLE TimerQueue,  
  HANDLE CompletionEvent 
);

Crearea unui timer

Pentru crearea unui timer se va apela funcția:

BOOL CreateTimerQueueTimer( 
  PHANDLE phNewTimer, // aici întoarce un HANDLE la timerul nou creat 
  HANDLE TimerQueue,  // coada la care este adăugat timerul.  
                            // Dacă e NULL se folosește o coadă implicită. 
  WAITORTIMERCALLBACK Callback, // callback de executat 
  PVOID Parameter,    // parametru trimis callbackului 
  DWORD DueTime,      // timerul va expira prima dată după 'DueTime' milisec. 
  DWORD Period,       // apoi timerul va expira periodic după 'Period' milisec. 
  ULONG Flags         // tipul callbackului: IO/NonIO, EXECUTEONLYONCE, ș.a. 
); 
  
// semnătura unui callback 
VOID WaitOrTimerCallback( 
  PVOID lpParameter,  
  BOOLEAN TimerOrWaitFired 
); 
  
// modificarea timpului de expirare al unui timer 
BOOL ChangeTimerQueueTimer( 
  HANDLE TimerQueue, // coada la care este adăugat timerul. 
                           // Dacă e NULL se folosește o coadă implicită. 
  HANDLE Timer,  // HANDLE la timerul de modificat 
  ULONG DueTime, // timerul va expira prima dată după 'DueTime' milisec. 
  ULONG Period   // apoi timerul va expira periodic după 'Period' milisec. 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 13/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]
  ULONG Period   // apoi timerul va expira periodic după 'Period' milisec. 
); 
  
// dezactivarea unui timer 
BOOL CancelTimerQueueTimer( 
  HANDLE TimerQueue,  
  HANDLE Timer 
); 
  
// dezactivarea ȘI distrugerea unui timer. 
// CompletionEvent e similar cu cel din DeleteTimerQueueEx. 
BOOL DeleteTimerQueueTimer( 
  HANDLE TimerQueue,  
  HANDLE Timer,  
  HANDLE CompletionEvent 
);

Registered Wait Functions
Funcțiile de așteptare înregistrate sunt funcții de așteptare executate de un fir de execuție din thread
pool. În momentul în care obiectul de sincronizare după care se așteaptă trece în starea signaled, se va
executa rutina callback asociată funcției de așteptare înregistrate, de un fir de execuție din thread pool.
În mod implicit, funcțiile de așteptare înregistrate se rearmează automat și rutinele callback sunt
executate de fiecare dată când obiectul de sincronizare după care se așteaptă trece în starea signaled,
sau intervalul de timeout expiră. Acest lucru se repetă până când înregistrarea funcției de așteptare
este anulată. Se poate seta, însă, ca funcția de așteptare înregistrată să se execute o singură dată.

Înregistrarea unei funcții de așteptare

Pentru înregistrarea în thread pool a unei funcții de așteptare se va apela funcția:

BOOL RegisterWaitForSingleObject( 
  PHANDLE phNewWaitObject, 
  HANDLE hObject, 
  WAITORTIMERCALLBACK Callback, 
  PVOID Context, 
  ULONG dwMilliseconds, 
  ULONG dwFlags 
);

De fiecare dată când hObject trece în starea signaled, și la fiecare dwMilliseconds, rutina
Callback va fi executată cu parametrul Context, de un fir de execuție din thread pool. Rutina
Callback trebuie să nu apeleze TerminateThread și să aibă următoarea signatură:

VOID CALLBACK WaitOrTimerCallback( 
  PVOID lpParameter, 
  BOOLEAN TimerOrWaitFired 
);

Parametrul TimerOrWaitFired va specifica dacă execuția rutinei Callback s­a declanșat în urma
trecerii în starea signaled a obiectului de sincronizare, sau în urma expirării intervalului de timeout
specificat.

Prin intermediul parametrului dwFlags se pot transmite caracteristici ale firului de execuție care va
executa rutina Callback, precum și dacă funcția de așteptare trebuie să se execute doar o singură
dată. Funcția va întoarce, prin parametrul phNewWaitObject, un handle ce va fi folosit pentru
deînregistrarea funcției de așteptare.

Deînregistrarea unei funcții de așteptare
Pentru a anula înregistrarea unei funcții de așteptare se va apela una dintre funcțiile:

BOOL UnregisterWait  (HANDLE WaitHandle); 
BOOL UnregisterWaitEx(HANDLE WaitHandle, HANDLE CompletionEvent);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 14/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Orice funcție de așteptare înregistrată va trebui deînregistrată prin apelul uneia dintre funcțiile de mai
sus.

Funcția UnregisterWaitEx va semnaliza event­ul CompletionEvent în cazul în care se termină cu
succes și rutina de callback s­a terminat cu succes. Dacă valoarea lui  CompletionEvent nu este
NULL, atunci funcția va aștepta finalizarea operației de așteptare și terminarea rutinei asociate.

Exerciții de laborator

Exercițiul 0 ­ Joc interactiv (2p)

Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Windows (9p)
În rezolvarea laboratorului folosiți arhiva de sarcini lab09­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab09­tasks.zip]

Pentru a deschide proiectul Visual Studio conținând exercițiile, deschideți fișierul lab09.sln.

Exercițiul 1 ­ Threading și priorități (1p)

Încărcați proiectul 1‐threading și setați­l ca StartUp Project. Compilați și rulați programul. Aflați câte
fire de execuție creează în total.

Lansați ProcessExplorer (check Desktop) și verificați răspunsul de la întrebarea de mai sus. (View
→ Select Columns → Process Performance → Threads)

Aflați prioritatea procesului threading.exe. (View → Select Columns → Process Performance → Base
Priority)

Experimentați schimbând prioritatea procesului (click­dreapta pe numele procesului → Set Priority).
Setați prioritatea astfel încât procesul threading.exe să primească mai mult timp pe procesor.

Dacă setați ca prioritate real‐time și comentați linia cu Sleep din bucla while, cel mai probabil vi
se va bloca mașina virtuală. Acest lucru s­ar întâmpla pentru că ar exista tot timpul un thread cu
prioritate mai mare ca cele pentru interfața grafică, de exemplu, gata să ruleze pe procesor. Vezi și
link [https://en.wikipedia.org/wiki/Starvation_(computer_science)].

Exercițiul 2 ­ Thread debugging (1p)

Deschideți sursa 2‐debug.c din proiectul 2‐debug și completați funcția StartThread pentru a
implementa crearea unui fir de execuție (urmăriți în cod secțiunea marcată cu TODO).

Compilați și rulați sursa. Aplicația trebuie pornită din consolă: Tools → PowerShell Command
Prompt. Observați că programul se blochează. Identificați și rezolvați problema.

Soluția nu implică comentarea funcției Sleep. Inspectați funcțiile MakeCake, MakeTiramisu și
MakeMarshmallows și observați ordinea în care se face WaitForSingleObject pe ingrediente
(semafoare). Amintiți­vă din laboratorul precedent care era problema de la Exercițiul 5
[http://ocw.cs.pub.ro/courses/so/laboratoare/laborator­08#exercitiul_5_­_blocked_15p].

Exercițiul 3 ­ Interlocked (2p)
În cadrul acestui exercițiu dorim să testăm diverse tipuri de incrementări atomice ale unei variabile,
comparându­le timpul de execuție. Deschideți sursa interlocked.c din proiectul 3‐interlocked.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 15/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Programul crează NO_THREADS fire de execuție, care incrementează circular o variabilă (când se
ajunge la o limită se resetează la 0).

Asigurați accesul exclusiv la variabila incrementată folosind Interlocked Variables deoarece mecanismul
e mai rapid decât o incrementare normală protejată cu Mutex sau CRITICAL_SECTION (folosiți funcția
InterlockedCompareExchange). Incrementarea circulară se va face în funcția thread_function
(urmăriți comentariile cu TODO 1 ). Veți avea nevoie de două operații interlocked
(InterlockedIncrement și InterlockedCompareExchange).

Identificați o problemă cu folosirea Interlocked Operations pentru a incrementa circular o
variabilă.

Adăugați un apel SwitchToThread() [https://msdn.microsoft.com/en­
us/library/windows/desktop/ms686352(v=vs.85).aspx] (echivalent al yield() din Linux) între cele două
operații interlocked. Compilați și rulați din nou programul. Observați că rezultatul nu mai este cel
așteptat. Acest lucru s­a întâmplat pentru că am forțat schedulerul să schimbe firul de execuție imediat
după incrementare. Chiar dacă fiecare operație în parte este atomică, succesiunea a două operații
atomice NU este atomică.

Comparați timpul de execuție al programului precedent în cazul în care se folosește un mutex care să
sincronizeze accesul la variabila count, completând funcția thread_function_mutex ( TODO 2 ). Nu
uitați să modificați și parametrul funcției CreateThread din funcția main.

Exercițiul 4 ­ TLS (1p)

Dorim să simulăm o implementare a funcției perror. Pentru aceasta vom avea variabila globală
myErrno, dar cu valori specifice (diferite) pentru fiecare fir de execuție. Deschideți sursa tls.c din
proiectul 4‐tls și urmăriți comentariile marcate cu TODO (revedeți secțiunea despre TLS).

Exercițiul 5 ­ TimerQueue (2p)

Deschideți sursa timer.c din proiectul 5‐timer. Creați un Timer‐Queue Timer, a cărui rutină
callback să fie declanșată de exact 3 ori, o dată la fiecare secundă. După 3 declanșări se va dezactiva
timerul și se vor distruge toate resursele create. Trebuie să sincronizați rutina timer­ului cu funcția
main care va dezactiva timer­ul; pentru aceasta puteți folosi orice mecanism de semnalizare: semafor,
event (revedeți secțiunea despre Timer Queues).

Exercițiul 6 ­ Barrier (2p)

Deschideți sursa barrier.c din proiectul 6‐Barrier. Implementați o barieră reutilizabilă folosind un
mutex și o variabilă de tip eveniment. Completați funcțiile de lucru cu bariera pentru a obține
funcționalitatea dorită (comentariile marcate cu TODO).

Pentru a putea semnaliza un obiect și a aștepta la un alt obiect de sincronizare în același timp, puteți
folosi funcția SignalObjectAndWait [http://msdn.microsoft.com/en­us/library/ms686293%28VS.85%29.aspx].
De asemenea, revedeți secțiunile despre lucrul cu mutex­uri și evenimente.

Bariera va fi reprezentată prin structura:

typedef struct { 
  HANDLE hGuard;      /* mutex to protect internal variable access */ 
  HANDLE hEvent;      /* auto‐resetable event */ 
  DWORD dwCount;      /* number of threads to have reached the barrier */ 
  DWORD dwThreshold;  /* barrier limit */ 
}THRESHOLD_BARRIER, *THB_OBJECT;

Folosiți mutexul pentru a sincroniza execuția în cadrul funcției WaitThresholdBarrier. Folosiți
eventul pentru a aștepta până când toate threadurile ajung să apeleze funcția
WaitThresholdBarrier. Folosiți funcția PulseEvent [http://msdn.microsoft.com/en­

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 16/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

us/library/ms684914%28VS.85%29.aspx] pentru a semnala toate threadurile care așteaptă asupra
eventului.

BONUS

2 so karma ­ Parallel Sort

Deschideți sursa sort.c din proiectul 7‐sort. Se dorește realizarea sortării unui șir de numere
aleatoare dintr­un fișier în următorul mod:

Se împarte vectorul în bucăți către fiecare fir de execuție
Un fir de execuție sortează bucata proprie folosind quicksort
Se face merge la bucăți, în următorul fel: 

Realizați partea de creare a firelor de execuție și împărțire a taskurilor în funcția init_setup().
După ce toate firele de execuție sortează chunk­ul static, unele vor incepe sa facă merge la chunk­urile
sortate. Completați funcția ThreadFunc pentru ca, în funcție de id, un fir de execuție să apeleze
funcția MergeChunks (care realizează interclasarea a doi vectori sortați) (urmăriți comentariile cu
TODO).

Șirul de numere este dat sub forma unui fișier binar care poate fi generat cu programul
generator.exe. Citirea șirului într­un vector este deja realizată în funcția init_setup, iar fiecare fir
de execuție primește o structură CHUNK care reprezintă dimensiunea unui vector de sortat, cât și
adresa inițială a vectorului. Interclasarea a două structuri CHUNK în care vectorii sunt deja sortați se
realizează cu funcția MergeChunks.

2 so karma ­ The dorm room problem

Deschideți sursa dorm_room.c din proiectul 8‐dean. Se dorește simularea/modelarea următoarei
probleme: decanul și studenții. Se dau următoarele constrângeri:

Orice număr de studenți poate intra într­o cameră în același moment
Decanul poate intra într­o cameră doar dacă nu sunt studenți acolo (pentru a realiza o
percheziție) sau dacă sunt mai mult de 25 de studenți (pentru a sparge petrecerea)
Cât timp Decanul este în cameră, studenții pot doar ieși, nu și intra
Decanul nu poate părăsi camera până când nu au ieșit toți studenții (s­a terminat sigur
petrecerea :P)
Există un singur Decan.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 17/18
6/11/2017 Laborator 09 ­ Thread­uri Windows [CS Open CourseWare]

Rezolvați problema scriind cod pentru entitățile respective: decan și student. Pentru firele de execuție
studenți completați funcțiile “enter_room” și “party”, iar pentru firul de execuție decan completați
funcția “break_party” (revedeți secțiunea despre mutex­uri și semafoare ).

Folosiți funcțiile “dbg_student” și “dbg_decan” pentru a afișa mesaje corespunzătoare de fiecare dată
când un fir de execuție își schimbă starea (ex: decanul intră în cameră, un student nu poate intra
deoarece decanul e deja în cameră etc.)

Soluții
Soluţii laborator 9 [http://elf.cs.pub.ro/so/res/laboratoare/lab09­sol.zip]
so/laboratoare/laborator­09.txt · Last modified: 2017/05/03 13:43 by theodor.stoican

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­09 18/18
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

Laborator 10 ­ Operații IO avansate ­ Windows

Materiale ajutătoare

lab10­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab10­slides.pdf]
lab10­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab10­refcard.pdf]

Nice to read

WSP4 ­ Chapter 14, Asynchronous Input/Output and Completion Ports

Windows ­ I/O asincron (overlapped)
Operațiile de intrare/ieșire sunt mai lente decât operațiile de procesare din cauza întârzierilor cauzate
de:

timpul de access la sectoarele hard­disk­urilor
rata de transfer scăzută dintre hard­disk și memoria RAM
transferul de date peste rețea

În Laboratorul 2 au fost studiate operațiile I/O sincrone: firul de execuție apelant așteaptă până când
operația de I/O se încheie. În cadrul acestui laborator vom afla cum un fir de execuție poate începe o
operație de I/O și continuă fără a aștepta ca acea operație de I/O să se încheie, adică cum poate
efectua o operație asincronă. În final, odată ce operațiile asincrone au fost înțelese vom analiza I/O
Completion Ports, cel mai eficient model de procesare a cererilor I/O, utilizat în construcția
serverelor scalabile.

În Windows există trei modalități de realizare a operațiilor asincrone. Acestea diferă atât în modul
folosit pentru a porni operațiile de I/O, cât și în modul prin care se determină dacă operația s­a
încheiat:

multithreaded I/O: fiecare fir de execuție efectuează operații I/O normale, însă celelalte fire
își pot continua execuția
overlapped I/O cu așteptare: un fir de execuție își continuă execuția după începerea unei
operații de I/O. Un fir de execuție (posibil altul decât cel care a inițiat operația I/O) care are
nevoie de rezultatele operației de I/O, va așteapta fie pe un file handle, fie pe un eveniment
specificat în structura overlapped folosită de către ReadFile [http://msdn.microsoft.com/en‐
us/library/aa365467%28VS.85%29.aspx] și WriteFile [http://msdn.microsoft.com/en‐
us/library/aa365747%28VS.85%29.aspx]
overlapped I/O cu rutine de terminare: sistemul de operare apelează o anumită
rutină de terminare (completion routine) atunci când respectiva operație de I/O s­a încheiat.
Acest tip de operație asincronă mai poartă și numele de extended I/O, nume derivat din cel al
funcțiilor folosite ReadFileEx [http://msdn.microsoft.com/en‐
us/library/aa365468%28VS.85%29.aspx] și WriteFileEx [http://msdn.microsoft.com/en‐
us/library/aa365748%28VS.85%29.aspx].

În continuare vom trata doar cazul operațiilor de tipul overlapped I/O cu așteptare.

Overlapped I/O cu așteptare

FILE_FLAG_OVERLAPPED

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 1/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

Prima cerință pentru operațiile I/O asincrone indiferent dacă sunt suprapuse (overlapped) sau extinse
este setarea atributului overlapped al handle­ului unui fișier. Acest lucru se realizează prin
specificarea flag­ului FILE_FLAG_OVERLAPPED pentru parametrul dwAttrsAndFlags la apelul funcției
CreateFile [http://msdn.microsoft.com/en‐us/library/aa363858%28VS.85%29.aspx] (sau orice
alt apel care creează fișiere, pipe­uri cu nume etc.):

HANDLE hFile = CreateFile("io.txt",  
                          GENERIC_READ,  
                          0,  
                          NULL,  
                          OPEN_EXISTING,  
                          FILE_FLAG_OVERLAPPED, /* this must be specified */ 
                          NULL);

În Windows, sockeţii au acest flag activat în mod implicit.

Operațiile I/O pe handle­uri care au flag­ul FILE_FLAG_OVERLAPPED setat (handle­uri asincrone) au un
comportament special:

Operațiile I/O nu blochează firul de execuție apelant. Apelurile ReadFile
[http://msdn.microsoft.com/en‐us/library/aa365467%28VS.85%29.aspx], WriteFile
[http://msdn.microsoft.com/en‐us/library/aa365747%28VS.85%29.aspx] se întorc imediat,
indiferent de durata completării cererii I/O.
O valoare FALSE întoarsă nu indică în mod obligatoriu eșecul apelului. Valoarea FALSE întoarsă
de funcțiile ReadFile [http://msdn.microsoft.com/en‐
us/library/aa365467%28VS.85%29.aspx], WriteFile [http://msdn.microsoft.com/en‐
us/library/aa365747%28VS.85%29.aspx] indică eșecul în cazul operațiilor de I/O sincrone. În
cazul operațiilor I/O asincrone, funcția GetLastError [http://msdn.microsoft.com/en‐
us/library/ms679360%28VS.85%29.aspx] va întoarce ERROR_IO_PENDING ceea ce indică faptul
că operația se desfășoară asincron.
Numărul de octeți transferați este de asemenea nefolositor dacă operația nu s­a încheiat.
Se pot face mai multe operații asincrone de citire/scriere pe același fișier, deci nici file
pointer­ul nu mai poate fi utilizat.

Structura Overlapped
Al doilea pas este transmiterea unei structuri de tip OVERLAPPED ca parametru ori de câte ori se face un
apel ReadFile [http://msdn.microsoft.com/en‐us/library/aa365467%28VS.85%29.aspx]/
WriteFile [http://msdn.microsoft.com/en‐us/library/aa365747%28VS.85%29.aspx]. Structura
OVERLAPPED [http://msdn.microsoft.com/en‐us/library/ms684342%28VS.85%29.aspx] conține
informația folosită în operațiile I/O și arată astfel:

typedef struct _OVERLAPPED { 
    ULONG_PTR Internal;      /* the error code for the I/O request*/ 
    ULONG_PTR InternalHigh;  /* the number of bytes transferred */ 
    union {                  /* the file position at which to start the I/O request */ 
           struct { 
               DWORD Offset; 
               DWORD OffsetHigh; 
           } ; 
           PVOID  Pointer;   /* reserved */ 
    } ; 
    HANDLE    hEvent;        /* handle to the event ‐ is set to signaled when operation has completed */ 
} OVERLAPPED, *LPOVERLAPPED;

Structura OVERLAPPED [http://msdn.microsoft.com/en‐us/library/ms684342%28VS.85%29.aspx]
este utilă pentru că:

Un program poate porni mai multe operații asincrone de citire sau scriere pe un singur handle de
fișier asincron. File pointer­ul asociat cu file handle­ul nu mai are nicio însemnătate.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 2/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

Un program trebuie să fie capabil să aștepte terminarea operațiilor I/O asincrone. În cazul în
care mai multe operații I/O asincrone sunt pornite, programul trebuie să poată determina care
dintre operații s­au terminat. Operațiile asincrone nu se termină în mod obligatoriu în ordinea în
care au fost pornite.

O operație de I/O asincronă este identificată de către file handle și de structura OVERLAPPED
[http://msdn.microsoft.com/en‐us/library/ms684342%28VS.85%29.aspx].

Nu trebuie să trecem cu vederea următoarele lucruri:

O structură OVERLAPPED [http://msdn.microsoft.com/en‐
us/library/ms684342%28VS.85%29.aspx] nu trebuie refolosită până când operația de I/O
asociată nu s­a încheiat.
Dacă există mai multe operații I/O este indicată folosirea evenimentelor pentru sincronizare.

Evenimentul hEvent trebuie creat de utilizator și trebuie să fie de tip manual‐reset (vezi laboratorul
9). Când o operație I/O asincronă se termină, evenimentul rămâne în starea signaled până când este
utilizat în altă operație I/O asincronă. Acest lucru este util pentru că putem avea mai multe fire de
execuție care să aștepte după aceeași operație asincronă.

Așteptarea și interogarea operațiilor asincrone

Pentru determinarea stării operației asincrone se poate folosi funcția GetOverlappedResult
[http://msdn.microsoft.com/en‐us/library/ms683209%28VS.85%29.aspx]. În cazul unei operații
OVERLAPPED [http://msdn.microsoft.com/en‐us/library/ms684342%28VS.85%29.aspx] apelurile
ReadFile [http://msdn.microsoft.com/en‐us/library/aa365467%28VS.85%29.aspx]/WriteFile
[http://msdn.microsoft.com/en‐us/library/aa365747%28VS.85%29.aspx] se vor întoarce imediat. În
cele mai multe cazuri, operația de I/O nu se va termina imediat astfel că apelurile ReadFile
[http://msdn.microsoft.com/en‐us/library/aa365467%28VS.85%29.aspx]/WriteFile
[http://msdn.microsoft.com/en‐us/library/aa365747%28VS.85%29.aspx] vor întoarce FALSE, iar
funcția GetLastError [http://msdn.microsoft.com/en‐us/library/ms679360%28VS.85%29.aspx] va
întoarce ERROR_IO_PENDING. Dacă totuși rezultatul întors este TRUE, înseamnă că operația s­a efectuat
și puteți cere imediat rezultatul.

Așteptarea după o operație I/O asincronă se poate face după oricare dintre următoarele:

Handle­ul evenimentului specificat în structura OVERLAPPED [http://msdn.microsoft.com/en‐
us/library/ms684342%28VS.85%29.aspx] ­ în caz că se dorește ca unul sau mai multe fire de
execuție să aștepte după aceeași operație asincronă.
Handle­ul fișierului ­ caz în care doar un singur fir de execuție va aștepta după operația asincronă
(parametrul hEvent al structurii OVERLAPPED [http://msdn.microsoft.com/en‐
us/library/ms684342%28VS.85%29.aspx] este lăsat NULL)

După așteptarea pe un obiect de sincronizare (un event sau un handle de fișier) ca operația de I/O să se
termine, trebuie să determinăm câți octeți au fost transferați. Acesta este scopul de bază al funcției
GetOverlappedResult [http://msdn.microsoft.com/en‐us/library/ms683209%28VS.85%29.aspx].

BOOL WINAPI GetOverlappedResult(  GetOverlappedResult( 
       HANDLE hFile,         myHandle, /* handle of file or event */ 
       LPOVERLAPPED lpOverlapped,         &ov,      /* overlapped structure */ 
       LPDWORD lpNumberOfBytesTransferred,         &nRead,   /* actual bytes transferred */ 
       BOOL bWait         TRUE);    /* wait for the operation to finish */
);

HANDLE hFile; 
OVERLAPPED ov; 
DWORD bytesTransferred; 
  
/* TODO ... start overlapped I/O operation */ 

  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 3/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]
  
/* wait for completion */ 
GetOverlappedResult(hFile, &ov, &bytesTransferred, TRUE);

Obiectul hFile și structura ov sunt folosite pentru a identifica unic operația de I/O a cărei stare dorim
să o aflăm.

Dacă parametrul bWait este TRUE, funcția GetOverlappedResult va aștepta până când operația de
I/O specificată se termină, în caz contrar se va întoarce imediat. În ambele cazuri funcția va întoarce
TRUE doar dacă operația de I/O s­a terminat cu succes.
Mai jos regăsiți un exemplu de așteptare a terminării unei operații I/O Overlapped folosind ca obiect de
sincronizare un eveniment. Exemplul prezintă folosirea unei operații de citire asincronă:

#include "utils.h" 
#include <windows.h> 
#include <stdlib.h> 
  
#define BUF_SIZE  (1024 * 1024) /* 1MB */ 
  
int main(int argc, char **argv) 

  OVERLAPPED ov; 
  HANDLE hFile; 
  HANDLE hEvent; 
  DWORD dwRet, dwErr, dwBytesRead; 
  char *buffer = malloc(BUF_SIZE * sizeof(char)); 
  
  /* Make sure overlapped structure is clean */ 
  ZeroMemory(&ov, sizeof(ov)); 
  memset(buffer, 0, BUF_SIZE); 
  
  /* Create manual‐reset event */ 
  hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 
  DIE(hEvent == INVALID_HANDLE_VALUE, "CreateEvent"); 
  
  ov.hEvent = hEvent; 
  
  hFile = CreateFile(argv[1], 
         GENERIC_READ,  /* access mode */ 
         FILE_SHARE_READ,  /* sharing option */ 
         NULL,    /* security attributes */ 
         OPEN_EXISTING,  /* open only if it exists */ 
         FILE_FLAG_OVERLAPPED,/* file attributes */ 
         NULL);    /* no template */ 
  DIE(hFile == INVALID_HANDLE_VALUE, "CreateFile"); 
  
  dwRet = ReadFile(hFile, buffer, BUF_SIZE, &dwBytesRead, &ov); 
  if (dwRet == FALSE) { 
    dwErr = GetLastError(); 
  
    switch (dwErr) { 
    case ERROR_HANDLE_EOF: 
      printf("End of File Reached\n"); 
      break; 
  
    case ERROR_IO_PENDING: 
      /* async io not ready */ 
      printf("Async IO not finished immediately\n"); 
  
      /* do some other work  in the meantime */ 
      Sleep(1); 
  
      /* Wait for it to finish */ 
      dwRet = GetOverlappedResult(ov.hEvent, &ov, 
        &dwBytesRead, TRUE); 
      printf("nRead = %d\n", dwBytesRead); 
      break; 
  
    default: 
      /* ReadFile failed */ 
      PrintLastError("ReadFile"); 
    } 
  } else { 
    printf("Async IO finished immediately\n"); 
    printf("nRead = %d\n", dwBytesRead); 
  } 

  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 4/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]
  
  dwRet = CloseHandle(hFile); 
  DIE(dwRet == FALSE, "CloseHandle"); 
  
  dwRet = CloseHandle(hEvent); 
  DIE(dwRet == FALSE, "CloseHandle"); 
  
  return 0; 
}

Când testați acest exemplu, este posibil ca apelul asincron să se întoarcă din prima dacă fișierul din care
se citește este deja mapat în RAM (ex: ați copiat înainte fișierul sau l­ați deshis pentru citire).

Windows ­ I/O Completion Ports
Mecanismul de completion ports este cel mai scalabil dintre toate cele prezentate până acum. Un
server care folosește completion ports poate face față la foarte multe (zeci de mii) conexiuni simultan,
fără probleme prea mari. Celelalte metode își ating limitările cu mult înainte.

Un completion port este un obiect în kernel cu care se asociază alți descriptori (fișiere, sockeți) și prin
intermediul căruia se transmit notificările de completare ale unor operații asincrone lansate anterior. Un
completion port are asociat un pool de worker threads. Aceste fire de execuție așteaptă să primească
notificări de completare a operațiilor asincrone. În momentul în care un fir de execuție primește o
notificare va deveni activ și va lucra o perioadă până se va întoarce din nou așteptând următoarea
notificare.

Crearea unui completion port

Funcția CreateIoCompletionPort [http://msdn.microsoft.com/en‐
us/library/aa363862%28VS.85%29.aspx] are dublu rol:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 5/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

creează un nou completion port
adaugă un nou handle pe care se va aștepta terminarea unei operații I/O

Pentru crearea unui completion port se folosește funcția CreateIoCompletionPort
[http://msdn.microsoft.com/en‐us/library/aa363862%28VS.85%29.aspx] ca în exemplul de mai jos:

HANDLE WINAPI CreateIoCompletionPort(  HANDLE iocp = CreateIoCompletionPort( 
         HANDLE FileHandle,           INVALID_HANDLE_VALUE, /* New Completion Port */ 
         HANDLE ExistingCompletionPort,           NULL,                
         ULONG_PTR CompletionKey,           NULL,   
         DWORD NumberOfConcurrentThreads           0                     /* No threads = No Procs */ 
); );

Pentru crearea unui nou completion port, primul parametru trebuie să fie INVALID_HANDLE_VALUE. În
acest caz, ultimul parametru indică numărul maxim de fire de execuție concurente care pot rula. În caz
că se specifică 0, atunci numărul de fire de execuție concurente este setat la numărul de procesoare.

Adăugarea unui descriptor la completion port
Pentru adăugarea unui descriptor deschis cu opțiunea de overlapped I/O la completion port se folosește
tot funcția CreateIoCompletionPort [http://msdn.microsoft.com/en­us/library/aa363862%28VS.85%29.aspx]. În
această situație primul argument va fi handle­ul fișierului/socket­ului care se dorește adăugat, iar al
doilea handle­ul completion port­ului obținut la crearea acestuia:

HANDLE iocp; 
HANDLE hFile; 
  
/* create completion port */ 
iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, (ULONG_PTR) NULL, 0); 
  
/* open file for overlapped I/O */ 
hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); 
  
/* add file handle to completion port */ 
CreateIoCompletionPort(hFile, iocp, (ULONG_PTR) hFile /* use handle as key */, 0);

După cum se observă, în cazul creării unui completion port, al doilea argument este NULL. La adăugarea
unui handle de fișier la completion port al doilea argument este handle­ul de completion port. Al treilea
argument este o cheie care va fi folosită pentru identificarea handle­ului în momentul recepționării unei
notificări.

Așteptarea încheierii unei operații asincrone
Firele de execuție worker sunt folosite pentru așteptarea încheierii operațiilor asincrone și a
prelucrărilor ulterioare. Firele de execuție vor primi notificări de la handle­ul completion port­ului
folosind funcția GetQueuedCompletionStatus [http://msdn.microsoft.com/en­
us/library/aa364986%28VS.85%29.aspx]:

BOOL WINAPI GetQueuedCompletionStatus(  bRet = GetQueuedCompletionStatus( 
         HANDLE CompletionPort,           iocp,    /* completion port handle */ 
         LPDWORD lpNumberOfBytes,           &bytes,  /* actual bytes transferred */ 
         PULONG_PTR lpCompletionKey,           &key,    /* return key to indentify the operation */ 
         LPOVERLAPPED *lpOverlapped,           &ov,     /* overlapped structure used */ 
         DWORD dwMilliseconds           INFINITE /* wait time */ 
); );

Pe baza cheii obținute se poate determina handle­ul care a generat notificarea.

Exemplu de folosire completion ports
În exemplul de mai jos este prezentată folosirea mecanismului de completion ports în cazul operațiilor
asincrone pe sockeți. Exemplul este similar cu cel prezentat în secțiunile dedicate funcțiilor de
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 6/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

multiplexare I/O pe Linux. Există un fir de execuție worker care va aștepta primirea notificărilor de la
completion port, iar firul de execuție principal va fi responsabil cu primirea de cereri de conexiune
(apeluri accept).

HANDLE iocp; 
  
/*** main thread ***/ 
  
SOCKET listenfd, sockfd;        /* listener socket; connection socket */ 
  
/* create I/O completion port */ 
iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, (ULONG_PTR) NULL, 0); 
  
/* TODO ... create server socket (listener) */ 
  
/* TODO ... create worker thread */ 
  
while (1) {        /* server loop */ 
   /* TODO ... accept connections */ 
  
   /* add socket to completion port */ 
   CreateIoCompletionPort(sockfd, iocp, (ULONG_PTR) sockfd/* use handle as key */, 0); 
  
   /* TODO ... start asynchronous operation */ 

  
  
/*** worker thread ***/ 
  
DWORD bytes; 
ULONG_PTR key; 
LPOVERLAPPED ov; 
  
while (1) { 
    /* wait for notification */
    GetQueuedCompletionStatus(iocp, &bytes, &key, &ov, INFINITE); 
  
    /* TODO ... process request */ 
}

Zero­copy I/O
Zero‐copy se referă la tehnica prin care procesorul evită operațiile de copiere a datelor dintr­o zonă de
memorie într­alta. Operațiile  zero‐copy reduc numărul de schimbări de context între spațiul utilizator
și spațiul kernel, resursele sistemului fiind utilizate eficient.

Dacă o aplicație dorește să transmită date dintr­un fișier pe un socket, va folosi în mod normal schema:

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 7/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

Se observă că există multiple copieri cu aceleași date. O schemă mai eficientă, care elimină o copiere și
totodată două context­switch­uri, este aceasta:

Mai multe detalii, inclusiv explicarea mai pe larg a contextului, puteți găsi aici
[http://www.ibm.com/developerworks/library/j­zerocopy/].

TransmitFile

Apelul TransmitFile [http://msdn.microsoft.com/en‐us/library/ms740565%28VS.85%29.aspx]
este folosit pentru a eficientiza transmiterea de fișiere în rețea. TransmitFile
[http://msdn.microsoft.com/en‐us/library/ms740565%28VS.85%29.aspx] folosește cache­ul
sistemului de operare. Este o operație zero‐copy ­ nu necesită alocarea de buffere în user­space și
diminuează numărul de apeluri de sistem.

Pentru a transmite un fișier, acesta trebuie deschis folosind flag­ul FILE_FLAG_OVERLAPPED. Apelul
TransmitFile [http://msdn.microsoft.com/en‐us/library/ms740565%28VS.85%29.aspx] primește
ca argument socket­ul pe care se realizează comunicația și handle­ul fișierului de trimis.

BOOL TransmitFile(  result = TransmitFile( 
       SOCKET hSocket,         hSocket,   /* destination socket handle */ 
       HANDLE hFile,         hFile,     /* source file handle */ 
       DWORD nNumberOfBytesToWrite,         0,         /* nr bytes to write. 0 == send entire file */ 
       DWORD nNumberOfBytesPerSend,         0,         /* block size. 0 == default block size */ 
       LPOVERLAPPED lpOverlapped,         &ov,       /* overlapped I/O structure */ 
       LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,         NULL,             
       DWORD dwFlags         0 
); );

O funcție similară este funcția TransmitPackets [http://msdn.microsoft.com/en‐
us/library/ms740566%28v=vs.85%29.aspx] care transmite date stocate în memorie pe un socket
folosind cache­ul intern al sistemului de operare. Datele sunt reprezentate de o structură
TRANSMIT_PACKETS_ELEMENT.

Exerciții de laborator

Exercițiul 0 ­ Joc interactiv (2p)

Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Windows (9p)
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 8/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

Înainte de a folosi o structură specifică Async I/O Win32 API, asigurați­vă că ați zero­izat­o.

În rezolvarea laboratorului folosiți arhiva de sarcini lab10­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab10­tasks.zip]

Exercițiul 1 ­ Test operații asincrone (1p)

Setați proiectul 1‐test_overlapp ca default ( detalii aici
[http://ocw.cs.pub.ro/courses/so/laboratoare/resurse/vs_tips#setarea_unui_subproiect_ca_default]).

Programul realizează citirea unui buffer de 64KB dintr­un fișier, folosind operații overlapped.

Compilați și testați programul:

.\1‐test_overlapp.exe C:\WINDOWS\explorer.exe

Exercițiul 2 ­ Zero­copy/TransmitFile (2p)
Un client dorește să trimită serverului un fișier folosind operații zero­copy IO.

Intrați în proiectul 2‐transmit și parcurgeți fișierele sock_util.h, sock_util.c, server.c și
transmit_client.c. Completați funcțiile marcate cu TODO din fișierul transmit_client.c. Clientul
transmite fișierul folosind TransmitFile [http://msdn.microsoft.com/en­us/library/ms740565(v=vs.85).aspx].
Folosiți NULL pentru argumentul de tipul LPOVERLAPPED.

Puteți genera fișiere de test folosind proiectul generator:

.\generator.exe size output_file

size este dimensiunea în octeți pe care doriți să o aibă fișierul; de exemplu 1024, iar output_file
este numele fișierului creat.

Într­o consolă porniți serverul și într­o altă consolă clientul. Serverul este implementat în cadrul
proiectului 2‐transmit‐server din Visual Studio. Compilați acel proiect pentru a obține executabilul
2‐transmit‐server cu care se pornește serverul. Serverul este pornit primul folosind comanda:

.\2‐transmit‐server

Clientul este pornit al doilea folosind comanda:

.\2‐transmit‐client output_file

În urma rulării serverul generează fișierul output.dat. Pentru a valida transferul corect al fișierului de
la client la server folosiți comanda:

comp output_file output.dat

Comanda vă va preciza dacă cele două fișiere sunt identice sau nu.

Exercițiul 3 ­ Operații sincrone/asincrone (3p)
Ne propunem să realizăm implementarea unor operații IO asincrone pentru popularea unor fișiere cu
conținut.

Intrați în proiectul 3‐aio și urmăriți implementarea funcției do_io_sync și implementați
do_io_async.
Alocați spațiu pentru structurile OVERLAPPED pentru toate cele 4 fișiere. Pentru inițializarea structurilor
OVERLAPPED se recomandă implementarea funcției init_overlapped. În cadrul funcției
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 9/10
6/11/2017 Laborator 10 ­ Operații IO avansate ­ Windows [CS Open CourseWare]

init_overlapped “zero­izați” structura de tipul OVERLAPPED și apoi completați câmpurile aferente
parametrilor transmiși.

Când apelați funcția init_overlapped (din cadrul funcției do_io_async), folosiți valorea 0 ca
argument pentru offset și NULL pentru event (nu vom folosi event­uri pentru a notifica încheierea
operației). Funcția init_overlapped este apelată într­un ciclu for, pentru fiecare element al array­ului
ov. Scrieți asincron conținutul bufferului g_buffer în cele 4 fișiere cu numele date de vectorul files.
Folosiți GetOverlappedResult pentru așteptarea operațiilor asincrone pe cele 4 fișiere. Folosiți
macro­ul IO_OP_TYPE pentru a determina comportamentul programului (revedeți secțiunea despre
Overlapped IO)

Rulați programul compilat folosind comanda:

.\3‐aio

Dacă ați lucrat corect, în urma rulării comenzii de mai sus se vor genera în directorul curent 4 fișiere
text (cu extensia .txt) de dimensiune BUFSIZ, conținând caractere random.

Exercițiul 4 ­ I/O completion ports (3p)
Vom folosi API­ul de I/O completion ports.

Crearea unui completion ports (1p)

Intrați în proiectul 4‐iocp și analizați conținutul fișierelor iocp.h și iocp.c. Completați cele 4 funcții
definite în fișierul iocp.c (revedeți secțiunea despre IO completion ports).

Operații I/O asincrone cu I/O completion ports (2p)

Analizați conținutul fișierului aio.c. Scopul exercițiului este folosirea I/O completion ports pentru
așteptarea încheierii operațiilor I/O asincrone (overlapped I/O).

Implementați funcțiile init_io_async și do_io_async. În funcția init_io_async folosiți funcția
init_overlapped pentru a inițializa elementul aferent al array­ului ov (folosiți valorea 0 ca argument
pentru offset și NULL pentru event).

Compilați și rulați programul.

Soluții
Soluţii laborator 10 [http://elf.cs.pub.ro/so/res/laboratoare/lab10­sol.zip]
so/laboratoare/laborator­10.txt · Last modified: 2017/05/03 21:25 by adrian.stanciu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­10 10/10
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Laborator 11 ­ Operații IO avansate ­ Linux

Materiale Ajutătoare

lab11­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab11­slides.pdf]
lab11­refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab11­refcard.pdf]

Nice to read

TLPI ­ Chapter 63, Alternative I/O models

Linux ­ multiplexarea I/O
Există situații în care un program trebuie să trateze operațiile I/O de pe mai multe canale ori de câte ori
acestea apar. Un astfel de exemplu este un program de tip server care folosește mecanisme precum pipe­
uri sau sockeţi pentru comunicarea cu alte procese. Un program trebuie să citească practic simultan
informații atât de la intrarea standard cât și de la un socket (sau mai mulți).

În aceste situații nu pot fi folosite operații obișnuite de citire sau scriere. Folosirea acestor operații are
drept consecință blocarea thread­ului curent până la încheierea operației. O posibilă soluție este folosirea
de operații non­blocante (spre exemplu folosirea flag­ul O_NONBLOCK) și interogarea succesivă a
descriptorilor de fișier. Totuși, interogarea succesivă (polling) este o formă de busy waiting și este
ineficientă.

Atunci când un fișier este deschis ( folosind apelul open) cu flag­ul O_NONBLOCK operațiile pe acel
descriptor de fișier nu se vor bloca în așteptarea terminării operației. În acest caz apelul read va
întoarce ­1 și errno este setat la valoarea EAGAIN sau EWOULDBLOCK dacă nu sunt date de citit.
Asemănător în cazul apelului write.

Soluția este folosirea unor mecanisme care permit unui thread să aștepte producerea unui eveniment I/O
pe un set de descriptori. Thread­ul se va bloca până când unul dintre descriptorii din set poate fi folosit
pentru citire/scriere. Un server care folosește un mecanism de acest tip are, de obicei, o structură de
forma:

set = setul de descriptori urmăriți 
while (true) { 
    așteaptă producerea unui eveniment pe unul dintre descriptori 
    pentru fiecare descriptor pe care s‐a produs un eveniment I/O { 
        tratează evenimentul I/O 
    } 
}

Detaliile variază de la o implementare la alta, dar secvența de pseudocod de mai sus reprezintă structura
de bază pentru serverele care folosesc multiplexarea I/O.

select
O primă soluție este utilizarea funcțiilor select [http://linux.die.net/man/2/select] sau pselect
[http://linux.die.net/man/2/select]. Folosirea acestor funcții conduce la blocarea thread­ului curent până la
producerea unui eveniment I/O pe un set de descriptori de fișier, a unei erori pe set sau până la expirarea
unui timer.

Funcțiile folosesc un set de descriptori de fișier pentru a preciza fișierele/sockeţii pe care thread­ul curent
va aștepta producerea evenimentelor I/O. Tipul de date folosit pentru definirea acestui set este fd_set,
care este, de obicei, o mască de biți.

Funcțiile select și pselect sunt definite conform POSIX.1­2001 în sys/select.h
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 1/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

#include <sys/select.h> 
  
int select( 
     int nfds,  
     fd_set *readfds,  
     fd_set *writefds,  
     fd_set *exceptfds,  
     struct timeval *timeout 
);

Nu vom insista asupra apelului select, căci standardul POSIX specifică un alt apel, poll, ce oferă o
performanţă mai bună.

Avantaje:

simplitate;
portabilitate: funcția select este disponibilă chiar și in API­ul Win32;

Dezavantaje:

lungimea setul de descriptori este definită cu ajutorul lui FD_SETSIZE, și implicit are valoarea 64;
este necesar ca seturile de descriptori să fie reconstruite la fiecare apel select;
la apariția unui eveniment pe unul dintre descriptori, toți descriptorii puși în set înainte de select
trebuie testați pentru a vedea pe care dintre ei a apărut evenimentul;
la fiecare apel, același set de descriptori este transmis în kernel.

poll

Funcția poll [http://linux.die.net/man/2/poll] consolidează argumentele funcției select și permite notificarea
pentru o gamă mai largă de evenimente. Funcția se definește ca mai jos:

#include <sys/poll.h> 
  
int poll( 
     struct pollfd *ufds,  
     unsigned int nfds,  
     int timeout 
);

Timeout­ul este specificat în milisecunde. În caz de valoare negativă, semnificația este de așteptare
pentru o perioadă nedefinită (“infinit”).

Structura pollfd este definită în sys/poll.h:

#include <sys/poll.h> 
  
struct pollfd { 
     int fd;        /* file descriptor */ 
     short events;  /* evenimente solicitate */ 
     short revents; /* evenimente apărute */ 
};

Funcția poll permite astfel așteptarea evenimentelor descrise de vectorul ufds de dimensiune nfds.

În cadrul structurii pollfd avem:

events este o mască de biți în care se specifică evenimentele urmărite de poll pentru
descriptorul fd (POLLIN ­ există date ce pot fi citite, POLLOUT ­ se pot scrie date).
revents este, de asemenea, o mască de biți completată de kernel cu evenimentele apărute în
momentul în care apelul se întoarce (POLLIN, POLLOUT) sau cu valori predefinite (POLLERR,
POLLHUP, POLLNVAL) pentru situații speciale.

În caz de succes, funcția returnează un număr diferit de zero reprezentând numărul de structuri pentru
care revents nu e zero (cu alte cuvinte toți descriptorii cu evenimente sau erori). Se returnează 0 dacă
a expirat timpul (timeout milisecunde) și nu a fost selectat nici un descriptor. În caz de eroare se

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 2/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

returnează ­1 și se setează errno. De asemenea, funcția poll poate fi întreruptă de semnale, caz în care
va întoarce ­1 și errno va fi setat la EINTR.

Un exemplu de utilizare pentru poll este prezentat în continuare:

#define MAX_PFDS        32 
  
[...] 
struct pollfd pfds[MAX_PFDS]; 
int nfds; 
int listenfd, sockfd;        /* listener socket; connection socket */ 
  
nfds = 0; 
  
/* read user data from standard input */ 
pfds[nfds].fd = STDIN_FILENO; 
pfds[nfds].events = POLLIN; 
nfds++; 
  
/* TODO ...  create server socket (listener) */ 
  
/* add listener socket */ 
pfds[nfds].fd = listenfd 
pfds[nfds].events = POLLIN; 
nfds++; 
  
while (1) {        /* server loop */ 
    /* wait for readiness notification */ 
    poll(pfds, nfds, ‐1); 
  
    if ((pfds[1].revents & POLLIN) != 0) { 
        /* TODO ... handle new connection */ 
    } 
    else if ((pfds[0].revents & POLLIN) != 0) { 
        /* TODO ... read user data from standard input */ 
    } 
    else { 
        /* TODO ... handle message on connection sockets */ 
    } 

[...]

Avantaje poll:

transmiterea setului de descriptori este mai simplă decât în cazul funcției select;
setul de descriptori nu trebuie reconstruit la fiecare apel poll.

Dezavantaje poll:

ineficiență ­ la apariția unui eveniment, trebuie parcurs tot setul de descriptori pentru a găsi
descriptorul pe care a apărut evenimentul;
la fiecare apel, același set de descriptori (care poate fi mare) este copiat în kernel și înapoi.

epoll

Funcțiile select și poll nu sunt scalabile la un număr mare de conexiuni pentru că la fiecare apel al lor
trebuie transmisă toată lista de descriptori. În astfel de situații, la fiecare pas, trebuie construită lista de
descriptori și apelat poll sau select care copiază tot setul în kernel. La apariția unui eveniment va fi
marcat corespunzător descriptorul. Utilizatorul trebuie să parcurgă tot setul de descriptori pentru a­și da
seama pe care dintre ei a apărut evenimentul. În acest fel se ajunge să se petreacă tot mai mult timp
scanând după evenimente în setul de descriptori și tot mai puțin timp făcând I/O.

Din acest motiv, diverse sisteme au implementat interfețe scalabile, dar non­portabile:

/dev/poll pe Solaris;
kqueue pe FreeBSD;
epoll pe Linux.

Aceste interfețe rezolvă problemele asociate cu select și poll, dar și problemele de scalabilitate.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 3/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Pentru a folosi epoll [http://linux.die.net/man/4/epoll], trebuie inclus sys/epoll.h. Interfața epoll oferă
funcții pentru:

crearea unui obiect epoll (epoll_create);
adăugarea sau eliminarea de descriptori de fișiere/sockeți la obiectul epoll (epoll_ctl);
așteptarea unui eveniment pe unul dintre descriptori (epoll_wait).

Crearea unui obiect epoll

Pentru crearea unui obiect epoll se folosește funcția epoll_create [http://linux.die.net/man/2/epoll_create]:

int epoll_create(int size);

Apelul epoll_create facilitează crearea unui descriptor de fișier ce va fi ulterior folosit pentru
așteptarea de evenimente. Descriptorul întors va trebui la final închis folosind apelul close.

Argumentul size este ignorat în versiunile recente ale nucleului, acesta ajustând dinamic dimensiunea
setului de descriptori asociat obiectului epoll.

Adăugarea/eliminarea descriptorilor la/de la obiectul epoll
Operațiile de adăugare/eliminare de descriptori se realizează cu ajutorul funcției epoll_ctl
[http://linux.die.net/man/2/epoll_ctl]:

int epoll_ctl( 
     int epollfd,  
     int op,  
     int fd,  
     struct epoll_event *event 
);

Apelul epoll_ctl permite specificarea evenimentelor care vor fi așteptate.

Primul argument al apelului epoll_ctl (epollfd) este descriptorul întors de epoll_create.

Câmpul event descrie evenimentul asociat descriptorului fd care poate fi adăugat, șters sau modificat în
funcție de valoarea argumentului op:

EPOLL_CTL_ADD: pentru adăugare;
EPOLL_CTL_MOD: pentru modificare;
EPOLL_CTL_DEL: pentru ștergere.

Structura epoll_event specifică evenimentele așteptate:

typedef union epoll_data { 
     void *ptr;              /* Pointer to user‐defined data */ 
     int fd;                 /* File descriptor */ 
     __uint32_t u32;         /* 32‐bit integer */ 
     __uint64_t u64;         /* 64‐bit integer */ 
} epoll_data_t; 
  
struct epoll_event { 
     __uint32_t events;      /* Epoll events (bit mask) */ 
     epoll_data_t data;      /* User data*/ 
};

Exemple de evenimente:

EPOLLIN ­ fișierul este disponibil pentru citire,
EPOLLOUT ­ fișierul este disponibil pentru scriere.

Așteptarea unui eveniment I/O

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 4/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Thread­ul curent așteaptă producerea unui eveniment I/O la unul dintre descriptorii asociați obiectului
epoll prin intermediul funcției epoll_wait [http://linux.die.net/man/2/epoll_wait]:

int epoll_wait( 
     int epollfd,  
     struct epoll_event* events,  
     int maxevents,  
     int timeout 
);

Funcția epoll_wait este echivalentul funcțiilor select și poll. Este folosită pentru așteptarea unui
eveniment la unul din descriptorii asociați obiectului epoll.

La revenirea apelului, utilizatorul nu va trebui să parcurgă toți descriptorii configurați, ci numai cei care
au evenimente produse. Argumentul events va marca o zonă de memorie unde vor fi plasate maxim
maxevents evenimente de nucleu. Presupunând că valoarea câmpului timeout este ­1 (așteptare
nedefinită), apelul se va întoarce imediat dacă există evenimente asociate, sau se va bloca până la
apariția unui eveniment.

La fel ca și în cazul select/pselect și poll/ppoll, există apelul epoll_pwait care permite
precizarea unei măști de semnale.

Edge­triggered sau level­triggered

Interfața epoll are două comportamente posibile: edge‐triggered sau level‐triggered. Se poate
folosi unul sau altul, în funcție de prezența flag­ului EPOLLET la adăugarea unui descriptor în lista epoll.

Presupunem existența unui socket funcționând în mod non‐blocant pe care sosesc 100 de octeți. În
ambele moduri (edge sau level triggered) epoll_wait va raporta EPOLLIN pentru acel socket.

Vom presupune că se citesc 50 de octeți din cei 100 primiți. Diferența între cele două moduri de
funcționare apare la un nou apel epoll_wait. În modul level­triggered se va raporta imediat EPOLLIN. În
modul edge­triggered nu se va mai raporta nimic, nici măcar la sosirea unor noi date pe socket. Se poate
observa cum modul edge­triggered sesizează schimbarea stării descriptorului în relație cu evenimentul,
iar level­triggered prezența stării. Modul edge­triggered este implementat mai eficient în kernel, chiar
dacă pare mai greu de folosit.

În continuare, în cele două spoilere de mai jos, sunt prezentate câteva reguli care trebuie urmărite cu o
metodă sau alta. Pentru ambele metode este recomandată folosirea sockeților în modul non­blocant.

La apariția unui eveniment EPOLLIN se poate citi oricât, la următorul apel epoll_wait se va raporta
din nou EPOLLIN dacă mai sunt date de citit.
EPOLLOUT nu trebuie configurat inițial pentru un socket pentru că astfel epoll_wait va raporta
imediat că este loc de scris în buffer (inițial bufferul de scriere asociat cu socketul este gol).
Acesta este o formă deghizată de busy waiting. Folosirea corectă implică scrierea normală pe
socket și numai dacă la un moment dat funcțiile de scriere raportează că nu mai este loc de
scriere in buffer (EAGAIN), se va activa EPOLLOUT pe descriptorul respectiv și salva ce mai este
de scris. Când în sfârșit se face loc, se va raporta EPOLLOUT și atunci se poate încerca să se scrie
datele păstrate. Dacă se reușește scrierea lor integrală, trebuie eliminat flagul EPOLLOUT pentru a
nu intra într­un nou ciclu de busy­waiting. În concluzie, EPOLLOUT trebuie activat doar când nu se
reușește scrierea integrală a datelor și scos imediat după ce acestea au fost scrise.

La apariția unui eveniment EPOLLIN pe un descriptor, trebuie citit tot ce se poate citi înainte de
reapelarea epoll_wait, altfel nu va mai fi raportat EPOLLIN niciodată.
Pentru scrierea folosind edge­triggered se poate activa de la început EPOLLOUT. Aceasta va cauza
apariția unui eveniment EPOLLOUT imediat după apelarea epoll_wait (pentru că bufferul de scriere
este gol) care ar trebui ignorat. La următorul apel epoll_wait nu se mai generează EPOLLOUT
pentru că nu s­a schimbat starea de la ultimul apel. Dacă la un moment dat se încearcă scrierea

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 5/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

unor date pe socket și acestea nu pot fi scrise integral, la următorul epoll_wait se generează
EPOLLOUT, pentru că s­a schimbat starea socketului. Mai pe scurt, asta are ca efect faptul că nu
mai trebuie activat/deactivat EPOLLOUT ca în cazul level­triggered.

Exemplu folosire epoll

Mai jos este prezentat un exemplu de utilizare a epoll echivalent cu exemplele pentru select și poll
(server care multiplexează mai multe conexiuni pe sockeți și intrarea standard):

#define EPOLL_INIT_BACKSTORE        2 
  
[...] 
int listenfd, sockfd;        /* listener socket; connection socket */ 
struct epoll_event ev; 
  
/* create epoll descriptor */ 
epfd = epoll_create(EPOLL_INIT_BACKSTORE); 
  
/* read user data from standard input */ 
ev.data.fd = STDIN_FILENO;        /* key is file descriptor */ 
ev.events = EPOLLIN; 
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); 
  
/* TODO ...  create server socket (listener) */ 
  
/* add listener socket */ 
ev.data.fd = listenfd;        /* key is file descriptor */ 
ev.events = EPOLLIN; 
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); 
  
while (1) {        /* server loop */ 
    struct epoll_event ret_ev; 
  
    /* wait for readiness notification */ 
    epoll_wait(epfd, &ret_ev, 1, ‐1); 
  
    if ((ret_ev.data.fd == listenfd) && ((ret_ev.events & EPOLLIN) != 0)) { 
        /* TODO ... handle new connection */ 
    } 
    else if ((ret_ev.data.fd == STDIN_FILENO) && 
                    ((ret_ev.events & EPOLLIN) != 0)) { 
        /* TODO ... read user data from standard input */ 
    } 
    else { 
        /* TODO ... handle message on connection sockets */ 
    } 

[...]

Linux ­ generalizarea multiplexării

O problemă a funcțiilor de multiplexare de mai sus (select, poll, epoll) este aceea că sunt limitate la
descriptori de fișier. Altfel spus, se pot aștepta doar evenimente asociate cu un fișier/socket: gata de
citire, gata de scriere. De multe ori însă se dorește să existe un punct comun de așteptare a unui semnal,
a unui semafor, a unui proces, a unei operații de intrare/ieșire, a unui timer. În Windows, acest lucru se
poate realiza cu ajutorul funcției WaitForMultipleObjects datorită faptului că majoritatea
mecanismelor din Windows sunt folosite cu ajutorul tipului de date HANDLE.

eventfd

Pentru a asigura în Linux posibilitatea așteptării de evenimente multiple s­a definit interfața  eventfd. Cu
ajutorul acestei interfețe și combinat cu interfețele de multiplexare I/O existente, kernel­ul poate notifica
o aplicație utilizator de orice tip de eveniment.

Interfața eventfd este prezentă în nucleul Linux începând cu versiunea 2.6.22 și este suportată de către
glibc începând cu versiunea 2.8.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 6/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Interfața eventfd permite unificarea mecanismelor de notificare ale kernel­ului într­un descriptor de
fișier care va fi folosit de utilizator.

Cele trei apeluri de bază pentru extinderea funcționalității multiplexării I/O sunt: eventfd
[http://linux.die.net/man/2/eventfd], signalfd [http://linux.die.net/man/2/signalfd] și timerfd_create
[http://www.unix.com/man­page/Linux/2/timerfd_create/].

#include <sys/eventfd.h> 
  
int eventfd(unsigned int initval, int flags);

#include <sys/signalfd.h> 
  
int signalfd(int fd, const sigset_t *mask, int flags);

#include <sys/timerfd.h> 
  
int timerfd_create(int clockid, int flags);

Toate cele trei apeluri întorc un descriptor de fișier pe care se vor putea primi notificări (evenimente,
semnale, timere). Operațiile posibile pe descriptorul de fișier întors sunt:

write: pentru transmiterea unui mesaj de notificare pe descriptor;
read: pentru primirea unui mesaj care înseamnă primirea notificării;
select, poll, epoll: pentru multiplexarea I/O;
close: pentru închiderea descriptorului și eliberarea resurselor asociate.

În următorul exemplu, apelul eventfd este folosit pentru notificarea procesului părinte de către procesul
fiu. Codul este cel prezent în pagina de manual (man eventfd).

[...] 
int efd; 
uint64_t u; 
  
/* create eventfd file descriptor */ 
efd = eventfd(0, 0); 
  
switch (fork()) { 
case 0: 
    /* notify parent process */
    s = write(efd, &u, sizeof(uint64_t)); 
  
    printf("Child completed write loop\n"); 
    exit(EXIT_SUCCESS); 
  
default: 
    printf("Parent about to read\n"); 
  
    /* wait for notification */
    s = read(efd, &u, sizeof(uint64_t)); 
    exit(EXIT_SUCCESS); 
[...]

signalfd
Apelul signalfd [http://linux.die.net/man/2/signalfd] este folosit în mod similar pentru recepționarea de
semnale prin intermediul unui descriptor de fișier. Pentru a putea recepționa un semnal cu ajutorul
interfeței signalfd, va trebui blocat în masca de semnale a procesului. La fel ca și exemplul de mai sus,
codul de mai jos este cel prezent în pagina de manual (man signalfd).

/* at this point Linux‐specific headers are required to use struct signalfd_siginfo */ 
#include <linux/types.h> 
#include <linux/signalfd.h> 
  
#define SIZEOF_SIG      (_NSIG / 8) 
#define SIZEOF_SIGSET   (SIZEOF_SIG > sizeof(sigset_t) ? \ 
                                  sizeof(sigset_t): SIZEOF_SIG)  
  
  
    [...] 

    sigset_t mask; 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 7/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]
    sigset_t mask; 
    int sfd; 
    struct signalfd_siginfo fdsi; 
  
    sigemptyset(&mask); 
    sigaddset(&mask, SIGINT);                /* CTRL‐C */ 
    sigaddset(&mask, SIGQUIT);               /* CTRL‐\ */ 
  
    /* 
     * Block signals so that they aren't handled 
     * according to their default dispositions 
     */ 
  
    sigprocmask(SIG_BLOCK, &mask, NULL); 
  
    /* create signalfd descriptor */ 
    sfd = signalfd(‐1, &mask, 0); 
  
    for (;;) { 
        /* wait for signals to be delivered by user */ 
        s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo)); 
  
        if (fdsi.ssi_signo == SIGINT) { 
             printf("Got SIGINT\n"); 
        } else if (fdsi.ssi_signo == SIGQUIT) { 
             printf("Got SIGQUIT\n"); 
             exit(EXIT_SUCCESS); 
        } else { 
             printf("Read unexpected signal\n"); 
        } 
    } 
    [...]

Linux ­ operații asincrone
În mod clasic, operațiile de lucru cu datele aflate pe suporturi externe înseamnă utilizarea apelurilor
sincrone de tipul read, write și fsync. Aceste apeluri garantează faptul că, la terminarea apelului,
datele sunt scrise/citite (de) pe suportul extern (sau în cache­ul asociat). Un astfel de apel poate întârzia
continuarea fluxului de instrucțiuni curent până la terminarea operației cerute.

Pentru fire de execuție care nu au nevoie frecvent de operații de intrare­ieșire, această abordare
funcționează. În schimb, pentru aplicații specializate pe lucrul cu memoria externă, folosirea apelurilor
sincrone (blocante) încetinește semnificativ execuția programului. Timpul necesar unui acces la memorie
(cu atât mai mult memoria externă) depășește cu mult timpul de execuție a unei instrucțiuni strict
aritmetice.

Linux AIO

Standardul POSIX.1b definește un nou set de operații I/O care pot reduce semnificativ timpul pe care o
aplicație îl petrece așteptând pentru I/O. Noile funcții permit unui program să inițieze una sau mai multe
operații de I/O și să­și continue lucrul normal în timp ce operațiile de I/O sunt executate în paralel.

Această funcționalitate este disponibilă dacă se instalează biblioteca libaio:

so$ apt‐cache search libaio 
libaio‐dev ‐ Linux kernel AIO access library ‐ development files 
libaio1 ‐ Linux kernel AIO access library ‐ shared library 
libaio1‐dbg ‐ Linux kernel AIO access library ‐ debugging symbols 
so$ sudo apt‐get install libaio1 libaio‐dev

Totodată, programul care folosește acest API trebuie să includă fișierul header libaio.h
[http://libaio.sourcearchive.com/documentation/0.3.109‐1ubuntu1/libaio_8h_source.html] și să
link­eze biblioteca libaio. Toate funcțiile și structurile de care vom vorbi în continuare se pot găsi în
acest fișier header. Dacă ați instalat pachetul, fișierul se găsește în /usr/include/libaio.h.

Structuri de bază Linux AIO

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 8/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Structura iocb folosită pentru încapsularea unei operații asincrone. Structura este definită în header­ul
libaio.h [http://libaio.sourcearchive.com/documentation/0.3.109‐
1ubuntu1/libaio_8h_source.html].

struct iocb { 
     PADDEDptr(void *data, __pad1);     /* Return in the io completion event */ 
     PADDED(unsigned key, __pad2);     /* For use in identifying io requests */ 
  
     short aio_lio_opcode;   
     short aio_reqprio;     
     int aio_fildes;                       /* Perform async IO on this file descriptor */ 
  
     union { 
          struct io_iocb_common c;         /* common read/write operation */ 
          struct io_iocb_vector v;         /* vectored read/write operations */ 
          struct io_iocb_poll poll;  
          struct io_iocb_sockaddr saddr;   /* socket read/write operations */ 
  } u; 
};

În principiu, nu se lucrează direct cu elementele din structura iocb. Pentru asta există funcții de
inițializare:

Pentru operații normale de citire/scriere:

void io_prep_pread( 
     struct iocb *iocb,  
     int fd,  
     void *buf,  
     size_t count,  
     long long offset 
);

void io_prep_pwrite( 
     struct iocb *iocb,  
     int fd,  
     void *buf,  
     size_t count,  
     long long offset 
);

Pentru operații Vectored I/O de citire/scriere:

void io_prep_preadv( 
     struct iocb *iocb,  
     int fd,  
     const struct iovec *iov,  
     int iovcnt,  
     long long offset 
);

void io_prep_pwritev( 
     struct iocb *iocb,  
     int fd,  
     const struct iovec *iov,  
     int iovcnt,  
     long long offset 
);

Pentru folosirea acesteia o aplicație va include libaio.h
[http://libaio.sourcearchive.com/documentation/0.3.109‐1ubuntu1/libaio_8h_source.html]. Un
exemplu de inițializare a acestei structuri este:

        #include <libaio.h> 
  
        /* ... */ 
  
        struct iocb iocb; 
  
        memset(&iocb, 0, sizeof(iocb)); 
        io_prep_pwrite(&iocb, fd, buffer, BUFER_SIZE, 0);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 9/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Context AIO

Orice operație sau set de operații Linux AIO sunt identificate printr­o valoare de tipul io_context_t ce
reprezintă un context de operații asincrone.

Inițializarea, respectiv distrugerea contextului se realizează cu ajutorul funcțiilor io_setup
[http://linux.die.net/man/2/io_setup] și io_destroy [http://linux.die.net/man/2/io_destroy]:

#include <libaio.h> 
  
int io_setup(unsigned nr_events, aio_context_t *ctxp);

#include <libaio.h> 
  
int io_destroy(aio_context_t ctx);

Un exemplu de inițializare și distrugere a contextului:

#include <libaio.h> 
  
io_context_t ctx; 
int num_ops = 10; 
  
/* crează un context de I/O asincron capabil să primească măcar num_ops evenimente */  
  
if (io_setup(num_ops, &ctx) < 0) { 
    /* handle error */     

  
/* do work */ 
/* ... */ 
  
/* distruge contextul și anulează toate operațiile I/O asincrone necompletate */ 
  
if (io_destroy(ctx) < 0) { 
    /* handle error */ 
}

Operații AIO
Pentru realizarea unei operații asincrone se folosește funcția io_submit
[http://linux.die.net/man/2/io_submit]. Această funcție declanșează pornirea operațiilor asincrone definite în
vectorul de pointeri de structuri iocb primit ca argument. Această funcție nu blochează procesul curent.

#include <libaio.h> 
  
int io_submit( 
     aio_context_t ctx_id,  
     long nr,  
     struct iocb **iocbpp 
);

Un exemplu de utilizare este:

#include <libaio.h> 
  
#define NUM_AIO_OPS       10 
  
struct iocb iocb[NUM_AIO_OPS];      /* array of asynchronous operations */ 
struct iocb *piocb[NUM_AIO_OPS];    /* array of pointers to asynchronous operations */ 
io_context_t ctx = 0; 
  
/* init context, iocb */ 
  
/* fill piocb */ 
for (i = 0; i < NUM_AIO_OPS; i++) 
    piocb[i] = &iocb[i]; 
  
/* 
  * Submit NUM_AIO_OPS async operations in context 'ctx' 
  * This does not wait for the operations to finish 

  */ 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 10/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]
  */ 
  
if (io_submit(ctx, NUM_AIO_OPS, piocb) < 0) { 
    /* handle error */ 

  
/* Do some other stuff in paralel with the execution of async I/O operations */

Pentru așteptarea încheierii unei operații AIO și obținerea de informații despre rezultatul acesteia se
folosește funcția io_getevents [http://linux.die.net/man/2/io_getevents]. Funcția folosește structura io_event
pentru a obține informații despre încheierea unei operații asincrone.

#include <linux/time.h> 
#include <libaio.h> 
  
int io_getevents( 
     aio_context_t ctx_id,  
     long min_nr,  
     long nr, 
     struct io_event *events,  
     struct timespec *timeout 
);

Un exemplu de utilizare este:

#include <libaio.h> 
#define       NUM_AIO_OPS  10 
  
io_context_t ctx = 0; 
struct io_event events[NUM_AIO_OPS];     /* aio result array */ 
/* ... */ 
  
/* 
  * Wait _exactly_ NUM_AIO_OPS async operations to finish 
  * min_nr ‐ min nummber of async aio to finish for the function to return 
  * max_nr ‐ max nummber of async aio operations that can be returned 
  */ 
rc = io_getevents(ctx, NUM_AIO_OPS, /* min_nr */ 
                       NUM_AIO_OPS, /* max_nr */ 
                       events,      /* vector to store completed events */
                       NULL);        /* no timeout */ 
if (rc < 0) {                      
    /* handle error */ 
}

Integrarea Linux AIO cu eventfd

Este utilă folosirea apelurilor de multiplexare I/O (select, poll, epoll) și pentru așteptarea încheierii
operațiilor asincrone. Pentru aceasta, interfața AIO a Linux 2.6 permite integrarea API­ului de operații
asincrone cu mecanismul eventfd.

Pentru aceasta se configurează flag­ul IOCB_FLAG_RESFD iar câmpul resfd al structurii iocb va conține
un descriptor eventfd ce va fi notificat în momentul încheierii operației asincrone. Acest lucru se poate
configura din start apelând funcția:

void io_set_eventfd(struct iocb *iocb, int eventfd)

Apelul io_getevents [http://linux.die.net/man/2/io_getevents] este în continuare util pentru a
obține informații despre încheierea operațiilor. eventfd oferă doar mecanismul de așteptare a acestora.

#include <libaio.h> 
int efd; 
  
/* creare event cu valoare inițială 0, fără flaguri speciale */ 
efd = eventfd(0, 0); 
  
/* ... */ 
struct iocb *iocb; 
  
/* ... */ 
/* use eventfd */ 
io_set_eventfd(&iocb[i], efd); 
  
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 11/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]
  
/* ... */ 
u_int64_t efd_val; 
if (read(efd, &efd_val, sizeof(efd_val)) < 0) { 
    /* handle error */ 

  
printf("%llu operations have completed\n", efd_val);

Citirea din descriptorul eventfd reprezintă numărul de operații I/O încheiate. Această valoare va fi, de
obicei, folosită ca al doilea și al treilea argument al io_getevents
[http://linux.die.net/man/2/io_getevents].

Util pentru Tema 5: Folosind integrarea operațiilor asincrone cu eventfd și mecanismele de multiplexare
I/O (select, poll, epoll) se poate aștepta unificat încheierea unei operații asincrone sau sosirea de
date pe sockeți.

Zero­copy I/O

Linux ­ splice
Este un apel de sistem ce permite transferul de date între 2 descriptori de fișier, dintre care cel puțin unul
este pipe. Avantajul este că nu se folosește un buffer (byte array) în userspace.

#define _GNU_SOURCE /* trebuie definit pentru că splice este o extensie nespecificată de standardele POSIX/SYSV/BSD/etc. */
  
#include <fcntl.h> 
  
long splice( 
     int fd_in,  
     loff_t *off_in,  
     int fd_out,  
     loff_t *off_out,  
     size_t len,  
     unsigned int flags 
);

dacă descriptorul fd_in reprezintă un pipe, atunci pointer­ul la offset off_in trebuie să fie NULL
altfel:
dacă off_in este NULL, atunci datele sunt citite de la fd_in de la offset­ul curent, acesta
modificându­se corespunzător
altfel, off_in trebuie să fie un pointer la un întreg care reprezintă offset­ul de start de la
care se va face citirea, iar offset­ul propriu descriptorului fd_in rămâne neschimbat
comportamentul de mai sus este valabil și pentru fd_out și off_out, la scriere
parametrul len specifică numărul maxim de octeți transferați
masca de biți flags poate specifica o operație non­blocantă sau hint­uri pentru nucleu. Citiți pagina
de manual a funcției pentru mai multe detalii.

Exemplu de folosire:

int pipe, file1, file2; 
loff_t offset = 0; 
size_t count = 4096; 
  
/* ... deschideri fișiere, creare pipe */ 
  
splice(file1, &offset, pipe, NULL, count, 0);
  
splice(pipe, NULL, file2, &offset, count, 0);

O altă funcție foarte utilă și care folosește zero­copy este sendfile [http://linux.die.net/man/2/sendfile]

Vectored I/O

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 12/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Vectored I/O (sau scatter/gather I/O) reprezintă o metodă prin intermediul căreia un singur apel permite
scrierea de date din mai multe buffere către un flux de ieșire sau citirea de date de la un flux de intrare
în mai multe buffere. Bufferele sunt precizate ca un vector de buffere, de unde și denumirea de vectored
I/O.

Apelurile din clasa vectored I/O sunt utile în momentul în care datele sunt disparate/dezasamblate în
memorie și se dorește “concatenarea” acestora într­un singur flux de scriere sau “desfacerea” acestora
dintr­un flux de citire. Un exemplu îl reprezintă pachetele de rețea în care headerele, datele și trailerele
se găsesc, de obicei, în locații de memorie diferite pentru a facilita prelucrarea acestora. Folosirea
Vectored I/O permite asamblarea/dezasamblarea pachetului în/din mai multe zone de memorie printr­o
singură operație. Nu este nevoie de crearea unui buffer nou cu pachetele concatenate, drept care Vectored
I/O poate fi considerat o formă de zero­copy.

Apeluri:

UNIX: readv [http://linux.die.net/man/2/readv], writev [http://linux.die.net/man/2/writev].
Windows (fișiere) ReadFileScatter, WriteFileGather.
Windows (sockeți) WSARecv,WSASend.

readv/writev
Funcțiile readv [http://linux.die.net/man/2/readv] și writev [http://linux.die.net/man/2/writev] sunt folosite în
sistemele Unix ca operații de tipul vectored I/O. Structura de bază folosită de aceste funcții este struct
iovec:

#include <sys/uio.h> 
  
struct iovec { 
     void *iov_base;   /* Starting address */ 
     size_t iov_len;   /* bytes to transfer */ 
};

#include <sys/uio.h> 
  
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

#include <sys/uio.h> 
  
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

Un apel readv [http://linux.die.net/man/2/readv] sau writev [http://linux.die.net/man/2/writev] va permite
recepționarea/transmiterea unui număr de buffere reprezentate de structura iovec. Funcțiile întorc
numărul total de octeți citiți sau scriși.

Apelul writev [http://linux.die.net/man/2/writev] scrie datele (reprezentate de elementele din iov în fișier),
în ordinea în care acestea apar în vector:

#include <sys/uio.h> 
  
/* ... */ 
char *str0 = "Ana "; 
char *str1 = "are multe "; 
char *str2 = "mere, pere etc."; 
struct iovec iov[3]; 
ssize_t nwritten; 
  
iov[0].iov_base = str0; 
iov[0].iov_len  = strlen(str0); 
iov[1].iov_base = str1; 
iov[1].iov_len  = strlen(str1); 
iov[2].iov_base = str2; 
iov[2].iov_len  = strlen(str2); 
  
nwritten = writev(fd, iov, 3); 
if (nwritten < 0) { 
     /* handle error */ 
}

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 13/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Exercițiul 0 ­ Joc interactiv (2p)
Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (9p)
În rezolvarea laboratorului folosiți arhiva de sarcini lab11­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab11­tasks.zip]

Acest laborator se desfășoară în echipe. O echipă este formată din 2 studenți. În cazul în care numărul de
studenți prezenți este impar, o singură echipă va avea dreptul de a avea 3 studenți.

Discutați exercițiile și colaborați pe parcursul întregului laborator. Have fun! 

Exercițiul 1 ­ poll (1p)

Intrați în directorul 1‐pollpipe. Parcurgeți fișierul poll.c pentru a vedea un exemplu de folosire al
funcției poll.

Programul creează folosind fork o aplicație de test pentru poll. Aplicația folosește un server (părintele)
și CLIENT_COUNT clienți (copiii) ce comunică prin pipe­uri anonime.

Server­ul:

construiește un vector de pipe­uri (în funcția main);
creează clienții;
se blochează în așteptarea datelor de la clienți și tipărește datele primite;
termină execuția după ce a primit date de la fiecare client;

Clienții:

așteaptă un număr aleator de secunde (mai mic decât 10);
scriu în pipe­ul corespunzător un șir de MSG_SIZE caractere de forma

<pid>:<caracter random> ('a' + random() % 30)

scrierile și citirile în pipe­uri de până la PIPE_BUF octeți (4096 pe Linux) sunt atomice.

Compilați și rulați programul. Pentru nelămuriri puteti consulta secțiunea poll și pipe­uri în Linux.

Exercițiul 2 ­ epoll (2p)

Intrați în directorul 2‐epollpipe și parcurgeți fișierul epoll.c. Considerând cerința de la exercițiul
anterior, în loc de poll folosiți epoll.

În partea de inițializare realizați următoare operații:

Inițializați, înainte de ciclul for, handle­ul de epoll folosing epoll_create.
Adăugați câte un eveniment pentru fiecare pipe (folosind variabila ev) folosind epoll_ctl cu
opțiunea EPOLL_CTL_ADD.
Câmpul events al variabilei ev îl veți inițializa la EPOLLIN.
Câmpul data.fd al variabilei ev îl veți inițializa la capătul de citire al pipelului.
Închideți capătul de scriere al pipe­ului.

În partea de așteptare realizați, în bucla while următoarele operații:

Așteptați un eveniment folosind funcția epoll_wait.

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 14/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

Descriptorul indicat în structura întoarsă de epoll_wait (adică ev.data.fd) este capătul
de citire al pipe­ului.
Din pipe citiți mesajul trimis de client, folosind read.
Eliminați pipe­ul din multiplexor (folosind epoll_ctl cu opțiunea EPOLL_CTL_DEL) și închideți­l
folosind close.
Incremenetați valoarea variabilei recv_msgs.

Exercițiul 3 ­ eventfd (2p)
Pornind de la codul scris pentru execițiul anterior, notificați serverul de terminarea unui client utilizând
eventfd. Clientul transmite mesaje către server și, când dorește să închidă comunicația, va trimite un
mesaj pe canalul de control reprezentat de eventfd. Acum, epoll este folosit pentru a demultiplexa atât
descriptorii de pipe, cât și descriptorul de eventfd.

Decomentați în epoll.c linia cu

#define USE_EVENTFD

Clientul va scrie mesaje în pipe­ul aferent, iar la sfârșit va genera un mesaj de notificare. Acesta va folosi
funcția set_event definită în program. Mesajul de notificare este un număr pe 64 de biți organizat astfel:

Primii 32 de biți conțin valoarea definită de macro­ul MAGIC_EXIT.
Ultimii 32 de biți conțin indexul clientului.

Serverul va adăuga descriptorul de eventfd în epoll (cu eveniment de tipul EPOLLIN). Evenimentele
vor fi așteptate folosind epoll_wait (așa cum făcea și până acum). Apelul epoll_wait se întoarce
când un descriptor din epoll are informații de citit. Descriptorul întors în urma epoll_wait (identificat
de câmpul ev.data.fd) poate fi descriptor de pipe sau poate fi descriptorul de eventfd.

Dacă descriptorul întors în urma epoll_wait este descriptorul de eventfd, atunci veți citi 64 de biți de
pe acest descriptor (folosind read). Dacă primii 32 biți sunt valoarea descrisă de macro­ul MAGIC_EXIT,
atunci scoate capătul pipe­ului corespunzător din epoll și închide acel capăt (adică folosește
epoll_ctl cu opțiunea EPOLL_CTL_DEL și apoi close). Pentru a extrage ultimii 32 de biți din mesajul
de 64 de biți primit pe descriptorul de eventfd, reprezentând indexul clientului, folosiți funcția
get_index definită local în program.

Exercițiul 4 ­ async I/O (KAIO) (2p)

Intrați în directorul 4‐kaio și parcurgeți fișierul kaio.c. Completați zonele lipsă pentru a programa
scrierea a 4 fișiere cu numele date de variabila files.

Folosiți API­ul KAIO (io_setup, io_destroy, io_submit, io_getevents). Folosiți doar io_getevents pentru
așteptarea încheierii operațiilor asincrone.

Parcurgeți secțiunea Linux AIO și consultați exemplul lui Davide Libenzi [http://www.xmailserver.org/eventfd­
aio­test.c]. Urmăriți comentariile cu TODO 1

Compilați și rulați programul. Va trebui să aveți 4 fișiere de dimensiune 8192 octeți create în /tmp.

Atenţie! În cazul în care, la compilare, header­ul 'libaio' nu este găsit rulaţi

so$ sudo apt‐get install libaio1 libaio‐dev

Exercițiul 5 ­ async I/O (KAIO) (2p)

Folosiți eventfd pentru așteptarea operațiilor asincrone. Porniți de la codul scris pentru execițiul anterior.

Decomentați în kaio.c linia cu

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 15/16
6/11/2017 Laborator 11 ­ Operații IO avansate ­ Linux [CS Open CourseWare]

#define USE_EVENTFD

La inițializarea structurilor iocb, folosiți funcția io_set_eventfd pentru a activa folosirea eventfd.
Completați funcția wait_aio pentru a aștepta terminarea operațiilor asincrone folosind eventfd.

Parcurgeți secțiunea Linux AIO și urmăriți comentariile cu TODO 2 . Consultați exemplul lui Davide Libenzi
[http://www.xmailserver.org/eventfd­aio­test.c].

Compilați și rulați programul. Va trebui să aveți 4 fișiere de dimensiune 8192 octeți create în /tmp.

BONUS
1. (1 so karma) signalfd
Modificați codul de la exercițiul 2 pentru a permite notificarea de terminare a clienților
bazată pe semnale.
Server­ul:
creează un descriptor via signalfd pentru semnalul SIGCHLD și îl adaugă la epoll
la primirea unui semnal, prin read(2) pe descriptorul creat, determină PID­ul
copilului defunct, afișează un mesaj și scoate pipe­ul din epoll.
Hints:
Consultați secțiunea signalfd
man signalfd [http://linux.die.net/man/2/signalfd]

Soluții
Soluţii laborator 11 [http://elf.cs.pub.ro/so/res/laboratoare/lab11­sol.zip]
so/laboratoare/laborator­11.txt · Last modified: 2017/05/19 00:29 by theodor.stoican

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­11 16/16
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]

Laborator 12 ­ Implementarea sistemelor de fișiere

Materiale Ajutătoare

lab12­slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab12­slides.pdf]

Nice to read

TLPI ­ Chapter 14, File Systems
TLPI ­ Chapter 15, File Attributes
TLPI ­ Chapter 18, Directories and Links

Resurse utile

How inodes work? [https://www.youtube.com/watch?v=ymYZPtrvgec]
What chroot is really for? [https://lwn.net/Articles/252794/]

Device Nodes
Nucleul gestionează fiecare device hardware sau virtual prin intermediul unui device driver. Un device
driver este o porțiune de cod din nucleu care implementează o serie de operații corespunzătoare
acțiunilor de I/E asociate cu un device hardware. Procesele din spațiul utilizator (userspace)
interacționează cu device driver­ul prin intermediul unor fișiere speciale denumite device nodes. API­ul
(interfața de programare) oferită de device drivere este fixată, și include următoarele operații:

open, close
read, write
ioctl, mmap
Unele device­uri sunt reale (mouse, tastatură, disc), altele sunt virtuale în sensul că nu au un device
hardware asociat (e.g /dev/zero, /dev/null). După modul în care se accesează datele, device­urile
sunt împărțite în două categorii:

device de tip caracter, datele sunt procesate octet cu octet. În această categorie se
înscriu: tastatura, linia serială, mouse­ul.
device de tip bloc, datele pot fi procesate la nivel de bloc (e.g hard disk).

Fișierele device node se găsesc în /dev și au asociat un identificator format din major ID și minor ID.

Majorul și minorul sunt dați de coloanele 5 și 6 din output­ul ls ‐l, separate prin virgulă.

Puteți vizualiza majorii folosiți în sistem din fișierul /proc/devices.

Crearea unui device node se face folosind funcția mknod [http://man7.org/linux/man­
pages/man2/mknod.2.html]

        int mknod(const char *pathname, mode_t mode, dev_t dev);

În general informații despre fișiere, și în particular despre device node­uri se pot afla cu funcțiile din
familia stat [http://man7.org/linux/man­pages/man2/stat.2.html].

       int stat(const char *path, struct stat *buf); 
       int fstat(int fd, struct stat *buf); 
       int lstat(const char *path, struct stat *buf);

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 1/7
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]

Toate aceste funcții completează informațiile despre un fișier în structura struct stat, care conține
următoarele câmpuri:

           struct stat { 
               dev_t     st_dev;     /* ID of device containing file */ 
               ino_t     st_ino;     /* inode number */ 
               mode_t    st_mode;    /* protection */ 
               nlink_t   st_nlink;   /* number of hard links */ 
               uid_t     st_uid;     /* user ID of owner */ 
               gid_t     st_gid;     /* group ID of owner */ 
               dev_t     st_rdev;    /* device ID (if special file) */ 
               off_t     st_size;    /* total size, in bytes */ 
               blksize_t st_blksize; /* blocksize for file system I/O */ 
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */ 
               time_t    st_atime;   /* time of last access */ 
               time_t    st_mtime;   /* time of last modification */ 
               time_t    st_ctime;   /* time of last status change */ 
           };

Sisteme de fișiere
Un sistem de fișiere este o colecție organizată de fișiere și directoare. Un sistem de fișiere este creat
folosind comanda mkfs [http://linux.die.net/man/8/mkfs]. Din punct de vedere funcțional sistemele de
fișiere se pot împărți în:

sisteme de fișiere pentru disc (ext2, ext3, reiserfs, fat, ntfs, etc.)
sisteme de fișiere pentru rețea (nfs, smbfs, ncp, etc.)
sisteme de fișiere virtuale (procfs, sysfs, sockfs, pipefs, etc.)

Tipurile de sisteme de fișiere suportate de nucleu pot fi observate în fișierul /proc/filesystems.

daniel@debian$ cat /proc/filesystems  
nodev  sysfs 
nodev  proc 
nodev  ramfs 
  ext4 
  fuseblk 

Pentru a putea fi folosit un sistem de fișiere trebuie atașat (montat) în ierarhia de directoare din
sistem. Acest lucru se realizează cu comanda mount(8) [http://linux.die.net/man/8/mount]:

mount ‐t type device dir 

sau apelul mount(2) [http://man7.org/linux/man­pages/man2/mount.2.html]:

    int mount(const char *source, const char *target, 
                 const char *filesystemtype, unsigned long mountflags, 
                 const void *data);

Operația inversă, demontarea sistemului de fișiere din ierarhia de directoare se face cu comanda
umount(8) [http://man7.org/linux/man­pages/man8/umount.8.html]:

       umount {dir|device}...

sau apelul: umount(2) [http://man7.org/linux/man­pages/man2/umount.2.html]

       int umount(const char *target);

Directoare și link­uri
Fiecare proces are două atribute legate de directoare:

directorul rădăcina, determină punctul de unde căile absolute sunt interpretate.
directorul curent, determină punctul de unde căile relative sunt interpretate.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 2/7
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]

Un director este stocat în sistemul de fișiere într­un mod similar cu un fișier obișnuit. Există două
lucruri diferite:

tipul din structura inode este diferit.
conținutul este diferit. Un director conține un vector de nume de fișiere și inode­uri.

Link­uri simbolice (soft)
Un link simbolic (sau soft link), este un tip special de fișier al cărui conținut reprezintă numele altui
fișier. Link­urile simbolice sunt create cu comanda ln ‐s sau cu apelul symlink(2)
[http://linux.die.net/man/2/symlink]

int symlink(const char *oldpath, const char *newpath); 

Ștergerea unui link simbolic se face cu comanda unlink sau cu apelul unlink(2)
[http://linux.die.net/man/2/unlink]

int unlink(const char *pathname); 

Crearea și ștergerea directoarelor

Un director poate fi creat folosind comanda mkdir sau apelul mkdir(2)) [http://man7.org/linux/man­
pages/man2/mkdir.2.html]

int mkdir(const char *pathname, mode_t mode);

Apelul rmdir(2) [http://man7.org/linux/man­pages/man2/rmdir.2.html] șterge directorul specificat în
argumentul pathname:

int rmdir(const char *pathname);

De asemenea, pentru a șterge un fișier sau un director gol se poate folosi funcția remove(3)
[http://linux.die.net/man/3/remove]

int remove(const char *pathname); 

Citirea directoarelor
După cum am precizat mai sus, un director conține nume de directoare sau fișiere.

Apelul opendir(3) [http://linux.die.net/man/3/opendir] deschide un director și întoarce un handle ce poate fi
folosit mai târziu pentru a referi directorul.

DIR *opendir(const char *name); 
DIR *fdopendir(int fd);

Apelul readdir(3) [http://linux.die.net/man/3/readdir] citește intrări succesive dintr­un stream de directoare
(DIR).

struct dirent *readdir(DIR *dirp);

Apelul readdir întoarce un pointer la următoarea structură struct dirent streamul referit de dir:

struct dirent { 
    ino_t          d_ino;       /* inode number */ 
    off_t          d_off;       /* offset to the next dirent */ 
    unsigned short d_reclen;    /* length of this record */ 
    unsigned char  d_type;      /* type of file; not supported 
                                   by all file system types */ 

    char           d_name[256]; /* filename */ 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 3/7
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]
    char           d_name[256]; /* filename */ 
};

Directorul curent al unui proces
Directorul curent al unui proces definește punctul de start pentru formarea căilor relative referite de
procesul respectiv. Un proces nou creat moștenește directorul curent de la procesul părinte.

Directorul curent al unui proces poate fi determinat folosind apelul getcwd(3)
[http://linux.die.net/man/3/getcwd]:

  char *getcwd(char *cwdbuf, size_t size); 

cwdbuf, trebuie alocat înainte de apel astfel încât să poată stoca cel puțin size octeți.
după apel cwdbuf va conține calea absolută a directorului curent.

Schimbarea directorului curent
Apelul chdir(2) [http://linux.die.net/man/2/chdir] schimbă directorul curent al procesului apelant către
numele absolut sau relativ primit ca argument.

 int chdir(const char *path); 

Schimbarea directorului rădăcina al unui proces
Fiecare proces are un director rădăcină reprezentând punctul de unde căile absolute sunt interpretate.
În mod implicit, acesta este directorul rădăcina real al sistemului de fișiere. Un proces nou moștenește
directorul rădăcină de la părintele său. Există situații (e.g pentru a ascunde o parte din sistemul de
fișiere) în care este util pentru un proces să­și schimbe directorul rădăcină. Acest lucru se realizează
folosind apelul chroot(2) [http://linux.die.net/man/2/chroot]

 int chroot(const char *path); 

Rezolvarea unei căi
Apelul realpath(3) [http://man7.org/linux/man­pages/man3/realpath.3.html] dereferențiază link­ul simbolic
primit ca argument și rezolvă toate referințele către '/.'și '/..' pentru a produce un șir de caractere
conținând calea absolută.

  char *realpath(const char *path, char *resolved_path); 

dirname și basename
Apelurile dirname(3) [http://linux.die.net/man/3/dirname] și basename(3)
[http://linux.die.net/man/3/basename] împart un șir de caractere reprezentând o cale în partea de
director și partea de fișier.

char *dirname(char *path); 
  
char *basename(char *path);

De exemplu:

path         dirname    basename 
"/usr/lib"    "/usr"    "lib" 
"/usr/"       "/"       "usr" 
"usr"         "."       "usr" 
"/"           "/"       "/" 

"."           "."       "." 
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 4/7
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]
"."           "."       "." 
".."          "."       ".." 

Exerciții

Exercițiu bonus ­ Completare feedback (1p)
Vă invităm să evaluați activitatea echipei de SO și să precizați punctele tari și punctele slabe și
sugestiile voastre de îmbunătățire a materiei. Feedback­ul vostru este foarte important pentru noi să
creștem calitatea materiei în anii următori și să îmbunătățim materiile pe care le veți face în
continuare.

Găsiți formularul de feedback în partea dreaptă a paginii principale de SO de pe cs.curs.pub.ro într­
un frame numit “FEEDBACK” (sau click aici [http://cs.curs.pub.ro/2016/blocks/feedbackacs/view.php?
courseid=122&blockid=3269]). Trebuie să fiți inrolați la cursul de SO, altfel veți primi o eroare de acces.

Vă mulțumim!

Exercițiul 0 ­ Joc interactiv (2p)
Detalii desfășurare joc [http://ocw.cs.pub.ro/courses/so/meta/notare#joc_interactiv].

Linux (8p)
Pentru rezolvarea laboratorului descărcați arhiva de lab12­tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab12­tasks.zip]. Codul va fi scris în fișierul mini.c din directorul 1‐
mini/. Pentru fiecare exercițiu decomentați linia TODO corespunzătoare.

Exercițiul 1 (1p)

Folosiți comanda ls ‐l /dev și precizați două device node­uri de tip caracter și două device node­uri
de tip bloc. Ce major și minor au?

Exercițiul 2 (1p)

Implementați comanda list <device_node>, ce va primi ca argument un device node și va afișa
pentru acesta tipul (c/b), identificatorii major, respectiv minor. Folosiți funcția stat(2)
[http://man7.org/linux/man­pages/man2/stat.2.html] pentru a obține o structură de tipul struct stat din
care veți extrage tipul device­ului (st_mode) (hint: S_ISCHR, S_ISBLK
[http://www.gnu.org/software/libc/manual/html_node/Testing­File­Type.html#Testing­File­Type] ) apoi din câmpul
st_rdev extrageți major [http://man7.org/linux/man­pages/man3/makedev.3.html] și minor
[http://man7.org/linux/man­pages/man3/makedev.3.html]. Nu uitați să decomentați linia marcată cu #define
TODO2

Exercițiul 3 (1p)

Creați punctul de montare /mnt/my. Ca root, rulați comanda:

mkdir /mnt/my

Parcurgeți paginile de manual ale funcțiilor mount [http://man7.org/linux/man­pages/man2/mount.2.html] și
umount [http://man7.org/linux/man­pages/man2/umount.2.html].

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 5/7
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]

Folosiți comenzile mount și umount din executabilul mini pentru a monta discul /dev/sda1 în punctul
de montare /mnt/my. Citiți secțiunea marcată cu TODO din fișierul mini.c. Pentru argumentul 4 și
argumentul 5 al funcției mount folosiți, respectiv, valorile 0 și NULL.

Testare: Rulați, ca root, comanda:

./mini

și apoi rulați comanda de montare în cadrul acestui shell:

mount /dev/sda1 /mnt/my ntfs

Într­o altă consolă, într­un shell obișnuit, verificați rezultatele folosind comanda

cat /proc/mounts

Pentru demontare rulați comanda:

umount /mnt/my

Exercițiul 4 (1p)

Adăugați suport pentru comenzile symlink și unlink în programul mini. Urmăriți TODO4 .

Pentru testare folosiți, în shell­ul aferent comenzii ./mini, comanda:

symlink /etc/passwd local‐passwd

Ca să verificați, într­o altă consolă, în același director cu cel în care ați rulat comanda ./mini, folosiți

ls ‐l

Pentru a șterge symlink­ul folosiți comanda

unlink local‐passwd

Pentru validare rulați din nou comanda

ls ‐l

Exercițiul 5 (1p)

Adăugați suport pentru comenzile mkdir și rmdir în programul mini. Urmăriți TODO5 .

Ca al doilea argument pentru funcția mkdir folosiți (mode_t) 0755.

Exercițiul 6 (2p)

Adăugați suport pentru comanda ls dirname/ în programul mini. Aceasta va trebui să afișeze
recursiv toate directoarele și fișierele începând cu directorul dat ca parametru (puteți parcurge recursiv
în adâncime arborele de fișiere). Urmăriți TODO6 și demo­ul 5 de la curs
[http://ocw.cs.pub.ro/courses/so/cursuri/curs­12]

Exercițiul 7 (1p)

Adăugați suport pentru comenzile pwd și chdir în programul mini. Urmăriți TODO7 .

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 6/7
6/11/2017 Laborator 12 ­ Implementarea sistemelor de fișiere [CS Open CourseWare]

Soluții
Soluții exerciții laborator 12 [http://elf.cs.pub.ro/so/res/laboratoare/lab12­sol.zip]
so/laboratoare/laborator­12.txt · Last modified: 2017/05/19 16:38 by laura.ruse

https://ocw.cs.pub.ro/courses/so/laboratoare/laborator­12 7/7

S-ar putea să vă placă și