Table of Contents

Examen CA/CB/CC 2016-2017

Urmăriți precizările din pagina de reguli.

Examen final

Puteți participa la un singur examen final.

Datele de examen de SO pentru sesiunea iunie 2017 sunt:

Datele de examen de SO pentru sesiunea septembrie 2017 sunt:

Detalii despre examen găsiți în secțiunea aferentă din pagina de reguli.

Foi de examen

Lucrare 1

3CA, varianta 1

  1. Ce se întâmplă cu shell-ul după executarea comenzii exec sleep 10? Dar după executarea comenzii sleep 10?
    • Răspuns: La execuția comenzii exec sleep 10, imaginea shell-ului este înlocuită cu imaginea executabilului sleep. Adică shell-ul nu mai există ca proces. La încheierea procesului sleep, acesta își încheie execuția, iar shell-ul nu mai există. În cazul execuției comenzii sleep 10 se creează un nou proces care folosește imaginea executabilului sleep. Procesul shell așteaptă încheierea procesului sleep și apoi își continuă execuția.
  2. Dați două exemple de evenimente care pot determina un proces să iasă din starea RUNNING.
    • Răspuns: Un proces poate ieși din starea RUNNING în momentul în care:
      • își încheie execuția;
      • realizează o operație blocantă și trece în starea WAITING;
      • îi expiră cuanta de timp alocată pentru rulare și trece în starea READY;
      • dă voie de bună voie la procesor (yield) și trece în starea READY;
      • există un proces prioritar care este planificat pe procesor și care forțează procesul curent să treacă în starea READY.
  3. Cu ce diferă sistemele Windows de cele Unix la crearea de procese?
    • Răspuns: Pe sistemele UNIX, un proces este creat prin apelul fork() care creează o clonă / un duplicat al procesului părinte. Pentru un proces cu imagine de executabil nouă se folosește apelul exec() după apelul fork(). În Windows procesele sunt create cu apelul CreateProcess care primește imaginea de executabil, ca o unificare a apelurilor fork() și exec() din UNIX.

3CA, varianta 2

  1. Explicați cum lansează shell-ul în execuție comanda ls > a.txt.
    • Răspuns: Pentru lansarea comenzii ls > a.txt shell-ul apelează fork() și creează o clonă a sa. În clonă folosește open() și dup()/dup2() pentru a înlocui ieșirea standard cu fișierul a.txt. Ulterior apelează o funcție din familia exec() pentru a înlocui imaginea de executabil a clonei cu executabilul /bin/ls aferent comenzii ls.
  2. Care este diferența dintre fork și fork + exec?
    • Răspuns: În cazul apelului fork() se creează un proces clonă, copie aproape identică a procesului părinte. Procesul copil și procesul părinte execută același cod. În urma fork() + exec() imaginea (codul executabil) aferentă procesului copil este înlocuită cu imaginea de executabil transmisă ca argument funcției exec() rezultând într-un cod diferit pentru procesul copil.
  3. Care este diferența dintre funcțiile fwrite și write atunci când scriem într-un fișier?
    • Răspuns: Funcția write() este un apel de sistem unbuffered, în vreme ce funcția fwrite() este un apel de bibliotecă buffered. Atunci când apelăm write() se execută apel de sistem care va transfera datele din user space în kernel space. În cazul apelului fwrite() datele sunt transferate într-un buffer intern al bibliotecii standard C urmând ca apelul de sistem efectiv (și flush-ul datelor) să aibă loc la un moment ulterior (de exemplu la newline sau când se umple buffer-ul intern). Apelul fwrite() consumă mai multă memorie pentru bufer-ul intern cu avantajul reducerii overhead-ului cauzat de apeluri de sistem.

