Sari la conținut

Sisteme de operare — teorie generală

Ce acoperă capitolul: procese și thread-uri, sincronizare, apeluri de sistem, fork/exec, memorie virtuală și paginare. Sursa: secțiunea „II. Sisteme de operare” din testele 2019 și 2020 — capitol care a dispărut din structura 2022–2025, unde a fost înlocuit de secțiunile separate Windows / Linux / Virtualizare.

Merită învățat oricum

Subiectele de aici (paginare, fork, mutex, thread-uri) sunt fundamentale și pot reapărea. Dacă timpul este scurt, prioritizează Rețele și Limbajul C, apoi revino aici.


Cuprins

Partea I — Teorie

  1. Structura unui sistem de operare
  2. Apeluri de sistem
  3. Procese vs. thread-uri
  4. fork(), exec() și crearea proceselor
  5. Sincronizare: mutex și semafoare
  6. Memorie virtuală și paginare

Partea a II-a — Toate întrebările, rezolvate (6 întrebări unice)


Partea I — Teorie

1. Structura unui sistem de operare

Un sistem de operare este structurat pe două componente software, corespunzătoare celor două moduri de execuție impuse de procesor:

graph TD
    subgraph U["USER MODE — privilegii reduse"]
        A["Aplicații: browser, editor, compilator"]
        S["Shell / Interfață grafică (GUI)"]
        L["Biblioteci: libc, kernel32.dll"]
    end
    subgraph K["KERNEL MODE — privilegii complete"]
        SC["Interfața de apeluri de sistem"]
        M["Managerul de procese · memorie · fișiere · I/O"]
        D["Drivere de dispozitiv"]
    end
    A --> L
    S --> L
    L -->|apel de sistem| SC
    SC --> M --> D --> HW["HARDWARE"]

Ce trebuie să știi exact:

Afirmație Adevărat?
Un SO este structurat pe 2 componente software — kernel mode și user mode
Un SO rulează direct peste hardware (nu are nevoie de un „kernel intermediar”)
Un SO are 3 componente: kernel mode, user mode și „superuser mode” „superuser/root” este un nivel de privilegiu al unui cont, nu un mod de execuție al procesorului
Interfața grafică comunică direct cu componentele hardware GUI-ul rulează în user mode și trece obligatoriu prin kernel
Interfața în linie de comandă este implementată în kernel mode shell-ul (bash, cmd, powershell) este un program obișnuit în user mode

Ideea centrală

nimic din user mode nu atinge hardware-ul direct. Orice acces trece prin apeluri de sistem → kernel → drivere → hardware.

2. Apeluri de sistem

Un apel de sistem (system call) este interfața prin care programele din user mode cer servicii kernelului: deschiderea unui fișier, alocarea de memorie, crearea unui proces, trimiterea pe rețea.

      Aplicație (user mode)
             │  printf("…")
      Bibliotecă (libc)                ← "wrapper" comod peste apelul de sistem
             │  write(1, buf, n)
   ╔═══ trecere în kernel mode ═══╗    ← instrucțiune specială (syscall / int 0x80)
      Kernel: sys_write()
      Driver → Hardware
Categorie Exemple POSIX Exemple Windows
Procese fork, execve, wait, exit CreateProcess, ExitProcess
Fișiere open, read, write, close CreateFile, ReadFile
Memorie mmap, brk VirtualAlloc
Rețea socket, bind, connect, send Winsock

Nuanța testată la grilă

Cine oferă interfața de acces la serviciile SO? Apelurile de sistem. Bibliotecile (libc) sunt doar un strat de comoditate peste ele, iar instrucțiunile assembly sunt mecanismul de comutare, nu interfața. Răspunsul corect este „apeluri de sistem”, nu „A, B, C”.

3. Procese vs. thread-uri

