Examen CA/CB/CC 2019-2020
Examen final
Puteți participa o singură dată pe sesiune la examenul final.
Sesiunea din toamnă, august 2020
Datele de examen de SO pentru sesiunea din toamnă, august 2020, sunt:
marți, 25 august 2020, 9-11.
miercuri, 26 august 2020, 9-11.
Sesiunea din vară, mai 2020
Datele de examen de SO pentru sesiunea mai 2020 sunt:
marți, 26 mai 2020, 9-11 și 12-14, seria 3CC.
miercuri, 27 mai 2020, 9-11 și 12-14, seria 3CA.
joi, 28 mai 2020, 9-11 și 12-14, seria 3CB.
Desfășurare examen
Având în vedere situația din acest an, examenul de SO din această sesiune se va desfășura exclusiv online. Modul de examinare va fi asemănător unui examen oral sau al unui interviu. Vom folosi Microsoft Teams ca mediu de comunicare.
Examenul de SO va consta într-un call / interviu de 15 minute cu doi supraveghetori. În decursul acestui examen oral, studentul va fi rugat să răspundă la câteva întrebări din materia de SO adresate de către supraveghetori. Răspunsurile date de către student la întrebarile supraveghetorilor vor fi evaluate cu un punctaj. Nota obținută în cadrul examenului va fi suma punctajelor obținute la toate întrebările.
În timpul examenului veți putea consulta orice material scris sau online. Nu este permisă comunicarea, fie fizică, fie online (Messenger, Instagram, WhatsApp etc.) cu alte persoane în afară de supraveghetori în timpul examenului.
Vom organiza o simulare de examen sâmbătă, 23 mai 2020, ora 14:00, pe Microsoft Teams, 2 ore cu 8 studenți. Vor putea participa toți studenții și toți asistenții dar doar 8 studenți vor fi examinați. Ceilalți studenți vor asista. Simularea se înregistrează, iar studenții nu vor porni camera (din considerente de privacy).
Precizăm că va trebui să aveți la dispoziție, în timpul examenului, o cameră video pentru a vă confirma identitatea, împreună cu un act de identitate. Dacă există situații obiective în care nu veți avea o cameră video în timpul examenului, trimiteți un e-mail către Dragoș cu subiectul [SO][Examen] Cameră video: Prenume NUME, grupă
, de exemplu [SO][Examen] Cameră video: Ana POPESCU, 332CB
.
Informații despre desfășurătorul și conținutul examenului găsiți în secțiunea aferentă din pagina de notare. În pregătirea examenului puteți să parcurgeți subiectele de examen anterioare.
Urmăriți precizările din pagina de reguli.
Lucrări
Lucrare 1
La începutul cursului 5:
09.03.2020, seria CA
05.03.2020, seria CB
04.03.2020, seria CC
3CA, varianta 1
Într-un sistem de operare cu suport de multithreading (sau cu mai multe procesoare), un proces are deschise fișiere doar cu descriptorii 0, 1 și 2. Ce se intamplă după executarea următoarei secvențe? Argumentați răspunsul.
close(0);
dup(1);
Care sunt operațiile ce trebuie executate pentru a realiza un apel de sistem?
Un proces apelează:
printf("123\n456");
int *p = NULL;
*p = 3;
Pe ecran apare doar 123
. De ce nu s-au afișat ambele numere?
3CA, varianta 2
Sunt echivalente întotdeauna următoarele doua secvente de instructiuni? Argumentați răspunsul.
/* Secvența 1 */ | /* Secvența 2 */
close(1); | dup2(3,1);
dup(2); |
De ce sunt necesare apelurile de sistem în sisteme de operare?
Răspuns: Există cel putin două nivele de privilegii de execuție pe procesor. Pentru a efectua acțiuni privilegiate precum accesul la periferice, este nevoie de nivelul privilegiat. Procesele nu pot executa toate instrucțiunile procesorului, fiind nevoie de un nivel de privilegii suplimentar.
Un proces apelează fgets (stdin, buffer, 100);
prima dată la pornire și a doua oară după 1 minut. Observăm că al doilea apel întoarce imediat în buffer un string Argumentați de ce/
Răspuns: Utilizatorul a introdus prima dată două rânduri de text, ambele mai scurte de 100 de caractere. Acestea au fost transferate în buffer-ul programului, al doilea apel al fgets
a luat datele direct din buffer.
3CB, varianta 1
Se creează un nou fișier folosind apelul open
și se scriu în acesta 512 octeți folosind apelul write
. Ulterior, se apelează lseek(file, -256, SEEK_CUR)
. Presupunând că toate apelurile de sistem s-au încheiat cu succes, iar apelul write
a scris tot buffer-ul, care va fi dimensiunea fișierului? Argumentați.
Răspuns: Dimensiunea fișierului va fi 512 octeți (deoarece apelul open a reușit, fișierul a fost creat; deoarece apelul write a scris tot buffer-ul, dimensiunea fișierului este 512). Apelul lseek
nu modifică dimensiunea fișierului, ci mută doar cursorul de fișier.
De ce este limitată dimensiunea tabelei de descriptori de fișier într-un sistem de operare? Argumentați răspunsul.
Răspuns: Dimensiunea tabelei de fișiere a unui proces este limitată din rațiuni de securitate. În cazul în care dimensiunea nu ar fi limitată, deschiderea un număr foarte mare de fișiere, de exemplu într-o buclă while, ar bloca sistemul de operare.
Precizați un avantaj al folosirii funcției CreateProcess
din Windows față de combinația fork + exec
din Linux. Argumentați răspunsul.
3CB, varianta 2
Se creează un nou fișier folosind apelul open
și se apelează funcția ftruncate(file, 512)
. Ulterior, în același fișier se scriu 32 de octeți. Care va fi dimensiunea finală a fișierului? Argumentați.
Răspuns: Considerând că fișierul este nou creat, dimensiunea acestuia înainte de ftruncate
este 0. Apelul ftruncate
modifică dimensiunea acestuia la 512 octeți, dar lasă cursorul de fișier nemodificat (valoare 0). În urma apelului write
, cursorul de fișier ajunge la valoarea 32, deci nu se modifică dimensiunea fișierului, care rămâne 512.
Numiți un avantaj al folosirii dup2
în detrimentul folosirii dup
. Argumentați.
Răspuns: Apelul dup2
primește ca parametru atât ce file descriptor să duplice, cât și cel pe care să îl suprascrie. Acest lucru permite duplicarea pe o poziție care nu este secvențial crescătoare față de descriptorii deja existenți - ex., într-un program care are doar descriptorii 0-2 deschiși, apelul dup2(1, 42
) ar putea fi imitat prin 40 de apeluri dup, urmată de 39 de apeluri de close.
Precizați două proprietăți pe care un proces copil (creat prin intermediul funcției fork()
) le moștenește de la procesul părinte. Explicați.
3CC, varianta 1
Dați un exemplu de operație / acțiune care trebuie realizată în spațiul kernel (kernel space) și una care poate fi realizată în spațiul utilizator (user space). Justificați
Răspuns: Apelul fork() trebuie realizat în spațiul kernel, deoarece se duplică structuri ce se află deja în kernel. Un apel către fprintf poate să se realizeze doar în user space. Momentul în care se face un apel de sistem este când este afișat un terminator de linie nouă, sau bufferul intern ajunge la capacitatea maximă. Alte apeluri care se realizează exclusiv în user space sunt apelurie de lucru pe șiruri (strcpy, strlen), care nu execută apeluri de sistem.
Un fișier este deschis. Dați un exemplu de apel care modifică atât dimensiunea fișierului, cât și cursorul de fișier. Și un exemplu de apel care modifică doar dimensiunea fișierului (nu și cursorul de fișier). Justificați.
Răspuns: Un apel de funcție care modifică atât dimensiunea cât și cursorul fișierului este write. Exemplu: write(myFd, bigBuf, 1) (cu presupunerea că avem cursorul la finalul fișierului). Un apel de funcție care modifică dimensiunea, dar nu și cursorul unui fișier este ftruncate. Exemplu: ftruncate(myFd, 1073741824)
Într-un shell rulăm ls /a/b/c/ && ps
. Câte apeluri fork()
, exec()
și wait()
au loc și în ce ordine? Justificați.
Răspuns: Presupunând că prima comandă nu întoarce exit code nenul, vom avea ordinea: Shell - fork(), Copil1 - exec(), Shell - wait(), Shell - fork(), Copil2 - exec(), Shell - wait(). În cazul în care prima comandă întoarce exit code nenul, se vor executa primele 3.
3CC, varianta 2
De ce tabela de descriptori de fișiere este reținută în spațiul kernel (kernel space) și nu în spațiul utilizator (user space)? Justificați.
Răspuns: Stocarea FDT în user space ar implica faptul că userland are pointeri valizi către structuri din kernel (din tabela de fișiere deschise). Deși nu avem acces de citire/scriere din userspace la spațiul de adresă al kernelului, un atacator ar putea ghici adrese unde se află alte structuri de tip open file, potențial aparținând altor utilizatori (inclusiv root), și astfel ar putea asambla un descriptor către un fișier care nu-i aparține, în care poate citi sau scrie.
Un program apelează funcția
print_all_info()
care afișează informații la ieșirea standard. Dorim ca acele informații (și doar acelea) să fie afișate în fișierul “results.txt”. Adică după apelul funcției, alte afișări să se facă în continuare la ieșirea standard. Completați zonele cu TODO din secvența de cod de mai jos care să ducă la rezultatul dorit:
/* TODO */
print_all_info(); /* prints in "results.txt" file */
/* TODO */
printf("aaa\n"); /* prints at standard output */
Răspuns:
backup_stdout = dup(STDOUT_FILENO);
file_fd = open(“results.txt”, O_RDWR | O_CREAT | O_TRUNC, 0644);
dup2(file_fd, STDOUT_FILENO);
close(file_fd);
print_all_info(); /* prints in “results.txt” file */
dup2(backup_stdout, STDOUT_FILENO);
close(backup_stdout);
printf("aaa\n"); /* prints at standard output */
De ce, în general, apelul exec()
durează mai mult decât apelul fork()
? Justificați.
Răspuns: Apelurile din familia exec() înlocuiesc imaginea procesului cu una specificată printr-o cale pe disk. Acest lucru implică I/O, posibilă încărcare de pe disk, apelarea loaderului. Toate acestea sunt mult mai costisitoare decât apelul fork(), care copiază doar date din RAM tot în RAM
Greșeli frecvente
Răspunsuri incomplete, ambigue, parțial corecte, pe lângă subiect.
Confuzie între apeluri de sistem și procese noi. O greșeală frecvent întâlnită este următoarea: Pentru un apel de sistem se foloseste fork() + execv + wait.
Procesul moștenește pid-ul procesului părinte. PID-ul nu este o proprietate care este moștenită.
Procesul copil “moștenește” sistemul de fișiere. Sistemul de fișiere este o resursă partajată între toate programele; sistemul de fișere nu este (cu excepția programelor rulate într-un sandbox de tip jail / chroot) specific unui anumit proces.
Odată închis stdout, el nu mai poate fi redeschis. STDOUT_FILENO este un simplu macro care se reduce, de cele mai multe ori, la numărul “1”.
Confuzii între apelul exec
și apelul system
.
Lucrări foarte bune
Lucrare 2
La începutul cursului 9:
24.03.2020, seria CA
25.03.2020, seria CB
25.03.2020, seria CC
3CA
Ce reprezintă în planificatorul sistemului Linux virtual runtime
și care este valoarea sa inițială? Argumentați de ce este așa.
Răspuns: Virtual runtime este o valoare care specifică câte unități de timp (nu neapărat măsurată în secunde) a rulat procesul respectiv. La pornire, un proces nou va primi mereu valoarea de virtual runtime minimă din lista de procese existente. Pentru că planificatorul planifică mereu procesul cu virtual runtime minim, dacă valoarea unui proces nou ar fi 0, acesta ar fi planificat în continuu până ajunge la valoarea celorlalte procese.
Într-un sistem care folosește un planificator cu cozi de priorități, ce se întâmplă dacă avem un proces CPU bound cu prioritate mare? Propuneți o soluție pentru problemă.
Răspuns: Procesul fiind CPU bound, nu va renunța la procesor (nu face multe apeluri blocante), astfel încât el va fi mereu disponibil pentru rulare în coadă. Astfel procesele cu prioritate mai mică nu vor fi planificate. O soluție ar fi scăderea temporară a priorității procesului dacă acesta depășește de mai multe ori cuanta de timp.
Argumentați ce înseamnă priority inversion
și ilustrați un exemplu.
Răspuns: Un proces cu prioritate mare așteaptă după resurse care sunt blocate de un proces cu prioritate mai mică, prioritatea efectivă a procesului fiind de fapt egală cu prioritatea procesului care a blocat resursele. Un exemplu ar fi un proces care descarcă date de pe rețea și stă după un proces antivirus care verifică date descărcate.
3CB
Pe un sistem de 32 de biti dorim să adresam mai mult de 4
GB de memorie. Cum putem face acest lucru?
Răspuns: Sistemul poate utiliza extensiile de adresare Physical Address Extensions, care permit unității de management a memoriei să adreseze o cantitate mai mare de memorie virtuală. Chiar dacă spațiul de adrese virtual al unui proces nu crește, paginile virtuale ale unui proces se vor putea mapa la o memorie fizică extinsă, deci numărul de procese care poate fi creat va fi mai mare.
Un proces care rulează într-un sistem este I/O bound. Numărul de schimbari de context involuntar este mai mare decat cel voluntar. Ce ar determina un astfel de comportament al planificatorului?
Răspuns: Un proces I/O bound interacționează predominant cu dispozitive I/O, acesta așteptând majoritatea timpului ca transferul de date să se termine. În general astfel de procese prelucrează date pentru un interval scurt, după care cedează voluntar controlul atunci când realizează operații I/O. În scenariul prezentat, scheduler-ul este configurat astfel încât procesul primește o cuantă de timp prea scurtă, sau există procese cu prioritate mai mare decât el care ajung să ruleze pe procesor, astfel încât procesul este evacuat în mod involuntar înainte să ajungă să execute operațiile I/O.
Cine se ocupă de translatarea unei adrese virtuale într-o adresă fizică? Pe baza cărei informații se realizează această translatare și de către cine este oferită?
3CC
De ce un proces shell are parte de mult mai multe schimbări de context voluntare decât schimbări de context nevoluntare? Argumentați răspunsul.
Răspuns: Deoarece shell-ul își petrece majoritatea timpului așteptând, fie că e vorba de date de la tastatură, de la alte procese sau terminarea unor procese. În astfel de cazuri se cedează voluntar cuanta pentru a nu face busy-waiting.
PTBR (Page Table Base Register) este un registru de tip pointer care stochează adresa tabelei de pagini. Este folosit de MMU (Memory Management Unit). Când este modificată valoarea din PTBR? Argumentați răspunsul.
Răspuns: La fiecare schimbare de context ce implică schimbarea cu alt proces, va trebui schimbată tabela de pagini, prin urmare se va schimba valoarea din PTBR. Dacă KPTI (kernel page table isolation) nu a fost dezactivat, PTBR se va mai schimba și la fiecare apel de sistem.
Pe un sistem Linux pe 32 de biți un program folosește într-o buclă apelul malloc() pentru a aloca bucăți de 1MB de memorie. După aproximativ câte iterații prin buclă este garantat că malloc() va întoarce valoarea NULL? Argumentați răspunsul.
Răspuns: Arhitecturile pe 32 de biți pot adresa maxim 4GiB de memorie. Spațiul virtual are o împărțire fixă între userland și kernel space, de obicei 3:1 (3GiB pentru userland). Neglijând restul de secțiuni ale procesului, malloc va întoarce null după maxim 3072 iterații.
Greșeli frecvente
O greșeală foarte des întâlnită este aceea în care se spune că procesele I/O intensive “stau” foarte mult pe procesor degeaba. Procesoarele I/O intensive așteaptă finalizarea operațiilor de I/O în coada WAITING, NU în coada RUNNING (adică nu “stau” pe CPU “degeaba”). Tot legat de greșeala anterioară, unui proces I/O bound NU i se dă o cuantă mai mare pentru că are nevoie de mai mult timp să execute operația, ci pentru că o să aibă multe schimbări de context voluntare.
O altă greșeală frecventă este aceea că proceselor din starea WAITING le expiră cuanta de timp. Doar procesele care rulează au o cuantă de timp asociată, procesele din starea WAITING NU rulează pe procesor când se află în această stare.
O altă greșeală frecventă este următoarea: un proces cu prioritate mare este CPU bound ⇒ ruleaza doar el ⇒ scheduler nepreemtiv. Preemția există, doar că la fiecare expirare a cuantei de timp se alege același proces, din cauza priorității celei mai mari.
Lucrări foarte bune
TUDOSOIU Marius-Florin
MANEA Ionut-Marius
ȚEPEȘ-ONEA Filip
MUSTAFA Taner
MATEESCU Cristina-Ramona
CONSTANTIN Ioan
MARINESCU Ana
ARGINT Dragoș-Iulian
TUDOSE Ionuț-Cristian
CRAIOVEANU Sergiu-Ionuț
NONEA Victor-Andrei
DUMITRU Alexandru-Călin
CRĂCIUN Ioana
POPA Andrei
STRĂTILĂ Andrei
CONDRUZ Cristian-Ioan
Lucrare 3
3CA
La prima pornire a unui process, observăm foarte multe page fault-uri. De ce?
Descrieți, prin intermediul pseudocodului și a explicațiilor, cum ați implementa un mecanism simplu de canary values.
Răspuns: Se folosește o variabila globală inițializată cu o valoare random. La intrare în fiecare funcție se pune o variabilă locală cu valoarea variabilei globale. Înainte de fiecare retur, se verifică dacă valoarea variabilei locale este aceeași cu valoarea variabilei globale. Dacă nu, se iese din proces. O idee bună este folosirea a două macro-uri, unul pentru INIȚIALIZARE și unul pentru RETURN(valoare).
static usize canary = random();
int f(...)
{
usize value = canary;
//
if (value != canary) abort (120);
else return …;
}
Explicați ce se întâmplă când un proces generează un page fault datorită accesării unei zone de memorie asupra căreia nu are permisiunile necesare pentru efectuarea operației de acces.
3CB
Cu ce puteți înlocui
TODO din secvența de cod de mai jos astfel încât să nu producă un page fault? Dar pentru a genera un page fault? Argumentați răspunsul.
pid_t pid;
int a = 5;
pid = fork();
if (pid == 0) {
TODO;
}
Răspuns: Pentru a nu genera un page fault, TODO poate fi înlocuit cu o instrucțiune care nu scrie date în memoria procesului, ex. printf(“%d”, a)
, sau cu șirul vid (nu se execută nicio instrucțiune). Pentru a genera un page fault se poate scrie în memoria procesului, ex. a = 5
(page fault-ul este cauzat de accesarea pentru scriere a unei pagini marcată ca fiind copy-on-write).
Aveti la dispozitie tool-ul de analiza dinamica strace. Cum puteti face deosebirea intre un proces ce a provenit dintr-un executabil link-at dinamic si unul ce a provenit dintr-un executabil link-at static?
Răspuns: Pentru procesele create din binare care folosesc link-are dinamică este nevoie de maparea bibliotecilor dinamice în memoria procesului. Astfel, la începutul rulării programului se observă apeluri de open
asupra fișierului ce conține biblioteca dinamică, plus apeluri mmap
pentru a face maparea zonelor de fișier în memorie.
Pot fi împiedicate complet atacurile de tip ret-to-libc prin mecanismul de securitate stack smashing protector (SSP / canary value)? Argumentați răspunsul.
Răspuns: Atacurile de tip return to libc se bazează pe reutilizarea codului de bibliotecă (în special biblioteca libc) prin suprascrierea adresei de return a unei funcții prin intermediul unui buffer overflow. Metoda de protecție prin intermediul unei valori de tip canar nu poate împiedica complet acest tip de atac, deoarece dacă atacatorul poate afla valoarea canarului plasat pe stivă (care este o valoare constantă pentru toate funcțiile pe toată durata execuției programului), o va putea integra în payload astfel încât verificarea valorii să nu termine programul, chiar și atunci când s-a executat un buffer overflow. Un vector de atac similar ar putea suprascrie un function pointer aflat pe stivă cu o adresă a unei funcții de bibliotecă.
3CC
Un proces este pornit dintr-un executabil static. Imediat după pornirea procesului observăm că memoria fizică ocupată de acesta (RSS: resident set size) este de 8KB, deși executabilul are dimensiunea de 630KB. Cum se explică spațiul mic de memorie fizică ocupat de proces (8KB) în ciuda dimensiunii mari a fișierului executabil (630KB)?
Răspuns: Datorită mecanismului de demand paging, după ce binarul este încărcat de loader procesul asociat va avea toate zonele mapate în memoria virtuală, dar fără ca acestea să existe în memoria RAM. Pe măsură ce memoria e accesată, accesul la paginile nemapate vor genera page fault-uri majore ce vor aduce în ram datele de pe disk. Astfel, imediat după pornirea executabilului, acesta va avea o valoare mică pentru memoria fizică folosită, urmând ca această să crească pe parcursul execuției.
Care dintre mecanismele defensive DEP (Data Execution Prevention) și ASLR (Address Space Layout Randomization) este adecvat pentru a proteja împotriva atacurilor de tip code reuse?
Răspuns: Data execution prevention (DEP) previne un atacator din a exploata interpretarea datelor din secțiunile de date drept instrucțiuni, prin urmare nu împiedică refolosirea de cod. Address Space Layout Randomization (ASLR) îngreunează refolosirea de cod deoarece adaugă un offset aleator spațiului de adresă al procesului, astfel adresele funcțiilor/secțiunilor de cod se schimbă la fiecare rulare (se introduce nedeterminism).
Când se creează un thread, se alocă o zonă de memorie dedicată pentru stiva acelui nou thread (de dimensiune tipică de 8MB pe Linux). Un thread dintr-un proces multithreaded apelează fork(). În urma apelului fork(), procesul copil va avea un singur thread, procesul părinte nu va fi afectat. Câte zone de memorie dedicate pentru stive va avea procesul copil?
Răspuns: La apelul fork, procesul copil va moșteni tot spațiul de adrese al părintelui, în care se află și zonele de stivă ale celorlalte fire de execuție.Ele, din punct de vedere tehnic, rămân accesibile, însă din punct de vedere logic nu sunt folosite. Execuția copilului va continua folosind stiva thread-ului care a apelat fork().
Greșeli frecvente
Foarte multe răspunsuri se bazază că page fault este același lucru cu segmentation fault. Apariția unui page-fault nu generează neapărat un eveniment care să ducă la încheierea forțată a execuției programului. De exemplu, dacă instrucțiunea care a generat page fault-ul încercă să citească memorie dintr-o pagină care se află în swap, se generează un page-fault pentru aducerea paginii din swap. De asemenea, dacă instrucțiunea respectivă încearcă să scrie într-o pagină marcată ca fiind copy-on-write, se generează un page-fault pentru duplicarea paginii.
Răspunsuri de forma ”exit(139)
” (și doar atât), fără a explica ce înseamnă și cum se leagă de subiectul întrebării. Răspunsuri de forma: “numărul de apeluri de sistem este mai mic” fără a aduce și o argumentate/justificare asupra afirmațiilor.
Răspunsuri în care frazele se contrazic (ex. “Da, pot fi împiedicate complet atacurile ret-to-libc prin spp. Putem afla canary value și suprascrie.”; pentru întrebarea “Pot fi împiedicate complet atacurile …?”, răspunsuri de forma “Nu. Atacurile sunt împiedicate complet”.).
Răspunsuri care nu adresează întrebarea, ci care prezintă conceptele teoretice din enunț, fără a răspunde efectiv la întrebare.
Bibliotecile dinamice nu sunt același lucru cu executate link-ate dinamic.
Multe răspunsuri indică faptul că apelul read
nu face page fault imediat după fork
deoarece citește date. În general, apelul read citește date dintr-un fișier și le scrie într-un buffer. După fork
, cel mai probabil, o scriere în memorie o să genereze page fault (paginile sunt marcate COW → la scriere se generează page fault → se duplică paginile).
Confuzii între demand-paging și copy-on-write.
Multe răspunsuri au referit memoria RAM ca fiind memoria cache.
Lucrări foarte bune
NICULAE Andrei-Ionuţ, 335CB
DREHUŢĂ Alexandra, 333CB
DOBOȘ Claudiu-Florin, 336CB
DOROBANȚU Florin-Claudiu
RĂILEANU Ana-Arina, 334CA
MACOVEI Antonio-Dan, 334CA
BĂLAŞA Andrei-Alin, 333CC
ROTARU Leonard-Claudiu, 333CC
BREZEANU Dan-Eugen 335CC
ANTONESCU Raluca 336CC
Lucrare 4
3CA
Explicați diferența dintre spinlock si mutex și descrieți un caz în care folosirea fiecăruia este eficientă.
Răspuns: Blocarea unui spinlock realizează busy waiting atunci când thread-ul încearcă să obțină un lock care este deja blocat, această operație ocupând timp inutil pe procesor. Prin contrast, un mutex va face ca thread-ul care a încercat blocarea lock-ului să fie scos de pe procesor, economisind cicli de ceas cât timp mutexul nu este eliberat. Datorită overhead-ului necesar pentru scoaterea de pe procesor a thread-ului, uun spinlock este mai eficient pentru zone critice mici, dar costisitor pentru zone critice de dimensiuni mai mari.
Argumentațiu diferența între operații non-blocante și operații asincrone. Scrieți câte un exemplu.
Răspuns: O operație de transfer non-blocantă asigură că sistemul nu se va bloca pentru așteptarea terminării operației, însă nu specifică modul de terminare a operației. Operațiile non-blocante se pot realiza sincron, iar rezultatul întors de funcția de transfer reprezintă cantitatea de date care a putut fi transmisă imediat, sau în mod asincron, atunci când transferul este pornit, dar executat în paralel de o altă unitate de execuție decât thread-ul care l-a pornit. Exemplu de operație non-blocantă: read care are setat flag-ul O_NONBLOCK. Exemplu de operație asincronă: ReadFile pe un fișier deschis cu FILE_FLAG_OVERLAPPED.
După repornirea forțată a unui sistem observăm că avem un fișier _special_ pe disc al cărui conținut se modifică din când în când fără ca data lui de scriere să se modifice. Explicați fenomenul.
Răspuns: Oprirea forțată a sistemului a dus sistemul de fișiere într-o stare inconsistentă în care doar inode-ul fișierului “special” a fost actualizat, fără ca bitmap-ul blocurilor de date să fie actualizat. Fișierul practic deține un bloc de date marcat ca fiind liber. Acesta va putea fi folosit de alte fișiere, explicând modificarea anormală a conținutului fișierului nostru “special”. Sistemele de fișiere moderne cu jurnalizare previn acest lucru.
3CB
Un programator dorește să proceseze un volum mare de date încărcate în memorie, stocate într-un vector. Pentru a le procesa în paralel, își definește global un pointer cu rol de iterator peste vector, și îl folosește în threadurile create. La rulare primește eroare de acces invalid la memorie. De ce se întâmplă acest lucru? Argumentați răspunsul.
Răspuns: Deși un pointer are dimensiunea cuvântului procesorului (adică poate fi scris din registru în memorie într-o singură operație), un pointer nealiniat va necesita două scrieri succesive. Greșeala fundamentală în abordare este că iteratorul nu este protejat de la accesul concurent, însă această greșeală ar cauza doar rezultate incorecte / nedeterministe. Motivul pentru care se primește segfault este că un thread citește iteratorul când acesta este scris pe jumătate, fiind în acel moment de timp un pointer invalid. O altă variantă posibilă: Dacă accesul la variabile nu este protejat, un thread care realizează citirea pointer-ului și verificarea limitelor array-ului, dar care este scos de pe procesor înainte de accesarea datelor, ar putea realiza un acces invalid la memorie prin utilizarea unei valori care a devenit invalidă între timp. Atât accesul la iterator, cât și la array ar trebui protejate prin intermediul unui mutex comun.
Este mai sigură folosirea instrucțiunii de asamblare “inc [ebp - 16]” decât setul “mov eax, [ebp - 16]; inc eax; mov [ebp - 16], eax” din punctul de vedere al sincronizării? Argumentați răspunsul.
Răspuns: Instrucțiunile x86 se traduc la rândul lor în instrucțiuni elementare, interne procesorului (microcod). Deși instrucțiunea inc este singulară la nivel de ISA, la nivel de microcod ea va fi alcătuită dintr-un fetch din RAM, adunare, și stocare înapoi în RAM. Prin urmare, la fel ca și varianta cu add, nu oferă nicio garanție de sincronizare pentru procesoare multicore.
Observăm că pe un sistem cu disk caching activat, citirea datelor dintr-un fișier de pe disc durează de fiecare dată foarte mult timp, chiar dacă sunt efectuate în mod repetat operații pe aceleași date. Care ar putea fi o cauză pentru acest fenomen? Argumentați răspunsul.
Răspuns: Un scenariu posibil ar fi citirea integrală și repetată a unui fișier mult mai mare față de memoria RAM liberă. Din cauză că SO-ul nu are un cache îndeajuns de mare, va fi nevoit să citească paginile de pe disk. O alternativă este încărcarea foarte mare a sistemului, astfel încât nu mai este suficient loc în memorie pentru cache-ul fișierelor.
3CC
O bibliotecă oferă funcția atomic_inc(&value) pentru incrementarea în mod atomic a unei variabile “value” de tip întreg (int), aflată în memorie la adresa &value. Precizați și motivați secvența în limbaj de asamblare x86 folosită pentru implementarea părții atomice a acestei funcții.
În mod obișnuit, atunci când se închide sistemul de calcul, se folosește apelul sync() care aduce datele din memorie pe disc. Ce s-ar întâmpla dacă, la închiderea sistemului de calcul, apelul sync() nu mai este apelat?
Rulăm comanda rm a.txt. În ce situație sunt eliberate de pe disc blocurile de date și inode-ul fișierului a.txt?
Greșeli frecvente
Lucrări foarte bune