3CB/CC, varianta 1

  1. Dați un exemplu de efect negativ care s-ar întâmpla dacă nu ar exista separația kernel mode / user mode.
    • Răspuns: Dacă nu ar exista kernel mode, o aplicație (un proces) ar avea acces la memoria altui proces și ar putea corupe buna funcționare a acestuia. De asemenea, un proces are acces nemediat la resursele hardware putând citi datele altor procese sau putând face un atac de tipul denial-of-service pe acele resurse.
  2. Dați exemplu de operație care modifică cursorul unui fișier fără a modifica dimensiunea fișierului.
    • Răspuns: Operația read() modifică cursorul de fișier. Dimensiunea nu este modificată pentru că nu ajunge să se scrie mai mult sau mai puțin. Operația write() modifică cursorul de fișier și nu modifică dimensiunea dacă nu scrie peste dimensiunea curentă a fișierului. Operația lseek() este definiția enunțului: doar modifică cursorul de fișier, fără alte acțiuni.
  3. Câte procese se pot găsi în starea RUNNING la un moment dat în sistemul de operare?
    • Răspuns: În starea RUNNING să găsește maxim un singur proces per procesor. Într-un sistem de operare avem maxim N procese în starea RUNNING, unde N este numărul de procesoare.

3CC, varianta 2

  1. Dați exemplu de situație în care o aplicație oarecare are nevoie să execute un apel de sistem.
    • Răspuns: O aplicație are nevoie să execute un apel de sistem în momentul în care dorește să interacționeze cu I/O: rețea, hard disc, terminal, tastatură. De asemenea, atunci când dorește să comunice cu alte procese (în orice formă posibilă). Nu în ultimul rând, atunci când face operații de lucru cu memoria.
  2. Ce conține o intrare din tabela de descriptori a unui proces?
    • Răspuns: Tabela de descriptori a unui proces conține pointeri către structurile de fișier deschis. Când un fișier este deschis se creează o structură de fișier deschis, iar în tabela de descriptori, în primul slot liber, se plasează un pointer la această structură.
  3. Dați două exemple de situații în care are loc o schimbare de context între două procese.
    • Răspuns: Schimbarea de context între două procese poate avea loc atunci când:
      • procesului care rulează îi expiră cuanta;
      • procesul care rulează efectuează o operație blocantă;
      • procesul care rulează cedează de bună voie procesorul (yield);
      • procesul care vrea să ruleze are o prioritate mai mare;
      • procesul care rulează își încheie execuția.

Greșeli frecvente

Lucrări foarte bune

Lucrare 2

3CA, varianta 1

  1. Explicați în ce situație două procese pot fi planificate inechitabil pe un procesor (adică ocupă timp diferit pe procesor) de către un planificator round-robin.
    • Răspuns: Planificatorul round-robin planifică procesele rând pe rând. Dacă un proces este CPU intensiv iar altul I/O intensiv, atunci cel CPU intensiv va consuma mai mult timp pe procesor. Fie până când îi expiră cuanta (round-robin preemptiv) fie până când își încheie execuția sau, în final, se blochează (round-robin cooperativ). Procesul I/O intensiv se va bloca mult mai repede și astfel va ocupa mai puțin timp pe procesor.
  2. Într-un sistem cu paginare, cum este transformată o adresă virtuală într-o adresă fizică?
    • Răspuns: Din adresa virtuală se obține adresa paginii virtuale și offset-ul în cadrul paginii. Adresa paginii este folosită ca index în cadrul tabelei de pagini. În poziția aferentă din cadrul tabelei de pagini se găsește adresa paginii fizice (frame) aferente. Această adrese este apoi cuplată cu offset-ul în cadrul paginii și se obține adresa fizică finală. Pentru eficiență se folosește TLB-ul în căutarea paginii fizice aferente paginii virtuale.
  3. Dați un avantaj al folosirii demand paging în sistemele cu memorie virtuală.
    • Răspuns: Avantajul demand paging este timpul scurt folosit pentru “alocarea” memoriei și reducerea consumului de memorie fizică. Întrucât nu se consumă memorie fizică, “alocarea” va fi mai rapidă și nu va consuma din memoria RAM, lăsând-o disponiilă pentru acțiuni urgente. Memoria RAM va fi alocată și consumată on-demand (la nevoie).