Proces Thread (fir de execuție)
Spațiu de adrese propriu, izolat partajat cu celelalte thread-uri ale procesului
Stivă proprie proprie (fiecare thread are stiva lui)
Registre / PC proprii proprii
Cod, date globale, heap, fișiere deschise proprii partajate
Cost de creare/distrugere mare (spațiu de adrese nou, tabele de pagini) mic
Cost de comutare (context switch) mare (schimbă tabela de pagini, golește TLB-ul) mic
Planificare independentă independentă
Comunicare IPC (pipe, socket, memorie partajată) direct prin variabile partajate
Efectul unei blocări doar procesul respectiv depinde — vezi mai jos
Efectul unui crash izolat omoară tot procesul

Ce partajează și ce nu partajează thread-urile

┌────────────────── PROCES ──────────────────┐
│  PARTAJAT:  cod · date globale · heap      │
│             fișiere deschise · semnale     │
│  ┌────────┐  ┌────────┐  ┌────────┐        │
│  │Thread 1│  │Thread 2│  │Thread 3│        │
│  │ stivă  │  │ stivă  │  │ stivă  │  ← PROPRII
│  │registre│  │registre│  │registre│        │
│  └────────┘  └────────┘  └────────┘        │
└────────────────────────────────────────────┘

Cele două capcane clasice

  1. „Thread-urile se creează mai LENT decât procesele”FALS. Este exact invers: crearea unui thread este mult mai ieftină, pentru că nu implică un spațiu de adrese nou. Acesta este principalul avantaj al thread-urilor, nu un dezavantaj.

  2. „Dacă un thread se blochează, celelalte nu sunt afectate”depinde de model:

Model Când un thread se blochează la I/O
Kernel-level threads (thread-uri native, cazul modern: Linux NPTL, Windows) kernelul vede fiecare thread → celelalte continuă
User-level threads (biblioteci în spațiul utilizator, modelul „many-to-one”) kernelul vede un singur thread → tot procesul se blochează

La grilă, afirmația falsă este aproape sigur cea cu „durata mai mare de creare și distrugere” — este falsă necondiționat. Afirmația despre blocare este adevărată în modelul modern (kernel-level), pe care îl presupune orice curs standard.

4. fork(), exec() și crearea proceselor

fork()

Creează un duplicat al procesului curent. Ambele procese (părinte și copil) continuă execuția de la instrucțiunea de după fork().

pid_t pid = fork();
// de aici încolo execută DOUĂ procese
if (pid == 0)      { /* copil  — fork() returnează 0        */ }
else if (pid > 0)  { /* părinte — fork() returnează PID-ul copilului */ }
else               { /* eroare — fork() returnează -1       */ }

Formula de numărare: n apeluri fork() executate necondiționat, în serie2ⁿ procese în total (inclusiv cel inițial).

Nr. de fork() Procese totale
1 2
2 4
3 8
4 16

De ce: fiecare fork() dublează numărul de procese existente, pentru că fiecare proces existent îl execută.

        start:                 1 proces
        fork() #1:      1 ──► 2 procese
        fork() #2:      2 ──► 4 procese
        fork() #3:      4 ──► 8 procese

execve()

Înlocuiește imaginea procesului curent cu un program nou. NU creează un proces nou — PID-ul rămâne același, dar codul, datele și stiva sunt înlocuite.

Regula de aur a lui exec

dacă execve() reușește, el nu se mai întoarce niciodată. Tot codul scris după un execve() reușit nu se execută niciodată, pentru că programul care l-ar fi executat nu mai există în memorie. execve() returnează (cu -1) doar în caz de eșec.

Capcana for fără acolade

for (int i = 0; i < 3; i++)
   fork();                      // ← DOAR aceasta este în corpul buclei
   execve(argv[0], &argv[0], envp);   // se execută O SINGURĂ DATĂ, după buclă
   fork();                            // se execută O SINGURĂ DATĂ, după buclă

În C, un for fără acolade cuprinde o singură instrucțiune. Indentarea nu are nicio semnificație pentru compilator. Codul de mai sus este echivalent cu:

