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
- Structura unui sistem de operare
- Apeluri de sistem
- Procese vs. thread-uri
fork(),exec()și crearea proceselor- Sincronizare: mutex și semafoare
- 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¶
-
„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.
-
„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 serie ⇒ 2ⁿ 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ă.
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:
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:
- Notează ordinea internă a fiecărui proces — în interiorul unui proces, instrucțiunile se execută strict în ordine.
- Marchează secțiunile critice — ce se află între
downșiup. - 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(). - 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.
- 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¶
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¶
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 (printf → write). 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¶
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¶
Î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ăexecvenu se întoarce”. - a) 4 — dacă ai număra
2². - 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)¶
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.
ABCDEFGHb.ABEFGHCDc.ABCEFGHDd.EFGHABCDe.EABCFGDH
Răspuns și explicație
Răspuns: b) ABEFGHCD
Metoda. Secțiunile critice sunt:
- P1:
B C(întredownșiup) - P2:
F G(întredownșiup)
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ă¶
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.
0x305Bb.0x3A5Bc.0x3058d.0x9AF8e.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:
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:
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 |