3CA, varianta 2

  1. Un programator apelează pipe(..). Cum poate fi transmis descriptorul rezultat pentru citire unui alt proces?
    • Răspuns: Descriptorii de fișiere pot fi transferați doar între procese înrudite. Un descriptor de proces va fi transferat către procesul/procesele copil în momentul apelării fork().
  2. Cum rezolvă MMU adresa fizică a tabelei de pagini a procesului curent?
    • Răspuns: 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.
  3. Explicați modul în care apelul fork() inițializează memoria procesului nou creat.
    • Răspuns: Procesul nou creat partajează spațiul de memorie fizică al procesului părinte. Fiecare proces are spațiu virtual propriu și tabelă de pagini proprie; pentru procesul copil se creează o tabelă de pagini nouă și se copiază conținutul tabelei de pagini a procesului părinte. Spațiile virtuale de adrese ale celor două procese sunt mapate peste același spațiu fizic, care est marcat read-only. La accesul de scriere la o pagină din partea unuia dintre cele două procese, are loc page fault și copy-on-write: pagina fizică în cauză este duplicată, este marcată read-write și apoi peste ea este mapată pagina virtuală în care a avut loc page fault-ul.

3CB/CC, varianta 1

  1. De ce un planificator de procese de pe un sistem de operare modern oferă, în general, o cuantă de timp mai mare proceselor I/O intensive?
    • Răspuns: Un proces I/O intensiv este așteptat să se blocheze repede și astfel, să cedeze procesorul. I se poate aloca o cuantă de timp mai mare (și o prioritate) mai mare pentru a rula cât mai rapid, pentru că oricum apoi va lăsa loc altor procese (CPU intensive sau I/O intensive).
  2. Care este un avantaj al paginării memoriei față de segmentare?
    • Răspuns: Paginarea conduce la eliminarea fragmentării externe: fragmentarea în afara blocurilor alocate. Spațiile libere sunt multiplu de pagină și pot fi alocate individual. În cazul segmentării ar trebui să găsim o zonă liberă suficientă pentru segmnetul care se alocă. Un alt avantaj este managementul mai ușor al memoriei din partea sistemului de operare, întreaga memorie fiind acum administrată la nivelul unei pagini (bloc de dimensiune fixă).
  3. Furnizați două exemple de situații în care un page fault nu conduce la trimiterea unei excepții de memorie (de tipul segmentation fault) procesului.
    • Răspuns: Situații în care un page fault nu generează segmentation fault sunt:
      • se accesează o pagină care este swappată
      • se accesează o pagină care nu este încă paginată (comisă în RAM) și care va fi paginată prin demand paging
      • se accesează pentru scriere o pagină read-only marcată copy-on-write

3CB/CC, varianta 2

  1. De ce este util să avem priorități dinamice pentru planificarea proceselor, nu priorități statice?
    • Răspuns: Dacă am folosi doar priorități statice, care nu se modifică pe durata execuției procesului, atunci procesul cu prioritatea statică cea mai bună ar acapara pentru o perioadă nedeterminată procesorul. Și alte procese ar ajunge să folosească puțin (spre deloc) procesorul, rezultând în starvation. Folosirea priorităților dinamice duce la alterararea la rulare a priorității proceselor și, în acest fel, la creșterea prioriății proceselor care nu au rulat și scăderea priorității celor care au rulat, ducând la o folosire mai echitabilă a procesorului și, în acest fel, la evitarea fenomenului de starvation.
  2. De câtă memorie RAM poate dispune un sistem cu procese având spațiu virtual de adrese de o dimensiune dată (de exemplu 4GB)?
    • Răspuns: Nu există o legătură directă între dimensiunea spațiului virtual de adrese al procesului. Dimensiunea spațiului virtual de adrese este dată de capacitatea de adresare virtuală, iar spațiul fizic (RAM) de capacitatea de adresare fizică (dimensiunea magistralei de adrese dintre procesor și memoria RAM). În teorie, sistemul poate dispune de oricâtă memorie RAM (cât îi permite magistrala de adrese) independent de capacitatea spațiului virtual de adrese al procesului. De exemplu, un output al comenzii cat /proc/cpuinfo: address sizes : 36 bits physical, 48 bits virtual
      • În exemplul de mai sus, pot fi maxim 2^36 = 64GB RAM și 2^48 = 256 TB spațiu virtual de adrese. Nu e obligatoriu să avem mai mulți biți pentru adresele virtuale decât pentru adresele fizice.
  3. Când apare fenomenul de “memory thrashing”?
    • Răspuns: Fenomenul de memory thrashing apare în momentul în care set size-ul (spațiul fizic folosit) al proceselor sistemului depășește capacitatea memoriei RAM: fie multe procese, fie procese cu consum mare de memorie RAM (set size mare). În acest caz, un proces care rulează are nevoie de multe pagini în RAM și le aduce de pe swap (swap in), evacuând pagini ale altui proces (swap out); apoi acel proces este planificat și face operația similară. Rezultă așadar un transfer constant între RAM și disc care afectează performanța.