for (int i = 0; i < 3; i++) { fork(); }
execve(argv[0], &argv[0], envp);
fork();

5. Sincronizare: mutex și semafoare

Secțiunea critică

O secțiune critică este o porțiune de cod care accesează o resursă partajată și care trebuie executată de un singur thread/proces la un moment dat.

down(&mutex);       // wait / P / lock / acquire — dacă e ocupat, aștept
   /* SECȚIUNE CRITICĂ — acces exclusiv */
up(&mutex);         // signal / V / unlock / release
Primitivă Descriere
Mutex semafor binar (0/1). Excludere mutuală: un singur deținător.
Semafor numărat contor ≥ 0; permite până la N accese simultane.
down() / wait() / P() decrementează; dacă ar deveni negativ, blochează apelantul
up() / signal() / V() incrementează; deblochează un proces care aștepta

Cum rezolvi o problemă de interleaving (pas cu pas)

Metoda care rezolvă orice variantă a acestui tip de întrebare:

  1. Notează ordinea internă a fiecărui proces — în interiorul unui proces, instrucțiunile se execută strict în ordine.
  2. Marchează secțiunile critice — ce se află între down și up.
  3. Regula fundamentală: două secțiuni critice nu se pot întrepătrunde niciodată. Odată ce un proces a intrat, celălalt așteaptă până la up().
  4. Verifică fiecare variantă: caută o literă din secțiunea critică a lui P2 apărută între literele secțiunii critice a lui P1 (sau invers). Dacă găsești → imposibil.
  5. Restul este liber: instrucțiunile din afara secțiunilor critice se pot intercala oricum.

6. Memorie virtuală și paginare

Ideea

Fiecare proces „vede” un spațiu de adrese propriu, continuu (adrese virtuale). Hardware-ul (MMU) traduce fiecare adresă virtuală într-o adresă fizică, folosind tabela de pagini a procesului.

Structura unei adrese

adresă virtuală  =  [ număr pagină ][ offset în pagină ]
adresă fizică    =  [ număr cadru  ][ acelaşi offset   ]
                          └── tabela de pagini: pagină → cadru (frame)

Offset-ul NU se schimbă niciodată la traducere — se schimbă doar partea de sus.

Formulele

Mărime Formulă
Biți de offset log₂(dimensiunea paginii)
Biți pentru numărul de pagină nr. total biți adresă − biți de offset
Traducere adresă_fizică = (frame × dimensiune_pagină) + offset
Echivalent pe biți adresă_fizică = (frame << biți_offset) | offset
Dimensiune pagină Biți de offset
1 KB = 2¹⁰ 10
2 KB = 2¹¹ 11
4 KB = 2¹² 12
8 KB = 2¹³ 13
4 MB = 2²² 22

Scurtătura hexazecimală

O cifră hexazecimală = exact 4 biți. Dacă offset-ul are 12 biți, atunci ocupă exact ultimele 3 cifre hexa ale adresei.

adresă virtuală 0x305B, pagini de 4KB (12 biți offset), adresă pe 16 biți
                 ▲▲▲▲
                 │└┴┴── offset  = 0x05B   (12 biți = 3 cifre hexa)
                 └───── pagina  = 0x3     ( 4 biți = 1 cifră hexa)

Adică: taie ultimele 3 cifre hexa → ce rămâne este numărul paginii; caută-l în tabelă; lipește cadrul la loc în fața acelorași 3 cifre.

Termeni conecși

Termen Sens
Page fault pagina cerută nu este în RAM → kernelul o aduce de pe disc (swap)
TLB cache hardware al traducerilor recente (Translation Lookaside Buffer)
Swapping / paging mutarea paginilor între RAM și disc
Fragmentare internă spațiul nefolosit din ultima pagină a unui proces

Partea a II-a — Toate întrebările, rezolvate


S1 · Structura unui sistem de operare

2020

