Documente Academic
Documente Profesional
Documente Cultură
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:
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)*/
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 */
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++;
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
• overheadul 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 bufferul nu va exista nici un apel de sistem și nici pagefaulturi.
Operația memcpy va presupune copierea datelor întro zonă alocată cu mmap. Durata de copiere este identică celei de mai sus, dar au loc
page faulturi 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 pseudoC) a funcției eat():
mutex_t 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
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 parcurge bitfieldul ș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 userspace. 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 roundrobin nonpreemptiv. Î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 roundrobin înseamnă că fiecare proces este planificat pe rând. Planificarea se face astfel:
010ms: procesul 1
10ms20ms: procesul 2 (procesul 1 așteaptă)
20ms30ms: procesul 3 (procesul 2 așteaptă, procesul 1 este gata de execuție)
30ms40ms: procesul 4 (procesul 3 așteaptă, procesul 1 și procesul 2 sunt gata de execuție)
40ms50ms: 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ă threaduri?
Nu se modifică. Threadurile partajează spațiul de adresă al procesului.
7. Are sens folosirea operațiilor asincrone în locul celor sincrone în situația de mai jos (pseudocod)?
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ă întrun apel de sistem. Overheadul 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ă întro singură pagină overheadul 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), overheadul (î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ă threaduri? 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. Întrun sistem de operare, 4 (patru) procese execută operațiile
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ă threaduri 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ă threaduri 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ă întrun registru (nu
folosește memoria).
Î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 threaduri, un thread execută următoarea secvență de cod (pseudo C):
int esp;
int stack_val;
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ă threadul anterior șia încheiat execuția).
5. Pe un sistem de fișiere MINIX se execută operația:
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 dentryul acestuia și se obține inodeul aferent și se citește inodeul în memorie.
Dacă fișierul nu există se creează un inode nou. Pentru aceastase parcurge bitmapul de inodeuri ș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 pointerii de blocuri ai inodeului ș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 faulturi 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;
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 harddisk?
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 userspace." 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 kernelspace 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 threadsafe. Dați exemplu de funcție care este threadsafe, dar nu
este reentrantă.
Toate funcțiile reentrante sunt threadsafe. Exemplu de funcție care este threadsafe dar nu reentrantă este malloc. Un
exemplu generic este o funcție care folosește un mutex pentru sincronizarea accesului la variabile partajate între threaduri:
funcția este threadsafe, dar nu este reentrantă (nu pot fi executate simultan două instanțe ale acestei funcții). Proprietatea de
reentranță sau threadsafety se referă la implementarea și interfața funcției, nu la contextul în care este folosită (o funcție
reentrantă poate fi folosită întrun context unsafe din punct de vedere al sincronizării, dar nu înseamnă că este nonthread
safe).
4. Întrun 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 întrun 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 threaduri î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 threadurilor 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 quadcore și 4GB RAM rulează un proces care planifică 3 threaduri 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 threaduri este ineficientă pentru că se execută mai ușor în cadrul
unui singur thread (apar overheaduri de creare, sincronizare și schimbare de context între threaduri). 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ă readonly. Acest lucru este avantajos din punct de vedere
al securității, întrucât împiedică suprascrierea codului executat. Ce alt avantaj important oferă?
Fiind readonly 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 OSlevel 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 hardwareul peste care rulează oferă suport (Intel VT
sau AMDV). 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 userspace. 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 1010lea apel open se întoarce cu eroare, iar errno are valoarea EMFILE (maximum
number of files open). Care este o posibilă explicație?
Timp de lucru: 70 de minute
NOTĂ: toate răspunsurile trebuie justificate
1. Știind că operațiile de lucru cu pipeuri sunt atomice, implementați în pseudocod un mutex cu ajutorul pipeurilor.
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 overheadul 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 userspace (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
threaduri kernellevel, acest lucru va presupune și un apel de sistem (planificatorul este implementat în kernelspace).
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 copyonwrite. 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ă copyonwrite. 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 singlethreaded 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ă perthread. Acest lucru se poate realiza cu ajutorul TLS/TSD. O variabilă comună errno pentru
toate threaduri ar conduce la incosistența informațiilor referitoare la erorile apărute.
5. Un sistem uniprocesor (singlecore) 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 threaduri, ce durează mai mult: schimbarea de context între două threaduri 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 threadurilor 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 rx libc2.7.so
00007f8c4842f000 12K r libc2.7.so
00007f8c48432000 8K rw libc2.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ă readwrite și nu poate fi partajată între două procese. Celelalte două zone sunt readonly și vor fi
partajate. Biblioteca va ocupa, așadar, 50*8K + 12K + 1320K.
7. Descrieți și explicați în pseudoasamblare 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 stackframeul curent ci din stack frameul
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 frameul 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 frameul 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 inodeului
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 dentryuri 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 întro 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 întro 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 9a 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 9a intrare (care vor referi prima și a 9a 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 pagefault î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 demandpaging).
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 *));
Î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)
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.
int aux_fd;
fputs ....
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”.
Î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?
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)
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.
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/20082009/so/examen$ time ./test_printf_write > out.txt
real 0m0.076s
user 0m0.060s
sys 0m0.020s
razvan@valhalla:~/school/20082009/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 inodeuri va folosi un hardlink către un fișier aflat pe un sistem de fișiere diferit?
Nu se pot crea hardlinkuri către un fișier aflat pe un alt sistem de fișiere. Un hardlink conține un nume și un număr de
inode. Inodeul 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 linkuri, iar comanda ln cu opțiunea s creează symbolic linkuri. Câte inodeuri,
respectiv dentryuri vor fi create în urma rulării comenzilor de mai sus?
Un hardlink 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 hardlink la a.txt
ln s b.txt c.txt → 1 dentry (c.txt) și 1inode (aferent simbolic linkului proaspăt creat)
Se creează 3 dentryuri și 2 inodeuri.
3. Un proces execută secvența următoare în două situații diferite:
a = malloc(5000);
memset(a, 0, 5000);
Întruna din situații rezultă două page faulturi, iar în alta trei page faulturi. Explicați acest comportament.
Pentru alocarea celor 5000 de octeți se folosește demandpaging. 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 faulturi.
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
faulturi 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 faulturi 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?
Un proces zombie este un proces care șia î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 readonly (fără a fi vorba de copyon 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ă dintro zonă marcată readonly (non copyonwrite), 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.
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
heapului, 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 întro 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 overheadului 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 întro 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 containerul 0). Astfel o eroare de nucleu apărută în nucleul
unui continaer OpenVZ se manifestă la nivelul tuturor containerelor și a sistemului de bază – este, de fapt, impropriu
exprimarea “nucleul unui container OpenVZ” nucleul este comun tutoror containerelor ș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, threadul principal și threadul 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 threadul principal după ce threadul 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. Întrun 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 zerouri în
momentul creării procesului (loadtime). 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 demandpaging),
va rezulta page fault, și apoi pagina va fi adusă în RAM sau alocată. Dacă pagina este marcată readonly dar de tip copyon
write (după un fork) atunci un acces de scriere la pagină va conduce la obținerea unui page fault; page faultul 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 frameuri).
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 frameuri).
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 frameuri, se obțin 9 page faulturi, iar în cazul folosirii a 4 page frameuri se obțin 10 page faulturi.
Acest fenomen poartă numele de anomalia lui Belady (http://en.wikipedia.org/wiki/Belady's_anomaly).
4. Se consideră următoarea schemă de segmentare:
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 schedulerului și un exemplu de situație în care
nu conduce la invocarea schedulerului.
Dacă apelul este blocant va conduce la invocarea schedulerului. Dacă apelul nu este blocant și nu se produce o analiză a
priorităților proceselor, nu va conduce la invocarea schedulerului. Apelul este blocant în momentul în care mutexul este
deja achiziționat. Apelul este neblocant dacă mutexul nu este achiziționat (adică este liber). În caz particular, dacă mutexul
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 inodeuri (un bit specifică folosirea sau nu a unui inode) de 32KB. Câte
symlinkuri 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 inodeuri. Întrucât un symbolic link ocupă un
inode pot fi create 256*2^10 inodeuri. Se pot scădea câteva inodeuri aferente directorului rădăcină și fișierelor reale către
care punctează symbolic linkurile.
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, threadul A a ajuns la punctul de întâlnire
* s_b, threadul B a ajuns la punctul de întâlnire.
Soluția de sincronizare este cea de mai jos:
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. Întro aplicație multithreaded 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 threaduri la nivel user, blocarea unui thread conduce la blocarea întregului proces. Threadul
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, threadul 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 threaduri 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);
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ă?
Întrun sistem cu paginare inversată, intrările din tabela de pagini conțin PIDul procesului și pagina virtuală aferentă.
Indexul intrării în tabelă reprezintă frameul aferent. Partajarea unei pagini se poate realiza în măsura în care se poate
asocia unui frame (unei pagini fizice) mai multe pagini virtuale. Întrun 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:
Î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 nonreentrantă. Explicaţi.
O funcție thread safe dar nonreentrantă 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ă threaduri, 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ă threaduri, 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 threadurilor diferite (registre precum cr2, cr3 pe o arhitectură x86).
7. De ce anumite zone din bibliotecile partajate sunt mapate readwrite? Dati un exemplu.
Bibliotecile partajate conține zone rx (cod), r (readonly data) și rw (date). Zonele readwrite 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.
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.
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 dentryuri. Numărul maxim de dentryuri 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 dentryuri, 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.
int *p_fcaller;
int *p_fcalled;
void f2(void)
{
int f2_local;
p_fcalled = &f2_local;
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ă 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ă.
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.
= SJF =
P1: [1,9]
P2: (2,12)[12,16]
P3: (3,9)[9,12]
P4: (4,16)[16,23]
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();
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
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)
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:
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?
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
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.
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.
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 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.
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?
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.
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.
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.
• 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:
Se vor folosi directoarele “directe” dupa care cele indirecte, dublu-indirecte, etc.
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:
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).
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.
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
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
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.
Nume s, i grupă:
15
Sisteme de Operare
24 mai 2014
Timp de lucru: 60 de minute
Notă: Toate răspunsurile trebuie justificate
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.
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.
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;
gets(buff);
if(strcmp(buff, "thegeekstuff")) {
} else {
pass = 1; }
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.
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.
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.
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.
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.
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
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 ) !
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.
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):
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:
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?
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
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.
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.
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.
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.
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?
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
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.
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.
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?
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?
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.
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)?
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.
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.
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):
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.
30
FARA NEGRU
Carina
CristianM
Prj Rosu <3
Remus TODO
Vlaicu
Cosminel
Mares Orange
Tanase
Iuga Turcu’
Anda
Baronescu <!>
Fabi
Trump
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.
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)
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
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).
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?
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.
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.
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)
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).
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)
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)
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); ???
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).
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.
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.
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.
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?
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?
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.
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 system API
user space
kernel space
SO2
• Curs
– Operating System Concepts
– Modern Operating Systems
• Laborator
– The Linux Programming Interface
– Windows System Programming
• 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ă)
https://en.wikipedia.org/wiki/Bus_%28computing%29
SO: Curs 1: Introducere 29
SO: Curs 1: Introducere 30
Memorie
Aplicații
Nucleu (kernel)
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)
• Securitate
• Dispozitive de mici dimensiuni (tinification)
• Scalare (CPU, memorie, disc), mașini virtuale
• Performanță, suport hardware pentru operații
specifice
SO: Curs 3
• Încapsularea/abstractizarea execuției în SO
• Rolul proceselor
• Atributele unui proces
• Planificarea proceselor
• Crearea unui proces
• Alte operații cu procese
• API pentru lucrul cu procese
Nucleul SO
Storage 9
SO: Curs 3: Procese
API folosit de procese
memorie descriptori
thread-uri
virtuală de fișier
• Interactive (foreground)
– interacționează cu utilizatorul
• Neinteractive (batch, background)
– servicii, daemoni
• PID
• parent PID
• pointeri către resurse
• stare (de rulare, așteptare)
• cuantă de timp de rulare
• contabilizare resurse consumate
• utilizator, grup
• 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?
• Componentă a SO
• Responsabilă cu planificarea proceselor
– asigurarea accesului proceselor la procesoare
– compromis (trade-off) între echitate și
productivitate
shell
child
fork()
shell
exec(“/bin/ls”)
child
ls
parent
shell wait()
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);
}
system("ps -u student");
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);
PROCESS_INFORMATION pi;
CreateProcess(NULL, “notepad”, NULL, NULL, ..., &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &retValue);
Process p = builder.start();
InputStream is = p.getInputStream();
OutputStream os = p.getOutputStream();
• Rolul proceselor
• Atribute ale unui proces
• Planificarea proceselor
• Crearea unui proces
• Alte operații cu procese
• API pentru lucrul cu procese
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 09
Cuprins
• Recapitulare thread-uri
• Contextul sincronizării
• Implementarea mecanismelor de
sincronizare
• Sincronizarea multi core
• Structuri de sincronizare
• Sisteme multiproces
- procese, thread-uri date comune
• Sisteme multicore
- magistrală comună, memorie comună
• Nevoie de comunicare
- canale de comunicație, date partajate
• 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ă
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);
[...]
}
fd = open("file", O_WRONLY);
write(fd, buffer, sizeof(buffer));
https://en.wikipedia.org/wiki/Time_of_check_to_time_of_use#Examples
• Ordonare
– wait()
– notify()
• Acces exclusiv
– lock()
– unlock()
• Acces exclusiv
– variabile atomice
– spinlock-uri
– mutex-uri
SO: Curs 09: Sincronizare 16
IMPLEMENTAREA MECANISMELOR
DE SINCRONIZARE
• Este a += 5 atomică în C?
• 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
• __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
lock = 0; /* init */
while (lock == 1)
; /* do nothing */
lock = 1; /* get lock */
• compare_and_exchange(lock, 0, 1);
• atomic for:
if (value == to_compare)
value = to_update;
return value;
• show demo
• 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
• acces concurent
- nevoie de acces exclusiv
• acces comunicativ/colaborativ
- nevoie de ordonare citire/scriere
- 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);
https://blog.grijjy.com/2017/01/12/expand-
your-collections-collection-part-2-a-generic-ring-
buffer/
• 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
• race conditions
• TOCTOU
• deadlock: așteptare mutuală
• livelock: “așteptare” cu spinlock-uri
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
3
Bit 0
Packet
Bit 15 Bit 16
I P
Bit 31
Source IP
Destination IP
TRNASPORT PROTOCOL
4
Transmisia
de
date
in
Internet
5
Transmisia
de
date
in
Internet
6
Transmisia
de
date
in
Internet
7
Cum
demul<plexam
pachetele?
8
Cum
demul<plexam
pachetele?
?
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
10
Bit 0 Bit 15 Bit 16 Bit 31
Source IP
Destination IP
TRANSPORT PROTOCOL
11
Porturi:
adresele
nivelului
transport
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)
14
API
Conexiuni
TCP:
Sumar
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
19
Transmisia
de
date
TCP
Data: 0-‐1000
20
Transmisia
de
date
TCP
Data:
Data:
0-‐1000
1000-‐2000
21
Transmisia
de
date
TCP
Data:
Data:
Data:
1-‐1000
2001-‐3000
1001-‐2000
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
23
Transmisia
de
date
TCP:
Numere
de
secventa
si
confirmari
24
Transmisia
de
date
TCP:
Numere
de
secventa
si
confirmari
SEQ
Data:
2001
2001-‐3000
25
Transmisia
de
date
TCP:
Numere
de
secventa
si
confirmari
26
Recep<a
de
date
cu
TCP
recv
(s,
buf,
max_len,
flags)
28
Recep<a
datelor:
receive
buffer
Receive
buffer
29
Recep<a
datelor:
receive
buffer
ACK
1001,
WND
2000
1-‐1000
Receive
buffer
30
Recep<a
datelor:
receive
buffer
SEQ
Data:
2001
2001-‐3000
1000-‐
1-‐1000
2000
Receive
buffer
31
Recep<a
datelor:
receive
buffer
1000-‐
1000-‐
1-‐1000
2000
3000
Receive
buffer
32
Recep<a
datelor:
receive
buffer
1000-‐
1000-‐
1-‐1000
2000
3000
Receive
buffer
33
Transmisia
de
date
cu
TCP
send
(s,
buf,
len)
35
Transmisia
datelor:
send
buffer
Send buffer
36
Transmisia
datelor:
send
buffer
37
Transmisia
datelor:
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
2
<
min(3,4)
?
2000-‐
3000-‐
3000
4000
Send
buffer
40
Transmisia
datelor:
send
buffer
3
<
min(3,4)
?
3000-‐
4000
Send
buffer
41
Transmisia
de
date
TCP:
Numere
de
secventa
si
confirmari
ACK 1001
3000-‐
4000
Send
buffer
42
Transmisia
de
date
TCP:
Numere
de
secventa
si
confirmari
SEQ
Data:
2001
2001-‐3000
3000-‐
4000
Send
buffer
43
Transmisia
de
date
TCP:
Numere
de
secventa
si
confirmari
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
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
uid = getuid();
if (setuid(uid)) {
perror("ping: setuid");
exit(-1);
}
[...]
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
lab01slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab01slides.pdf]
lab01refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab01refcard.pdf]
Visual Studio Tutorials [http://elf.cs.pub.ro/so/res/tutorial/asistvisualstudio/]
Video Introducere [http://elf.cs.pub.ro/so/res/tutorial/lab01introducere/]
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/WindowsProgramming
AddisonWesleyMicrosoftTechnology/dp/0321657748] WSP4
General
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator01 1/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
lista de discuții [http://cursuri.cs.pub.ro/cgibin/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, APIul 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 softwareului 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 multifrontend, multibackend 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ă, GCCul 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/laborator01 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 dintrun 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 dintrun 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/laborator01 3/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
#include – pentru includerea fişierelor header întrun 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/laborator01 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 printro 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 macrourilor 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 sa 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/laborator01 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, linkeditare, 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/laborator01 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/laborator01 7/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
Exemplele de până acum tratează programe scrise întrun singur fișier sursă. În realitate, aplicațiile
sunt complexe și scrierea întregului cod întrun 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 sa 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 linkedită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 linkeditarea 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/laborator01 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ă linkeditarea. Dacă sar fi obținut un executabil direct din surse atunci sar fi compilat toate
cele trei fișiere și apoi refăcută linkeditarea. 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, linkerul 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 sa rezolvat simbolul cbrt. După legarea bibliotecii matematice,
programul sa 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 întro 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/laborator01 9/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
Atenție: lintro trebuie să apară după specificarea sursei
Linkerul returnează eroare precizând că nu găsește biblioteca libintro. Aceasta deoarece linkerul
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 linkerului. 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 linkeditare. De partea cealaltă, în cazul bibliotecilor
partajate, codul este încărcat în memorie în momentul rulării.
Astfel, în momentul rulării unui program, loaderul (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. Loaderul 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 ia 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 sa precizat, pentru obținerea unui
executabil provenind din mai multe surse este ineficientă compilarea de fiecare dată a fiecărui fișier și
apoi linkeditarea. 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/laborator01 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.
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 dintrun 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/laborator01 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ă linkeditarea 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 targetului all trebuie obținut targetul hello, care este un nume de
executabil
pentru obținerea targetului hello trebuie obținut targetul hello.o
pentru obținerea targetului 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 targetului 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
sa 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/laborator01 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 (flagurile) 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 targetului.
$^ 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#AutomaticVariables]).
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/laborator01 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 linkeditarea 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/laborator01 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 warningurile
/LIBPATH:<dir> această opțiune indică linkerului 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 linkeditare.
/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/enus/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/laborator01 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/laborator01 16/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
__declspec(dllimport), este folosit pentru a importa o funcție dintro bibliotecă.
__declspec(dllexport), este folosit pentru a exporta o funcție dintro bibliotecă.
Exemplul de mai jos prezintă trei programe: două dintre ele vor fi legate întro 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/laborator01 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 parserului 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/laborator01 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 fullscreen (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 lab01tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab01tasks.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/lab01tasks.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 outputul 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 linkul 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/laborator01 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 sa oprit în
primul breakpoint întâlnit (cel adăugat anterior).
Pentru a continua execuția stepbystep, 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 watchuri 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 stepbystep (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/laborator01 20/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
Exercițiul 3 Makefiles (2p)
Acest set de exerciții se rulează din commandshellul 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/laborator01 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, folosinduvă 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/laborator01 22/23
6/11/2017 Laborator 01 Introducere [CS Open CourseWare]
lab01sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab01sol.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/ProgramLibraryHOWTO/]
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/enus/library/ms930369.aspx]
3. Nmake Macros [http://msdn.microsoft.com/enus/library/ms933742.aspx]
4. Dynamic link library [http://en.wikipedia.org/wiki/Dynamiclink_library]
5. Creating and using DDL's [http://www.flipcode.com/archives/Creating_And_Using_DLLs.shtml]
6. Dynamic libraries [http://herbert.thelittleredhairedgirl.org/en/ctips/windows/index.html]
so/laboratoare/laborator01.txt · Last modified: 2017/02/27 17:10 by elena.sandulescu
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator01 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
lab02slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab02slides.pdf]
lab02refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab02refcard.pdf]
Video Operații IO [http://elf.cs.pub.ro/so/res/tutorial/lab02operatiiio/]
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 casesensitive (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 homeul
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 întrunul 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 dintro 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ă întrun shell Unix, procesul
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator02 1/16
6/11/2017 Laborator 02 Operații I/O simple [CS Open CourseWare]
părinte (shellul) 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 handleul, 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 intrareieșire pe Windows, sa ales ca
toate apelurile să facă parte din APIul 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 (offsetul) 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 (handleului) 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 dintrun fișier înseamnă copierea unui bloc de date întrun buffer; după ce se
3)
realizează citirea se actualizează cursorul de fișier . ( Linux, Windows )
scrierea întrun fișier înseamnă copierea unui bloc de date dintrun buffer în fișier;
4)
efectuarea scrierii înseamnă și actualizarea cursorului de fișier . ( Linux, Windows )
poziționarea întrun 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/laborator02 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 flagul 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:
io01.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 flagul O_CREAT setat.
Scrierea și citirea
read
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator02 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 sa 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 offseturi de 64 de biți pentru a putea specifica
offseturi 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 offsetul
este pe 64 de biți.
Trunchierea fișierelor
Pe lângă trunchierea la 0 care se poate face prin apelul open cu flagul 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/laborator02 4/16
6/11/2017 Laborator 02 Operații I/O simple [CS Open CourseWare]
Exemplu utilizare operații I/O
io2.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
linkurile 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/laborator02 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 flagurile pentru fd
F_SETFD setează flagurile pentru fd la valoarea specificată de arg
F_GETFL citește flagurile de stare pentru fd
F_SETFL setează flagurile 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 PIDului 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/enus/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/enus/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/laborator02 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 handleul 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/enus/library/aa365239(VS.85).aspx] și
ReplaceFile [http://msdn.microsoft.com/enus/library/aa365512(VS.85).aspx]. Un exemplu de schimbare a
atributelor găsiți aici [http://msdn.microsoft.com/enus/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 handleuri
CloseHandle [http://msdn.microsoft.com/enus/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/enus/library/aa363915%28VS.85%29.aspx]
CloseHandle(hFile);
DeleteFile("myfile.txt");
unde DeleteFile [http://msdn.microsoft.com/enus/library/aa363915%28VS.85%29.aspx] are signatura
BOOL DeleteFile(LPCTSTR lpFileName);
Citirea și scrierea
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator02 7/16
6/11/2017 Laborator 02 Operații I/O simple [CS Open CourseWare]
ReadFile
ReadFile [http://msdn.microsoft.com/enus/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) întrun buffer și întoarce întro 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/enus/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/enus/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 sa ajuns la sfârșitul de fișier.
WriteFile
Apelul WriteFile [http://msdn.microsoft.com/enus/library/aa365747%28VS.85%29.aspx] copiază în mod
sincron sau asincron un număr specificat de octeți dintrun buffer în conținutul unui fișier și returnează
întro 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 */
); );
Handleul 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/enus/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/enus/library/aa365541(VS.85).aspx]:
DWORD SetFilePointer( /* Example: How to get current position */
HANDLE hFile,
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator02 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 handleul 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
deal 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/enus/library/aa365542(VS.85).aspx] a apelului
SetFilePointer [http://msdn.microsoft.com/enus/library/aa365541(VS.85).aspx] memorează valoarea
cursorului întrun 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/laborator02 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;
}
Wrappere
Î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ândule 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.
iowrapper.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/laborator02 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ă wrapperul
De asemenea, se observă că semnăturile funcțiilor definite sunt identice pentru ambele sisteme de
operare. Iată un exemplu de implementare a lor:
iowrapper.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 întrun 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/laborator02 11/16
6/11/2017 Laborator 02 Operații I/O simple [CS Open CourseWare]
În rezolvarea laboratorului folosiți arhiva de sarcini lab02tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab02tasks.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 macroul 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 skillurile de programare, fiind răsplătiți cu o
bursă a cărei valoare depinde de țară [https://developers.google.com/opensource/gsoc/help/studentstipends]
(pagină principală GSOC [https://developers.google.com/opensource/gsoc]).
UPB se află în top ca număr de studenți acceptați; în fiecare an fiind undeva la aprox. 3040 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
filedescriptori.
În cod, observați parametrii cu care sa 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/laborator02 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 macroul 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 outputul să fie redirectat întrun 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/laborator02 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 promptul 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 CRCul pentru fiecare
bucată de 512 bytes din fișier și o salvează întrun 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 CRCul, iar ca al doilea
argument fișierul în care se salvează CRCurile 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/laborator02 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 dintrun 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/enus/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 dintrun 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 întro structură de forma: WIN32_FIND_DATA
[http://msdn.microsoft.com/enus/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/enus/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/laborator02 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 APIuri
Soluții
lab02sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab02sol.zip]
Resurse utile
1. Low level I/O [http://www.gnu.org/software/libc/manual/html_node/Low_002dLevelI_002fO.html] (info
libc “LowLevel 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/alpfolder/alpapBlowlevelio.pdf] (Advanced
Linux Programming)
4. File management functions [http://msdn.microsoft.com/enus/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/laborator02.txt · Last modified: 2017/03/06 17:44 by elena.sandulescu
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator02 16/16
6/11/2017 Laborator 03 Procese [CS Open CourseWare]
Laborator 03 Procese
Materiale ajutătoare
lab03slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab03slides.pdf]
lab03refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab03refcard.pdf]
Video Procese [http://elf.cs.pub.ro/so/res/tutorial/lab03procese/]
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 întro 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
handlerele 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ă întrun mediu specificat printrun 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 printro serie de apeluri de bibliotecă (Linux, Windows).
Pipeurile (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 pipeului 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 pipeuri:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator03 1/19
6/11/2017 Laborator 03 Procese [CS Open CourseWare]
pipeuri 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 pipeuri nu mai există după ce
procesele șiau terminat execuția.
pipeuri 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 pidul procesului nou creat (copil).
În procesul copil apelul va întoarce 0.
Pentru aflarea PIDului 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
PIDul procesului apelant:
pid_t getpid(void);
Funcția getppid [http://linux.die.net/man/3/getppid]
întoarce PIDul 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 dintrun 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 handlerele
semnalelor.
PIDul și descriptorii de fișier care nu au setat flagul CLOSE_ON_EXEC rămân neschimbați
(implicit, flagul 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/laborator03 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 sa 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 sa î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);
Dintrun program C există trei moduri de invocare a acestui apel de sistem:
1. apelul _exit [http://linux.die.net/man/2/exit] (POSIX.12001 [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/laborator03 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 sa 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
la 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ă întro 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 sa 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/laborator03 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
(lockuri, cursor, flaguri) 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).
Flagul 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 flagul CLOSE_ON_EXEC
setat.
fcntl
Pentru a seta flagul 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 flagul 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/laborator03 5/19
6/11/2017 Laborator 03 Procese [CS Open CourseWare]
Pentru a rezolva această condiție de cursă, sau 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 deal 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);
Pipeuri în Linux
Pipeuri anonime în Linux
Pipeul 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ă printrun pipe anonim trebuie să aibă un grad de rudenie; de obicei,
un proces care creează un pipe va apela după aceea fork, iar pipeul se va folosi pentru comunicarea
între părinte și fiu. În orice caz, procesele care comunică prin pipeuri 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/laborator03 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 pipeuri este atomică dacă nu se citesc/scriu mai mult de PIPE_BUF
octeți.
citirea/scrierea din/în pipeuri se realizează cu ajutorul funcțiilor read/write.
Majoritatea aplicațiilor care folosesc pipeuri î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 dintrun 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 pipeurilor
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 întrun 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 pipeurile, 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 pipeului în procesul părinte).
Alte funcții utile: popen [http://linux.die.net/man/3/popen], pclose [http://linux.die.net/man/3/pclose].
Pipeuri 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 întrun 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 pipeurilor de tip FIFO este mkfifo [http://linux.die.net/man/3/mkfifo]:
int mkfifo(const char *pathname, mode_t mode);
După ce pipeul 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/laborator03 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 dintrun 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
APIul Windows mai pune la dispoziție câteva funcții înrudite precum CreateProcessAsUser
[http://msdn.microsoft.com/enus/library/ms682429%28VS.85%29.aspx], CreateProcessWithLogonW
[http://msdn.microsoft.com/enus/library/ms682431%28VS.85%29.aspx] ori CreateProcessWithTokenW
[http://msdn.microsoft.com/enus/library/ms682434%28VS.85%29.aspx], care permit crearea unui proces
întrun context de securitate diferit de cel al utilizatorului curent.
Pentru a se obține un handle al unui proces, cunoscânduse PIDul procesului respectiv, se va apela
funcția OpenProcess [http://msdn.microsoft.com/enus/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/enus/library/ms683179(VS.85).aspx]:
HANDLE GetCurrentProcess(void);
Pentru a obține PIDul 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 handleuri ale proceselor
create de el, însă handleurile pot fi duplicate între procese ceea ce duce la situația în care un proces
deține handleuri ale unor procese care nu sunt create de el, deci ierarhia implicită dispare.
Deoarece funcția CreateProcess [http://msdn.microsoft.com/enus/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/laborator03 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 șia
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 sa terminat un anumit proces, se va apela funcția
GetExitCodeProcess [http://msdn.microsoft.com/enus/library/ms683189.aspx]:
BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD lpExitCode);
Dacă procesul hProcess nu sa terminat încă, funcția va întoarce în lpExitCode codul de terminare
STILL_ACTIVE. Dacă procesul sa 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/enus/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 DLLurile 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/laborator03 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/enus/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/enus/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/enus/library/ms686714(VS.85).aspx] este periculoasă
și se recomandă folosirea ei doar în cazuri extreme, deoarece ea nu notifică DLLurile 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/laborator03 10/19
6/11/2017 Laborator 03 Procese [CS Open CourseWare]
bRes = GetExitCodeProcess(pi.hProcess, &dwRes);
DIE(bRes == FALSE, "GetExitCode");
return 0;
}
Moștenirea handleurilor la CreateProcess
După un apel CreateProcess [http://msdn.microsoft.com/enus/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/enus/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 handlerelor î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.
Handleurile 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/enus/library/ms683231(VS.85).aspx]:
HANDLE GetStdHandle(DWORD nStdHandle);
cu unul din parametrii:
STD_INPUT_HANDLE
STD_OUTPUT_HANDLE
STD_ERROR_HANDLE
Pentru redirectarea handleurilor 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 handleuri să rămână implicite, li se
poate atribui handleul î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 handleuri ale unor zone de memorie alocate de procesul părinte și nici
pseudodescriptori precum cei întorși de funcția GetCurrentProcess [http://msdn.microsoft.com/en
us/library/ms683179%28VS.85%29.aspx].
Handleul din procesul părinte și cel moștenit în procesul copil vor referi același obiect, exact ca în
cazul duplicării. De asemenea, handleul moștenit în procesul copil are aceeași valoare și aceleași
drepturi de acces ca și handleul din procesul părinte. Pentru a folosi handleul moștenit, procesul copil
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator03 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
printrun 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/enus/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/enus/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 Blockul 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/enus/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/enus/library/ms683151(VS.85).aspx]:
BOOL FreeEnvironmentStrings(
LPTSTR lpszEnvironmentBlock
);
Un proces copil va moșteni Environment Blockul părintelui dacă acesta apelează CreateProcess,
cu parametrul lpEnvironment = NULL.
Se poate obține Environment Blockul unui alt utilizator prin intermediul funcției
CreateEnvironmentBlock [http://msdn.microsoft.com/enus/library/bb762270(VS.85).aspx]:
BOOL CreateEnvironmentBlock(
LPVOID* lpEnvironment,
HANDLE hToken,
BOOL bInherit
);
Trebuie să pasăm hToken, tokenul 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/enus/library/aa378184.aspx]:
BOOL LogonUser(
LPTSTR lpszUsername,
LPTSTR lpszDomain,
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator03 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 Blockul, obținut prin CreateEnvironmentBlock, poate fi transmis ca parametru
funcției CreateProcessAsUser [http://msdn.microsoft.com/enus/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
);
Pipeuri in Windows
Pipeuri anonime în Windows
Ca și pe Linux, pipeurile 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/enus/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/enus/library/aa379560%28v=VS.85%29.aspx] să fie
setat pe TRUE.
CreatePipe creează atât pipeul, cât și handlerurile folosite pentru scriere/citire din/în pipe cu
ajutorul funcțiilor ReadFile [http://msdn.microsoft.com/enus/library/aa914377.aspx] și WriteFile
[http://msdn.microsoft.com/enus/library/aa910675.aspx].
ReadFile [http://msdn.microsoft.com/enus/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/enus/library/aa910675.aspx] se termină atunci când toți octeții au fost
scriși. Dacă bufferul pipeului este plin înainte ca toți octeții să fie scriși, WriteFile
[http://msdn.microsoft.com/enus/library/aa910675.aspx] rămâne blocat până când alt proces sau thread
folosește ReadFile [http://msdn.microsoft.com/enus/library/aa914377.aspx] pentru a face loc în buffer.
Pipeurile 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.
Pipeuri 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/laborator03 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 handleuri și buffere.
Pipeurile 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/enus/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 pipeului la una din funcțiile CreateFile
[http://msdn.microsoft.com/enus/library/aa363858%28VS.85%29.aspx] sau CallNamedPipe
[http://msdn.microsoft.com/enus/library/aa365144(VS.85).aspx] ultima funcţie este mai utilă pentru
transmiterea de mesaje.
Un exemplu funcțional folosind pipeuri cu nume se află aici [http://msdn.microsoft.com/en
us/library/aa365588%28v=VS.85%29.aspx]
Mai multe detalii despre moștenirea pipeurilor 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 pipeurile 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/laborator03 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 pipeurile 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/enus/library/aa365443%28VS.85%29.aspx] întoarce
informații despre anumite atribute cum ar fi: tipul de comunicare (mesaj sau bytestream), numărul
de instanțe, dacă a fost deschisă în mod blocant sau neblocant.
SetNamedPipeHandleState [http://msdn.microsoft.com/enus/library/aa365787%28VS.85%29.aspx] permite
modificarea atributelor unui pipe
Exerciții
În rezolvarea laboratorului folosiți arhiva de sarcini lab03tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab03tasks.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 skillurile de programare, fiind răsplătiți cu o
bursă a cărei valoare depinde de țară [https://developers.google.com/opensource/gsoc/help/studentstipends]
(pagină principală GSOC [https://developers.google.com/opensource/gsoc]).
UPB se află în top ca număr de studenți acceptați; în fiecare an fiind undeva la aprox. 3040 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/laborator03 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țil 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 ), pidul procesului părinte
(coloana
PPID ) devine 1, întrucât procesul este adoptat de
init după terminarea procesului său
părinte.
Exercițiul 3 TinyShell (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. Shellul 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/laborator03 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 tinyshell trebuie să suporte redirectarea outputului unei
comenzi (stdout) întrun 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 TinyShell on Windows compilarea se va realiza din Visual Studio sau din
commandpromptul 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 1bomb.exe
Exercițiul 2 TinyShell on Windows (3.5p)
Ne propunem să continuăm implementarea de TinyShell.
Compilarea se va realiza din Visual Studio sau din commandpromptul de Visual Studio, iar rularea
executabilului tiny.exe se va realiza din Cygwin.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator03 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 1tiny. 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 HANDLErelor.
Completați funcția RedirectHandle.
Atenție! Trebuie inițializate toate handlerele structurii STARTUPINFO.
Urmăriți în cod comentariile cu TODO 1.
Revedeți secțiunea Moștenirea handleurilor.
Atenție la metoda de moștenire a handlerelor
Pentru testare puteți folosi comenzile:
./tiny
> ls ‐al > out
> cat out
> exit
2c. Implementarea unei comenzi cu pipeuri (1.5p)
Shellul 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 pipeurilor.
Pentru redirectari, folosițivă de funcția RedirectHandle.
Revedeți secțiunea despre Pipeuri anonime în Windows.
Pentru testare puteți folosi comenzile:
./tiny
> cat Makefile | grep tiny
> exit
BONUS
1 so karma Pipeuri cu nume (Linux/Windows)
Realizați două programe, denumite server și client, care interacționează printrun pipe cu nume.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator03 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 Pipeuri cu nume în Linux.
Windows:
Citiți secțiunea Pipeuri cu nume în Windows.
Puteți porni de la exemplul [http://msdn.microsoft.com/enus/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
lab03sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab03sol.zip]
Resurse utile
1. Fork Wikipedia [http://en.wikipedia.org/wiki/Fork_(operating_system)]
2. About Fork and Exec [http://wwwh.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/laborator03.txt · Last modified: 2017/03/14 12:51 by ioana_elena.ciornei
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator03 19/19
6/11/2017 Laborator 04 Semnale [CS Open CourseWare]
Laborator 04 Semnale
Materiale ajutătoare
lab04slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab04slides.pdf]
lab04refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab04refcard.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, carei 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 oferindui 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ânduse. 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/laborator04 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 întrunul 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 sa 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/laborator04 2/16
6/11/2017 Laborator 04 Semnale [CS Open CourseWare]
Când un semnal este generat, el intră întro 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 handlerul rulează, acel tip de semnal este în
mod normal blocat (deblocarea se va face printro cerere explicită în handlerul 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/laborator04 3/16
6/11/2017 Laborator 04 Semnale [CS Open CourseWare]
Observați că semnalul SIGKILL nu poate fi handleuit (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 sa 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, shellul 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, printro 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 handlerul 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 deal doilea. De la glibc v2 incoace, comportamentul este
acelasi ca si pe BSD, totul depinzand de definitia macrouului _BSD_SOURCE. Comportamentul poate fi
controlat de catre programator folosind sigaction cu flagul 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/laborator04 4/16
6/11/2017 Laborator 04 Semnale [CS Open CourseWare]
Numele de semnale sunt definite în headerul 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/manpages/online/pages/man3/strsignal.3.html] și psignal
[http://www.kernel.org/doc/manpages/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/manpages/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/laborator04 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/manpages/online/pages/man2/sigprocmask.2.html] este SIG_SETMASK, care
specifică pur și simplu că vechea mască (al treilea parametru) e înlocuită cu cel deal 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 handlere pentru tratarea unui semnal era signal
[http://www.kernel.org/doc/manpages/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/laborator04 6/16
6/11/2017 Laborator 04 Semnale [CS Open CourseWare]
Dacă în câmpul sa_flags se precizează flagul SA_SIGINFO, handlerul folosit este cel specificat de
sa_sigaction. Altfel, handlerul folosit este sa_handler. Masca de semnale care ar trebui blocate în
timpul execuției handlerului 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ă flagul SA_SIGINFO este setat, se folosește câmpul sa_sigaction al structurii sigaction
pentru a specifica handlerul asociat semnalului. Handlerul 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/manpages/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/laborator04 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ă pidul 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 handlerul
de semnal. În mod evident, nu are sens transmiterea de pointeri dintrun 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 busywaiting) 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/manpages/online/pages/man3/sleep.3.html]:
while (!signal_has_arrived) {
sleep(1);
}
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator04 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/manpages/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, deadlockuri, 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/manpages/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/manpages/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țânduse 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 întrun 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/laborator04 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 întro 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 timerelor 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, timerul expiră. În Linux, expirarea timerului 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 handlerului care va rula. Configurarea handlerului se poate realiza atât prin intermediul
funcției sigaction (în momentul in care timerul expiră se generază un semnal, care la rândul lui
generează rularea handlerului asociat), sau direct prin intermediul parametrilor funcției timer_create
[http://www.kernel.org/doc/manpages/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)
Timerul creat se identifică prin timerid. Prin intermediul structurii sigevent se setează modul în
care va interacționa timerul cu procesul/threadul care la 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 userspace și kernelspace. La timeout timerul 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 timerului presupune completarea structurii itimerspec în care specifică timpul de pornire al
timerului, cât și intervalul de expirare al timeoutului (intervalele sunt măsurate în secunde și
nanosecunde). Exemplu de folosire:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator04 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 timerelor este implementarea funcțiilor de așteptare de tipul sleep
[http://www.kernel.org/doc/manpages/online/pages/man3/sleep.3.html] sau nanosleep
[http://www.kernel.org/doc/manpages/online/pages/man2/nanosleep.2.html]. Avantajul folosirii funcției sleep
[http://www.kernel.org/doc/manpages/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/manpages/online/pages/man2/nanosleep.2.html] are un apel mai complex, dar
oferă rezoluție până la ordinul nanosecundelor și este signalsafe (nu interacționează cu semnale).
Timere sub Windows
În Windows există mai multe mecanisme de notificare a proceselor: evenimente
[http://msdn.microsoft.com/enus/library/ms682655(VS.85).aspx], APCuri [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. APCurile 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/enus/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 timerul 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/enus/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/laborator04 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 lab04tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab04tasks.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 skillurile de programare, fiind răsplătiți cu o
bursă a cărei valoare depinde de țară [https://developers.google.com/opensource/gsoc/help/studentstipends]
(pagină principală GSOC [https://developers.google.com/opensource/gsoc]).
UPB se află în top ca număr de studenți acceptați; în fiecare an fiind undeva la aprox. 3040 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/laborator04 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? Întro 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/manpages/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 RealTime 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 întro consolă programul signals:
./signals
Pentru cazul semnalelor normale, întro altă consolă rulați scriptul send_normal.sh:
./send_normal.sh
Pentru semnalele real‐time, întro 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 “Realtime 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/allaboutlinuxsignals?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). Handlerul 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 signalsafe.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator04 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 întrun fișier definit
prin macroul NOHUP_OUT_FILE.
Folosiți apelul isatty [http://www.kernel.org/doc/manpages/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.
Dintro 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 shellul 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 TODOurile).
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/laborator04 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 handlerului 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 timeoutului. Timeoutul
este negativ și expiră la atingerea valorii 0. Al treilea argument al SetWaitableTimerObject
[http://msdn.microsoft.com/enus/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 timerului și afișarea mesajului.
folosiți exemplul de utilizare a timerelor cu APCuri din documentația MSDN
[http://msdn.microsoft.com/enus/library/ms686898(VS.85).aspx].
ignorați warningurile de compilare.
completați funcțiile din fișierul sursă.
folosiți ctime [http://www.kernel.org/doc/manpages/online/pages/man3/ctime.3.html] și time
[http://www.kernel.org/doc/manpages/online/pages/man2/time.2.html] pentru afișarea timpului curent.
ctime adaugă un caracter newline (\n) la sfârșitul șirului întors.
folosiți SleepEx [http://msdn.microsoft.com/enus/library/ms686307(VS.85).aspx] și argumentele
INFINITE, pentru a aștepta nedefinit, și TRUE, pentru a forța intrarea procesului întro stare
alertabilă (care să declanșeze rularea APCului).
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/manpages/online/pages/man2/timer_create.2.html] și
timer_settime [http://www.kernel.org/doc/manpages/online/pages/man2/timer_settime.2.html] pentru a crea
și arma timerul. Semnalul generat de timer va fi SIGALRM. Urmăriți secțiunea Timere în Linux
Folosiți sigsuspend [http://www.kernel.org/doc/manpages/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/manpages/online/pages/man2/sigprocmask.2.html], masca procesului curent și
transmitețio 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/laborator04 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/programmingposixthreads/sigev_thread.c].
Soluții
Soluții exerciții laborator 4 [http://elf.cs.pub.ro/so/res/laboratoare/lab04sol.zip]
Resurse utile
TLPI Chapter 22: Signals Advanced features
TLPI Chapter 23: Timers and Sleeping
WSP Chapter 14: Asynchronous IO Waitable Timers
so/laboratoare/laborator04.txt · Last modified: 2017/03/30 13:10 by theodor.stoican
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator04 16/16
6/11/2017 Laborator 05 Gestiunea memoriei [CS Open CourseWare]
Laborator 05 Gestiunea memoriei
Materiale ajutătoare
lab05slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab05slides.pdf]
lab05refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab05refcard.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 leakuri, 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 userspace pentru un proces.
Cele 4 zone importante din spațiul de adresă al unui proces sunt zona de date, zona de cod, stiva și
heapul. După cum se observă și din figură, stiva și heapul 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/laborator05 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ă readonly 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 întrun
program și variabilele de tipul readonly. Î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 frameuri”. 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
heapul 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/laborator05 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.
Heapul
Heapul este zona de memorie dedicată alocării dinamice a memoriei. Heapul este folosit pentru
alocarea de regiuni de memorie a căror dimensiune este determinată la runtime.
La fel ca și stiva, heapul este o regiune dinamică care își modifică dimensiunea. Spre deosebire de
stivă, însă, heapul 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/laborator05 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 dintrun 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ă întro 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 heapului 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 heapului aici
[http://academicearth.org/lectures/heapmanagement] ș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/laborator05 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], pointerul 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/laborator05 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/UnconstrainedAllocation.html#UnconstrainedAllocationl] ș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ă Heapul 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 heapului.
Pentru crearea, respectiv distrugerea unui Heap se vor folosi funcțiile HeapCreate
[http://msdn.microsoft.com/enus/library/aa366599%28VS.85%29.aspx] și HeapDestroy
[http://msdn.microsoft.com/enus/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 heapului implicit al procesului (în cazul în care nu dorim crearea altor
heapuri) se va apela funcția GetProcessHeap [http://msdn2.microsoft.com/enus/library/aa366569.aspx].
Pentru a obține descriptorii tuturor heapurilor procesului se va apela GetProcessHeaps
[http://msdn2.microsoft.com/enus/library/aa366571.aspx].
Există, de asemenea, funcții care enumeră toate blocurile alocate întrun heap, validează unul sau
toate blocurile alocate întrun heap sau întorc dimensiunea unui bloc pe baza descriptorului de heap și a
adresei blocului: HeapWalk [http://msdn.microsoft.com/enus/library/aa366710%28VS.85%29.aspx],
HeapValidate [http://msdn.microsoft.com/enus/library/aa366708%28VS.85%29.aspx], HeapSize
[http://msdn.microsoft.com/enus/library/aa366706%28VS.85%29.aspx].
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator05 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 heapul 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/laborator05 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.
leakurile 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ă întro 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 întro pagină validă,
hardwareul ș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 heapului ș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/laborator05 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 frameurile tuturor funcțiilor apelate imbricat.
GDB dă posibilitatea utilizatorului să examineze frameurile prezente în stivă. Astfel, utilizatorul poate
alege oricare din frameurile prezente folosind comanda frame. După cum sa observat, exemplul
anterior are un bug ce se manifestă atunci când numărul introdus de la tastatură depășește
dimensiunea bufferului 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 sa 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 sa 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/laborator05 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 sa î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 heapului
glibc permite verificarea consistenței heapului prin intermediul apelului mcheck
[http://www.gnu.org/software/libc/manual/html_node/HeapConsistencyChecking.html#HeapConsistency
Checking] definit în mcheck.h. Apelul mcheck [http://www.gnu.org/software/libc/manual/html_node/Heap
ConsistencyChecking.html#HeapConsistencyChecking] 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 heapului:
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/laborator05 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
ConsistencyChecking.html#HeapConsistencyChecking] din manualul glibc
[http://www.gnu.org/software/libc/manual].
Leakuri 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 leakurile 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
leakurilor 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/mcmanual.html], un utilitar care permite detectarea de
erori de lucru cu memoria (accese nevalide, memory leakuri 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/laborator05 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/mcmanual.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 leakuri
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.
Sa folosit utilitarul Memcheck [http://valgrind.org/docs/manual/mcmanual.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/laborator05 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/laborator05 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ă leakuri 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 leakurilor de memorie ale unui
program.
Utilitarul mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracingmalloc.html#Tracingmalloc] se
folosește cu apelurile mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracing
malloc.html#Tracingmalloc] și muntrace [http://www.gnu.org/software/libc/manual/html_node/Tracing
malloc.html#Tracingmalloc] implementate în biblioteca standard C:
void mtrace(void);
void muntrace(void);
Utilitarul mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracingmalloc.html#Tracingmalloc]
introduce handlere pentru apelurile de biblioteca pentru lucrul cu memoria (malloc, realloc, free).
Apelurile mtrace [http://www.gnu.org/software/libc/manual/html_node/Tracingmalloc.html#Tracingmalloc] și
muntrace [http://www.gnu.org/software/libc/manual/html_node/Tracingmalloc.html#Tracingmalloc] 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/laborator05 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/AllocationDebugging.html#AllocationDebugging] 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/laborator05 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/laborator07#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 lab05tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab05tasks.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 dumpul 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țile.
Observați că se generează leakuri 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/laborator05 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 frameul curent cu frameul 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țio.
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 6stack ș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/laborator05 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 sa 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 (IPul 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.
Folosinduvă 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 conventionul pe baza căruia se construiește stack frameul 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 (x8664 [https://aaronbloomfield.github.io/pdr/book/x8664bitcccchapter.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țio (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. Folosinduvă de variabila w afișați
numărul n=0xDEADBEEF.
Ce tip de arhitectură se folosește? (bigendian sau littleendian, vezi aici
[http://en.wikipedia.org/wiki/Endianness] pentru detalii). Gândițivă 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
frameul 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/laborator05 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() întro 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 pointerul de eliberat (se apelează HeapFree
și pointerul 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
heapului (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
lab05sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab05sol.zip]
Resurse utile
Linux System Programming Chapter 8 Memory Management
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator05 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/enus/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_satishgiridhar/]
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/laborator05.txt · Last modified: 2017/03/29 12:11 by adrian.stanciu
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator05 20/20
6/11/2017 Laborator 06 Memoria virtuală [CS Open CourseWare]
Laborator 06 Memoria virtuală
Materiale ajutătoare
lab06slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab06slides.pdf]
lab06refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab06refcard.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 sa 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/laborator06 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/laborator05#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/curs06#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 întro 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 faulturi, 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 faulturi.
Apelul malloc este mai eficient decât apelul calloc pentru că nu parcurge spațiul alocat pentru al
umple cu zerouri. 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 dintrun vector. Eficiența metodei vine din faptul că zona de memorie este
gestionată similar cu memoria virtuală, supunânduse 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. Socketurile, pipeurile, 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/laborator06 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 sa 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 offsetul 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 sa 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 copyonwrite; 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/laborator06 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ă flagul MAP_ANONYMOUS.
Maparea dispozitivelor
Există chiar și posibilitatea ca aplicațiile să mapeze în spațiul de adresă al unui proces un dispozitiv de
intrareieș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 întrun
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 sar folosi flagul 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 întro 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/laborator06 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 kernelul (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 harddisk, kernelul 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/laborator06 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 flaguri:
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ă:
Flagul 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ă sau 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ă sa făcut un acces nealiniat la memorie, BUS_ADRERR pentru a arăta că sa î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/laborator06 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 underrunul, î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 linkeditare
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/laborator06 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 12lea 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, întro 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/enus/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/enus/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 handleul fișierului care se dorește a fi mapat, atribute de securitate
care controlează accesul la handleul obiectului FileMapping creat, tipul mapării (PAGE_READONLY,
PAGE_READWRITE, PAGE_WRITECOPY pentru copyonwrite) ș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/enus/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/enus/library/aa366761%28VS.85%29.aspx]:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator06 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 copyonwrite), offsetul î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/enus/library/aa366887%28VS.85%29.aspx] sau VirtualAllocEx
[http://msdn.microsoft.com/enus/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/enus/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 copyonwrite. 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/laborator06 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/enus/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/enus/library/aa366892%28VS.85%29.aspx] și VirtualFreeEx
[http://msdn.microsoft.com/enus/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/enus/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 dea 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/enus/library/aa366898%28VS.85%29.aspx] și VirtualProtectEx
[http://msdn.microsoft.com/enus/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/laborator06 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/enus/library/aa366902%28VS.85%29.aspx] și VirtualQueryEx
[http://msdn.microsoft.com/enus/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/enus/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/laborator06 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/enus/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ă sa efectuat o operație de citire sau 1
dacă sa efectuat o operație de scriere. A doua intrare din ExceptionInformation va conține adresa
virtuală la care sa î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/laborator06 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 lab06tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab06tasks.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
Întro 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 dea 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
(readexecute), zona .rodata (readonly) și zona .data (readwrite).
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 outputul 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/laborator06 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 makefileul 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 hinturi:
Î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 readonly.
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/manpages/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/laborator06 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 întrun 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 faulturi (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/opensource/commandline
hacks/linuxmonitorprocessusingpidstat]) din pachetul sysstat pentru a monitoriza page faulturile
făcute de un proces.
Dacă întâmpinați probleme în instalarea pachetului sysstat, descărcațil de aici
[http://ro.archive.ubuntu.com/ubuntu/pool/main/s/sysstat/sysstat_11.2.01_i386.deb] și instalațil 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. Întro altă consolă executați comanda
pidstat ‐r ‐T ALL ‐p $(pidof fork‐faults) 5
pentru a urmări page faulturile. 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 outputul
comenzii pidstat. Urmăriți evoluția numărului de page faulturi pentru cele două procese: părinte și
copil. Page faulturile care apar în cazul unui copyonwrite î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 flagul ENABLED din
/etc/default/sysstat. Cu ajutorul utilitarului sar puteți monitoriza informații precum încărcarea
CPUului, utilizarea memoriei și a paginilor, operațiile de I/O, activitatea proceselor. Detalii puteți afla
din tutorial sar [http://www.cyberciti.biz/tips/identifyinglinuxbottleneckssargraphswithksar.html].
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator06 15/18
6/11/2017 Laborator 06 Memoria virtuală [CS Open CourseWare]
Exercițiul 7 Blocarea paginării (1p)
Vă aflați întro situație în care trebuie să procesați în timp real datele dintrun 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, întro 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 pidul 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/laborator06 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ă sa
folosit GetFileAttributesEx [http://msdn.microsoft.com/enus/library/aa364946(VS.85).aspx]. Fișierul
destinație trebuie trunchiat la dimensiunea fișierului sursă folosind SetFilePointer
[http://msdn.microsoft.com/enus/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 întro situație în care trebuie să procesați în timp real datele dintrun 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 dintrun fișier, aflați în această
arhivă [http://elf.cs.pub.ro/so/res/laboratoare/lab06extra.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/manpages/man2/mmap.2.html] din Linux?
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator06 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/lab06sol.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/enus/library/ms810627.aspx]
MSDN: Managing MemoryMapped Files in Win32 [http://msdn2.microsoft.com/en
us/library/ms810613.aspx]
MSDN: Structured Exception Handling [http://msdn2.microsoft.com/enus/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/laborator06.txt · Last modified: 2017/04/12 15:11 by theodor.stoican
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator06 18/18
6/11/2017 Laborator 07 Profiling & Debugging [CS Open CourseWare]
Laborator 07 Profiling & Debugging
Materiale ajutătoare
lab07slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab07slides.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/electricalengineeringandcomputer
science/6172performanceengineeringofsoftwaresystemsfall2010/videolectures/lecture5performanceengineeringwith
profilingtools/]
Latency Comparison Numbers
Credits:
By Jeff Dean: http://research.google.com/people/jeff/ [http://research.google.com/people/jeff/]
Originally by Peter Norvig: http://norvig.com/21days.html#answers [http://norvig.com/21days.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
Profilerele 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)
Profilerele 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 întro perioadă de timp.
Suport pentru profiler
Suportul pentru profilere este disponibil la nivel de:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator07 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 missuri.
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, cachemissuri, 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, cpuclock, 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 perflist [http://manpages.ubuntu.com/manpages/natty/man1/perflist.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/laborator07 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
perfstat [http://manpages.ubuntu.com/manpages/lucid/man1/perfstat.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 perftop [http://manpages.ubuntu.com/manpages/natty/man1/perftop.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 perftop
corespunzător rulării comenzii de listare recursivă a directorului home.
perf record
man perfrecord [http://manpages.ubuntu.com/manpages/natty/man1/perfrecord.1.html]
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator07 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 perfreport [http://manpages.ubuntu.com/manpages/natty/man1/perfreport.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, outputul va fi pus în
fişierul filename
‐p pid, pidul 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 sa 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/laborator05#valgrind].
Alte utilitare
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator07 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=d6e952598d9d4c2289c4fad382eddcd1&displaylang=en]
este un echivalent al oprofile pentru Windows.
KCachegrind [http://kcachegrind.sourceforge.net/html/Home.html]
perftools [http://code.google.com/p/googleperftools/]
XPerf [http://blogs.msdn.com/ntdebugging/archive/2008/04/03/windowsperformancetoolkitxperf.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 lab07tasks.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab07tasks.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/laborator07 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 perfscriptpython [http://man7.org/linux/manpages/man1/perfscriptpython.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 columnmajor sau rowmajor (rowmajororder
[http://en.wikipedia.org/wiki/Rowmajor_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 cachemissuri 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 întrun ș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 outputul.
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 deal
treilea element al arrayului 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/laborator07 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.
Folosinduvă 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/lab07sol.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/gprofvalgrindgperftoolsevaluationtoolsapplicationlevelcpuprofilinglinux/]
Linux Profiling Tools and Techniques [http://www.pixelbeat.org/programming/profiling/]
so/laboratoare/laborator07.txt · Last modified: 2017/04/11 08:16 by adrian.stanciu
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator07 7/7
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
Laborator 08 Threaduri Linux
Materiale ajutătoare
lab08slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab08slides.pdf]
lab08refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab08refcard.pdf]
Nice to read
TLPI Chapter 29, Threads: Introduction
TLPI Chapter 30, Threads: Thread Synchronization
TLPI Chapter 31, Threads: Thread Safety and PerThread 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 întrun sistem. Ca și procesele,
firele de execuție reprezintă un mecanism prin care un calculator poate sǎ ruleze mai multe taskuri
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 lea
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 dintrun 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
pipeuri
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/laborator08 1/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
crearea/distrugerea unui fir de execuție durează mai puțin decât crearea/distrugerea unui
proces
durata context switchului î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 CPUintensive 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. Kernelul 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 dintrun fir de execuție în kernel
kernelul întoarce controlul unui alt fir de execuție.
User Level Threads
Kernelul 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 kernelului, 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 SOuri 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/laborator08 2/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
kernelul 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
userlevel ⇒ 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 userlevel cu cele ale firelor de execuție
kernellevel. O modalitate de a face acest lucru este de a utiliza fire kernellevel pe care să fie
multiplexate fire userlevel. 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. Kernelul știe doar de KLTurile 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 kernelspace. Linux le implementează în kernelspace, 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 headerul 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 întro structură, în
câmpul arg punânduse 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 (printrun pthread_exit
[http://linux.die.net/man/3/pthread_exit] sau printrun return din rutina utilizată la pthread_create
[http://linux.die.net/man/3/pthread_create]).
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator08 3/23
6/11/2017 Laborator 08 Threaduri 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 sa 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 threadurilor
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) întro 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/laborator08 4/23
6/11/2017 Laborator 08 Threaduri 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 TSDurilor 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/laborator08 5/23
6/11/2017 Laborator 08 Threaduri 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 kernelul
decide că este rentabil să facă un context switch
ia 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/laborator08 6/23
6/11/2017 Laborator 08 Threaduri 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 singlethreaded 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/laborator08 7/23
6/11/2017 Laborator 08 Threaduri 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
Mutexurile (mutual exclusion locks) sunt obiecte de sincronizare utilizate pentru a asigura accesul
exclusiv întro 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 mutexul 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/laborator08 8/23
6/11/2017 Laborator 08 Threaduri 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);
}
Mutexul 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 mutexuri
Folosind atributele de inițializare se pot crea mutexuri 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 mutexul 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/laborator08 9/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
cea mai mare prioritate
PTHREAD_PRIO_PROTECT – dacă firul de execuție curent deține unul sau mai multe
mutexuri, 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 mutexului
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 – mutexurile 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ă mutexul este liber în momentul apelului, acesta va fi ocupat de firul apelant și funcția va
întoarce imediat. Dacă mutexul este ocupat de un alt fir, apelul va bloca până la eliberarea mutex
ului. Dacă mutexul este deja ocupat de firul curent de execuție (lock recursiv), comportamentul
funcției este dictat de tipul mutexului:
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/laborator08 10/23
6/11/2017 Laborator 08 Threaduri 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 mutexurilor
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
Futexuri
Mutexurile din firele de execuție POSIX sunt implementate cu ajutorul futexurilor, din considerente
de performanță.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator08 11/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
Optimizarea constă în testarea și setarea atomică a valorii mutexului (printro instrucțiune de tip test
andsetlock) în userspace, eliminânduse trapul în kernel în cazul în care nu este necesară
blocarea.
Numele de futex vine de la Fast Userspace 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ă mutexul 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 kernelmode
(pentru a intra întro stare de așteptare). Optimizarea constă în testarea și setarea atomică a valorii
mutexului (printro instrucțiune de tip testandsetlock) în userspace, eliminânduse trapul în
kernel în cazul în care nu este necesară blocarea.
Futexul poate fi orice variabilă dintro zonă de memorie partajată între mai multe fire de execuție sau
procese. Așadar, operațiile efective cu futexurile 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 mutexurilor 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ă. Mutexurile 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 întro 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 printrun ș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/laborator08 12/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
Comportamentul este similar cu cel de la deschiderea fișierelor. Dacă flagul 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/laborator08 13/23
6/11/2017 Laborator 08 Threaduri 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țândui 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.
Mutexurile ș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 carei 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 raceului 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ânduse astfel
semnalul. Așadar, operațiile de semnalizare, testare a condiției logice și blocare la variabila condiție
trebuie efectuate având ocupat mutexul asociat variabilei condiție. Condiția logică este testată sub
protecția mutexului, iar dacă nu este îndeplinită, firul apelant se blochează la variabila condiție,
eliberând atomic mutexul. În momentul deblocării, un fir de execuție va încerca să ocupe mutexul
asociat variabilei condiție. De asemenea, testarea predicatului logic trebuie făcută întro buclă,
deoarece, dacă sunt eliberate mai multe fire deodată, doar unul va reuși să ocupe mutexul asociat
condiției. Restul vor aștepta ca acesta săl elibereze, însă este posibil ca firul care a ocupat mutexul
să schimbe valoarea predicatului logic pe durata deținerii mutexului. Din acest motiv celelalte fire
trebuie să testeze din nou predicatul pentru că, altfel, șiar î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 macroul 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 mutexuri:
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/laborator08 14/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
Firul de execuție apelant trebuie să fi ocupat deja mutexul asociat, în momentul apelului. Funcția
pthread_cond_wait va elibera mutexul ș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 mutexului 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 mutexului 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 întro 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, șiar 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 mutexului
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 șiau început așteptarea.
Firul de execuție apelant trebuie să dețină mutexul 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/laborator08 15/23
6/11/2017 Laborator 08 Threaduri 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 mutexului asociat variabilei condiție. Firul de execuție apelant trebuie să
dețină mutexul 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/laborator08 16/23
6/11/2017 Laborator 08 Threaduri 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 macroul _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/laborator08 17/23
6/11/2017 Laborator 08 Threaduri 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 Nlea), 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/laborator08 18/23
6/11/2017 Laborator 08 Threaduri 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 lab08tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab08tasks.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 threadurilor.
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, întro 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/laborator08 19/23
6/11/2017 Laborator 08 Threaduri 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 4lea 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 threaduri 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 threadsafe 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ă threaduri create la incrementarea variabilei
b, declarată în funcția thread_function, și o altă condiție de cursă între toate threadurile 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 threaduri care alocă memorie în NUM_ROUNDS runde.
Sunt șanse mari ca threadurile 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 threadsafe. Pentru a putea fi siguri trebuie să consultăm paginile de manual și codul sursă.
Este important de știut că anumite funcții sunt threadsafe iar altele nu. Găsiți o listă cu funcțiile care
nu sunt threadsafe î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 threadsafe, lucru indicat în pagina de manual
malloc(3) [http://man7.org/linux/manpages/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 threaduri 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 întrun 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 întro 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/laborator08 20/23
6/11/2017 Laborator 08 Threaduri 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
toolurile valgrind, pentru a detecta problema:
$ valgrind ‐‐tool=helgrind ./blocked
Așa cum ne arată și helgrind, problema constă în faptul că cele două threaduri 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țivă 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 TODOurile 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 bufferul nu este plin și se va bloca atunci când
bufferul este plin. Producătorul va fi trezit de consumator după ce bufferul nu va mai fi plin.
Consumatorul va scoate obiecte din buffer atât timp cât bufferul nu este gol și se va bloca atunci când
bufferul este gol. Consumatorul va fi trezit de producător după ce bufferul nu va mai fi gol.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator08 21/23
6/11/2017 Laborator 08 Threaduri Linux [CS Open CourseWare]
Sincronizați accesul la bufferul 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 mutexul ocupat se va bloca. Atunci când un fir de execuție
găsește spinlockul ocupat va face busywaiting.
Soluții
lab08sol.zip [http://elf.cs.pub.ro/so/res/laboratoare/lab08sol.zip]
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator08 22/23
6/11/2017 Laborator 08 Threaduri 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/laborator08.txt · Last modified: 2017/04/25 14:33 by ioana_elena.ciornei
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator08 23/23
6/11/2017 Laborator 09 Threaduri Windows [CS Open CourseWare]
Laborator 09 Threaduri Windows
Materiale ajutătoare
lab09slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab09slides.pdf]
lab09refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab09refcard.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:
printrun HANDLE, obținut la crearea firului de execuție, sau folosind funcția OpenThread
[http://msdn.microsoft.com/enus/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/laborator09 1/18
6/11/2017 Laborator 09 Threaduri Windows [CS Open CourseWare]
printrun pseudo‐HANDLE, o valoare specială care indică funcțiilor de lucru cu HANDLEuri că
este vorba de HANDLEul asociat cu firul de execuție curent (obținut, de exemplu, apelând
GetCurrentThread [http://msdn.microsoft.com/enus/library/ms683182%28VS.85%29.aspx]). Pentru a
converti un pseudo‐HANDLE întrun HANDLE veritabil, trebuie folosită funcția DuplicateHandle
[http://msdn.microsoft.com/enus/library/ms724251%28VS.85%29.aspx]. De asemenea, nu are sens
să facem CloseHandle [http://msdn.microsoft.com/enus/library/ms724211%28VS.85%29.aspx] pe un
pseudo‐HANDLE. Pe de altă parte, handleul obținut cu DuplicateHandle
[http://msdn.microsoft.com/enus/library/ms724251%28VS.85%29.aspx] trebuie închis dacă nu mai
este nevoie de el.
printrun identificator al firului de execuție, de tipul DWORD, întors la crearea firului, sau obținut
folosind GetCurrentThreadId [http://msdn.microsoft.com/enus/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 leakurile, trebuie să apelăm CloseHandle
[http://msdn.microsoft.com/enus/library/ms724211%28VS.85%29.aspx].
Handleul 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 flagurile specificate la crearea
lui. Prin funcția DuplicateHandle [http://msdn.microsoft.com/enus/library/ms724251%28VS.85%29.aspx], se
poate crea un nou handle cu mai puține drepturi. Handleul este valid până când este închis, chiar dacă
firul de execuție pe care îl reprezintă sa 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/laborator09 2/18
6/11/2017 Laborator 09 Threaduri 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/enus/library/ms686717%28VS.85%29.aspx] și
TerminateProcess [http://msdn.microsoft.com/enus/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/enus/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/enus/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 șia 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 întro 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 flagul 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/enus/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/enus/library/ms686307%28VS.85%29.aspx]
care este un Sleep [http://msdn.microsoft.com/enus/library/ms686298%28VS.85%29.aspx] alertabil (ceea
ce înseamnă că se pot prelucra APCuri Asynchronous Procedure Call pe durata execuției lui
SleepEx [http://msdn.microsoft.com/enus/library/ms686307%28VS.85%29.aspx]).
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator09 3/18
6/11/2017 Laborator 09 Threaduri Windows [CS Open CourseWare]
Funcția SwitchToThread [http://msdn.microsoft.com/enus/library/ms686352%28VS.85%29.aspx] este
asemănătoare cu Sleep [http://msdn.microsoft.com/enus/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 (timeslice).
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 pseudohandle 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 handleului 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 TLSului, 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 întrun 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 dintrun TLS apelând funcția TlsGetValue
[http://msdn.microsoft.com/enus/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/enus/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/laborator09 4/18
6/11/2017 Laborator 09 Threaduri 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/enus/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/laborator09 5/18
6/11/2017 Laborator 09 Threaduri 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 Userspace Threads, numite fibre. Kernelul 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 contextswitch, nu
mai este necesară interacțiunea cu kernelul), 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/enus/library/ms682425%28VS.85%29.aspx],
CreateProcessAsUser [http://msdn.microsoft.com/enus/library/ms682429%28VS.85%29.aspx],
CreateProcessWithLogonW [http://msdn.microsoft.com/enus/library/ms682431%28VS.85%29.aspx],
CreateThread [http://msdn.microsoft.com/enus/library/ms682453%28VS.85%29.aspx] sau
CreateRemoteThread [http://msdn.microsoft.com/enus/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 al schimba funcția SetSecurityInfo
[http://msdn.microsoft.com/enus/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/laborator09 6/18
6/11/2017 Laborator 09 Threaduri Windows [CS Open CourseWare]
PACL pSacl
);
Handleul î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/enus/library/ms683182%28VS.85%29.aspx], sistemul întoarce un pseudohandle
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/enus/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/enus/library/ms682655(VS.85).aspx]
Timer: Win32.
Standardul POSIX specifică funcții de sincronizare pentru fiecare tip de obiect de sincronizare. APIul
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/enus/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/enus/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/enus/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/laborator09 7/18
6/11/2017 Laborator 09 Threaduri 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 interproces.
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/enus/library/ms682530%28VS.85%29.aspx] sunt
echivalente mutexurilor 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 întrun
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țândui 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 întro singură instrucțiune de asamblare de tip
testandsetlock (TSL). CriticalSection este echivalentul futexului din Linux.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator09 8/18
6/11/2017 Laborator 09 Threaduri 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/enus/library/aa915072.aspx], InitializeCriticalSectionAndSpinCount
[http://msdn.microsoft.com/enus/library/ms683476(v=vs.85).aspx], SetCriticalSectionSpinCount
[http://msdn.microsoft.com/enus/library/windows/desktop/ms686197(v=vs.85).aspx], DeleteCriticalSection
[http://msdn.microsoft.com/enus/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 busywaiting, î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 busywaiting 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 mutexurilor 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/enus/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 întro secțiune critică fără a se bloca, un fir de execuție trebuie să apeleze
TryEnterCriticalSection [http://msdn.microsoft.com/enus/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/enus/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/laborator09 9/18
6/11/2017 Laborator 09 Threaduri Windows [CS Open CourseWare]
[http://msdn.microsoft.com/enus/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/enus/library/ms686211%28VS.85%29.aspx#] pentru
semnalizarea evenimentului. Dacă evenimentul este de tip autoreset, atunci un singur fir de
execuție va fi trezit, iar evenimentul se resetează automat. Dacă evenimentul este de tip
manualreset, atunci evenimentul rămâne semnalizat până când un fir de execuție apelează
ResetEvent [http://msdn.microsoft.com/enus/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/enus/library/ms685081%28VS.85%29.aspx] asigură trecerea
evenimentului în starea nonsignaled. Se utilizează împreună cu un eveniment de tip manual
reset.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator09 10/18
6/11/2017 Laborator 09 Threaduri Windows [CS Open CourseWare]
PulseEvent [http://msdn.microsoft.com/enus/library/ms684914%28VS.85%29.aspx] deblochează
toate firele de execuție care așteaptă la un eveniment de tip manualreset, iar evenimentul este
apoi resetat. Dacă funcția este folosită în conjucție cu un eveniment autoreset, 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 sa 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 întrun spațiu de memorie partajată. Funcțiile interlocked reprezintă cel mai
simplu mod de evitare a raceului 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/enus/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 întro 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/laborator09 11/18
6/11/2017 Laborator 09 Threaduri 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.
Contextswitchurile între fire de execuție pot cauza pagefaulturi 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 taskurile 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 întrun threadpool 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/enus/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/laborator09 12/18
6/11/2017 Laborator 09 Threaduri 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 TimerQueue 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/laborator09 13/18
6/11/2017 Laborator 09 Threaduri 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 sa 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/laborator09 14/18
6/11/2017 Laborator 09 Threaduri 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 eventul CompletionEvent în cazul în care se termină cu
succes și rutina de callback sa 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 lab09tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab09tasks.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țil 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 (clickdreapta 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 sar î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țivă din laboratorul precedent care era problema de la Exercițiul 5
[http://ocw.cs.pub.ro/courses/so/laboratoare/laborator08#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ândule timpul de execuție. Deschideți sursa interlocked.c din proiectul 3‐interlocked.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator09 15/18
6/11/2017 Laborator 09 Threaduri 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 sa î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 timerului cu funcția
main care va dezactiva timerul; 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/enus/library/ms686293%28VS.85%29.aspx].
De asemenea, revedeți secțiunile despre lucrul cu mutexuri ș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/laborator09 16/18
6/11/2017 Laborator 09 Threaduri 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 dintrun 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ă chunkul static, unele vor incepe sa facă merge la chunkurile
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 întrun 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 întro cameră în același moment
Decanul poate intra întro 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 (sa terminat sigur
petrecerea :P)
Există un singur Decan.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator09 17/18
6/11/2017 Laborator 09 Threaduri 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 mutexuri ș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/lab09sol.zip]
so/laboratoare/laborator09.txt · Last modified: 2017/05/03 13:43 by theodor.stoican
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator09 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
lab10slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab10slides.pdf]
lab10refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab10refcard.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 harddiskurilor
rata de transfer scăzută dintre harddisk ș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 sa
î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 sa î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/laborator10 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 handleului unui fișier. Acest lucru se realizează prin
specificarea flagului 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, pipeuri 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 handleuri care au flagul FILE_FLAG_OVERLAPPED setat (handleuri 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 sa încheiat.
Se pot face mai multe operații asincrone de citire/scriere pe același fișier, deci nici file
pointerul 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 pointerul asociat cu file handleul nu mai are nicio însemnătate.
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator10 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 sau 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 sa î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 sa efectuat
și puteți cere imediat rezultatul.
Așteptarea după o operație I/O asincronă se poate face după oricare dintre următoarele:
Handleul 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ă.
Handleul 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/laborator10 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 sa 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/laborator10 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 laț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/laborator10 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/enus/library/aa363862%28VS.85%29.aspx]. În
această situație primul argument va fi handleul fișierului/socketului care se dorește adăugat, iar al
doilea handleul completion portului 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 handleul de completion port. Al treilea
argument este o cheie care va fi folosită pentru identificarea handleului î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 handleul completion portului
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 handleul 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/laborator10 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 */
}
Zerocopy I/O
Zero‐copy se referă la tehnica prin care procesorul evită operațiile de copiere a datelor dintro zonă de
memorie întralta. 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 dintrun fișier pe un socket, va folosi în mod normal schema:
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator10 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ă contextswitchuri, este aceasta:
Mai multe detalii, inclusiv explicarea mai pe larg a contextului, puteți găsi aici
[http://www.ibm.com/developerworks/library/jzerocopy/].
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 cacheul
sistemului de operare. Este o operație zero‐copy nu necesită alocarea de buffere în userspace și
diminuează numărul de apeluri de sistem.
Pentru a transmite un fișier, acesta trebuie deschis folosind flagul FILE_FLAG_OVERLAPPED. Apelul
TransmitFile [http://msdn.microsoft.com/en‐us/library/ms740565%28VS.85%29.aspx] primește
ca argument socketul pe care se realizează comunicația și handleul 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 cacheul 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/laborator10 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țivă că ați zeroizato.
În rezolvarea laboratorului folosiți arhiva de sarcini lab10tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab10tasks.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 dintrun fișier, folosind operații overlapped.
Compilați și testați programul:
.\1‐test_overlapp.exe C:\WINDOWS\explorer.exe
Exercițiul 2 Zerocopy/TransmitFile (2p)
Un client dorește să trimită serverului un fișier folosind operații zerocopy 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/enus/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.
Întro consolă porniți serverul și întro 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/laborator10 9/10
6/11/2017 Laborator 10 Operații IO avansate Windows [CS Open CourseWare]
init_overlapped “zeroizaț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 eventuri pentru a notifica încheierea
operației). Funcția init_overlapped este apelată întrun ciclu for, pentru fiecare element al arrayului
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
macroul 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 APIul 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 arrayului 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/lab10sol.zip]
so/laboratoare/laborator10.txt · Last modified: 2017/05/03 21:25 by adrian.stanciu
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator10 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
lab11slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab11slides.pdf]
lab11refcard.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab11refcard.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 threadului curent până la încheierea operației. O posibilă soluție este folosirea
de operații nonblocante (spre exemplu folosirea flagul 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 flagul 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. Threadul 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 threadului 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 threadul 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.12001 în sys/select.h
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator11 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 APIul 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
);
Timeoutul 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/laborator11 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 nonportabile:
/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/laborator11 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/laborator11 4/16
6/11/2017 Laborator 11 Operații IO avansate Linux [CS Open CourseWare]
Threadul 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.
Edgetriggered sau leveltriggered
Interfața epoll are două comportamente posibile: edge‐triggered sau level‐triggered. Se poate
folosi unul sau altul, în funcție de prezența flagului 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 leveltriggered se va raporta imediat EPOLLIN. În
modul edgetriggered nu se va mai raporta nimic, nici măcar la sosirea unor noi date pe socket. Se poate
observa cum modul edgetriggered sesizează schimbarea stării descriptorului în relație cu evenimentul,
iar leveltriggered prezența stării. Modul edgetriggered 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 nonblocant.
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 întrun nou ciclu de busywaiting. Î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 edgetriggered 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 sa schimbat starea de la ultimul apel. Dacă la un moment dat se încearcă scrierea
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator11 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ă sa schimbat starea socketului. Mai pe scurt, asta are ca efect faptul că nu
mai trebuie activat/deactivat EPOLLOUT ca în cazul leveltriggered.
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 sa definit interfața eventfd. Cu
ajutorul acestei interfețe și combinat cu interfețele de multiplexare I/O existente, kernelul 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/laborator11 6/16
6/11/2017 Laborator 11 Operații IO avansate Linux [CS Open CourseWare]
Interfața eventfd permite unificarea mecanismelor de notificare ale kernelului întrun 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/manpage/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/laborator11 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 cacheul 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 intrareieș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ă
linkeze 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/laborator11 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 headerul
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/laborator11 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 printro 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/laborator11 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 APIului de operații
asincrone cu mecanismul eventfd.
Pentru aceasta se configurează flagul 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/laborator11 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.
Zerocopy 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 pointerul la offset off_in trebuie să fie NULL
altfel:
dacă off_in este NULL, atunci datele sunt citite de la fd_in de la offsetul curent, acesta
modificânduse corespunzător
altfel, off_in trebuie să fie un pointer la un întreg care reprezintă offsetul de start de la
care se va face citirea, iar offsetul 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 nonblocantă sau hinturi 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 zerocopy este sendfile [http://linux.die.net/man/2/sendfile]
Vectored I/O
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator11 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 întrun singur flux de scriere sau “desfacerea” acestora
dintrun 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 printro
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 zerocopy.
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/laborator11 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 lab11tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab11tasks.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 pipeuri anonime.
Serverul:
construiește un vector de pipeuri (î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 pipeul corespunzător un șir de MSG_SIZE caractere de forma
<pid>:<caracter random> ('a' + random() % 30)
scrierile și citirile în pipeuri 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 pipeuri î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, handleul 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 pipeului.
Î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/laborator11 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 pipeului.
Din pipe citiți mesajul trimis de client, folosind read.
Eliminați pipeul din multiplexor (folosind epoll_ctl cu opțiunea EPOLL_CTL_DEL) și închidețil
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 pipeul 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 macroul 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 macroul MAGIC_EXIT,
atunci scoate capătul pipeului 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 APIul 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
aiotest.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, headerul '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/laborator11 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/eventfdaiotest.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.
Serverul:
creează un descriptor via signalfd pentru semnalul SIGCHLD și îl adaugă la epoll
la primirea unui semnal, prin read(2) pe descriptorul creat, determină PIDul
copilului defunct, afișează un mesaj și scoate pipeul 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/lab11sol.zip]
so/laboratoare/laborator11.txt · Last modified: 2017/05/19 00:29 by theodor.stoican
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator11 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
lab12slides.pdf [http://elf.cs.pub.ro/so/res/laboratoare/lab12slides.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 driverul prin intermediul unor fișiere speciale denumite device nodes. APIul
(interfața de programare) oferită de device drivere este fixată, și include următoarele operații:
open, close
read, write
ioctl, mmap
Unele deviceuri 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, deviceurile
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ă, mouseul.
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 outputul 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 nodeuri se pot afla cu funcțiile din
familia stat [http://man7.org/linux/manpages/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/laborator12 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/manpages/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/manpages/man8/umount.8.html]:
umount {dir|device}...
sau apelul: umount(2) [http://man7.org/linux/manpages/man2/umount.2.html]
int umount(const char *target);
Directoare și linkuri
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/laborator12 2/7
6/11/2017 Laborator 12 Implementarea sistemelor de fișiere [CS Open CourseWare]
Un director este stocat în sistemul de fișiere întrun 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 inodeuri.
Linkuri 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. Linkurile 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/manpages/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 dintrun 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/laborator12 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/manpages/man3/realpath.3.html] dereferențiază linkul 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/laborator12 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. Feedbackul 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 lab12tasks.zip
[http://elf.cs.pub.ro/so/res/laboratoare/lab12tasks.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 nodeuri de tip caracter și două device nodeuri
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/manpages/man2/stat.2.html] pentru a obține o structură de tipul struct stat din
care veți extrage tipul deviceului (st_mode) (hint: S_ISCHR, S_ISBLK
[http://www.gnu.org/software/libc/manual/html_node/TestingFileType.html#TestingFileType] ) apoi din câmpul
st_rdev extrageți major [http://man7.org/linux/manpages/man3/makedev.3.html] și minor
[http://man7.org/linux/manpages/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/manpages/man2/mount.2.html] și
umount [http://man7.org/linux/manpages/man2/umount.2.html].
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator12 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
Întro altă consolă, întrun 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 shellul aferent comenzii ./mini, comanda:
symlink /etc/passwd local‐passwd
Ca să verificați, întro altă consolă, în același director cu cel în care ați rulat comanda ./mini, folosiți
ls ‐l
Pentru a șterge symlinkul 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 demoul 5 de la curs
[http://ocw.cs.pub.ro/courses/so/cursuri/curs12]
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/laborator12 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/lab12sol.zip]
so/laboratoare/laborator12.txt · Last modified: 2017/05/19 16:38 by laura.ruse
https://ocw.cs.pub.ro/courses/so/laboratoare/laborator12 7/7