Greșeli frecvente

Lucrări foarte bune

Lucrare 3

3CA, varianta 1

  1. Enumerați o caracteristică a limbajelor C/C++ care permite apariția vulnerabilităților de tip buffer overflow.
    • Răspuns: Nu exista bounds checking.
  2. Dați un exemplu de cod care folosește instrucțiunea cmpxchg pentru a implementa un mutex.
    • Răspuns:
      void lock (struct mutex* m){
      	while(atomic_cmpxchg(s->val,1,0)==0){
      		add_proc_to_mutex_waiting_list;
      		scheduler();
      	}
      }
      void unlock(struct mutex* m){
      	atomic_set(s->val,1);
      	mark_waiting_processes_as_ready;
      }
  3. Explicați cum pot fi lansate atacuri de tip buffer overflow împotriva unui server care execută fork() pentru a procesa cererile fiecărui client. Se folosesc stack canaries pe 32 biți.
    • Răspuns: Valoarea canary de pe stiva clientului va fi aceeasi dupa fiecare fork; atacatorul poate incerca toate valorile posibile (4 miliarde) executand overflow cu un canar ales pana o gaseste pe cea corecta. Pentru a minimiza nr. de incercari se poate atata octet cu octet: astfel canary value va fi gasita in 1024 de incercari.

3CA, varianta 2

  1. De ce nu sunt posibile buffer overflows în Java?
    • Răspuns: In Java, toate accesele la memorie sunt verificate sa fie “in bounds”.
  2. Dați un exemplu de cod care folosește instrucțiunea cmpxchg pentru a implementa un semafor.
    • Răspuns:
      void sem_up (struct sem* s){
      	while(atomic_cmpxchg(s->mutex,1,0)==0);
      	m->semval++;
      	atomic_set(s->mutex,1)
      	wake_up_waiting_processes;
      }
      void sem_down(struct sem* s){
      	while (1){
      		while(atomic_cmpxchg(s->mutex,1,0)==0);
      		if (m->semval>0){
      			m->semval--;
      			atomic_set(s->mutex,1);
      			return;
      		}
      		else {
      			Add_process_to_waiting_list_for_s;
      			atomic_set(s->mutex,1);
      		}
      	}
      }
  3. Explicați cum putem afla adresa aproximativă a segmentului de cod pe un sistem de 64 biți cu ASLR care are o vulnerabilitate de tip buffer overflow. Procesul vulnerabil execută fork() pentru a procesa fiecare cerere a clienților.
    • Răspuns: Adresa de intoarcere de pe stiva poate fi leaked catre atacator astfel; atacatorul poate incerca sa ghiceasca primul octet executand overflow cu o valoare aleasa de el (e.g. 0); daca clientul continua sa functioneze, adresa a fost buna; altfel conexiunea se va inchide pentru ca client a fost terminat de SO; atacatorul incearca apoi urmatoarea valoare (adresa de retur este aceeasi pentru ca serverul face fork la fiecare client), si asa mai departe pana gaseste un octet bun. Se continua la fel cu urmatorii octeti pana cand se gaseste o adresa completa valida in segementul de cod de la server.