Referitor la sistemele de operare, care din următoarele variante este adevărată?

  • a. Un sistem de operare nu poate rula direct peste hardware, fiind necesar un kernel intermediar
  • b. Un sistem de operare este structurat pe 3 componente — kernel mode, user mode și superuser mode
  • c. Interfața grafică a sistemului de operare comunică, în mod direct, cu componentele hardware de citire/afișare
  • d. Un sistem de operare este structurat pe 2 componente software — kernel mode și user mode
  • e. Interfața în linie de comandă este implementată în kernel mode, iar interfața grafică în user mode
Răspuns și explicație

Răspuns: d)

Separarea user mode / kernel mode este impusă chiar de procesor (inelele de protecție: Ring 3 pentru aplicații, Ring 0 pentru kernel).

De ce cad celelalte:

Greșeala
a Sistemul de operare este stratul de deasupra hardware-ului — nu există „kernel intermediar”. (Un hypervisor poate fi interpus, dar este opțional.)
b „Superuser” / „root” / „Administrator” este un nivel de privilegiu al unui cont de utilizator, nu un mod de execuție al procesorului. Un proces al lui root rulează tot în user mode.
c GUI-ul rulează în user mode. Accesul la placa video se face prin apeluri de sistem → driver → hardware. Niciun cod din user mode nu atinge hardware-ul direct.
e Inversat pe jumătate: și CLI-ul (bash, cmd), și GUI-ul rulează în user mode. Shell-ul este un program obișnuit — îl poți înlocui.

S2 · Interfața de acces la serviciile SO

20192020recurentă

Interfața de acces la serviciile puse la dispoziție de un sistem de operare este oferită de:

  • a. Apeluri de sistem
  • b. Instrucțiuni assembly
  • c. Biblioteci
  • d. A, B, C
  • e. Niciuna dintre variantele de mai sus
Răspuns și explicație

Răspuns: a) Apeluri de sistem

Apelul de sistem este, prin definiție, interfața dintre user mode și kernel. Este singurul mod prin care un program poate cere kernelului să facă ceva.

De ce cad celelalte:

Rolul real
b) instrucțiuni assembly syscall / int 0x80 / sysenter sunt mecanismul de comutare (cum se face trecerea), nu interfața (ce servicii sunt oferite)
c) biblioteci libc oferă wrappere de comoditate peste apelurile de sistem (printfwrite). Poți ocoli complet biblioteca și apela direct kernelul; invers, nu.
d) A, B, C tentant, dar întrebarea cere interfața, nu tot lanțul de mecanisme
e) niciuna fals

Testul mental

„Care este contractul dintre aplicație și kernel?” → lista de apeluri de sistem. Bibliotecile și instrucțiunile sunt detalii de implementare de-o parte și de alta a contractului.


S3 · Afirmația falsă despre thread-uri

20192020recurentă

Care dintre următoarele afirmații este falsă cu privire la firele de execuție (thread-uri)?

  • a. Un dezavantaj al utilizării thread-urilor îl reprezintă durata mai mare de creare și distrugere decât în cazul proceselor
  • b. Thread-urile împart spațiul de adrese al procesului de care aparțin
  • c. Fiecare thread are propria stivă pe care se execută
  • d. Thread-urile pot fi planificate independent unul de celălalt
  • e. Dacă un thread se blochează (așteptare după I/O, mutex, semafor etc.), celelalte thread-uri din cadrul aceluiași proces nu sunt afectate
Răspuns și explicație

Răspuns: a) — este singura afirmație falsă.

Adevărul este exact pe dos: thread-urile se creează și se distrug mult mai repede decât procesele. Acesta este principalul lor avantaj, motivul pentru care există.

Operație Proces (fork) Thread
Spațiu de adrese nou trebuie creat se refolosește
Tabele de pagini noi
Copierea descriptorilor
Cost relativ mare mic

De ce celelalte sunt adevărate:

  • b) prin definiție: thread-urile partajează codul, datele globale, heap-ul și fișierele deschise.
  • c) fiecare thread are stivă proprie (variabilele locale și lanțul de apeluri sunt private) plus registre și PC proprii.
  • d) planificatorul kernelului tratează thread-urile ca entități separate de planificare.
  • e) în modelul kernel-level (Linux NPTL, Windows — standardul modern), kernelul vede fiecare thread separat, deci blocarea unuia nu îi afectează pe ceilalți. (Nuanță: la thread-urile pur user-level, „many-to-one”, blocarea unuia ar bloca tot procesul — dar acesta este un model istoric, iar afirmația (a) rămâne oricum falsă necondiționat, deci ea este răspunsul.)

S4 · Câte procese rezultă din secvența fork / execve

20192020recurentă

În urma execuției următoarei secvențe, câte procese se vor executa în sistem? Se pornește de la un singur proces. Considerați și procesul inițial. Se consideră că toate apelurile reușesc.

char *argv[] = {"/bin/sh", "-c", "env", 0};
char *envp[] = {"PATH=/bin:/usr/bin"};

for (int i = 0; i < 3; i++)
   fork();
   execve(argv[0], &argv[0], envp);
   fork();
  • a. 4 b. 9 c. 16 d. 12 e. 8
Răspuns și explicație

Răspuns: e) 8

Rezolvarea are două capcane, ambele trebuie văzute.

Capcana 1 — for fără acolade. În C, un for fără { } cuprinde o singură instrucțiune. Indentarea nu contează pentru compilator. Codul real este:

for (int i = 0; i < 3; i++) {
   fork();                            // ← singura instrucțiune din buclă
}
execve(argv[0], &argv[0], envp);       // o singură dată, după buclă
fork();                               // o singură dată, după buclă

Capcana 2 — execve reușit nu se mai întoarce. Enunțul spune explicit „toate apelurile reușesc”. Un execve() reușit înlocuiește complet imaginea procesului — codul de după el nu mai există în memorie, deci ultimul fork() nu se execută niciodată.

Numărătoarea:

Moment Procese
start 1
după fork() #1 2
după fork() #2 4
după fork() #3 8
toate cele 8 execută execve 8 (își schimbă doar programul, PID-urile rămân)
ultimul fork() nu se executăexecve nu s-a întors

Total: 2³ = 8 procese.

De unde vin distractorii:

  • c) 16 — dacă ai presupune că se execută și ultimul fork() (2⁴) — greșeala „am uitat că execve nu se întoarce”.
  • a) 4 — dacă ai număra .
  • b) 9 și d) 12 — dacă ai aduna în loc să înmulțești (3·3, 4·3).

S5 · Secvența care NU poate fi afișată (mutex)

20192020recurentă

Presupunem că următoarele două procese sunt executate simultan. Care dintre următoarele secvențe NU va fi afișată niciodată?

        P1                          P2
   printf("A");                printf("E");
   down(&mutex);               down(&mutex);
   printf("B");                printf("F");
   printf("C");                printf("G");
   up(&mutex);                 up(&mutex);
   printf("D");                printf("H");
  • a. ABCDEFGH b. ABEFGHCD c. ABCEFGHD d. EFGHABCD e. EABCFGDH
Răspuns și explicație

Răspuns: b) ABEFGHCD

Metoda. Secțiunile critice sunt:

  • P1: B C (între down și up)
  • P2: F G (între down și up)

Regula: BC și FG sunt atomice una față de cealaltă — odată ce un proces a intrat în secțiunea critică, celălalt nu poate intra până la up(). Deci nicio literă din {F,G} nu poate apărea între B și C, și invers.

Verificăm fiecare variantă:

Variantă Analiză Verdict
a ABCDEFGH P1 rulează complet, apoi P2 posibil
b AB EFGH CD E,F,G,H apar între B și C → P2 a intrat în secțiunea critică (F) în timp ce P1 o ținea IMPOSIBIL
c ABC EFGH D P1: A, down, B, C, up → P2: E, down, F, G, up, H → P1: D. D este în afara secțiunii critice, poate fi amânat oricât posibil
d EFGH ABCD P2 complet, apoi P1 posibil
e E ABC FG D H E (P2, înainte de down) → P1 intră, B, C, iese → P2 intră, F, G, iese → D (P1) → H (P2). Nicio suprapunere de secțiuni critice posibil

Scurtătura de examen

Caută varianta în care literele unei secțiuni critice (B…C) sunt întrerupte de litere din cealaltă (F sau G). Aceea este răspunsul.

Verificat prin enumerarea exhaustivă a tuturor intercalărilor legale: doar ABEFGHCD este de negenerat.


S6 · Traducerea adresei virtuale în adresă fizică

2019

Pentru un sistem cu adresare pe 16 biți, un proces are tabela de pagini de mai jos. Care este adresa fizică corespunzătoare adresei virtuale 0x305B, știind că dimensiunea unei pagini este de 4 KB?

Page Page Frame Page Page Frame
0x00 0x0A 0x08 0x00
0x01 0x03 0x09 0x01
0x02 0x04 0x0A 0x0E
0x03 0x09 0x0B 0x08
0x04 0x0A 0x0C 0x07
0x05 0x0F 0x0D 0x02
0x06 0x05 0x0E 0x06
0x07 0x0F 0x0F 0x0B
  • a. 0x305B b. 0x3A5B c. 0x3058 d. 0x9AF8 e. 0x905B
Răspuns și explicație

Răspuns: e) 0x905B

Pasul 1 — împarte adresa. Pagina de 4 KB = 2¹² ⇒ offset-ul are 12 biți. Adresa are 16 biți ⇒ numărul de pagină are 16 − 12 = 4 biți.

Pasul 2 — folosește scurtătura hexa. O cifră hexa = 4 biți, deci 12 biți = exact 3 cifre hexa:

   0x  3   0 5 B
       ▲   ▲▲▲▲▲
       │   └┴┴── offset  = 0x05B   (12 biți)
       └─────── pagina  = 0x3     ( 4 biți)

Pasul 3 — caută în tabelă. Pagina 0x03 → cadrul (frame) 0x09.

Pasul 4 — recompune. Cadrul înlocuiește numărul de pagină; offset-ul rămâne neschimbat:

   frame 0x9  +  offset 0x05B   →   0x9 05B  =  0x905B

Verificare aritmetică: 0x09 × 4096 + 0x05B = 36864 + 91 = 36955 = 0x905B

De unde vin distractorii:

Greșeala
a) 0x305B nu s-a făcut traducerea deloc
b) 0x3A5B s-a citit greșit din tabelă (cadrul 0x0A, al paginii 0x00 sau 0x04) și s-a pus pe poziție greșită
c) 0x3058 s-a modificat offset-ul — offset-ul nu se schimbă niciodată
d) 0x9AF8 frame corect, dar offset complet inventat

Rețeta în 10 secunde

taie ultimele 3 cifre hexa (la pagini de 4 KB), caută restul în tabelă, lipește cadrul la loc.


Rezumatul capitolului

Concept Esența
Structura SO 2 componente: kernel mode + user mode
Interfața cu kernelul apeluri de sistem (bibliotecile sunt doar wrappere)
Thread vs. proces thread-ul e mai ieftin; partajează memoria, are stivă proprie
Afirmația falsă tipică „thread-urile se creează mai lent decât procesele”
n × fork() 2ⁿ procese
execve() reușit nu se mai întoarce — codul de după el nu rulează
for fără acolade cuprinde o singură instrucțiune
Secțiuni critice nu se pot întrepătrunde niciodată
Traducere de adresă pagină → cadru; offset-ul rămâne neschimbat
Pagini de 4 KB offset = 12 biți = 3 cifre hexa

Virtualizare · Index · ➡ Limbajul C