3CB/CC, varianta 1

  1. Un program are o vulnerabilitate de tipul buffer overflow. Sistemul pe care rulează are suport DEP (Data Execution Prevention). Cum puteți obține un shell din exploatarea programului?
    • Răspuns: Dacă sistemul are suport DEP, nu putem să injectăm cod pe care să-l executăm (în forma unui shellcode). Va trebui să apelăm cod deja existent. Pentru a obține un shell cel mai bine este să apelăm system(“/bin/bash”) adică să generăm un payload care să apeleze cod din biblioteca standard C (return-to-libc attack); facem acest lucru prin suprascrierea adresei de retur cu adresa funcției system() și a transmiterii corespunzătoare a adresei șirului “/bin/bash”.
  2. Numiți un avantaj și un dezavantaj al folosirii thread-urilor în locul proceselor pentru o aplicație care folosește paralelism în execuție.
    • Răspuns: În momentul în care avem o aplicației cu paralelism în execuție, folosirea thread-urilor are avantajul productivității: fiecare thread va rula pe un core și vom paraleliza astfel programul și vom obține speedup. Este de asemenea, foarte facil să partajăm informațiile între thread-uri. Dezavantajul este că thread-urile pot să acceseze în mod incoerent datele partajate (întreg spațiul de adrese al procesului) rezultând în erori în execuție sau rezultate nedeterministe. De asemenea, dacă un thread execută o operație nevalidă, întreg procesul își va încheia execuția.
  3. Mai multe thread-uri folosesc o listă simplu înlănțuită ca structură comună. Parcurg, adaugă și șterg elemente din listă. Ce se întâmplă dacă nu asigurăm accesul exclusiv la listă?
    • Răspuns: Dacă nu asigurăm accesul exclusiv la listă pot avea loc ștergeri de noduri în vreme ce alte thread-uri accesează acele noduri. În acea situație, se poate ajunge ca un thread să acceseze date care acum sunt nevalide sau eliberate (use-after-free) sau să rezulte în segmentation fault.

3CB/CC, varianta 2

  1. Descrieți o situație în care un apel fgets() are o vulnerabilitate de tipul buffer overflow. Semnătura funcției fgets() este char *fgets(char *s, int size, FILE *stream);
    • Răspuns: Funcția fgets() are o vulnerabilitate în cazul în care se citesc mai mulți octeți decât este dimensiunea buffer-ului. Astfel dacă avem o definiție de forma char buffer[32]; și apelăm fgets(stdin, 64, buffer), astfel încâte 64 > 32, vom avea buffer overflow și se va suprascrie dincolo de limitele buffer-ului.
  2. Pe un sistem Unix modern, crearea unui proces folosind fork() este foarte rapidă. Totuși, durează de câteva ori mai mult decât crearea unui thread. Care e cauza principală pentru această diferență?
    • Răspuns: Atunci când un thread se creează în afara creării unei structurii de tipul TCB (Thread Control Block) nu se execută multe operații. În cazul unui proces, pe un sistem Unix modern, se creează un spațiu virtual de adresă nou, dar cu aceleași informații fizice ale procesului vechi. Adică se creează o tabelă de pagini nouă care este populată cu tabela de pagini a vechiului proces, rezultând într-un timp mai lung de creare a unui proces față de un thread.
  3. Când este recomandat să folosim spinlock-uri în loc de mutex-uri?
    • Răspuns: Folosim spinlock-uri în loc de mutex-uri în cazul în care regiunea critică pe care o vom proteja este de mici dimensiuni. În această situație, overhead-ul de lock pe mutex durează prea mult față de timpul petrecut în regiunea critică și atunci folosim spinlock-uri. Regiunea critică de mici dimensiuni și fără operații blocante este un candidat pentru folosirea de spinlock-uri în loc de mutex-uri.

Greșeli frecvente

Lucrări foarte bune

Lucrare 4

3CA, varianta 1

  1. Cum putem transfera datele de la un dispozitiv de intrare-ieșire utilizând cât mai puțin procesorul?
    • Răspuns: Intreruperi + DMA. Motive pentru care intreruperile ar fi nocive (e.g. dispozitiv de I/O prea rapid).
  2. Un programator folosește două apeluri send() pentru a transmite cu TCP dimensiunea mesajului și conținutul său, respectiv, după care așteaptă răspunsul apelând recv(). Ce probleme de performanță pot aparea?
    • Răspuns: Interactiunea dintre algoritmul lui Nagle si mecanismul Delayed Ack care reduce performanta dramatic (aproximativ 5 mesaje pe secunda). Doua syscalls per mesaj in loc de un singur syscall - overhead mai mare.
  3. Explicați diferența dintre un hard link și symlink într-un sistem de fișiere UNIX.
    • Răspuns: Un hardlink reprezinta legatura dintre dentry (sau proces in executie cu fisier deschis) si i-node. Un softlink este legatura inversa, de la un inode la un dentry.

3CA, varianta 2

  1. Dați un exemplu în care este mai eficient să folosim polling decât întreruperi pentru a aștepta date de la un dispozitiv de intrare-ieșire.
    • Răspuns: Atunci cand dispozitivul de I/O genereaza intreuperi mai repede decat poate procesa sistemul gazda. Exemplu: placa de retea de 10Gbps sau mai rapida atunci cand pachetele sunt mici.
  2. Ce ni se garantează atunci când apelul send(socket, …) întoarce N > 0?
    • Răspuns: Datele au fost copiate in buffer-ul nucleului de operare si vor fi transmise daca acest lucru e posibil (i.e. reteaua functioneaza corect).
  3. De ce nu este corect să măsurăm performanța unui dispozitiv de stocare folosind doar apelul write()?
    • Răspuns: Apelul write se intoarce cand datele sunt scrise in memorie (in buffer cache) - trebuie sa fortam si sincronizarea ( fsync) daca vrem sa masuram performanta dispozitivului.

3CC, varianta 1

  1. Precizați un avantaj și un dezavantaj în folosirea operațiilor I/O asincrone.
    • Răspuns: Avantaj este că nu se blochează programul și putem rula mai multe acțiuni (asincrone) în paralel. Dezavantajul este că trebuie să avem mecanisme de așteptare a operațiilor asincrone care fac programarea mai dificilă; în momentul în care o operație se încheie ne va notifica de acest lucru (care va întrerupe fluxul normal de execuție) sau va trebui să apelăm o funcție dedicată (de obicei blocantă) de așteptare. Uzual este nevoie de un automat de stări care să mențină starea operațiilor asincrone, lucru de asemenea dezavantajos.
  2. De ce este necesar suportul de TCP Offload Engine pentru legături de rețea de mare viteză (precum 10Gbit)?
    • Răspuns: Fără suport de TCP Offload Engine, prelucrarea pachetelor (la nivelul stivei TCP) în codul driverului/sistemului de operare durează suficient de mult încât să nu ajungă la viteza maximă de 10Gbit.
  3. Un director conține 100 de intrări identificate ca fișiere obișnuite (regular files). Care este numărul minim de inode-uri referite de acele intrări?
    • Răspuns: Fiecare intrare de tip regular file pointează către un inode. Dacă toate intrările sunt link-uri hard ale aceluiași inode, atunci numărul minim de inode-uri este 1.

3CC, varianta 2

  1. De ce este avantajoasă folosirea DMA (Direct Memory Access) în cazul operațiilor de intrare/ieșire?
    • Răspuns: Folosirea DMA duce la eliminarea procesorului din fluxul de prelucrare a unor date. Datele ajung de la dispozitiv la memoria RAM și invers fără intervenția procesorului. În felul acesta procesorul poate fi folosit pentru activități precum rularea codului proceselor, mărind productivitatea sistemului.
  2. Un apel send se blochează pentru că buffer-ul de send din kernel al socket-ului este plin. Care este o cauză posibilă pentru acest lucru?
    • Răspuns: Buffer-ul de send al unui socket se poate bloca din cauză că a apărut o congestie la un middlebox pe traseul pachetului. Până la eliberarea congestiei bufferul de send rămâne plin și blochează apelul send(). Pe lângă aceasta, buffer-ul de receive al destinatarului/receiver-ului se poate să fie umplut și nu poate primi noi date, blocând și buffer-ul de send. Deblocarea se va face în momentul în care se realizează un apel de tip recv() la receiver care va goli parte din buffer-ul de receive și astfel și parte din buffer-ul de send.
  3. Ce conțin blocurile de date indicate de inode-ul unui director?
    • Răspuns: Blocurile de date indicate de inode-ul unui director conțin un vector de dentry-uri (intrări de tip directory) pentru intrările conține de director. Acestea conțin, în general, numele intrărilor și indexul inode-ului.

Greșeli frecvente

Lucrări foarte bune

Examene anterioare