Sunteți pe pagina 1din 165

SISTEME DE OPERARE MODERNE

Ediția a patra
Andrew S. Tanenbaum
Herbert Bos

Pretince Hall PTR


Upper Saddle River, New Jersey 07458
www.phptr.com
1. INTRODUCERE............................................................................................................................... 4
1.1 Ce este un sistem de operare?..................................................................................................... 6
1.1.1 Sistemul de operare ca mașină extinsă..................................................................................... 6
1.1.2 Sistemul de operare ca administrator de resurse ...................................................................... 8
1.2 Istoria sistemelor de operare ........................................................................................................ 9
1.2.1 Prima generatie (1945 - 1955): tuburi cu vid ............................................................................. 9
1.2.2 A doua generatie (1955 - 1965):tanzistoarele și tratarea pe loturi ........................................... 10
1.2.3 Generatia a treia (1965 - 1980): Circuite integrate si multiprogramare .................................... 12
1.2.4 Generatia a patra (1980 - până în prezent). Calculatoare personale ....................................... 16
1.2.5 Generația a cincea (din 1990 până în prezent): calculatoarele mobile .................................... 20
1.3 Notiuni din domeniul resurselor fizice ale calculatorului .............................................................. 20
1.3.1 Procesoarele........................................................................................................................... 21
1.3.2 Cipuri multi-fire și multi-nuclee ................................................................................................ 23
1.3.3 Memoria .................................................................................................................................. 25
1.3.4 Discurile .................................................................................................................................. 27
1.3.5 Dispozitivele de intrare/ieșire .................................................................................................. 29
1.3.6 Magistralele ............................................................................................................................ 32
1.3.7 Pornirea computerului ............................................................................................................. 34
1.4 Gradina zoologica a sistemelor de operare ................................................................................ 35
1.4.1 Sisteme de operare pentru masinile mari de calcul ................................................................. 35
1.4.2 Sisteme de operare pentru servere ......................................................................................... 36
1.4.3 Sisteme de operare multiprocesor .......................................................................................... 36
1.4.4 Sisteme de operare pentru calculatoare personale ................................................................. 36
1.4.5 Sisteme de operare în timp real .............................................................................................. 36
1.4.6 Sisteme de operare încorporate .............................................................................................. 37
1.4.7 Sisteme de operare pentru nodurile de senzori ....................................................................... 37
1.4.8 Sisteme de operare de timp real ............................................................................................. 37
1.4.9 Sisteme de operare pentru carduri inteligente ......................................................................... 38
1.5 Concepte din domeniul sistemelor de operare ............................................................................ 38
1.5.1 Procese .................................................................................................................................. 39
1.5.2 Spații de adrese ...................................................................................................................... 40
1.5.3 Fișierele .................................................................................................................................. 41
1.5.4 Intrări/Ieșiri .............................................................................................................................. 44
1.5.5 Protecția și securitatea ............................................................................................................ 44
1.5.6 Interpretorul de comenzi (shell) ............................................................................................... 44
1.6 APELURI DE SISTEM ................................................................................................................ 45
1.6.1 Apeluri de sistem pentru administrarea proceselor.................................................................. 48
1.6.2 Apeluri de sistem pentru administrarea fisierelor ..................................................................... 51
1.6.3 Apeluri de sistem pentru administrarea cataloagelor ............................................................... 51
1.6.4 Alte apeluri de sistem .............................................................................................................. 53
1.6.5 Interfața pentru programarea aplicațiilor Windows Win32 API ................................................. 53
1.7 STRUCTURA SISTEMELOR DE OPERARE ............................................................................. 55
1.7.1 Sisteme monolitice .................................................................................................................. 56
1.7.2 Sisteme structurate pe nivele .................................................................................................. 57
1.7.3 Modelul microkernel ................................................................................................................ 58
1.7.4 Modelul client-server ............................................................................................................... 59
1.7.5 Masini virtuale ......................................................................................................................... 60
1.7.6 Despre Exokernel ................................................................................................................... 63
1.8 DIN LUMEA LIMBAJULUI C ....................................................................................................... 63
1.8.1 Despre limbajul C.................................................................................................................... 64
1.8.2 Fișiere antet ............................................................................................................................ 64
1.8.3 Proiecte mari de programare .................................................................................................. 65
1.9 Cercetări în domeniul sistemelor de operare .............................................................................. 66
1.10 Rezumat ..................................................................................................................................... 68
2. PROCESE SI FIRE DE EXECUTIE ............................................................................................... 70
2.1 Procese ...................................................................................................................................... 71
2.1.1 Modelul de proces................................................................................................................... 71
2.1.2 Crearea proceselor ................................................................................................................. 73
2.1.3 Terminarea proceselor ............................................................................................................ 75
2.1.4 Ierarhii de procese .................................................................................................................. 75
2.1.5 Starile unui proces .................................................................................................................. 76
2.1.6 Implementarea proceselor ...................................................................................................... 78
2.1.7 Modelarea multiprogramării .................................................................................................... 79
2.2 Fire de executie .......................................................................................................................... 81
2.2.1 Utilizarea firelor de executie .................................................................................................... 81
2.2.2 Modelul clasic al firelor ............................................................................................................ 85
2.2.3 Fire POSIX ............................................................................................................................. 88
2.2.4 Implementarea firelor în spațiul utilizatorului ........................................................................... 90
2.2.5 Implementarea firelor în nucleu ............................................................................................... 92
2.2.6 Implementări hibride ............................................................................................................... 93
2.2.7 Activizarea planficatorului ....................................................................................................... 94
2.2.8 Fire de executie de tip pop-up ................................................................................................. 95
2.2.9 Transformarea codului monofir în cod multifir ......................................................................... 96
2.3 Comunicarea între procese ........................................................................................................ 99
2.3.1 Conditii de concurență ............................................................................................................ 99
2.3.2 Secțiuni critice........................................................................................................................100
2.3.3 Excluderea mutuală folosind asteptarea activă ......................................................................101
2.3.4 Suspendarea și activarea.......................................................................................................106
2.3.5 Semaforul ..............................................................................................................................108
2.3.6 Mutex .....................................................................................................................................111
2.3.7 Monitoarele ............................................................................................................................116
2.3.8 Transmiterea mesajelor .........................................................................................................121
2.3.9 Bariere ...................................................................................................................................123
2.3.10 Evitarea blocajelor: citire-copiere-actualizare .........................................................................124
2.4 Planificarea ...............................................................................................................................126
2.4.1 Introducere în planificare........................................................................................................126
2.4.2 Planificarea în sistemele cu prelucrare pe loturi .....................................................................132
2.4.3 Planificarea în sistemele interactive .......................................................................................134
2.4.4 Planificarea în sistemele de timp real .....................................................................................139
2.4.5 Politici versus mecanisme ......................................................................................................140
2.4.6 Planificarea firelor de executie ...............................................................................................140
2.5 Probleme clasice ale comunicării între procese .........................................................................142
2.5.1 Problema Cina filozofilor ........................................................................................................142
2.5.2 Problema Cititorilor si Scriitorilor ............................................................................................144
2.6 Cercetări asupra proceselor și firelor de execuție ......................................................................146
2.7 Rezumat ....................................................................................................................................147
3. BLOCAJE RECIPROCE (IMPAS) .................................................................................................155
3.1 Resurse .....................................................................................................................................155
3.1.1 Resurse preemptibile si non-preemptibile ..............................................................................156
3.1.2 Accesul la resursă..................................................................................................................157
3.2 Introducere în blocaje ................................................................................................................158
3.2.1 Conditii pentru blocaje reciproce ............................................................................................159
3.2.2 Modelarea blocajelor..............................................................................................................159
3.3 Algoritmul struțului .....................................................................................................................161
3.4 Detectarea blocajelor și recuperarea .........................................................................................162
3.4.1 O singură resursă de fiecare tip .............................................................................................162
3.4.2 Mai multe resurse din fiecare tip ............................................................................................164
PREFAȚA
Ediția a patra a acestei cărți este foarte diferită de a treia. Întrucât dezvoltarea sistemelor de
operare nu rămâne pe loc, aducerea materialului într-o stare modernă a necesitat un număr
mare de mici modificări. Capitolul despre sistemele de operare multimedia a fost mutat pe
Internet, în principal pentru a face loc materialelor noi și pentru a împiedica cartea să crească
până la o dimensiune absolut incontrolabilă. Capitolul Windows Vista a fost șters deoarece
Vista nu a reușit să se ridice la nivelul așteptărilor companiei Microsoft. La fel și capitolul
Symbian, deoarece sistemul este acum mult mai puțin răspândit. Vista a fost înlocuit cu
Windows 8, iar Symbian cu Android. În plus, a fost inclus un capitol nou despre virtualizare.

1. INTRODUCERE
Un computer modern este format dintr-unul sau mai multe procesoare, memoria operativă,
memoria secundară, o imprimantă, o tastatură, un mouse, un display, echipamente de rețea și
diverse dispozitive de intrare/ieșire. În consecință, este un sistem extrem de complex. Dacă un
programator simplu ar fi obligat să înțeleagă complexitatea tuturor acestor dispozitive, atunci
nu-i va mai rămâne timp pentru a scrie cod. Mai mult, gestionarea tuturor acestor componente
și utilizarea lor optimă este o sarcină foarte dificilă. Din acest motiv, calculatoarele sunt
echipate cu un software special numit Sistem de Operare, sarcina căruia este gestionarea
programelor utilizatorului, precum și administrarea tuturor resurselor menționate anterior.
Majoritatea cititorilor au deja experiență cu sisteme de operare, cum ar fi Windows, Linux, Unix
FreeBSD sau Mac OS X, dar aspectul lor poate varia. Programele folosite de utilizatori pentru a
interacționa cu calculatorul, de obicei numite shell, atunci când se bazează pe utilizarea textului
sau interfața grafică de utilizator (GUI), nu fac parte din sistemul de operare, deși folosesc
acest sistem.
Schematic, principalele componente luate în considerare aici sunt prezentate în Fig. 1.1.

Fig. 1.1. Locul sistemului de operare în cadrul resurselor program


Partea inferioară a imaginii reprezintă resursele fizice (hardware). Include microcircuite, plăci,
discuri, tastatură, monitor și alte componente fizice. Mai sus de hardware este software-ul.
Majoritatea computerelor au două moduri de operare: modul kernel și modul utilizator. Sistemul
de operare este cea mai fundamentală piesă de software care funcționează în modul kernel
(acest mod se mai numește și modul supervizor). În acest mod, sistemul are acces complet la
toate componentele tehnice și poate folosi orice instrucțiune pe care calculatorul este capabil
să o execute. Restul software-ului rulează în modul utilizator, în care este disponibil doar un
subset de instrucțiuni ale mașinii. De exemplu, programelor care operează în modul utilizator
sunt interzise să utilizeze instrucțiuni care controlează mașina sau să efectueze operațiuni de
Intrare/Ieșire (I/O). Vom reveni de mai multe ori la diferențele dintre modurile kernel și utilizator.
Aceste diferențe au o influență decisivă asupra sistemului de operare.
Programele de interfață ale utilizatorului - shell-ul sau interfața grafică - se află la cel mai
inferior nivel de software executat în modul utilizator și permit utilizatorului să ruleze alte
programe, cum ar fi un browser web, un cititor de e-mail sau un player de muzică. Aceste
programe de asemenea folosesc în mod activ sistemul de operare.
Locul sistemului de operare este prezentat în Fig. 1.1. Acesta lucrează direct cu hardware-ul și
reprezintă baza pentru restul software-ului.
O diferență importantă între sistemul de operare și software-ul obișnuit (care operează în modul
utilizator) este următoarea: dacă utilizatorul nu este mulțumit de un anumit cititor de e-mail,
poate alege un alt program sau, dacă dorește, își poate scrie propriul program, dar nu poate
scrie propriul program de gestionare a întreruperilor ceasului de sistem, care face parte din
sistemul de operare și este protejat la nivel hardware de orice încercare de modificare din
partea utilizatorului. Această distincție este uneori mai puțin pronunțată în sistemele încorporate
(care pot să nu dispună de modul kernel) sau în sistemele interpretate (cum ar fi sistemele
bazate pe limbajul Java, în care pentru separarea componentelor nu sunt utilizate resursele
tehnice ci un interpretor).
Multe sisteme au, de asemenea, programe care rulează în modul utilizator, dar care ajută
sistemul de operare sau îndeplinesc funcții speciale. De exemplu, programele care permit
utilizatorilor să își schimbe parolele sunt destul de frecvente. Acestea nu fac parte din sistemul
de operare și nu rulează în modul kernel, dar se înțelege că acestea îndeplinesc o funcție
importantă și trebuie protejate în mod special. În unele sisteme, această idee este dusă la o
formă extremă, iar acele zone care aparțineau în mod tradițional sistemului de operare (de
exemplu, sistemul de fișiere) funcționează în spațiul utilizatorului. În aceste sisteme este dificil
de trasat o graniță clară. Toate programele care rulează în modul kernel fac parte în mod clar
din sistemul de operare, însă unele programe care rulează în afara acestui mod pot face parte
din sistemul de operare sau cel puțin sunt strâns legate de el.
Sistemele de operare diferă de programele de utilizator (adică de aplicații) nu numai prin locația
lor. Caracteristic pentru ele sunt volumul relativ mare al codului, structura complexă și durata de
viață lungă. Codul sursă al unui sistem de operare precum Linux sau Windows constă din
aproximativ 5 milioane de linii. Pentru a ne imagina acest volum, să tipărim mental 5 milioane
de linii în format carte de 50 de linii pe pagină și 1000 de pagini în fiecare volum (care este mai
mare decât această carte). Ar fi nevoie de 100 de volume pentru a tipări această cantitate de
cod, ceea ce înseamnă practic un întreg raft de cărți. Vă puteți imagina dacă vi s-ar da sarcina
să asigurați mentenanța unui astfel de sistem și în prima zi șeful dumneavoastră v-ar duce la
un raft de cărți și v-ar spune: "Asta e tot ce trebuie să înveți." Iar aceasta este doar pentru
partea care rulează în modul kernel. Dacă includem și bibliotecile comune necesare, Windows
are peste 70 de milioane de linii de cod (tipărite pe hârtie, acestea ar ocupa 10-20 de rafturi),
fără a mai pune la socoteală principalele programe de aplicații (cum ar fi Windows Explorer,
Windows Media Player etc.).
Acest lucru explică de ce sistemele de operare trăiesc atât de mult timp - sunt foarte greu de
creat și, odată ce a fost creat unul, proprietarul este reticent să îl arunce și să înceapă să
creeze unul nou. Acesta este motivul pentru care sistemele de operare evoluează pe o
perioadă lungă de timp. Familia Windows 95/98/Me a fost în esență un singur sistem de
operare, iar familia Windows NT/2000/XP/Vista/Windows 7 a fost un altul. Pentru utilizator,
acestea semănau unul cu celălalt, deoarece Microsoft s-a asigurat că interfața de utilizator la
Windows 2000/XP/Vista/Windows 7 era foarte asemănătoare cu cea a sistemului pe care îl
înlocuia, care, de cele mai multe ori, era Windows 98. Cu toate acestea, Microsoft a avut
câteva motive destul de întemeiate pentru a renunța la Windows 98, pe care le vom revedea
atunci când vom intra într-un studiu detaliat al sistemului Windows în Capitolul 11.
Un alt exemplu, folosit pe parcursul cărții, este sistemul de operare UNIX, variantele și clonele
sale. Acesta a evoluat de-a lungul anilor, existând în versiuni bazate pe sistemul original, cum
ar fi System V, Solaris și FreeBSD. Linux, pe de altă parte, are o nouă fundație software, deși
modelul său se bazează mult pe UNIX și are un grad ridicat de compatibilitate cu acesta.
Exemplele referitoare la UNIX vor fi folosite pe tot parcursul cărții, în timp ce Linux este tratat în
detaliu în capitolul 10.
Acest capitol abordează pe scurt unele dintre aspectele cheie ale sistemelor de operare,
învățând ce sunt acestea, examinând istoria lor și tot ceea ce s-a întâmplat în jurul lor,
analizând câteva concepte de bază ale sistemelor de operare și structura acestora.
Multe subiecte importante vor fi abordate mai detaliat în capitolele următoare.

1.1 Ce este un sistem de operare?


Este dificil să se dea o definiție precisă a unui sistem de operare (SO). Se poate spune că este
un software care rulează în modul kernel, dar nici măcar această afirmație nu va fi întotdeauna
adevărată. O parte a problemei constă în faptul că sistemele de operare îndeplinesc două
funcții semnificativ diferite: ele (1) oferă programatorilor de aplicații (și, bineînțeles, aplicațiilor)
un set de resurse abstracte ușor de înțeles, care înlocuiesc setul neordonat de resurse fizice
și (2) gestionează aceste resurse abstracte. În funcție de persoana care vorbește, este posibil
să auzim mai mult despre prima sau despre a doua dintre aceste funcții. Nouă ne revine
sarcina să analizăm ambele funcții.

1.1.1 Sistemul de operare ca mașină extinsă


Arhitectura majorității calculatoarelor (sistemul de instrucțiuni, organizarea memoriei, sistemul
de intrare/ieșire și magistralele) la nivelul limbajului cod mașină este prea primitivă și incomodă
pentru a fi utilizată în programe, în special în sistemele I/O. Pentru a pune discuția în context,
să ne uităm la hard disk-urile moderne SATA (Serial ATA), care sunt utilizate în majoritatea
computerelor. O carte publicată de Anderson Publishing în 2007, care descria interfața de disc
pe care programatorii trebuiau să o învețe pentru a utiliza unitatea, conținea peste 450 de
pagini. De atunci, interfața a fost revizuită de mai multe ori și a devenit chiar mai complexă
decât era în 2007. Este clar că niciun programator sănătos la cap nu ar dori să se ocupe de o
astfel de unitate la nivel hardware. În ajutorul programatorului, hardware-ul este gestionat de un
software numit driver de disc, care oferă, fără alte precizări, o interfață pentru citirea și scrierea
blocurilor de disc. SO conțin multe drivere pentru a controla dispozitivele de I/O.
Dar, pentru majoritatea aplicațiilor, chiar și acest nivel este prea scăzut. Din această cauză
toate sistemele de operare oferă un alt nivel (mai înalt) de abstractizare pentru utilizarea
discului - fișierele. Utilizând această abstractizare, programele pot crea, scrie și citi fișiere, fără
a intra în detaliile legate de modul în care funcționează de fapt hardware-ul.
Această abstractizare este cheia pentru gestionarea complexității. O bună abstractizare
transformă o sarcină aproape imposibilă în două sarcini ușor de gestionat. Prima dintre aceste
sarcini constă în definirea și implementarea abstracțiilor, iar cea de-a doua este utilizarea
acestor abstracții pentru a rezolva problema curentă. O abstractizare pe care aproape orice
utilizator de calculator o poate înțelege este fișierul menționat anterior. Acesta reprezintă un set
de informații utile, de exemplu o fotografie digitală, un mesaj de e-mail salvat sau o pagină web.
Fotografiile, e-mailurile și paginile web sunt mult mai ușor de utilizat decât discurile SATA (sau
alte componente de disc). Sarcina unui sistem de operare este de a crea o abstractizare bună,
iar apoi de a implementa și gestiona obiectele abstracte create în cadrul acestei abstractizări. În
această carte, abstracțiunile vor avea o prioritate foarte mare, deoarece ele reprezintă una
dintre cheile pentru înțelegerea sistemelor de operare.
Având în vedere importanța acestui concept, merită să îl exprimăm în cuvinte ușor diferite. Cu
tot respectul pentru proiectanții Macintosh, trebuie remarcat faptul că hardware-ul nu se
evidențiază în mod deosebit prin finețe sau eleganță. Procesoarele, blocurile de memorie,
discurile și alte componente din lumea reală sunt mult prea complexe, oferind interfețe dificile,
incomode, incoerente și inconsistente pentru cei care trebuie să scrie cod pentru ele. Uneori,
acest lucru se datorează necesității de a susține compatibilitatea retroactivă, alteori dorinței de
a economisi bani, iar uneori, dezvoltatorii de hardware pur și simplu nu realizează (sau nu vor
să realizeze) cât de multe probleme le creează dezvoltatorilor de software. Unul dintre
obiectivele principale ale unui sistem de operare este de a ascunde hardware-ul și software-ul
existent (și dezvoltatorii acestuia) sub abstracțiuni frumoase, elegante și neschimbătoare, care
au fost create pentru a le înlocui și a funcționa corect. Sistemele de operare transformă
urâțenia în frumusețe (figura 1.2).

Fig. 1.2. Sistemul de operare transformă resursele tehnice urâte în abstracții frumoase
Trebuie remarcat faptul că adevărații "beneficiari" ai sistemelor de operare sunt aplicațiile
(desigur, nu fără ajutorul programatorilor de aplicații). Anume aceștia sunt cei care lucrează
direct cu sistemul de operare și cu abstracțiunile acestuia. Iar utilizatorii finali lucrează cu
abstracțiunile oferite de interfața utilizatorului - fie o linie de comandă shell, fie o interfață
grafică. Abstracțiunile interfeței utilizatorului pot fi similare abstracțiunilor furnizate de sistemul
de operare, dar nu întotdeauna este cazul. Pentru a clarifica acest aspect, luați în considerare
desktop-ul normal al SO Windows și linia de comandă. Ambele sunt programe care rulează pe
sistemul de operare Windows și utilizează abstracțiile furnizate de acest sistem, dar oferă
interfețe de utilizator semnificativ diferite. În mod similar, utilizatorii de Linux care rulează în
Gnome sau KDE văd o imagine total diferită decât utilizatorii de Linux care rulează pe X
Window System, însă abstracțiunile care stau la baza SO sunt aceleași în ambele cazuri.
În această carte vom studia în detaliu abstracțiunile disponibile pentru aplicații, dar nu ne vom
aprofunda prea mult în interfețele cu utilizatorul. Acesta este un subiect complex și important,
dar care are prea puțin de-a face cu sistemele de operare.

1.1.2 Sistemul de operare ca administrator de resurse


Punctul de vedere conform căruia sistemul de operare oferă în principal abstracțiuni pentru
programele de aplicații este o viziune de sus în jos (top down). Susținătorii punctului de vedere
alternativ, de jos în sus (down-top), consideră că sistemul de operare există pentru a controla
toate părțile unui sistem complex. Calculatoarele moderne sunt compuse din procesoare,
memorie, cronometre, discuri, mouse-uri, interfețe de rețea, imprimante și o gamă largă de alte
dispozitive. Susținătorii viziunii de jos în sus consideră că sarcina sistemului de operare este de
a asigura o alocare ordonată și controlată a procesoarelor, memoriei și dispozitivelor de I/O
între diversele programe care pretind să le utilizeze.
Sistemele de operare moderne permit rularea simultană a mai multor programe. Imaginați-vă
ce s-ar întâmpla dacă toate cele trei programe care rulează pe același computer ar încerca să
tipărească simultan pe aceeași imprimantă. Primele câteva rânduri ale imprimării pot fi din
programul nr. 1, următoarele câteva rânduri din programul nr. 2, apoi câteva rânduri din
programul nr. 3 și așa mai departe. Rezultatul va fi un haos total. Sistemul de operare este
conceput pentru a face ordine în haosul potențial prin stocarea pe disc a tuturor ieșirilor
destinate imprimantei. Sistemul de operare dispune de o memorie tampon pe disc pentru toate
datele de ieșire destinate imprimantei. Atunci când un program își termină activitatea sistemul
de operare poate direcționa către imprimantă fișierul de pe disc în care au fost stocate datele
programului și, în același timp, un alt program poate continua să genereze date către
imprimantă fără să-și dea seama că ieșirea nu ajunge de fapt (pentru moment) la imprimantă.
Atunci când mai mulți utilizatori lucrează cu un computer (sau o rețea) nevoile de gestionare și
securizare a memoriei, intrărilor/ieșirilor și a altor resurse cresc dramatic. În caz contrar,
utilizatorii vor interfera cu munca celorlalți. În plus, utilizatorii sunt adesea nevoiți să partajeze
nu numai hardware-ul, dar și informațiile (fișiere, baze de date etc.). Pe scurt, susținătorii
acestei viziuni asupre sistemului de operare consideră că sarcina principală a acestuia este de
a ține evidența care program utilizează o resursă, pentru a satisface solicitările de resurse, fiind
responsabil pentru utilizarea acestora și de a lua decizii cu privire la solicitările contradictorii din
partea diferitor programe și utilizatori diferiți.
Gestionarea resurselor implică multiplexarea (alocarea) resurselor în două moduri diferite: în
timp și în spațiu. Atunci când o resursă este partajată în timp, diferite programe sau utilizatori o
folosesc pe rând: mai întâi unul, apoi altul și așa mai departe. De exemplu, în cazul în care
există un singur procesor și mai multe programe care încearcă să ruleze pe el, sistemul de
operare alocă mai întâi procesorul unui program. După ce acesta a fost rulat suficient de mult
timp, procesorul este alocat unui alt program, apoi unui al treilea program și tot așa. În cele din
urmă, primul program poate primi din nou procesorul. Stabilirea exactă a modului în care
resursa va fi alocată în timp - cine va fi următorul consumator și pentru cât timp - este sarcina
sistemului de operare. Partajarea imprimantei reprezintă alt exemplu de multiplexare în timp.
Atunci când în coada de așteptare a unei imprimante există mai multe lucrări de imprimare,
trebuie să se decidă care va fi următoarea lucrare de tipărire.
Un alt tip de partajare a resurselor este partajarea spațială. În loc să alterneze, fiecare client
primește o parte din resursa partajată. De exemplu, memoria RAM este, de obicei, partajată
între mai multe programe în curs de execuție, astfel încât acestea să poată fi toate în memorie
în orice moment (de exemplu, utilizând pe rând procesorul). Cu condiția să existe suficientă
memorie pentru mai multe programe, este mai eficient să se aloce memoria mai multor
programe simultan, decât să fie alocată toată memoria unui singur program, mai ales dacă
acesta are nevoie doar de o mică parte din spațiul total. Desigur, în acest caz pot apărea
probleme de partajare echitabilă, de securitate și așa mai departe, care trebuie rezolvate de
sistemul de operare. O altă resursă partajată spațial este discul rigid. Mai multe sisteme permit
stocarea fișierelor care aparțin diferitor utilizatori pe aceeași unitate în același timp. Alocarea
spațiului pe disc și ținerea evidenței cine folosește care blocuri de disc este o sarcină tipică a
unui sistem de operare.

1.2 Istoria sistemelor de operare


Istoria sistemelor de operare datează de mulți ani. În următoarele compartimente ale acestei
cărți vom discuta pe scurt câteva dintre principalele momente ale acestei evoluții. Deoarece
sistemele de operare au apărut și au evoluat în procesul de creare a calculatoarelor, aceste
evenimente sunt strâns legate din punct de vedere istoric. Din acest motiv, pentru a obține un
tablou al dezvoltării sistemelor de operare, vom examina generațiile succesive de calculatoare.
O atare schemă a relațiilor dintre generațiile de sisteme de operare și de calculatoare este
foarte aproximativă, dar oferă o anumită structură fără de care ar fi complicat de înțeles ceva.
Primul calculator digital adevărat a fost inventat de matematicianul englez Charles Babbage
(1792-1871). Deși Babbage și-a petrecut cea mai mare parte a vieții construindu-și mașina
analitică, acesta nu a reușit niciodată să o facă să funcționeze. Fiind o mașină pur mecanică,
tehnologia din acea vreme nu era suficient de avansată pentru a realiza multe dintre piesele și
mecanismele de mare precizie. Inutil să spunem că mașina sa nu avea un sistem de operare.
Este un fapt istoric interesant că Babbage și-a dat seama că pentru o mașină analitică avea
nevoie de software, așa că a angajat o tânără pe nume Ada Lovelace, fiica celebrului poet
britanic George Byron. Dânsă a devenit primul programator din lume, iar limbajul de
programare Ada®, creat în zilele noastre, a fost numit în cinstea ei.

1.2.1 Prima generatie (1945 - 1955): tuburi cu vid


După eforturile nereușite ale lui Babbage, s-au înregistrat puține progrese în proiectarea
calculatoarelor digitale până în timpul celui de-al Doilea Război Mondial, care a stimulat o
explozie de lucrări în acest sens. Profesorul John Atanasoff și doctorandul său Clifford Berry au
creat ceea ce este considerat în prezent primul calculator digital funcțional la Universitatea din
Iowa. Acesta folosea 300 de tuburi electronice. Cam în aceeași perioadă, Konrad Zuse din
Berlin a construit computerul Z3 bazat pe relee electromecanice. În 1944, un grup de oameni
de știință (inclusiv Alan Turing) de la Bletchley Park din Marea Britanie a construit și programat
“Colossus”, la Harvard Howard Aiken a construit ”Mark 1”, iar la Universitatea din Pennsylvania
William Mauchley și doctorandul său John Presper Eckert au construit ”Eniac”. Unele dintre
aceste mașini erau digitale, altele foloseau tuburi electronice, erau programabile dar destul de
primitive și necesitau multe secunde pentru a produce chiar și cele mai simple calcule.
La începuturile erei calculatoarelor, fiecare mașină era proiectată, construită, programată,
operată și întreținută de același grup de persoane (de obicei ingineri). Toate programările se
făceau exclusiv în limbajul mașinilor sau, chiar mai rău, prin asamblarea circuitelor electrice, iar
mii de fire trebuiau conectate la panouri de conexiuni pentru a controla funcțiile de bază ale
mașinilor. Limbajele de programare (chiar și cele de asamblare) nu erau încă cunoscute.
Nimeni nu auzise niciodată de sisteme de operare. Modul de operare al programatorului era să
se înscrie pentru un anumit timp de lucru într-un registru special, apoi, cînd îi revine timpul, să
coboare în camera de control, să-și conecteze panoul de patch-uri la computer și să petreacă
următoarele câteva ore, sperând că niciunul dintre cele aproximativ 20 000 de tuburi electronice
nu vor ieși din funcție în timpul procesului. În esență, toate sarcinile care trebuiau îndeplinite se
reduceau la calcule matematice și numerice simple, cum ar fi precizrea tabelelor de sinusuri,
cosinusuri și logaritmi sau calcularea traiectoriilor proiectilelor de artilerie.
Când s-a propus folosirea cartelelor perforate, la începutul anilor 1950, situația s-a îmbunătățit
oarecum. În loc să se folosească panouri de comutare, acum programele puteau fi scrise pe
cartele și citite de pe acestea, dar în rest procedura de operare a rămas neschimbată.

1.2.2 A doua generație (1955 - 1965): tanzistoarele și tratarea pe loturi


Inventarea tranzistoarelor în anul 1948 și introducerea lor în locul tuburilor cu vid (mijlocul anilor
1950) a schimbat radical situația. Calculatoarele au devenit suficient de fiabile pentru a putea fi
fabricate ân scopuri comerciale, cu speranța că vor continua să funcționeze suficient de mult
timp pentru a face o muncă utilă. Acuma se produce prima separare între proiectanți,
constructori, operatori, programatori și personalul de întreținere.
Aceste mașini, numite acum mainframe-uri, erau închise în săli mari speciale de calculatoare,
cu aer condiționat, cu operatori profesioniști care să le gestioneze. Numai marile corporații,
marile agenții guvernamentale sau universități își puteau permite să plătească prețul de mai
multe milioane de dolari. Pentru a rula o lucrare (adică un program sau un set de programe), un
programator scria mai întâi programul pe hârtie (în FORTRAN sau în limbaj de asamblare), îl
transpunea pe cartele perforate, aducea pachetul de cartele în camera de introducere a datelor,
îl înmâna unuia dintre operatori și se ducea să bea o cafea până când rezultatul era gata.
Când execuția unei lucrări era încheiată, un operator se ducea la imprimantă, selecta listingul și
îl ducea în camera de ieșire, astfel încât programatorul să-l poată prelua mai târziu. Apoi,
operatorul lua unul dintre pachetele de cartele din camera de intrare și îl introducea. Dacă era
nevoie de compilatorul FORTRAN, operatorul trebuia să îl ia dintr-un dulap de fișiere și să îl
încarce în memoria calculatorului. Se pierdea mult timp cu calculatorul cu această plimbare a
operatorilor prin sala de mașini.
Având în vedere costul ridicat al echipamentului, nu este surprinzător faptul că oamenii au
căutat rapid modalități de a reduce timpul pierdut. Soluția adoptată a fost tratarea pe loturi.
Ideea era de a colecta o cutie plină de lucrări în camera de intrare și apoi de a le stoca pe o
bandă magnetică folosind un calculator mic (relativ) ieftin, cum ar fi IBM 1401, care era destul
de bun la citirea cartelelor, la copierea benzilor și la imprimarea rezultatelor, dar deloc bun la
calcule numerice. Alte mașini mult mai scumpe, cum ar fi IBM 7094, erau fost folosite pentru
calculul real. Această situație este prezentată în fig. 1-3.
Fig. 1-3. Tratarea pe loturi. (a) Programatorii aduc cartelele perforate la 1401. (b) 1401 citește
lotul de lucrări pe bandă. (c) Operatorul duce banda de intrare la 7094. (d) 7094 efectuează
calculul. (e) Operatorul duce banda de ieșire la 1401. (f) 1401 tipărește ieșirea.
După aproximativ o oră de colectare a unui lot de lucrări, cardurile erau citite pe o bandă
magnetică, care era transportată în sala mașinilor, unde era montată pe o unitate de bandă.
Operatorul încărca apoi un program special (strămoșul sistemului de operare de astăzi), care
citea prima lucrare de pe bandă și o executa. Rezultatul era scris pe o a doua bandă, în loc să
fie tipărit. După ce o lucrare se termina, sistemul de operare citea automat următoarea lucrare
de pe bandă și începea să o ruleze. Când întregul lot era gata, operatorul scotea benzile de
intrare și de ieșire, înlocuia banda de intrare cu următorul lot și aducea banda de ieșire la un
1401 pentru imprimare off line (adică fără a fi conectată la calculatorul principal).

Fig. 1.4. Structura unei lucrări în Fortran Monitor System (FMS)


Structura unei sarcini de intrare (lucrări, job) tipice este prezentată în figura 1-4. Aceasta începe
cu o cartelă $JOB, specificând timpul maxim de execuție în minute, numărul de cont care
trebuie debitat și numele programatorului. Apoi venea o cartelă $FORTRAN, indicând sistemului
de operare să încarce compilatorul FORTRAN de pe banda de sistem. Aceasta era urmată
direct de programul care urma să fie compilat și apoi de o cartelă $LOAD, care indica sistemului
de operare să încarce programul obiect tocmai compilat. (Programele compilate erau deseori
scrise pe o bandă scratch1 și trebuiau să fie încărcate în mod explicit). Apoi venea cardul $RUN,
care îi spunea sistemului de operare să ruleze programul cu datele care îl urmau. În cele din
urmă, cardul $END marca sfârșitul lucrării. Aceste carduri de control primitive au fost precursorii
shell-urilor moderne și ai interpretorilor de linii de comandă.

1
O bandă de scratch este o bandă magnetică care este utilizată pentru stocare temporară și care poate fi reutilizată sau
ștearsă după finalizarea unei lucrări sau a unei execuții de procesare.
Computerele mari din a doua generație au fost utilizate în principal pentru calcule științifice și
inginerești, cum ar fi rezolvarea ecuațiilor diferențiale parțiale care apar adesea în fizică și
inginerie. Acestea erau în mare parte programate în FORTRAN și/sau în limbaj de asamblare.
Sistemele de operare tipice erau FMS (Fortran Monitor System) și IBSYS, sistemul de operare
al IBM pentru 7094.

1.2.3 Generatia a treia (1965 - 1980): Circuite integrate si multiprogramare


La începutul anilor 1960 majoritatea producătorilor de calculatoare aveau două linii de produse
distincte și incompatibile. Pe de o parte, existau computerele științifice de mari dimensiuni,
orientate pe cuvinte, cum ar fi IBM 7094, care erau utilizate pentru calcule numerice de putere
industrială în știință și inginerie. Pe de altă parte, erau computerele comerciale, orientate spre
caractere, cum ar fi IBM 1401, utilizate pe scară largă pentru sortare și imprimare de către
bănci și companii de asigurări.
Dezvoltarea și întreținerea a două linii de produse complet diferite era o soluție costisitoare
pentru producători. În plus, mulți clienți noi la început aveau nevoie de mașini mici, dar mai
târziu doreau o mașină mai mare, care să le ruleze toate programele vechi, dar mai rapid. IBM
a încercat să rezolve aceste două probleme dintr-o singură lovitură prin introducerea mașinilor
System/360. Linia 360 era o serie de mașini compatibile cu software-ul de la modele 1401 până
la unele mult mai mari, mai performante decât puternica 7094. Mașinile se deosebeau doar prin
preț și performanță (capacitatea memoriei, viteza procesorului, numărul de dispozitive I/O
permise și așa mai departe). Deoarece toate aveau aceeași arhitectură și același set de
instrucțiuni, programele scrise pentru o mașină puteau rula pe toate celelalte - cel puțin în
teorie. (Dar, așa cum se zice că a spus Yogi Berra: "În teorie, teoria și practica sunt la fel; în
practică, nu sunt.") Deoarece System/360 a fost proiectat pentru a gestiona atât calculele
științifice (numerice), cât și cele comerciale, o singură familie de mașini putea satisface nevoile
tuturor clienților. În anii următori, IBM a scos pe piață succesori compatibili cu linia 360, folosind
o tehnologie mai modernă, cunoscută sub numele de 370, 4300, 3080 și 3090. ZSeries este cel
mai recent descendent al acestei linii, deși s-a îndepărtat considerabil de original.
IBM 360 a fost prima linie majoră de calculatoare care a utilizat circuite integrate (de mici
dimensiuni), oferind astfel un avantaj considerabil de preț/performanță față de mașinile din a
doua generație, care erau construite din tranzistori individuali. A fost un succes imediat, iar
ideea unei familii de computere compatibile a fost adoptată în scurt timp de toți ceilalți mari
producători. Descendentele acestor mașini sunt utilizate și astăzi în centrele de calcul. În
prezent, ele sunt adesea folosite pentru gestionarea unor baze de date uriașe (de exemplu,
pentru sistemele de rezervare a companiilor aeriene) sau ca servere pentru site-urile World
Wide Web care trebuie să proceseze mii de cereri pe secundă.
Cel mai mare avantaj al ideii ''familiei unice'' a reprezentat în același timp și cea mai mare
slăbiciune a acesteia. Intenția inițială era ca toate programele, inclusiv sistemul de operare,
OS/360, să funcționeze pe toate modelele. Sistemul de operare trebuia să funcționeze pe
sisteme mici, care adesea înlocuiau doar 1401 pentru copierea cartelelor perforate pe bandă și
pe sisteme foarte mari, care înlocuiau 7094 pentru a face prognoze meteo și alte calcule grele,
să funcționeze atât pe sisteme cu puține periferice, cât și pe sisteme cu multe periferice, să
funcționeze în medii comerciale și în medii științifice. Plus la toate, trebuia să fie eficient pentru
toate aceste utilizări diferite.
IBM (sau oricine altcineva, de altfel) nu avea cum să scrie un software care să îndeplinească
toate aceste cerințe contradictorii. Rezultatul a fost un sistem de operare enorm și extraordinar
de complex, probabil cu două-trei ordine de mărime mai mare decât FMS. Acesta era format
din milioane de linii de cod scrise de mii de programatori și conținea mii și mii de erori, care
necesitau un flux continuu de noi versiuni în încercarea de a le corecta. Fiecare nouă versiune
corecta unele erori și introducea altele noi, astfel încât numărul de erori rămânea probabil
constant în timp.
Unul dintre proiectanții OS/360, Fred Brooks, a scris ulterior o carte spirituală și incisivă
(Brooks, 1995) în care descrie experiența proprie cu OS/360. Deși ar fi imposibil să rezumăm
cartea aici, este suficient să spunem că pe copertă apare o turmă de animale preistorice
blocate într-o groapă cu smoală. Coperta cărții lui Silberschatz et al. (2012) face o remarcă
similară cu privire la faptul că sistemele de operare sunt dinozauri.
În ciuda dimensiunii și a problemelor sale enorme, OS/360 și sistemele de operare similare din
a treia generație produse de alți producători de calculatoare satisfăceau de fapt destul de bine
majoritatea cerințelor clienților. Mai mult, aceste sisteme de operare au făcut populare mai
multe tehnici-cheie absente în sistemele de operare din generația a doua. Probabil cea mai
importantă dintre acestea a fost multiprogramarea. Pe mașina 7094, atunci când lucrarea
curentă se întrerupea pentru a aștepta finalizarea intrării de pe o bandă sau a unei alte
operațiuni de intrare/ieșire, unitatea centrală de procesare stătea pur și simplu inactivă până la
terminarea operațiunii de intrare/ieșire. În cazul calculelor științifice puternic legate de procesor,
I/O nu sunt frecvente, astfel încât acest timp pierdut nu este semnificativ. Însă, la prelucrarea
datelor comerciale, timpul de așteptare pentru I/O poate reprezenta adesea 80 sau 90% din
timpul total, așa că trebuia să se facă ceva pentru a evita ca procesorul (costisitor) să fie atât
de mult timp inactiv.
Soluția acestei probleme a fost de a împărți memoria în mai multe părți,numite partiții, cu o
sarcină diferită în fiecare partiție, așa cum este prezentat în figura 1-5.

Fig. 1.5. Sistem multitasking cu trei lucrări în memorie


În timp ce o lucrare aștepta finalizarea I/O, o altă lucrare putea utiliza procesorul. Dacă în
memoria principală se puteau afla simultan suficiente lucrări, procesorul central putea fi ținut
ocupat aproape 100% din timp. Pentru a avea simultan mai multe lucrări în memorie erau
necesare mecanisme hardware speciale care să asigure protecția reciprocă între lucrări.
Calculatoarele din seria 360 și alte sisteme din generația a treia erau echipate cu astfel de
mecanisme.
O altă caracteristică importantă a sistemelor de operare din generația a treia era capacitatea de
a citi lucrările de pe cartelele perforate pe disc imediat ce erau aduse în sala de calculatoare.
Apoi, ori de câte ori o lucrare în curs de execuție se termina, sistemul de operare putea încărca
o nouă lucrare de pe disc în partiția acum goală și o putea executa. Această tehnică se
numește spooling (de la Simultaneous Peripheral Operation On Line) și era utilizată și pentru
ieșire. Cu introducerea spooling-ului, nu mai era nevoie de mașina 1401 și a dispărut o mare
parte din lucrul cu benzile.
Deși sistemele de operare din generația a treia erau potrivite pentru calcule științifice de mare
anvergură și pentru execuții masive de procesare a datelor comerciale, acestea erau încă
sisteme de tip batch. Mulți programatori tânjeau după zilele primei generații, când aveau
mașina numai pentru ei timp de câteva ore, pentru a-și putea depana rapid programele. În
cazul SO din generația a treia, timpul dintre trimiterea unei sarcini și obținerea rezultatelor era
adesea de câteva ore, astfel încât o singură virgulă greșit plasată putea face ca o compilare să
eșueze, iar programatorul să piardă jumătate de zi. Programatorilor nu le plăcea acest lucru.
Această dorință de a avea un răspuns rapid a deschis calea pentru timesharing, o variantă de
multiprogramare, în care fiecare utilizator are un terminal online. Într-un sistem de timesharing,
dacă 20 de utilizatori sunt conectați și 17 dintre ei gândesc, vorbesc sau beau cafea, unitatea
centrală poate fi alocată succesiv celor trei joburi care doresc să fie deservite. Deoarece
oamenii care depanează programe emit de obicei comenzi scurte (de exemplu, compilați o
procedură de cinci pagini2) mai degrabă decât comenzi lungi (de exemplu, sortați un fișier cu un
milion de înregistrări), calculatorul poate oferi servicii rapide și interactive unui număr de
utilizatori și, poate, de asemenea, să lucreze la sarcini mari de lucru pe loturi în fundal, atunci
când procesorul este liber. Primul sistem de uz general cu partajarea timpului, CTSS
(Compatible Time Sharing System), a fost dezvoltat la M.I.T. pe un 7094 special modificat
(Corbato´ și al., 1962). Totuși timesharing-ul a devenit cu adevărat popular doar atunci când
hardware-ul de protecție reciprocă necesar a fost implementat pe larg.
După succesul sistemului CTSS, M.I.T., Bell Labs și General Electric (la acea vreme un
important producător de calculatoare) au decis să se lanseze în dezvoltarea unui ''calculator
utilitar'', adică a unei mașini care să suporte câteva sute de utilizatori simultani. Ei s-au inspirat
din sistemul energetic - atunci când ai nevoie de energie electrică, nu trebuie decât să introduci
ștekerul în priză și, în limitele rezonabile, va exista atâta energie cât ai nevoie. Proiectanții
acestui sistem, cunoscut sub numele de MULTICS (MULTiplexed Information and Computing
Service), au avut în vedere o mașină uriașă care să ofere putere de calcul pentru toți locuitorii
din zona Boston. Ideea că mașinile de 10.000 de ori mai rapide decât mainframe-ul lor GE-645
vor fi vândute (pentru mult sub 1.000 de dolari) cu milioanele era pe atunci din domeniul
science fiction, și va fi implementată doar 40 de ani mai târziu. Foarte asemănătoare cu ideea
trenurilor supersonice subacvatice transatlantice.
MULTICS a avut un succes discutabil. A fost conceput pentru a susține sute de utilizatori pe o
mașină doar puțin mai puternică decât un PC cu Intel 386, deși avea o capacitate de I/O mult
mai mare. Era o idee nu chiar trăsnită la vremea respectivă, deoarece oamenii de atunci erau
capabili să creeze programe mici și eficiente, adică aveau o abilitate care s-a pierdut între timp.
Au existat multe motive pentru care MULTICS nu a cucerit lumea, printre care nu ultimul fiind
acela că a fost scris în limbajul de programare PL/I compilator pentru care a apărut doar peste
câțiva ani, iar prima lui versiune poate fi numită funcțională doar cu mari rezerve. În plus,
MULTICS a fost extrem de ambițios pentru epoca sa, la fel ca mașina analitică a lui Charles
Babbage în secolul al XIX-lea.
MULTICS a introdus multe idei fundamentale în informatică, dar transformarea sa într-un
produs serios și într-un succes comercial major s-a dovedit a fi mult mai dificilă decât era de
așteptat. Bell Labs a renunțat la proiect, iar General Electric a renunțat cu totul la afacerea cu
calculatoare. Cu toate acestea, M.I.T. a persistat și a reușit să facă MULTICS să funcționeze. În
cele din urmă acesta a fost vândut ca produs comercial unei companii (Honeywell), care a
cumpărat afacerea cu calculatoarele de la General Electric și a instalat aproximativ 80 de copii

2
În această carte vom folosi termenii “procedură”, “subrutină” și “funcție” având același sens.
pe calculatoarele unor companii și universități importante din întreaga lume. Deși numărul de
utilizatori MULTICS era mic, aceștia au dat dovadă de un angajament ferm față de sistem. De
exemplu, General Motors, Ford și Agenția Națională de Securitate a SUA au renunțat la
MULTICS abia la sfârșitul anilor '90, la 30 de ani de la lansare și după mai mulți ani în care au
încercat să convingă Honeywell să actualizeze hardware-ul.
Spre sfârșitul secolului XX, ideea de a crea un astfel de calculator utilitar s-a stins, dar s-ar
putea să revină sub forma cloud computing, în care computere relativ mici (inclusiv
smartphone-uri, tablete și altele asemenea) sunt conectate la servere din centre de date vaste
și îndepărtate, unde se realizează toate calculele, computerul local ocupându-se doar de
interfața cu utilizatorul. Motivația în acest caz este că majoritatea oamenilor nu doresc să
administreze sisteme informatice tot mai complexe și mai pretențioase și preferă ca această
muncă să fie efectuată de o echipă de profesioniști, de exemplu, persoane care lucrează pentru
compania care administrează centrul de date. Comerțul electronic evoluează deja în această
direcție, diverse companii rulează e-mailuri pe servere multiprocesor la care se conectează
mașini client simple, în spiritul proiectului MULTICS.
În ciuda lipsei de succes comercial, MULTICS a avut o influență uriașă asupra sistemelor de
operare ulterioare (în special UNIX și derivatele sale, FreeBSD, Linux, iOS și Android). Acesta
este descris în mai multe lucrări și într-o carte (Corbato´ et al., 1972; Corbato´ și Vyssotsky,
1965; Daley și Dennis, 1968; Organick, 1972; și Saltzer, 1974). Are, de asemenea, un site web
activ, aflat la adresa www.multicians.org, cu informații despre sistem, proiectanți și utilizatori.
O altă evoluție majoră în timpul celei de-a treia generații a fost creșterea fenomenală a
minicalculatoarelor, începând cu DEC PDP-1 în 1961. PDP-1 avea doar 4K de cuvinte pe 18
biți, dar la 120.000 de dolari pentru o mașină (mai puțin de 5% din prețul unui 7094), s-a vândut
ca pâinea caldă. Pentru anumite tipuri de lucrări nenumerice, era aproape la fel de rapid ca
7094 și a dat naștere unei noi industrii. A fost urmat rapid de o serie de alte PDP-uri (spre
deosebire de familia IBM, toate incompatibile) culminând cu PDP-11.
Unul dintre informaticienii de la Bell Labs care lucrase la proiectul MULTICS, Ken Thompson, a
găsit ulterior un mic minicalculator PDP-7 pe care nu-l folosea nimeni și a început să scrie o
versiune simplificată, pentru un singur utilizator, a sistemului MULTICS. Această lucrare s-a
transformat mai târziu în sistemul de operare UNIX, care a devenit popular în lumea
academică, în agențiile guvernamentale și în multe companii. Istoria dezvoltării SO UNIX a fost
în repetate rânduri povestită în multe cărți (de exemplu, Salus, 1994). O parte din această
poveste va fi prezentată în Cap. 10. Deocamdată, este suficient să spunem că, deoarece codul
sursă era pe larg disponibil, diverse organizații și-au dezvoltat propriile versiuni (incompatibile),
ceea ce a dus la un haos total. Au fost dezvoltate două versiuni de bază, System V, de la
AT&T, și BSD (Berkeley Software Distribution) de la Universitatea California din Berkeley.
Acestea au avut și unele variante minore.
Pentru a face posibilă scrierea de programe care să ruleze pe orice sistem UNIX, Institutul
Inginerilor Electrotehniști și Electroniști (IEEE) a dezvoltat un standard pentru UNIX, numit
POSIX, pe care majoritatea versiunilor UNIX îl acceptă în prezent. POSIX definește o interfață
minimă pentru apelurile de sistem pe care sistemele UNIX compatibile trebuie să o suporte. De
fapt, în prezent și alte sisteme de operare suportă interfața POSIX.
Ca o paranteză, merită menționat faptul că, în 1987, autorul acestei cărți a lansat o mică clonă
de UNIX, numită MINIX, în scopuri educaționale. Din punct de vedere funcțional, MINIX este
foarte asemănător cu UNIX, inclusiv respectă POSIX. De atunci, versiunea originală a evoluat
în MINIX 3, care este foarte modular și are o fiabilitate foarte ridicată. Acesta are capacitatea de
a detecta și înlocui din mers modulele defecte sau chiar prăbușite (cum ar fi driverele de
dispozitive I/O) fără repornire și fără a perturba programele în execuție. De asemenea, este
disponibilă o carte care descrie funcționarea sa internă și listează codul sursă într-o anexă
(Tanenbaum și Woodhull, 2006). Sistemul MINIX 3 este disponibil gratuit (inclusiv codul sursă)
la adresa www.minix3.org.
Dorința de a avea o versiune industrială gratuită (spre deosebire de cea educațională) a
sistemului MINIX l-a determinat pe un student finlandez, Linus Torvalds, să scrie Linux. Acest
sistem a fost inspirat direct din MINIX și dezvoltat pe baza acestuia, suportând inițial diverse
caracteristici MINIX (de exemplu, sistemul de fișiere MINIX). De atunci, a fost extins în multe
feluri de către multe persoane, dar a păstrat în continuare o anumită structură de bază comună
cu MINIX și cu UNIX. Cititorii interesați de o istorie detaliată a SO Linux și a mișcării Open
Source pot citi cartea lui Glyn Moody (2001). Cea mai mare parte din ceea ce se va spune
despre UNIX aici se aplică la System V, MINIX, Linux și alte versiuni și clone de UNIX.

1.2.4 Generatia a patra (1980 - până în prezent). Calculatoare personale


Următoarea perioadă în evoluția sistemelor de operare este asociată apariției circuitelor
integrate mari (LSI, Large Scale Integration- Integrare pe scară largă) - cipuri de siliciu care
conțin mii de tranzistori pe centimetru pătrat. Din punct de vedere al arhitecturii, calculatoarele
personale (denumite inițial microcalculatoare) semănau foarte mult cu minicalculatoarele din
clasa PDP-11, dar, bineînțeles, cu un preț diferit. În timp ce apariția minicalculatoarelor a
permis departamentelor de companii și universități să dețină un calculator, apariția
microprocesoarelor a oferit tuturor posibilitatea de a cumpăra un calculator personal.
În 1974, la lansarea microprocesorului Intel 8080, prima unitate centrală de procesare pe 8 biți
de uz general, Intel avea nevoie de un sistem de operare care să poată fi folosit pentru a-l
testa. Intel l-a angajat pe unul dintre consultanții săi, Gary Kildall, pentru a proiecta și a scrie
sistemul de operare necesar. Kildall cu un prieten au proiectat mai întâi un controler pentru o
unitate de dischetă de 8 inchi recent lansată de Shugart Associates și au atașat unitatea la un
procesor Intel 8080. Astfel s-a născut primul microcalculator cu unitate de disc. Kildall a creat
apoi un sistem de operare pe disc numit CP/M (Control Program for Microcomputers). Atunci
când Kildall și-a revendicat drepturile asupra CP/M, Intel i-a acceptat cererea deoarece nu
credea că microcalculatoarele cu disc rigid au un viitor. Ulterior, Kildall a format Digital
Research pentru a dezvolta și vinde CP/M.
În 1977, Digital Research a reproiectat CP/M pentru a-l face potrivit pentru microcalculatoarele
cu Intel 8080, Zilog Z80 și alte procesoare. Multe programe de aplicații au fost scrise pentru a
rula pe CP/M, permițând sistemului să dețină poziția de top în lumea microcalculatoarelor timp
de 5 ani.
La începutul anilor 1980, IBM a dezvoltat IBM PC (Personal Computer – calculator personal)3 și
a început să caute software pentru acesta. Personalul IBM l-a contactat pe Bill Gates pentru a
obține o licență de utilizare a interpretorului său pentru limbajul BASIC. De asemenea, l-au
întrebat dacă poate știe de un sistem de operare care să funcționeze pe un IBM PC. Gates i-a
sfătuit să meargă la Digital Research, pe atunci principala companie de sisteme de operare.
Dar Kildall a refuzat să se întâlnească cu IBM, trimițându-și în schimb subalternul. Pentru a
înrăutăți lucrurile, avocatul său a refuzat chiar să semneze un acord de confidențialitate în ceea

3
Este important de reținut că, în acest caz, "IBM PC" este numele unui anumit model de calculator, în timp ce "PC" poate fi
considerat ca o abreviere pentru "Personal Computer", o denumire a unei clase de calculatoare. Denumirea consacrată a
acestui model ca fiind pur și simplu "PC" nu este corectă - în aceeași perioadă existau și alte calculatoare personale.
ce privește IBM PC-ul care urma să fie lansat, distrugând complet cazul. Corporația IBM i-a
cerut din nou lui Gates să îi furnizeze un sistem de operare. După o a doua solicitare, Gates a
aflat că un producător local de calculatoare, Seattle Computer Products, avea un sistem DOS
(Disk Operating System). S-a dus la acea companie cu o ofertă de a cumpăra DOS (ar fi fost
vorba de $50.000), pe care Seattle Computer Products a acceptat-o cu bucurie. Gates a creat
apoi pachetul software DOS/BASIC, care a fost cumpărat de IBM. Când IBM a vrut să aducă
unele îmbunătățiri la sistemul de operare, Bill Gates l-a invitat pe Tim Paterson, cel care a scris
DOS și care a devenit primul angajat al Microsoft, compania nou înființată de Gates. Sistemul
modificat a fost redenumit în MS-DOS (MicroSoft Disk Operating System) și a devenit rapid
dominant pe piața PC-urilor IBM. Decisivă a fost decizia lui Gates (extrem de înțeleaptă) să
vândă sistemul de operare MS-DOS companiilor de calculatoare pentru a fi instalat împreună
cu hardware-ul lor, spre deosebire de încercarea lui Kildall de a vinde CP/M direct utilizatorilor
finali (cel puțin la început).
Atunci când în anul 1983 a apărut IBM PC/AT (o dezvoltare ulterioară a familiei IBM PC) cu un
procesor Intel 80286, MS-DOS era deja bine stabilit, iar CP/M își ducea ultimele sale zile.
Ulterior, MS-DOS a fost utilizat pe scară largă pe calculatoarele cu procesoarele 80386 și
80486. Deși versiunea originală a MS-DOS era mai degrabă primitivă, versiunile ulterioare erau
mult mai avansate, multe dintre acestea fiind împrumutate de la UNIX. (Corporația Microsoft
cunoștea foarte bine SO UNIX, în primii ani chiar a vândut o versiune UNIX pentru
microcalculatoare - XENIX.)
CP/M, MS-DOS și alte sisteme de operare pentru primele microcalculatoare erau bazate în
întregime pe comenzi de la tastatură. Cu timpul, datorită cercetărilor efectuate în anii 1960 de
Doug Engelbart de la Stanford Research Institute, această situație s-a schimbat. Engelbart a
inventat interfața grafică cu utilizatorul (GUI) cu ferestre, pictograme, sisteme de meniuri și
mouse-ul. Cercetătorii de la Xerox PARC au preluat această idee și au folosit-o în mașinile pe
care le-au construit.
Într-o zi, Steve Jobs, unul dintre autorii computerului Apple proiectat în garajul său, a vizitat
PARC, unde a văzut GUI-ul și a realizat imediat potențialul pe care aceasta o deținea, potențial
subestimat de conducerea Xerox. Jobs a demarat apoi crearea unui computer Apple echipat cu
interfața grafică. Acest proiect a dus la crearea computerului Lisa, care a fost prea scump și nu
a avut succes comercial. A doua încercare a lui Jobs, computerul Apple Macintosh, a fost un
succes nu numai pentru că era mult mai ieftin decât Lisa, ci și pentru că avea o interfață de
utilizator mai prietenoasă, concepută pentru utilizatorii care nu erau familiarizați cu computerele
și care nu erau dornici să învețe nimic. Macintosh a cunoscut o susținere semnifucativă în
rândul persoanelor creative - designeri grafici, fotografi digitali profesioniști și companii
profesionale de producție video digitală - care l-au adoptat. În 1999, Apple a împrumutat nucleul
derivat din microkernelul Mach dezvoltat inițial de Universitatea Carnegie Mellon pentru a
înlocui nucleul BSD UNIX. Prin urmare, Mac OS X este un sistem de operare bazat pe UNIX,
deși cu o interfață foarte specială.
Când Microsoft a decis să creeze un succesor pentru MS-DOS, a fost foarte impresionată de
succesul lui Macintosh. Rezultatul a fost un sistem bazat pe o interfață grafică cu utilizatorul,
numită Windows, care a fost inițial un add-on pentru MS-DOS (adică mai mult un shell decât un
sistem de operare propriu-zis). Timp de aproximativ 10 ani, din 1985 până în 1995, Windows a
fost doar un shell grafic peste MS-DOS. Dar, în 1995 a fost lansată o versiune independentă
Windows - SO Windows 95. Acesta îndeplinea direct majoritatea funcțiilor sistemului de
operare, folosind sistemul MS-DOS inclus doar pentru a porni și a rula programe vechi
concepute pentru MS-DOS. În 1998, a fost lansată o versiune ușor modificată a sistemului,
numită Windows 98. Cu toate acestea, atât Windows 95, cât și Windows 98 conțineau încă o
cantitate destul de mare de cod de asamblare scris pentru procesoarele Intel pe 16 biți.
Celălalt sistem de operare Microsoft a fost Windows NT (NT înseamnă New Technology), care
era, într-o anumită măsură, compatibil cu Windows 95. Windows NT a fost rescris și a devenit
un sistem complet pe 32 de biți. Principalul dezvoltator al Windows NT a fost David Cutler, co-
dezvoltătorul sistemului de operare VAX VMS, astfel încât unele idei din VMS sunt prezente în
NT (atât de mult încât proprietarul VMS, DEC, a dat în judecată Microsoft. Conflictul a fost
soluționat pe cale amiabilă pentru o sumă decentă). Microsoft se aștepta ca prima versiune să
înlocuiască MS-DOS și toate celelalte versiuni de Windows, deoarece era mult superioară, dar
așteptările nu au fost îndeplinite. Doar Windows NT 4.0 a reușit în cele din urmă să câștige o
popularitate ridicată, în special în rețelele corporative. Cea de-a cincea versiune a Windows NT
a fost redenumită Windows 2000 la începutul anului 1999. Acesta a fost destinat să înlocuiască
atât Windows 98, cât și Windows NT 4.0.
Dar aceste planuri nu erau sortite să se împlinească în totalitate, așa că Microsoft a lansat o
altă versiune de Windows 98, numită Windows Me (Millennium edition). În 2001 a fost lansată o
versiune ușor actualizată a Windows 2000, numită Windows XP. Această versiune a fost
lansată pentru o perioadă mult mai lungă, înlocuind practic toate versiunile anterioare de
Windows.
Cu toate acestea, noi versiuni au continuat să fie lansate. După Windows 2000, Microsoft a
împărțit familia Windows într-o linie de produse pentru clienți și una pentru servere. Linia bazată
pe client se baza pe XP și succesorii săi, în timp ce linia bazată pe server era formată din
Windows Server 2003 și Windows 2008. Puțin mai târziu a apărut o a treia linie destinată lumii
sistemelor de operare incorporate. Variațiile sub formă de service pack-uri au fost separate de
toate aceste versiuni de Windows. Acest lucru a fost suficient pentru a-i liniști pe unii
administratori (și pe autorii manualelor de sisteme de operare).
Apoi, în ianuarie 2007, Microsoft a lansat o versiune finală a succesorului Windows XP, numită
Vista. Avea o nouă interfață grafică, o securitate îmbunătățită și multe programe de utilizator noi
sau actualizate. Microsoft a sperat că va înlocui complet Windows XP, dar nu a reușit. În
schimb, a primit numeroase critici și articole de presă, în special din cauza cerințelor de sistem
ridicate, a condițiilor restrictive de acordare a licențelor și a suportului pentru tehnologia de
protecție a drepturilor de autor (tehnologie care îngreunează copierea de către utilizatori a
materialelor protejate).
Odată cu apariția lui Windows 7, un sistem de operare nou și mai puțin pretențios, mulți au
decis să renunțe complet la Vista. Windows 7 nu a introdus multe caracteristici noi, dar a fost
relativ mic și destul de stabil. Windows 7 a câștigat mai multă cotă de piață în mai puțin de trei
săptămâni decât a câștigat Vista în șapte luni. În 2012, Microsoft a lansat succesorul lui
Windows 7, Windows 8 - un sistem de operare cu un aspect complet nou, conceput pentru
ecrane tactile. Compania a sperat că noul design va face din sistemul de operare o alegere
dominantă pentru o gamă largă de dispozitive: desktop-uri, laptop-uri, tablete, telefoane și
computere personale folosite ca home theater. Dar, până în prezent, pătrunderea sa pe piață a
fost mult mai lentă decât în cazul Windows 7.
Celălalt concurent principal din lumea PC-urilor este sistemul de operare UNIX (și diferitele sale
derivate). UNIX are o poziție mai puternică pe serverele de rețea și industriale, dar devine din
ce în ce mai răspândit și pe desktop-uri, laptop-uri, tablete și smartphone-uri. Pe computerele
cu procesor Pentium, sistemul de operare Linux devine o alternativă populară la Windows
pentru studenți și pentru un număr tot mai mare de utilizatori corporativi.
În această carte, termenul x86 va fi aplicat tuturor procesoarelor moderne bazate pe familia de
arhitecturi cu seturi de instrucțiuni provenite de la procesorul 8086 creat în anii 1970.
Companiile AMD și Intel au produs o mulțime de astfel de procesoare care, de multe ori, se
diferențiau considerabil: procesoarele pot fi pe 32 sau 64 de biți, cu un număr mic sau mare de
nuclee, cu o adâncime diferită a conveierilor etc. Totuși, ele sunt destul de asemănătoare
pentru un programator și toate pot rula codul unui procesor 8086 scris acum 35 de ani. În cazul
în care diferențele joacă un rol important, se vor face referiri la modele specifice, iar termenii
x86-32 și x86-64 vor fi utilizați pentru a indica variantele pe 32 și 64 de biți.
Sistemul de operare FreeBSD este, de asemenea, un derivat popular al SO UNIX, creat în
cadrul proiectului BSD de la Berkeley. Toate computerele Macintosh moderne rulează o
versiune modificată a FreeBSD (OS X). UNIX este, de asemenea, SO standard pe stațiile de
lucru echipate cu procesoare RISC de înaltă performanță. Derivatele sale au fost utilizate pe
scară largă pe dispozitivele mobile care rulează iOS 7 sau Android.
În timp ce mulți utilizatori UNIX, în special programatori experimentați, preferă o interfață bazată
pe linia de comandă, aproape toate sistemele UNIX acceptă sistemul X Window (sau X11),
creat la Massachusetts Institute of Technology. Acest sistem efectuează operațiuni de bază de
control al ferestrelor, permițând utilizatorilor să creeze, să șteargă, să mute și să
redimensioneze ferestrele cu ajutorul mouse-ului. Adesea, o interfață grafică completă pentru
utilizator, cum ar fi Gnome sau KDE, poate fi utilizată ca un add-on la X11, conferind UNIX un
aspect și o senzație oarecum asemănătoare cu Macintosh sau Microsoft Windows.
La mijlocul anilor 1980, a început să se dezvolte un fenomen interesant - creșterea rețelelor de
calculatoare personale care rulează sisteme de operare de rețea și sisteme de operare
distribuite (Tanenbaum și Van Steen, 2007). În sistemele de operare de rețea, utilizatorii sunt
conștienți de existența mai multor calculatoare și se pot conecta la un calculator la distanță și
pot copia fișiere de pe un calculator pe altul. Fiecare mașină rulează propriul sistem de operare
local și are propriul utilizator (utilizatori) local(i).
Sistemele de operare de rețea nu sunt semnificativ diferite de sistemele de operare cu un
singur procesor. În mod evident, acestea au nevoie de un controler de interfață de rețea și de
un software de nivel scăzut pentru a opera acest controler, precum și de programe pentru a se
conecta la mașina de la distanță și a accesa fișiere de la distanță, dar aceste adăugiri nu
schimbă structura de bază a sistemului de operare.
În schimb, un sistem de operare distribuit se prezintă utilizatorilor săi ca un sistem tradițional cu
un singur procesor, când, de fapt, este format din mai multe procesoare. Utilizatorii nu trebuie
să știe unde rulează programele lor sau unde se află fișierele lor - toate acestea trebuie să fie
gestionate automat și eficient de către sistemul de operare însuși.
Adevăratele sisteme de operare distribuite necesită mult mai multe modificări decât simpla
adăugare a unei cantități mici de cod la un sistem de operare cu un singur procesor, deoarece
sistemele distribuite și cele centralizate sunt foarte diferite. De exemplu, sistemele distribuite
permit adesea ca aplicațiile să ruleze simultan pe mai multe procesoare, ceea ce necesită
algoritmi mai complecși pentru a distribui munca procesoarelor în vederea optimizării gradului
de procesare paralelă a datelor. Prezența latenței (întârzierilor) în transmiterea datelor în rețea
implică adesea faptul că acești (și alți) algoritmi trebuie să funcționeze în condiții de informații
incomplete, depășite sau chiar incorecte. Această situație este fundamental diferită de cea a
unui sistem cu un singur procesor.
1.2.5 Generația a cincea (din 1990 până în prezent): calculatoarele mobile
Încă de pe timpurile benzilor desenate din anii 1940 când detectivul Dick Tracy a început să
comunice folosind un radioemițător montat în ceasul de mână, oamenii au dorit un dispozitiv de
comunicare pe care să-l poată purta cu ei oriunde mergeau. Primul telefon mobil adevărat a
apărut în 1946 și cântărea aproximativ 40 de kilograme. Îl puteai lua cu tine oriunde te duceai,
dacă aveai o mașină în care să îl transporți.
Primul telefon portabil cu adevărat a apărut în anii 1970 și, cântărind aproximativ un kilogram, a
fost acceptat relativ pozitiv. Era cunoscut cu afecțiune sub numele de "cărămida". Curând, toată
lumea și-a dorit unul. Astăzi, gradul de penetrare a telefoanelor mobile este de aproape 90%
din populația globală. Putem efectua apeluri nu doar cu telefoanele noastre portabile și cu
ceasurile de mână, ci în curând și cu ochelarii și alte obiecte portabile. În plus, partea cu
telefonul nu mai este atât de interesantă. Primim e-mailuri, navigăm pe internet, trimitem
mesaje text prietenilor noștri, ne jucăm în jocuri, și aflăm depsre cât de aglomerat este traficul
fără să ne mai mire toate astea.
Deși ideea de a combina telefonul și calculatorul într-un singur dispozitiv exista încă din anii
1970, primul smartphone real a apărut abia la mijlocul anilor 1990, când Nokia a lansat N9000,
care a combinat la propriu două dispozitive în mare parte separate: un telefon și un PDA
(Personal Digital Assistant). În 1997, Ericsson a inventat termenul de smartphone pentru
modelul său GS88 ''Penelope''.
Acum, când smartphone-urile au devenit omniprezente, concurența dintre diferite sisteme de
operare este acerbă, iar rezultatul este chiar mai puțin clar decât în lumea PC-urilor. Când sunt
redactate aceste rânduri, Android de la Google este sistemul de operare dominant, iar iOS de
la Apple este clar pe locul al doilea, dar nu a fost întotdeauna așa și s-ar putea ca totul să fie
din nou diferit în doar câțiva ani. Dacă ceva este clar în lumea smartphone-urilor, este faptul că
nu este ușor să rămâi regele muntelui pentru mult timp.
Majoritatea smartphone-urilor din primul deceniu de la înființare rulau Symbian OS. Acesta a
fost sistemul de operare ales de mărci populare precum Samsung, Sony Ericsson, Motorola și,
mai ales, Nokia. Cu toate acestea, alte sisteme de operare precum Blackberry OS de la RIM
(introdus pentru smartphone-uri în 2002) și iOS de la Apple (lansat pentru primul iPhone în
2007) au început să preia cota de piață de la Symbian. Mulți se așteptau ca RIM să domine
piața de afaceri, în timp ce iOS va fi regele dispozitivelor de consum. Cota de piață a Symbian
s-a prăbușit. În 2011, Nokia a renunțat la Symbian și a anunțat că se va concentra pe Windows
Phone ca platformă principală. Pentru o perioadă, Apple și RIM au fost cele mai tari (deși nu la
fel de dominante precum fusese Symbian), dar nu a durat foarte mult până când Android, un
sistem de operare bazat pe Linux lansat de Google în 2008, să își depășească toți rivalii.
Pentru producătorii de telefoane, Android avea avantajul de a fi open source și disponibil sub o
licență permisivă. Prin urmare, aceștia puteau să îl modifice și să-l adapteze cu ușurință la
propriul hardware. De asemenea, dispune de o comunitate uriașă de dezvoltatori care scriu
aplicații, majoritatea în limbajul de programare Java, care le este familiar. Chiar și așa, ultimii
ani au arătat că această dominație ar putea să nu dureze, iar concurenții Android sunt dornici
să recupereze o parte din cota de piață. Vom analiza Android în detaliu în secțiunea 10.8.

1.3 Notiuni din domeniul resurselor fizice ale calculatorului


Un sistem de operare este strâns legat de hardware-ul computerului pe care rulează. Acesta
extinde setul de instrucțiuni al computerului și îi gestionează resursele. Pentru corecta
funcționare sunt necesare cunoștințe adânci despre hardware, cel puțin despre modul în care
acesta apare în ochii programatorului. Din acest motiv, haideți să trecem în revistă pe scurt
resurselor fizice ale calculatorului care intră în componența calculatoarelor personale moderne.
După aceasta vom putea începe studiul detaliat a ceea ce fac și cum funcționează sistemele de
operare.
Din punct de vedere conceptual, un computer personal simplu poate fi abstractizat la un model
asemănător cu cel din figura 1-6.

Fig. 1.6. Componentele unui calculator personal


Unitatea centrală, memoria și dispozitivele I/O sunt conectate cu ajutorul magistralei (bus) de
sistem și comunică între ele prin intermediul acesteia. Calculatoarele personale moderne au o
structură mai complicată, care implică mai multe magistrale, pe care le vom examina mai târziu.
Deocamdată, acest model va fi suficient. În secțiunile următoare, vom trece în revistă pe scurt
aceste componente și vom examina unele dintre problemele hardware care îi preocupă pe
proiectanții de sisteme de operare. Inutil să mai spunem că acesta va fi un rezumat foarte
compact. S-au scris multe cărți pe tema hardware-ului și a organizării calculatoarelor. Două
dintre cele mai cunoscute sunt Tanenbaum și Austin (2012) și Patterson și Hennessy (2013).

1.3.1 Procesoarele
Unitatea centrală de procesare (procesorul) este 'creierul'' calculatorului. Acesta preia
instrucțiunile din memorie și le execută. Ciclul de bază al fiecărei unități centrale de procesare
constă din (1) preluarea unei instrucțiuni din memorie, (2) decodificarea acesteea pentru a-i
determina tipul și operanzii și (3) executarea. Ciclul se repetă pornind de la prima instrucțiune
(punctul de intrare în program) până când programul se termină. În acest fel sunt executate
programele.
Fiecare unitate centrală de procesare are un set specific de instrucțiuni pe care le poate
executa. Astfel, un x86 nu poate executa programe ARM, iar un procesor ARM nu poate
executa programe x86. Deoarece accesarea memoriei pentru a obține o instrucțiune sau un
cuvânt de date necesită mult mai mult timp decât executarea unei instrucțiuni, toate CPU-urile
conțin unele registre în interior pentru a păstra variabilele cheie și rezultatele temporare. Astfel,
setul de instrucțiuni conține, în general, instrucțiuni pentru a încărca un cuvânt din memorie într-
un registru și pentru a stoca un cuvânt dintr-un registru în memorie. Alte instrucțiuni combină
doi operanzi din registre, din memorie sau din ambele într-un rezultat, cum ar fi adunarea a
două cuvinte și stocarea rezultatului într-un registru sau în memorie.
În afară de registrele generale utilizate pentru a păstra variabilele și rezultatele temporare,
majoritatea calculatoarelor dispun de mai multe registre speciale care sunt accesibile
programatorului. Unul dintre acestea este contorul de instrucțiuni, care conține adresa de
memorie a următoarei instrucțiuni care urmează să fie preluată. După ce instrucțiunea
respectivă a fost preluată, contorul de program este actualizat pentru a indica adresa
următoarei instrucțiuni.
Un alt registru, numit pointer de stivă, indică topul stivei curente din memorie. Stiva conține câte
un cadru (zonă de memorie) pentru fiecare procedură în care programul a intratat, dar din care
nu a ieșit încă. Cadrul de stivă (mediul procedurii curente) al unei proceduri conține parametrii
de intrare, variabilele locale și variabilele temporare care nu sunt păstrate în registre.
Un alt registru este PSW (Program Status Word). Acest registru conține biții de cod de stare,
care sunt setați de instrucțiunile de comparare, biții pentru gestiunea priorității, modul (utilizator
sau kernel) și diverși alți biți de control. Programele de utilizator pot citi, în mod normal, întregul
PSW, dar, de obicei, pot scrie doar în unele dintre câmpurile acestuia.PSW joacă un rol
important în apelurile de sistem și în operațiile de I/O.
Sistemul de operare trebuie să cunoască totul despre toate registrele. La multiplexarea
temporală a unității centrale, sistemul de operare va opri adesea programul în curs de execuție
pentru a (re)porni un altul. De fiecare dată când oprește un program în execuție, sistemul de
operare trebuie să salveze toate registrele, astfel încât să poată fi restaurate atunci când va fi
reluată execuția programului.
Pentru a îmbunătăți performanțele, proiectanții de unități centrale de procesare au abandonat
de mult timp modelul simplu de preluare, decodificare și executare a câte unei instrucțiuni pe
rând. Multe unități centrale de procesare moderne dispun de facilități pentru a executa mai mult
de o instrucțiune în același timp. De exemplu, o unitate centrală poate avea unități separate de
preluare, decodificare și execuție, astfel încât, în timp ce execută instrucțiunea n, ar putea, de
asemenea, să decodifice instrucțiunea n + 1 și să preia instrucțiunea n + 2. O astfel de
organizare se numește pipeline și este ilustrată în Fig. 1-7(a) pentru un pipeline cu trei etape.
De obicei pipeline-urile sunt mai lungi. În cele mai multe modele de pipeline, odată ce o
instrucțiune a fost preluată în pipeline, aceasta trebuie executată, chiar dacă instrucțiunea
precedentă a fost o ramificare condiționată. Pipeline-urile dau mari bătăi de cap dezvoltatorilor
de compilatoare și de sisteme de operare, deoarece aceștia sunt expuși complexității mașinii
fizice și sunt obligați să rezolve problemele care apar aici.

Fig. 1.7. (a) Pipeline cu trei etape. (b) Unitate centrală superscalară
Procesorul superscalar, prezentat în figura 1.7, b, are un design mai avansat decât procesorul
pipeline. Acesta are mai multe unități de execuție, de exemplu: una pentru aritmetica numerelor
întregi, una pentru aritmetica în virgulă mobilă și una pentru operațiile logice. Două sau mai
multe instrucțiuni sunt preluate în același timp, decodificate și plasate într-un buffer de stocare
în așteptarea executării. De îndată ce unitatea de execuție devine disponibilă, aceasta caută în
memoria tampon de stocare o instrucțiune pe care o poate executa și, dacă este disponibilă, o
preia din memoria tampon și o execută. Ca urmare, comenzile programului adesea nu respect
ordine de execuție. De asigurarea corectitudinii rezultatului final (că acesta va fi același cu cel
care rezultă din respectarea ordinei de execuție) sunt, de obicei, resursele tehnice. Însă, după
cum vom vedea mai târziu, această abordare introduce complicații neplăcute în sistemul de
operare.
După cum s-a menționat, majoritatea procesoarelor, cu excepția celor mai simple utilizate în
sistemele incorporate, au două moduri de funcționare: modul kernel și modul utilizator. De
obicei, modul este controlat de un bit special din cuvântul de stare a programului, PSW. Atunci
când funcționează în modul kernel, procesorul poate executa orice comandă din setul său de
comenzi și poate utiliza orice posibilități ale resurselor fizice. Pe desktopuri și pe calculatoarele
de tip server, sistemul de operare rulează de obicei în modul kernel, ceea ce îi oferă acces la
toate capacitățile resurselor fizice. Pe majoritatea sistemelor incorporate, doar o mică parte a
sistemului de operare rulează în modul kernel, restul sistemului de operare rulând în modul
utilizator.
Programele de utilizator rulează întotdeauna în modul utilizator, adică sunt executate doar
instrucțiuni din cadrul unei submulțini de comenzi (numite nepriveligiate) și au acces la un
subset de capacități ale hardware-ului. De regulă, toate comenzile referitoare la operațiunile de
I/O și la protecția memoriei sunt interzise în modul utilizator. De asemenea, este bineînțeles
interzisă setarea modului kernel prin modificarea valorii bitului PSW.
Pentru a obține servicii de la sistemul de operare, programul utilizatorului trebuie să genereze
un apel de sistem care este interceptat în kernel și care apelează sistemul de operare.
Instrucțiunea de interceptare TRAP hook realizează comutarea din modul utilizator în modul
kernel și pornește sistemul de operare. Când procesarea apelului este finalizată, controlul este
returnat programului utilizator și se execută comanda care urmează apelului de sistem. Detaliile
mecanismului de apelare a sistemului vor fi tratate mai târziu în acest capitol, dar pentru
moment ar trebui să fie considerat un tip special de instrucțiune de apelare a procedurii care
are proprietatea suplimentară de a trece din modul utilizator în modul kernel. De acum încolo,
apelurile de sistem vor fi evidențiate cu același font ca în acest cuvânt: read.
Desigur, calculatoarele au și alte întreruperi de sistem care nu sunt concepute pentru a
intercepta instrucțiunea de apel de sistem. Dar majoritatea celorlalte întreruperi de sistem sunt
declanșate de întreruperi hardware pentru a avertiza asupra unor situații excepționale, cum ar fi
încercările de împărțire la zero sau dispariția unui ordin în timpul unei operații în virgulă mobilă.
În toate cazurile, controlul se transferă către sistemul de operare, care trebuie să decidă ce
trebuie să facă în continuare. Uneori, programul trebuie să fie încheiat printr-un mesaj de
eroare. În alte cazuri, eroarea poate fi ignorată (de exemplu, atunci când ordinea unui număr
dispare, se poate presupune că acesta este zero). În sfârșit, atunci când programul a declarat
în prealabil că va gestiona singur unele dintre situațiile posibile, controlul ar trebui să fie returnat
programului pentru ca acesta să poată rezolva singur problema.

1.3.2 Cipuri multi-fire și multi-nuclee


Legea lui Moore afirmă că numărul de tranzistori într-un cip se dublează la fiecare 18 luni.
Această ''lege'' nu este un fel de lege a fizicii, cum ar fi conservarea impulsului, ci este o
observație a cofondatorului Intel, Gordon Moore, cu privire la rapiditatea cu care inginerii de la
companiile de semiconductori reușesc să micșoreze tranzistorii. Legea lui Moore este valabilă
de peste trei decenii și se preconizează că va mai fi valabilă cel puțin încă unul. După aceea,
numărul de atomi pe tranzistor va deveni prea mic, iar mecanica cuantică va începe să joace
un rol important, împiedicând reducerea în continuare a dimensiunilor tranzistorului.
Abundența tranzistoarelor duce la o problemă: ce să facem cu toate acestea? Am văzut mai
sus o abordare: arhitecturi superscalare, cu unități funcționale multiple. Dar, pe măsură ce
numărul de tranzistori într-o unitate de volum crește, sunt posibile și mai multe unități
funcționale. Un lucru evident de făcut este să punem memorii cache mai mari pe cipul
procesorului. Aceasta se și întâmplă, dar, în cele din urmă, se va ajunge la punctul de
descreștere a randamentului.
Următorul pas evident este replicarea nu numai a unităților funcționale, ci și a unei părți din
logica de control. Intel Pentium 4 a introdus această proprietate, numită multithreading sau
hyperthreading (denumirea Intel), în procesorul x86, ca și în cazul altor câteva cipuri de
procesor, inclusiv SPARC, Power5, Intel Xeon și familia Intel Core. Într-o primă aproximație,
această tehnologie permite procesorului să păstreze starea a două fire diferite și să se
comuteze între ele timp de câteva nanosecunde. (Un fir este un fel de proces ușor, care, la
rândul său, este un program în curs de execuție; vom intra în detalii în cap. 2.) De exemplu,
dacă unul dintre procese trebuie să citească un cuvânt din memorie (ceea ce durează multe
cicluri de ceas), un CPU cu mai multe fire poate pur și simplu să treacă la un alt fir.
Multithreading-ul nu oferă un paralelism adevărat. Doar un singur proces la un moment dat
rulează, dar timpul de comutare a firelor este redus la ordinul unor nanosecunde.
Multithreading-ul are implicații pentru sistemul de operare, deoarece fiecare fir apare pentru
sistemul de operare ca o unitate centrală de procesare separată. Să ne închipuim un sistem cu
două procesoare reale, fiecare cu două fire de execuție. Sistemul de operare va vedea patru
procesoare. Dacă există lucru doar pentru a menține ocupate două procesoare la un anumit
moment de timp, acesta poate programa din greșeală două fire pe același processor fizic, iar
celălalt processor să rămână complet inactiv. Această alegere este cu mult mai puțin eficientă
decât utilizarea unui singur fir pe fiecare CPU.
Dincolo de multithreading, multe cipuri CPU au acum patru, opt sau mai multe procesoare sau
nuclee complete pe ele. Cipurile multicore din Fig. 1-8 conțin efectiv patru miniprocesoare,
fiecare cu propriul CPU independent. (Despre cache vom explica mai jos.) Unele procesoare,
cum ar fi Intel Xeon Phi și Tilera TilePro, au deja mai mult de 60 de nuclee pe un singur cristal.
Folosirea unui astfel de procesor multicore va necesita cu siguranță un sistem de operare
multiprocesor.

Fig. 1.8. (a) Un cip cu patru nuclee cu o memorie cache L2 partajată. (b) Un cip patru nuclee cu
memorii cache L2 separate
De altfel, în ceea ce privește numărul absolut de nuclee, nimic nu întrece un GPU (unitate de
procesare grafică) modern. Un GPU este un procesor cu, literalmente, mii de nuclee minuscule.
Acestea sunt foarte bune pentru multe calcule mici efectuate în paralel, cum ar fi redarea
poligoanelor în aplicațiile grafice. Nu sunt la fel de bune pentru sarcini seriale. De asemenea,
sunt greu de programat. În timp ce GPU-urile pot fi utile pentru sistemele de operare (de
exemplu, criptarea sau procesarea traficului de rețea), este puțin probabil ca partea principală a
sistemului de operare să ruleze pe astfel de procesoare.

1.3.3 Memoria
A doua componentă principală a oricărui calculator este memoria. În mod ideal, memoria ar
trebui să fie cât mai rapidă (mai rapidă decât execuția unei singure instrucțiuni, astfel încât
procesorul să nu fie încetinit de accesarea memoriei), destul de mare și extrem de ieftină. Nici o
tehnologie modernă nu poate îndeplini toate aceste cerințe, astfel încât se folosește o altă
abordare. Sistemul de memorie este conceput ca o ierarhie de niveluri (figura 1.9).

Fig. 1.9. Ierarhia unei memorii tipice. Valorile numerice sunt foarte aproximative.
Nivelurile superioare au o viteză mai mare, o capacitate mai mică și un cost unitar de stocare a
unui bit de informație mai mare decât nivelurile inferioare, uneori de miliarde de ori.
Nivelul superior este format din registrele interne ale procesorului. Acestea sunt alcătuite din
același material ca și procesorul și, prin urmare, sunt la fel de rapide. În consecință, nu există
nicio întârziere în accesarea lor. Capacitatea de stocare disponibilă în ele este, de obicei, de 32
×32 biți la un CPU pe 32 de biți și de 64×64 biți la un CPU pe 64 de biți. În ambele cazuri acest
volum nu depășește vloarea de 1 KB. Programele singure pot să gestioneze registrele (adică
să decidă ce să păstreze în ele) fără implicarea hardware-ului.
Urmează memoria cache, care este în mare parte controlată de hardware. Memoria principală
este împărțită în linii de memorie cache, de obicei câte 64 octeți, cu adresele 0 până la 63 în
linia cache 0, 64 până la 127 în linia cache 1, și așa mai departe. Cele mai utilizate linii de
cache sunt păstrate într-o memorie cache de mare viteză situată în interiorul sau foarte
aproape de CPU. Atunci când programul trebuie să citească un cuvânt de memorie, hardware-
ul cache-ului verifică dacă nu cumva linia necesară se află în memoria cache. Dacă da, ceea ce
se numește "cache hit", cererea este satisfăcută din memoria cache și nu este trimisă nicio
cerere de memorie pe magistrala către memoria principală. În mod normal, accesarea cache-
ului durează aproximativ două cicluri de ceas. În cazul lipsei cuvântului necesar în linia cache,
trebuie să se meargă la memorie operativă, ceea ce va însemna întârziere suplimentară
substanțială în timp. Memoria cache este limitată ca dimensiune din cauza costul ridicat. Unele
mașini au două sau chiar trei niveluri de memorie cache, fiecare mai lent și mai mare decât cel
anterior.
Memorizarea în cache joacă un rol esențial în multe domenii ale informaticii, nu doar pentru
păstrarea șirurilor RAM. Caching-ul este utilizat frecvent pentru a îmbunătăți performanța atunci
când există o resursă mare care poate fi împărțită în părți, dintre care unele sunt mult mai des
utilizate decât altele. Sistemele de operare folosesc caching peste tot. De exemplu, majoritatea
sistemelor de operare păstrează fișierele (sau părți de fișiere) foarte utilizate în memoria RAM,
evitând astfel să fie nevoie să le citească din nou în mod repetat de pe disc. Analogic,
rezultatele conversiilor numelor lungi de fișiere, cum ar fi
/home/ast/projects/minix3/src/kernel/clock.c,
la adresa de disc unde se află fișierul pot fi stocate în memoria cache pentru a evita necesitatea
unor căutări repetate. În cele din urmă, se poate stoca în memoria cache pentru utilizare
ulterioară rezultatul conversiei dintre adresa unei pagini web (URL) și adresa de rețea (adresa
IP). Există multe alte utilizări ale tehnologiei de stocare în memoria cache.
În orice sistem de caching apar destul de repede o serie de probleme.
1. Când se plasează un element nou în memoria cache?
2. În ce zonă de memorie cache ar trebui plasat un nou element?
3. Ce intrare să fie eliminată (“victima”) din memoria cache atunci când dorim să obținem
spațiu liber?
1. Unde anume va fi plasată “victima” (în memoria secundară)?
Nu toate aceste întrebări au legătură cu memoria cache. De exemplu, în procesul de stocare
șirurilor de caractere RAM în memoria cache a procesorului, un nou element va fi de obicei
introdus în memoria cache de fiecare dată când accesarea memoriei cache a fost fără succes.
Atunci când se calculează linia de memorie cache dorită pentru a plasa un nou element, de
obicei sunt utilizați unii dintre biții superiori ai acelei adrese de memorie, la care se face
referință. De exemplu, dacă există 4096 de linii de memorie cache de 64 de octeți și adrese de
32 de biți, biții de la 6 la 17 ar putea fi utilizați pentru a determina linia de memorie cache, iar
biții de la 0 la 5 ar putea fi utilizați pentru a determina octetul din linia de memorie cache. În
acest caz, elementul care urmează să fie șters este același cu cel care conține noile date, dar
în alte sisteme este posibil ca această ordine să nu fie respectată. În sfârșit, atunci când o linie
de memorie cache este suprascrisă în memoria RAM (dacă a fost modificată în timpul
procesului de caching), locația de memorie în care urmează să fie suprascrisă este definită
explicit prin adresa din cerere.
Utilizarea memoriei cache s-a dovedit a fi atât de reușită încât multe procesoare moderne au
două niveluri de memorie cache. Primul nivel, sau memoria cache L1, face întotdeauna parte
din procesor și, de obicei, furnizează instrucțiuni decodate motorului de execuție a
instrucțiunilor procesorului. Multe procesoare au o a doua memorie cache L1 pentru acele
cuvinte de date care sunt foarte utilizate.
În mod obișnuit, fiecare dintre memoriile cache L1 are o dimensiune de 16 Kbyte. În plus față
de această memorie cache, procesoarele au adesea un al doilea nivel de memorie cache numit
L2 cache, care conține câțiva megabytes de cuvinte de memorie utilizate recent. Diferența
dintre memoria cache L1 și L2 este reprezentată de diagrama de sincronizare. Accesul la
memoria cache de prim nivel nu are întârziere, în timp ce accesul la memoria cache de al
doilea nivel necesită o întârziere de unul sau două cicluri de ceas.
Atunci când proiectează procesoare multi-core, proiectanții trebuie să decidă unde să plaseze
memoria cache. Figura 1.8 (a) prezintă o singură memorie cache L2 partajată de toate
nucleele. Această abordare este utilizată în procesoarele multi-core de la Intel. Pentru
comparație, figura 1.8 (b) arată că fiecare nucleu are propriul cache L2. Aceasta este
abordarea utilizată de AMD. Fiecare abordare are propriile sale argumente pro și contra. De
exemplu, memoria cache L2 partajată de la Intel necesită controler de cache mai complex, în
timp ce calea aleasă de AMD face dificilă menținerea unei stări consecvente a memoriei cache
L2 între nuclee.
Următorul în ierarhia prezentată în figura 1.9 este memoria operativă. Aceasta este principala
zonă de lucru a sistemului de memorie al mașinii. Memoria operativă este adesea denumită
memorie cu acces aleatoriu (Random Access Memory (RAM)). Veteranii o numesc uneori
memorie cu miez, deoarece în anii 1950 și 1960, în memoria RAM se foloseau mici miezuri de
ferită magnetizabile. Au trecut decenii, dar numele s-a păstrat. În zilele noastre, blocurile de
memorie au volume diferite de la sute de megaocteți la mai mulți gigaocteți, iar acest volum
crește rapid. Toate cererile procesorului care nu pot fi satisfăcute de memoria cache sunt
direcționate către la RAM.
Pe lângă memoria RAM, multe calculatoare sunt echipate cu o mică memorie cu acces
aleatoriu, nevolatilă, numită Memorie numai pentru citire (ROM). Spre deosebire de RAM, ROM
nu-și pierde conținutul atunci când se întrerupe alimentarea cu energie și, prin urmare, este
nevolatilă. ROM-ul este programat din fabrică și nu poate fi modificat ulterior. Acest tip de
memorie se caracterizează prin viteză mare și costuri reduse. Unele computere au un
încărcător de pornire inițial în memoria ROM care este utilizat pentru a porni calculatorul. Unele
controlere I/O au, de asemenea, acest tip de memorie pentru controlul de nivel scăzut al
dispozitivului.
Există și alte tipuri de memorie nevolatilă care pot fi șterse și rescrise, spre deosebire de ROM-
uri: memoriile permanente programabile șterse electric (EEPROM), cunoscute și sub numele
de (Electrically Erasable PROM) și memoria flash. Deoarece scrierea EEPROM-urilor durează
cu câteva ordine de mărime mai mult decât a RAM-urilor, acestea sunt utilizate în același scop
ca și ROM. De asemenea, acestea au și caracteristica suplimentară de a putea corecta erorile
din programele pe care le conțin prin suprascrierea spațiului de memorie pe care îl ocupă.
Memoria flash este utilizată în mod obișnuit ca suport de stocare pentru dispozitivele
electronice portabile. Menționez doar două domenii de utlizare: servește drept "film (peliculă)"
în aparatele foto digitale și "disc" în playerele muzicale portabile. în ceea ce privește
performanța memoria flash se află la mijloc înttre RAM și disc. De asemenea, spre deosebire
de un dispozitiv de stocare de tip "disc", dacă memoria flash este ștearsă sau suprascrisă de
prea multe ori, aceasta devine vulnerabilă.
Un alt tip de memorie este memoria CMOS, care este nevolatilă. Multe calculatoare folosesc
memoria CMOS pentru a stoca data și ora curentă. Memoria CMOS și circuitele electronice de
cronometrare a timpului sunt alimentate de o baterie miniaturală (sau de un pachet de baterii),
astfel încât ora curentă este întotdeauna actualizată chiar și atunci când calculatorul este
deconectat de la o sursă de alimentare externă. Memoria CMOS poate stoca, de asemenea,
parametrii de configurare care să indice, de exemplu, de la ce unitate trebuie să pornească
sistemul. Consumul de energie al memoriei CMOS este atât de redus încât utilizarea unei
baterii instalată din fabrică durează adesea mai mulți ani. Însă, atunci când bateria începe să
cedeze, computerul poate da semne de "Alzheimer" și poate "uita" lucruri pe care le ținea minte
de ani de zile, cum ar fi de exemplu de pe care hard disk să pornească.

1.3.4 Discurile
Următorul nivel al ierarhiei memoriei, după RAM, este discul magnetic (hard disk). O unitate de
disc este de douzeci de ori mai ieftin decât memoria RAM în termeni de biți de informație, iar
capacitatea de stocare este cu mult mai mare. Singura problema este că accesul la date este
cu aproximativ trei ordine de mărime mai lent. Acest lucru se datorează faptului că este un
dispozitiv mecanic a cărui construcție este prezentată în mod convențional în figura 1.10.
Fig. 1.10. Reprezentarea schematică a unui disc rigid.
Un hard disk este format din una sau mai multe plăci metalice care se rotesc la 5400, 7200,
10800 sau mai multe rotații pe minut. O unitate mecanică (capul de citire-acriere - CCS) se
rotește la un anumit unghi deasupra acestor plăci, similar cu un vechi pick-up cu 33 de rotații pe
minut. Informațiile sunt scrise pe disc într-o secvență de cercuri concentrice. La fiecare poziție
de acționare dată, fiecare cap poate citi o secțiune circulară numită pistă. Un cilindru este
format din combinația tuturor pistelor pentru o anumită poziție de acționare.
Fiecare pistă este împărțită într-un număr de sectoare, de obicei 512 octeți pe sector. La
unitățile moderne, cilindrii exteriori conțin mai multe sectoare decât cilindrii interiori. Este nevoie
de aproximativ 1 ms pentru a muta unitatea de la un cilindru la altul. Deplasarea la un cilindru
selectat în mod arbitrar durează de obicei între 5 și 10 ms, în funcție de unitatea specifică.
Atunci când CCP este poziționat deasupra pistei dorite, trebuie să se aștepte ca sectorul dorit
să ajungă sub cap. Acest lucru are ca rezultat o altă întârziere de 5 până la 10ms în funcție de
viteza de rotație a unității. Odată ce sectorul corespunzător se află sub capul unității, se va
efectua o operațiune de citire sau de scriere, de la 50 MBytes/s (pentru unitățile cu viteză mai
mică) până la 160 MB/s (pentru unitățile de mare viteză).
Uneori vorbim despre discuri care nu sunt cu adevărat discuri, cum ar fi discurile cu stare solidă
(Solid State Disk - SSD). Acestea nu au părți mobile, nu au plăci metalice, iar datele sunt
stocate în memoria flash. Sunt similare cu hard disk-urile și conțin o cantitate mare de date care
nu se pierd atunci când se oprește alimentarea cu energie electrică.
Multe computere acceptă o schemă numită memorie virtuală. Acest aspect va fi abordat mai
în detaliu în capitolul 3. Memoria virtuală vă permite să executați programe mai mari decât
memoria fizică a computerului, plasând programul pe disc și folosind memoria RAM ca un fel
de cache pentru părțile programului necesare la un moment de timp dat. Această schemă
necesită o traducere transparentă a adresei generate de program în adresa fizică la care este
stocat cuvântul în memoria RAM. Această mapare a adreselor este realizată de o parte a
procesorului, numită unitatea de gestionare a memoriei (Memory Management Unit - MMU) sau
managerul de memorie (figura 1.6).
Utilizarea memoriei cache și a MMU poate avea un impact semnificativ asupra performanței. În
cazul lucrului cu multiprogramare, când se trece de la un program la altul, operație numită
comutarea contextului, poate fi necesar să fie eliminate toate blocurile modificate din cache și
modificate registrele de mapare din MMU. Ambele operațiuni sunt “foarte scumpe”, iar
programatorii încearcă din răsputeri să le evite. Unele consecințe ale tacticilor utilizate în acest
scop vor fi analizate puțin mai târziu.

1.3.5 Dispozitivele de intrare/ieșire


Procesorul și memoria nu sunt singurele resurse pe care un sistem de operare trebuie să le
gestioneze. Sistemul de operare interacționează în mod activ cu dispozitivele de intrare/ieșire.
Figura 1.6 arată că dispozitivele de intrare/ieșire sunt de obicei alcătuite din două componente:
dispozitivul propriu-zis și controlerul acestuia. Controlerul este un cip sau un set de cipuri care
gestionează dispozitivul la nivel fizic. Acceptă comenzi de la sistemul de operare, cum ar fi
citirea datele din dispozitiv și apoi le execută.
Destul de des, controlul direct al unui dispozitiv este foarte complex și necesită un nivel ridicat
de detalii, astfel încât sarcina controlerului este de a oferi o interfață simplă (dar nu simplistă)
cu sistemul de operare. De exemplu, un controler de disc poate primi o comandă de citire a
sectorului 11206 de pe discul 2. La primirea comenzii, controlerul trebuie să convertească acest
simplu număr secvențial de sector într-un număr de cilindru, sector și cap. Operațiunea de
conversie poate fi complicată de faptul că cilindrii exteriori au mai multe sectoare decât cilindrii
interiori, iar numerele rele de sector sunt alocate altor sectoare. Controlerul trebuie apoi să
determine pe ce cilindru se află acum actuatorul capului și să emită o comandă pentru a-l
deplasa înainte sau înapoi cu numărul necesar de cilindri. Apoi trebuie să aștepte până când
sectorul corect ajunge sub cap și apoi să citească și să stocheze biții pe măsură ce aceștia sunt
citiți, eliminând anteturile și calculând sumele de control. În cele din urmă, trebuie să asambleze
biții în cuvinte și să le stocheze în memorie. Pentru a face toate acestea, controlerele includ
adesea mici computere incorporate, programate să îndeplinească atfel de sarcini.
Cealaltă componentă este dispozitivul propriu zis. Dispozitivele au interfețe destul de simple,
deoarece, în primul rând, au capacități foarte modeste și, în al doilea rând, trebuie să respecte
standarde comune. Aceasta din urmă este necesar pentru a se asigura că, de exemplu, orice
controler de disc SATA va funcționa cu orice disc SATA. SATA înseamnă Serial ATA, iar ATA
înseamnă conexiune AT. În cazul în care nu știați ce înseamnă AT, acesta a fost derivat din a
doua generație de calculatoare personale IBM cu tehnologie avansată (PC Advanced
Technology), care se baza pe procesorul 80286, foarte puternic la acea vreme, cu o frecvență
de 6 MHz și lansat de IBM în 1984. Din acest fapt, putem concluziona că industria informatică
are obiceiul de a completa constant acronimele existente cu noi prefixe și sufixe. În plus, se
poate concluziona că adjective precum "avansat" trebuie folosit cu grijă pentru a nu părea
stupid peste 30 de ani.
SATA este tipul standard de unitate de disc pentru multe computere din zilele noastre.
Deoarece interfața dispozitivului este ascunsă de către controlerul acestuia, toate sistemele de
operare văd doar interfața controlerului, care poate fi foarte diferită de interfața dispozitivului.
Deoarece toate tipurile de controlere sunt diferite, pentru a le gestionae este nevoie software
diferit. Software-ul conceput pentru a comunica cu controlerul, pentru a-i transmite comenzi și a
primi răspunsuri de la acesta, se numește driver de dispozitiv. Fiecare producător de controlere
trebuie să furnizeze drivere de dispozitiv pentru fiecare sistem de operare acceptat. De
exemplu, scanerul poate fi livrat cu drivere pentru sistemele de operare OS X, Windows 7,
Windows 8 și Linux, etc.
Pentru a fi utilizat, driverul trebuie să fie plasat în sistemul de operare, permițându-i astfel să
ruleze în modul kernel. În general, driverele pot opera și în modul utilizator, iar suportul pentru
modul kernel este oferit în prezent atât în sistemele de operare Linux, cât și Windows. Marea
majoritate a driverelor sunt încă executate mai jos de granița nucleului. Driverele rulează în
modul utilizator doar pe foarte puține sisteme existente, cum ar fi MINIX 3. Driverele care sunt
executate în mod utilizator trebuie să aibă permisiunea de a accesa dispozitivul într-un mod
controlat, ceea ce reprezintă o sarcină departe de a fi simplă.
Există trei moduri de a instala un driver în kernel. Prima este de a recompila kernelul cu noul
driver și apoi de a reporni sistemul. Multe sisteme UNIX vechi fac acest lucru. A doua metodă
este de a crea o intrare într-un fișier special din sistemul de operare, informându-l despre ceea
ce este necesar cu repornirea ulterioară a sistemului. Când sistemul de operare pornește,
caută driverele de care are nevoie și le încarcă. Acesta este modul în care funcționează
Windows. În cea de-a treia metodă - încărcarea dinamică a driverelor - sistemul de operare
poate accepta drivere noi din mers și le poate instala rapid, fără a fi necesară o repornire.
Această metodă era folosită anterior destul de rar, dar acum devine din ce în ce mai răspândită.
Dispozitive externe conectabile la cald (hotpluggable)4 cum ar fi dispozitivele USB și IEEE
1394, de exemplu, trebuie să aibă întotdeauna drivere care pot fi încărcate dinamic.
Fiecare controler are un număr mic de registre cu care trebuie să comunice. De exemplu, un
controler de disc simplu poate avea registre pentru specificarea unei adrese pe unitate, adresa
de memorie, contorul de sectoare și direcția de transfer a informațiilor (citire sau scriere).
Pentru a activa un controler, driverul primește o comandă de la sistemul de operare, apoi o
traduce în valorile corespunzătoare pentru a o scrie în registrele dispozitivului. Toate registrele
dispozitivului se combină pentru a forma spațiul porturilor I/O, la care vom reveni în capitolul 5.
La unele calculatoare, registrele dispozitivului sunt mapate în spațiul de adrese al sistemului de
operare (la acele adrese pe care acesta le poate utiliza), astfel încât stările registrelor pot fi
citite și scrise în același mod ca și cuvintele obișnuite în memoria principală. Astfel de
computere nu necesită nici un fel de comenzi I/O, iar programele de utilizator pot fi ținute în
afara hardware prin plasarea acestor adrese în afara domeniului de acțiune al programelor (de
ex. prin utilizarea registrelor de bază și a registrelor de delimitare a zonei de memorie). La alte
calculatoare, registrele dispozitivelor sunt plasate într-un spațiu special de porturi I/O (I/O port
space), unde fiecare registru are o adresă de port. Pe aceste mașini, există comenzi speciale
de i/O executate în modul kernel (de obicei notate IN și OUT) pentru a permite driverelor să
citească și să scrie date în registre. Prima schemă elimină necesitatea unor comenzi speciale
de I/O, dar utilizează o parte din spațiul de adrese. Cea de-a doua schemă nu utilizează niciun
spațiu de adrese, dar necesită instrucțiuni speciale. Ambele scheme sunt utilizate pe larg.
Introducerea și extragerea datelor se poate face în trei metodea diferite. În cea mai simplă
dintre acestea, programul utilizatorului execută un apel de sistem care este tradus de kernel
într-o procedură de apel de driver corespunzătoare. Driverul continuă apoi procesul de I/O. În
acest timp, driverul execută un ciclu foarte scurt, interogând dispozitivul și monitorizând
finalizarea operațiunii (de obicei, un bit special indică faptul că dispozitivul este ocupat). Când
operațiunea de I/O este finalizată, driverul plasează datele (dacă există) acolo unde trebuie să
ajungă și returnează controlul. Sistemul de operare returnează apoi controlul către programul
apelant. Această metodă se numește așteptare activă sau așteptare a finalizării, iar
dezavantajul este că procesorul este ocupat pe toată durata procesului de I/O.
În cadrul metodei a doua driverul lansează dispozitivul și îi solicită acestuia să-i trimită o
întrerupere atunci când execuția comanzii este finalizată (intrarea sau ieșirea este încheiată).
Imediat driverul returnează controlul. Sistemul de operare blochează programul apelant, dacă

4
Adică, dispozitive care pot fi conectate sau deconectate de la calculator fără a fi nevoie să opriți sistemul de operare și să
deconectați calculatorul de la sursa de alimentare. - Nota editorului.
este necesar, și trece la îndeplinirea altor sarcini. Când controlerul detectează sfârșitul
transferului de date, generează o întrerupere pentru a semnala sfârșitul operațiunii.
Întreruperile joacă un rol foarte important în funcționarea sistemului de operare, așa că haideți
să le analizăm mai îndeaproape. Figura 1.11 (a) prezintă pașii procesului de I/O. În cadrul
pasului 1 driverul trimite o comandă către controler prin scrierea ei informații în registrele
acestuia. Controlerul pornește apoi dispozitivul propriu-zis. La pasul 2, când controlerul termină
de citit sau de scris un anumit număr de octeți, el setează la cipul de control al întreruperilor
semnalul pentru microschema controlerului, folosind în acest scop anumite linii ale magistrașei.
La pasul 3, dacă controlerul de întreruperi este pregătit să primească o întrerupere (și este
întreruperi (este posibil să nu fie gata să primească întreruperea, dacă procesează o
întrerupere cu o prioritate mai mare), acesta semnalizează pinul respectiv de pe cipul CPU
informându-l că operațiunea s-a încheiat. În cel de-al patrulea pas, controlerul de întreruperi
setează numărul dispozitivului pe magistrală, astfel încât procesorul să îl poată citi și să afle
care dispozitiv a terminat numai ce lucrul (deoarece mai multe dispozitive pot funcționa în
același timp).

Fig. 1.11. a) Etapele de pornire a unui dispozitiv de I/O și de obținere a unei întreruperi. (b)
Prelucrarea întreruperii implică preluarea întreruperii, rularea operatorului de întrerupere, și
revenirea la programul utilizatorului.
De îndată ce procesorul decide să accepte o întrerupere, conținutul contorului de instrucțiuni și
al cuvântului de stare a programului sunt plasate, de obicei, pe stiva curentă iar procesorul
trece în modul kernel. Numărul dispozitivului poate fi folosit ca un index pentru porțiunea de
memorie utilizată pentru a găsi adresa operatorului de întreruperi a dispozitivului respectiv.
Această porțiune de memorie se numește vector de întrerupere. Atunci când un operator de
întreruperi (care face parte din driverul dispozitivului, care emite cererea de întrerupere) își
începe operațiunea, acesta extrage din stivă contorului de comenzi și cuvântul de stare a
programului și le stochează, apoi interoghează dispozitivul pentru a determina starea acestuia.
După tratarea întreruperii controlul estel returnat programului utilizatorului la instrucțiunea
imediat următoare, care nu a fost încă executată (v. fig. 1.11 (b)).
Cea de-a treia metodă de intrare/ieșire utilizează un controler special de acces direct la
memorie (DMA). Controlerul de acces direct la memorie este utilizat pentru a controla fluxul de
biți între memoria principală și unele dintre controllere, fără a fi nevoie de o intervenție din
partea unității centrale de procesare. Procesorul central configurează controlerul DMA
spunându-i câți octeți să transfere, ce dispozitiv și adresele care trebuie utilizate, în ce direcție
trebuie transferate datele, și apoi îl lasă să funcționeze independent. Atunci când controlerul
DMA termină lucrul, va emite o cerere de întrerupere, care este tratată în ordinea pe care am
discutat-o anterior. Controlerul DMA și dispozitivele de intrare/ieșire vor fi prezentate mai
detaliat în capitolul 5.
Întreruperile apar adesea în momente foarte nepotrivite, de exemplu în timpul lucrului unui
operator al altei întreruperi. Din acest motiv, procesorul are capacitatea de a interzice tratarea
unei întreruperi cu eliminarea ulterioară a interzicerii. În timp ce întreruperile sunt interzise,
toate dispozitivele care și-au terminat activitatea vor continua să emită cereri de întrerupere,
dar CPU nu se oprește din funcționare până când întreruperile nu vor fi permise din nou. Dacă
mai multe dispozitive își finalizează solicitările de întrerupere în timpul perioadei de inhibare a
întreruperilor, controlerul decide care din ele trebuie tratată prima, bazându-se de obicei pe
priorități statice atribuite fiecărui dispozitiv. Dispozitivul cu cea mai mare prioritate este cel care
câștigă. Toate celelalte dispozitive trebuie să își aștepte rândul.

1.3.6 Magistralele
Structura prezentată în figura 1.6 a fost utilizată timp de mulți ani în minicalculatoare și, de
asemenea, în primul PC IBM. Dar, pe măsura sporirii vitezei de procesoare și a capacității
memoriei posibilitățile magistralei unice (și, desigur, a mgistralei IBM PC) pentru a susține toate
procesele de comunicare au fost epuizate. Trebuia de făcut ceva. Ca urmare, au apărut
magistrale suplimentare, atât pentru dispozitive de intrare/ieșire mai rapide, cât și pentru
schimbul de date între procesor și memorie. Ca o consecință a acestei evoluții, sistemul x86
produs în masă are în prezent forma prezentată în figura 1.12.

Fig. 1.12. Structura sistemului x86 extins.


Acest sistem are mai multe magistrale (de exemplu, magistrala cache, magistrala de memorie,
de asemenea magistralele PCIe, PCI, USB, SATA și DMI), fiecare cu o viteză diferită de
transfer de date și cu scop propriu. Pentru ca sistemul de operare să realizeze funcții de
configurare și control, acesta trebuie să cunoască totul despre aceste magistrale. Magistrala
principală este magistrala PCI (Peripheral Component Interconnect interfața dispozitivelor
periferice).
Magistrala PCIe a fost inventată de Intel ca succesor al magistralei mai vechi PCI, care, la
rândul său, a înlocuit magistrala ISA (Industry Standard Architecture) originală. Deoarece poate
transfera date la viteze de zeci de gigabiți pe secundă, magistrala PCIe funcționează mult mai
rapid decât predecesoarea sa. Este, de asemenea, de natură foarte diferită. Până la
introducerea sa în 2004, majoritatea magistralelor erau paralele și partajate. O arhitectură de
magistrală partajată înseamnă că aceiași conductori sunt utilizați de diferite dispozitive pentru a
transmite date. Prin urmare, atunci când mai multe dispozitive au date de transferat, este
necesar un arbitru pentru a determina care dispozitiv va avea dreptul de a utiliza magistrala. În
schimb, magistrala PCIe utilizează conexiuni directe dedicate punct-la-punct. O arhitectură de
magistrală paralelă similară cu cea a PCI implică trimiterea fiecărui cuvânt de date pe mai mulți
conductori. De exemplu, pe o magistrală PCI tipică, un singur număr de 32 de biți este trimis pe
32 de fire paralele. În schimb, PCIe utilizează o arhitectură de magistrală serială, iar toți biții
unui mesaj sunt trimiși printr-un singur fir, cunoscut sub numele de pistă (lane), ceea ce este
foarte asemănător cu trimiterea unui pachet de rețea. Acest lucru simplifică foarte mult sarcina,
deoarece nu este nevoie să se asigure că toți cei 32 de biți ajung la destinație exact în același
timp. Dar paralelismul este în continuare utilizat deoarece mai multe benzi pot funcționa în
paralel. De exemplu, 32 de piste pot fi utilizate pentru a transmite 32 de mesaje în paralel.
Datorită creșterii rapide a vitezelor de transfer de date ale dispozitivelor periferice, cum ar fi
plăcile de rețea și adaptoarele grafice, standardul PCIe este actualizat la fiecare 3 - 5 ani. De
exemplu, 16 benzi de PCIe 2.0 ofereau viteze de 64 Gbit/s. Un upgrade la PCIe 3.0 dublează
această viteză, iar un upgrade la PCIe 4.0 ar dubla-o din nou.
În același timp, există încă multe dispozitive tradiționale pentru standardul PCIe mai vechi.
După cum se poate observa din figura 1.12, aceste dispozitive sunt conectate la un hub
separat. În viitor, când PCI nu va mai fi considerat doar o magistrală veche, ci una străveche,
va fi posibil ca toate dispozitivele PCI să fie conectate la un alt hub care, la rândul său, le va
conecta la hub-ul principal, creând astfel un arbore al magistralelor.
În această configurație, procesorul comunică cu memoria prin intermediul unei magistrale
rapide DDR3, cu dispozitivul grafic extern prin intermediul unei magistrale PCIe și cu toate
celelalte dispozitive prin intermediul concentratorului prin intermediul magistralei DMI (Direct
Media Interface). La rândul său, hub-ul conectează toate celelalte dispozitive folosind o
magistrală serială universală pentru comunicarea cu dispozitivele USB, o magistrală SATA
pentru comunicarea cu hard disk-urile și unitățile DVD și o magistrală PCIe pentru transferul de
cadre Ethernet. Dispozitivele PCI vechi care utilizează o magistrală PCI tradițională au fost deja
menționate aici.
Magistrala USB (Universal Serial Bus) a fost concepută pentru a conecta la computer toate
dispozitivele de intrare/ieșire de viteză redusă, cum ar fi tastaturile și mouse-ul. Dar nu ar fi
normal să numim "lent" un dispozitiv USB 3.0 de 5Gb/s într-o generație în care computerul IBM
PC era considerat a fi un computer de masă cu magistrala ISA de 8 Mbits/s. USB utilizează un
un conector mic cu (în funcție de versiune) 4 - 11 pini, dintre care unii alimentează dispozitivele
USB sau sunt conectați la masă.
USB este o magistrală centralizată în care dispozitivul principal (rădăcină) interoghează
dispozitivele de I/O la fiecare milisecundă pentru a vedea dacă acestea au date de transferat.
Standardul USB 1.0 putea oferi o rată de transfer agregată de 12 Mbps, USB 2.0 a crescut la
480 Mbps, iar USB 3.0 a atins un maxim de cel puțin 5 Gbps. Un dispozitiv USB putea fi
conectat la calculator și putea funcționa imediat, fără a fi nevoie de repornire, necesară pentru
unele dispozitive înainte de USB, ceea ce îngrozea o generație întreagă de utilizatori frustrați.
SCSI (Small Computer System Interface) este o magistrală de mare viteză concepută pentru
unități de înaltă performanță, scannere și alte dispozitive care necesită o lățime de bandă mare.
În prezent, aceste magistrale sunt utilizate în principal la servere și stații de lucru. Se pot atinge
viteze de transfer de date de până la 640 MB/s.
Pentru a funcționa în mediul prezentat în figura 1.12, sistemul de operare trebuie să știe ce
periferice sunt conectate la computer și să configureze aceste dispozitive. Această cerință i-a
determinat pe Intel și Microsoft să dezvolte un sistem pentru computerele PC compatibile,
numit plug and play. Acesta se bazează pe un concept similar implementat inițial în Apple
Macintosh. Înainte de plug and play, fiecare placă I/O avea un nivel fix de solicitare a
întreruperilor și adrese fixe pentru registrele lor de I/O. De exemplu, tastatura utiliza
întreruperea 1 și adresele de I/O de la 0x60 la 0x64; controlerul de dischetă utiliza întreruperea
6 și adresele de I/O de la 0x3F0 la 0x3F7; imprimanta utiliza întreruperea 7 și adresele de I/O
de la 0x378 la 0x37A etc.
Toate acestea au funcționat foarte bine o perioadă. Problemele au început atunci când
utilizatorul cumpăra o plachetă de sunet și un modem intern și constata că ambele dispozitive
foloseau, să zicem, întreruperea 4. Apărea un conflict care împiedica dispozitivele să lucreze
împreună. Soluția a fost apariția comutatoarelor DIP sau jumperi pe fiecare placă de I/O. Însă,
utilizatorul trebuia să fie instruit să selecteze nivelurile de solicitare a întreruperilor și adresele
de intrare/ieșire pentru dispozitiv care să nu intre în conflict cu toate celelalte întreruperi și
adrese utilizate în sistemul său. Uneori, adolescenții care și-au dedicat viața rezolvării puzzle-
urilor legate de hardware-ul calculatoarelor reușeau să îndeplinească aceste cerințe fără erori.
Dar, din nefericire, aproape nimeni în afară de ei nu a putut face acest lucru, ceea ce a dus la
un haos total.
Tehnologia Plug and play forțează sistemul să colecteze automat informații despre dispozitivele
de I/O, atribuind în mod centralizat niveluri de solicitare a întreruperilor și adrese I/O, iar apoi
spunând fiecărei plăci ce valori îi sunt atribuite. Această acțiune este strâns legată de pornirea
calculatorului și ar trebui să examinăm acest proces, deoarece nu este atât de simplu pe cât
pare la prima vedere.

1.3.7 Pornirea computerului


Succint, un computer pornește după cum urmează. Fiecare calculator personal are o placă de
bază (care în SUA acum, ca urmare a răspândirii corectitudinii politice în industria informatică,
se numește placă de bază)5. Pe placa de bază se află un program, numit BIOS (Basic Input
Output System). BIOS-ul conține software de nivel scăzut pentru I/O, inclusiv proceduri pentru
citirea stării tastaturii, de afișare a informațiilor pe ecran și de execuție a operațiilor de I/O cu
discul. În zilele noastre acest software este stocat într-o memorie flash nevolatilă cu acces
aleatoriu care poate fi actualizată. BIOS este actualizat de către sistemul de operare în cazul în
care sunt detectate diverse erori.
La pornirea inițială a computerului, BIOS-ul primul începe lucrul. Acesta verifică mai întâi
cantitatea de memorie RAM instalată pe computer și prezența tastaturii, precum și instalarea și
răspunsul normal al altor dispozitive importante. Totul începe cu scanarea magistralelor PCIe și
PCI pentru a identifica dispozitivele conectate la acestea. Unele dintre aceste dispozitive sunt
moștenite din trecut (adică dezvoltate înainte ca tehnologia plug and play să fie creată).

5
Este, de asemenea, denumită, în mod corect, placa de sistem sau placa principală. - Nota editorului.
Acestea au niveluri de întrerupere și adrese de I/O fixe (eventual setate prin comutatoare sau
jumperi pe placa I/O, dar care nu pot fi modificate de sistemul de operare). Aceste dispozitive
sunt înregistrate. De asemenea, sunt înregistrate și dispozitivele compatibile cu Plug and play.
În cazul în care dispozitivele prezente sunt diferite de cele înregistrate în sistem la ultima
pornire, sunt configurate noile dispozitive.
În continuare, BIOS-ul va determina dispozitivul de pe care va fi realizată încărcarea, prin
verificarea incrementală a dispozitivelor din lista salvată în memoria CMOS. Utilizatorul poate
face modificări la această listă intrând în programul de configurare BIOS imediat după pornirea
inițială. De obicei, se încearcă o pornire de pe un CD-ROM (uneori de pe un stick USB), dacă
sistemul conține unul. În caz de eșec, sistemul pornește de pe hard disk. Primul sector este citit
de pe dispozitivul de pornire în memorie și apoi este executat programul stocat în el. De obicei,
acest program verifică tabelul de partiții, care se află la sfârșitul sectorului de pornire, pentru a
determina care partiție are statutul activ. Un încărcător de boot secundar este apoi citit de pe
această partiție, care la rândul său citește de pe partiția activă și pornește sistemul de operare.
Sistemul de operare solicită apoi BIOS-ului informații despre configurația computerului. Se
verifică dacă există drivere pentru fiecare dispozitiv. Dacă vre-un nu este disponibil, sistemul de
operare vă solicită să instalați CD-ul cu driverul. (furnizat de producătorul dispozitivului) sau
descarcă driverul de pe site-ul Internet. Odată ce are în posesie toate driverele de dispozitiv,
sistemul de operare le încarcă în kernel. Apoi își inițializează propriile tabele, creează toate
procese de fundal de care are nevoie și rulează programul de logare sau interfață grafică cu
utilizatorul.

1.4 Gradina zoologica a sistemelor de operare


Istoria sistemelor de operare datează de mai bine de o jumătate de secol. În acest timp, a fost
dezvoltată o mare varietate de sisteme de operare, dar nu toate sunt cunoscut o răspândire
largă. În această secțiune, vom examina pe scurt nouă sisteme de operare. Vom reveni asupra
unora dintre aceste diferite tipuri de sisteme de operare Vom reveni asupra unora dintre aceste
tipuri de sisteme de operare în paginile acestei cărți.

1.4.1 Sisteme de operare pentru masinile mari de calcul


Cea mai înaltă categorie include sistemele de operare pentru mainframe-uri – calculatoare
mari, care ocupă săli întregi și care se găsesc încă în centrele de date ale marilor întreprinderi.
Acestea diferă de calculatoarele personale prin cantitatea de date introduse și extrase.
Mainframe-urile cu mii de discuri și petabytes de date sunt foarte obișnuite, iar un computer
personal cu un astfel de arsenal ar fi invidiat de toată lumea. Mainframe-urile sunt, de
asemenea, utilizate ca servere web puternice, servere pentru magazine online de mari
dimensiuni și servere pentru tranzacții între companii.
Sistemele de operare pentru mainframe-uri sunt concepute în principal pentru a gestiona mai
multe sarcini simultan, majoritatea necesitând cantități enorme de date de intrare/ieșire.
Acestea oferă de obicei trei tipuri de servicii: procesare pe loturi, procesare de tranzacții și
partajare a timpului. Procesarea pe loturi este un sistem de procesare a lucrărilor standard fără
intervenția utilizatorului. Prelucrarea pe loturi este procesarea cererilor de despăgubire ale
companiilor de asigurări sau a rapoartelor de vânzări ale lanțului de magazine. Sistemele de
procesare a tranzacțiilor gestionează un număr mare de cereri mici, cum ar fi procesarea
cecurilor bancare sau rezervarea biletelor de avion. Fiecare tranzacție elementară este mică în
volum, dar sistemul poate gestiona sute sau mii de tranzacții pe secundă. Lucrul în regim de
partajare a timpului face posibilă pentru mai mulți utilizatori la distanță să își execute simultan
sarcinile pe un computer, cum ar fi interogarea simultană a unei baze de date mar. Toate
aceste funcții sunt strâns legate între ele și adesea sistemele de operare ale mașinilor
universale le execută pe toate împreună. Un exemplu de sistem de operare pentru mașini
universale este OS/390, succesorul lui OS/360. Însă, aceste sisteme de operare sunt treptat
înlocuite cu variante de sisteme de operare UNIX, de exemplu Linux.

1.4.2 Sisteme de operare pentru servere


La un nivel un pic mai jos se află sistemele de operare pentru servere. Acestea rulează pe
servere, care sunt computere personale foarte puternice, stații de lucru sau chiar mașini
universale. Acestea deservesc simultan prin rețea mai mulți utilizatori, oferindu-le acces comun
la resurse hardware și software. Serverele pot furniza servicii de imprimare, stocare de fișiere
sau servicii web. De obicei, furnizorii de servicii Internet operează mai multe servere pentru a-și
servi clienții. La deservirea site-urilor web, serverele stochează paginile web și gestionează
cererile primite. Sisteme de operare tipice pentru servere sunt Solaris, FreeBSD, Linux și
Windows Server 201x

1.4.3 Sisteme de operare multiprocesor


Astăi din ce în ce mai mult este utilizată combinarea mai multor procesoare centrale într-un
singur sistem pentru a obține o putere de calcul demnă de marile ligi. În funcție de modul exact
în care are loc această agregare și de resursele partajate, aceste sisteme se numesc
computere paralele, multicomputere sau sisteme multiprocesor. Acestea necesită sisteme de
operare speciale, care sunt adesea versiuni ale sistemelor de operare pentru servere, echipate
cu funcții speciale de comunicare, interfațare și sincronizare.
Odată cu apariția recentă a procesoarelor multi-core pentru calculatoarele personale, chiar și
sistemele de operare convenționale pentru desktop-uri și laptop-uri au început să ruleze cel
puțin un mic sistem multiprocesor. În timp, se pare că numărul de nuclee nu va face decât să
crească. Din fericire, de-a lungul anilor de cercetare anterioară s-au acumulat cunoștințe vaste
despre sistemele de operare multiprocesor, iar utilizarea acestui arsenal în sistemele multi-core
nu ar trebui să cauzeze prea multe complicații. Partea cea mai dificilă va fi găsirea de aplicații
care să poată exploata toată această putere de calcul. Multe sisteme de operare populare,
inclusiv Windows și Linux, pot rula pe sisteme multiprocesor.

1.4.4 Sisteme de operare pentru calculatoare personale


Următoarea categorie include sistemele de operare pentru calculatoare personale. Toți
reprezentanții lor moderni acceptă multitaskingul. Destul de des, zeci de programe rulează
simultan încă din timpul procesului de pornire. Sarcina sistemelor de operare pentru
calculatoarele personale este de a sprijini activitatea utilizatorului individual într-un mod
calitativ. Acestea sunt utilizate pe scară largă pentru procesarea de texte, crearea de foi de
calcul, jocuri și accesarea Internetului. Exemple tipice sunt Linux, FreeBSD, Windows 7,
Windows 8 și OS X de la Apple. Sistemele de operare pentru calculatoare personale sunt atât
de cunoscute încât nu au nevoie de o prezentare specială. De fapt, multe oamenii nu sunt
conștienți că există și alte tipuri de sisteme de operare.

1.4.5 Sisteme de operare în timp real


Continuând să coborâm spre sisteme din ce în ce mai simple, am ajuns acum la tablete,
smartphone-uri și alte calculatoare portabile. Aceste computere, cunoscute inițial sub numele
de PDA (Personal Digital Assistant), sunt calculatoare de mici dimensiuni care se țin în mână în
timpul lucrului. Cele mai cunoscute dintre acestea sunt smartphone-urile și tabletele. După cum
s-a menționat deja, această piață este dominată de sistemele de operare Android de la Google
și iOS de la Apple, dar acestea au mulți concurenți. Cele mai multe dintre aceste dispozitive
dispun de procesoare multi-core, GPS, camere și alți senzori, memorie amplă și sisteme de
operare sofisticate. În plus, toate acestea au mai multe aplicații terțe (apps) pentru memorii
USB decât vă puteți imagina.

1.4.6 Sisteme de operare încorporate


Sistemele încorporate rulează pe calculatoare care controlează diverse dispozitive. Deoarece
aceste sisteme nu sunt concepute pentru a avea programe de utilizator instalate, ele nu sunt
considerate de obicei computere. Printre exemplele de dispozitive în care sunt instalate
calculatoare încorporate se numără cuptoarele cu microunde, televizoarele, mașinile,
inscriptoarele DVD, telefoanele convenționale și playerele MP3. În general, sistemele
încorporate se deosebesc prin faptul că, în nici un caz, pee le nu vor putea fi lansate aplicații de
la terți. Nu este posibil să descărcați o nouă aplicație pe un cuptor cu microunde, deoarece
toate programele sale sunt stocate în memoria ROM. În consecință, nu este nevoie să protejăm
aplicațiile una de cealaltă, iar sistemul de operare poate fi simplificat. Embedded Linux, QNX și
VxWorks sunt considerate a fi cele mai populare sisteme de operare din acest domeniu.

1.4.7 Sisteme de operare pentru nodurile de senzori


Rețelele alcătuite din noduri senzoriale miniaturale conectate între ele și la stația de bază prin
canale fără fir sunt utilizate în diverse scopuri. Astfel de rețele de senzori sunt utilizate pentru
protejarea perimetrelor clădirilor, securizarea frontierelor naționale, detectarea incendiilor de
pădure, măsurarea temperaturii și a nivelului precipitațiilor pentru prognoza meteo, colectarea
de informații despre mișcările inamicului pe câmpul de luptă și multe altele.
Nodurile din această rețea sunt computere miniaturale alimentate cu baterii, cu un sistem radio
încorporat. Acestea au o putere limitată și trebuie să funcționeze timp îndelungat în mod
nesupravegheat în aer liber, adesea în condiții climatice dificile. Rețeaua trebuie să fie suficient
de fiabilă și permită defecțiuni ale componentelor individuale, care, pe măsură ce bateriile își
pierd capacitatea vor apărea din ce în ce mai des.
Fiecare nod de senzori este un computer real dotat cu un procesor, memorie RAM și memorie
permanentă și unul sau mai mulți senzori. Acesta rulează un sistem de operare mic, dar real,
de obicei bazat pe evenimente și care răspunde la evenimente externe sau face măsurători
periodice pe baza semnalelor ceasului încorporat. Sistemul de operare ar trebui să fie mic și
simplu, deoarece principalele probleme ale acestor noduri sunt capacitatea redusă a memoriei
RAM și autonomia limitată a bateriei. Ca și în cazul sistemelor incorporate, toate programele
sunt preîncărcate și utilizatorii nu pot rula un program descărcat din Internet, simplificând astfel
întregul proiect. Un exemplu bine cunoscut un exemplu de sistem de operare pentru noduri de
sensori este TinyOS.

1.4.8 Sisteme de operare de timp real


O altă varietate de sisteme de operare sunt sistemele de timp real. Aceste sisteme se
caracterizează prin faptul că aici timpul este un parametru cheie. De exemplu, în sistemele de
control al proceselor industriale, calculatoarele în timp real trebuie să colecteze informații
despre procese și să le utilizeze pentru a controla mașinile din fabrică. Destul de des, acestea
trebuie să îndeplinească cerințe foarte stricte în materie de sincronizare. De exemplu, atunci
când o mașină se deplasează de-a lungul unei linii de asamblare, trebuie să aibă loc operațiuni
specifice în anumite momente. Dacă un robot de sudură începe să sudeze prea devreme sau
prea târziu, utilajul va fi nefuncțional. În cazul în care operațiunea trebuie efectuată exact la
timp (sau la un anumit moment în timp), sistemul este unul de timp real. Multe astfel de sisteme
se regăsesc în controlul proceselor de producție, în aviație și în industria aerospațială, în
domeniul militar și în alte aplicații similare. Acestea trebuie să ofere asigurări absolute că
anumite acțiuni vor fi efectuate la un moment dat.
Un alt tip de astfel de sisteme este sistemul soft real-time, în care, deși nedorit, este permis ca
o acțiune să nu cauzeze un prejudiciu ireparabil prin nerespectarea unui termen limită. Această
categorie include sistemele audio sau multimedia digitale. Telefoanele inteligente sunt, de
asemenea, sisteme soft real-time.
Deoarece cerințele pentru sistemele în timp real sunt foarte stricte, sistemele de operare sunt
uneori simple biblioteci interfațate cu programe de aplicații, în care totul este strâns
interconectat și nu există nicio protecție reciprocă. Un exemplu de astfel de sistem ar fi eCos.
Categoriile de sisteme de operare pentru PDA-uri, sistemele incorporate și sistemele de timp
real se suprapun în mare măsură în ceea ce privește caracteristicile lor intrinseci. Aproape
toate acestea au cel puțin unele aspecte ale sistemelor soft real-time. Sistemele încorporate și
sistemele în timp real funcționează numai cu software-ul introdus în ele de către dezvoltatorii
acestor sisteme; utilizatorii nu pot adăuga propriul software la acest arsenal, ceea ce face mult
mai ușoară protejarea lor. PDA-urile și sistemele încorporate sunt concepute pentru
consumatorii individuali, în timp ce sistemele în timp real sunt mai des utilizate în producția
industrială. Cu toate acestea, în ciuda tuturor acestor lucruri, ele au în comun o serie de
asemănări.

1.4.9 Sisteme de operare pentru carduri inteligente


Cele mai mici sisteme de operare rulează pe carduri inteligente. Cardul inteligent este un
dispozitiv de mărimea unui card de credit cu procesor propriu. Sistemele de operare pentru
acestea sunt supuse unor limitări foarte stricte în ceea ce privește puterea de procesare a
procesorului și volumul memoriei. Unele dintre cardurile inteligente sunt alimentate de
contactele cititorului în care sunt introduse, în timp ce altele, cardurile inteligente fără contact,
sunt alimentate prin inducție, ceea ce le limitează semnificativ capacitățile. Unele pot face față
unei singure funcții, cum ar fi plata electronică, dar există și carduri inteligente multifuncționale.
Acestea sunt adesea sunt adesea sisteme proprietare.
Unele carduri inteligente sunt concepute pentru a utiliza limbajul Java. Acest lucru înseamnă că
ROM-ul unui card inteligent conține un interpretor Java Virtual Machine (JVM). Applet-urile Java
(programe mici) sunt încărcate pe card și sunt executate de către interpretorul JVM. Unele
dintre aceste carduri sunt capabile să gestioneze mai multe applet-uri Java în același timp,
ceea ce presupune lucrul în modul multiprogram și necesitatea de a prioritiza executarea
programelor. Atunci când două sau mai multe applet-uri sunt executate în același timp, devin
relevante aspectele de gestionare a resurselor și de securitate care trebuie rerzolvate de către
sistemul de operare disponibil pe card (de obicei destul de primitiv).

1.5 Concepte din domeniul sistemelor de operare


Majoritatea sistemelor de operare folosesc anumite concepte de bază și abstractizări, precum
procese, spații de adrese și fișiere, care joacă un rol major în înțelegerea sistemelor. În
următoarele secțiuni vom analiza succint, așa cum ar trebui să fie într-o introducere, câteva
dintre aceste concepte de bază. Vom reveni la detaliile fiecărui concept în capitolele următoare.
Pentru a ilustra aceste concepte, din când în când vom folosi exemple luate în principal din
UNIX. De regulă, exemple similare pot fi găsite și în alte sisteme de operare, iar unele dintre ele
vor fi luate în considerare ulterior.

1.5.1 Procese
Conceptul cheie în toate sistemele de operare este procesul. Un proces este în esență un
program în timpul executării sale. Fiecare proces are spațiul de adrese asociat - o listă de
adrese de celule de memorie de la zero la un anumit maxim, de unde procesul poate citi date și
unde le poate scrie. Spațiul de adrese conține programul executabil, datele programului și stiva.
În plus, fiecare proces are asociat un set de resurse, care, de obicei, include regiștri (inclusiv un
contor de comenzi sau contorul ordinal și un indicator de stivă), o listă de fișiere deschise,
diferite semnale - avertizări neprocesate, o listă de procese asociate și toate celelalte informații
necesare în timpul execuției programului. Astfel, un proces este un container care conține toate
informațiile necesare pentru ca programul să funcționeze.
Conceptul unui proces va fi discutat mai detaliat în Capitolul 2, iar acum, pentru a dezvolta o
idee intuitivă a procesului, considerăm un sistem care funcționează în regim multiprogram.
Utilizatorul poate porni programul de editare video și specifica conversia fișierului video cu
durata de o oră în orice format specific (procesul va dura câteva ore), apoi poate trece la
navigarea prin Internet. În același timp, un proces de fundal poate începe să funcționeze, care
se „trezește” periodic pentru a verifica e-mailurile primite. Vom avea astfel (cel puțin) trei
procese active: un editor video, un browser web și un program de e-mail (client). Periodic,
sistemul de operare va lua decizii de a opri un proces și de a începe altul, posibil datorită
faptului că primul și-a epuizat cota de timp de procesorului în secunda anterioară.
Dacă procesul este suspendat în acest fel, ulterior ar trebui să fie continuat din starea în care a
fost oprit. Aceasta înseamnă că în timpul perioadei de suspendare, toate informațiile despre
proces trebuie stocate explicit undeva. De exemplu, un proces poate avea mai multe fișiere
deschise simultan pentru citire. Un indicator pentru poziția curentă este asociat cu fiecare dintre
aceste fișiere (adică numărul de octeți sau înregistrări care urmează să fie citite în continuare).
Când procesul se întrerupe, toate aceste indicii trebuie salvate astfel încât apelul read, care va
fi executat după reluarea procesului, să conducă la citirea corectă a datelor necesare. În multe
sisteme de operare, toate informațiile despre fiecare proces, cu excepția conținutului propriului
spațiu de adrese, sunt stocate într-un tabel al sistemului de operare, care se numește tabelul
proceselor și este un tablou (sau o listă legată) de structuri, una pentru fiecare dintre procesele
existente la fiecare moment de timp dat.
Astfel, procesul (inclusiv unul suspendat) constă din spațiul propriu de adrese, denumit în mod
obișnuit imaginea în memorie, și intrările din tabelul proceselor cu conținutul regiștrilor săi,
precum și alte informații necesare pentru reluarea ulterioară a procesului.
Principalele apeluri de sistem utilizate în controlul proceselor sunt cele asociate cu crearea și
terminarea proceselor. De exemplu, procesul numit interpretorul comenzilor, sau shell,
citește comenzile de la terminal. Dacă utilizatorul introduce o comandă de compilare a unui
program, shell-ul va crea un nou proces care pornește compilatorul. Când acest proces
finalizează compilarea, va efectua un apel de sistem pentru a-și încheia existența.
Dacă un proces este capabil să creeze alte procese (numite procese descendent), iar aceste
procese, la rândul lor, pot crea propriile procese, relația respectivă poate fi reprezentată printr-
un arbore de procese similar cu cel dinn fig. 1-13.
Fig. 1-13. Arborele proceselor
Procesele conexe care lucrează împreună pentru a finaliza o sarcină trebuie adesea să facă
schimb de date între ele și să-și sincronizeze acțiunile. Această relație se numește comunicare
între procese și va fi discutată detaliat în Capitolul 2.
Alte apeluri de sistem concepute pentru a controla procesul permit solicitarea alocării de
memorie suplimentară (sau eliberarea memoriei inactive), organizarea așteptării finalizării
procesului descendent sau încărcarea unui alt program peste acesta.
Uneori, este necesară transmiterea unor informații către un proces lansat anterior, care nu este
în stare de așteptare a acestor informații. Un exemplu este un proces care comunică cu un alt
proces care rulează pe un alt computer și trimite un mesaj către procesul de la distanță din
rețea. Pentru a se asigura contra unei posibile pierderi a mesajului sau a răspunsului la mesaj,
expeditorul poate solicita sistemul său de operare să-l anunțe după un anumit interval de timp
care-i situația, astfel încât să poată retrimite mesajul dacă nu primește confirmarea primirii
acestuia mai devreme. După setarea unui astfel de cronometru, programul poate continua să
facă alte lucrări.
La expirarea intervalului setat, sistemul de operare trimite un semnal alarmă la proces. Acest
semnal forțează procesul să întrerupă lucrarea în desfășurare, să salveze starea regiștrilor săi
și să înceapă o procedură specială pentru procesarea alarmei, pentru a transmite repetat, de
exemplu, mesajul presupus pierdut. Când manipulatorul de semnal își va termina activitatea,
procesul întrerupt își va relua execuția din aceeași stare în care a fost înainte de sosirea
semnalului. Semnalele sunt analogi software cu întreruperile hardware. Ele pot fi generate în
diverse situații și nu numai după ora stabilită în cronometru. Multe întreruperi hardware, cum ar
fi încercarea de a executa o instrucțiune interzisă sau de a accesa o adresă interzisă, sunt
transmise procesului care a generat eroarea.
Fiecare utilizator căruia i se permite să lucreze cu sistemul i se atribuie un ID de identificare a
utilizatorului (User Identification (UID)) de către administratorul de sistem. Fiecare proces are
UID-ul utilizatorului care l-a lansat. Procesele descendent au același UID ca și procesul părinte.
Utilizatorii pot face parte dintr-un grup, fiecare grup având propriul identificator (Group
Identification (GID)).
Un utilizator cu o valoare UID specială, numită superuser pe UNIX și administrator pe Windows,
are privilegii speciale care pot să încalce multe reguli de securitate. În sistemele de calcul mari,
numai administratorul de sistem cunoaște parola necesară pentru obținerea drepturilor de
utilizator, dar mulți utilizatori obișnuiți (în special studenții) depun eforturi considerabile pentru a
găsi vulnerabilitățile sistemului care le-ar permite să devină superuser fără parolă.

1.5.2 Spații de adrese


Fiecare computer are o anumită cantitate de memorie operativă folosită de programele
executabile. În cele mai simple sisteme de operare, există un singur program în memorie.
Pentru a începe un alt program, mai întâi trebuie eliminat programul curent după care să fie
încărcat celălalt program în locul primului.
Sistemele de operare mai sofisticate permit încărcarea mai multor programe în memoria
centrală. Pentru a elimina interferențele reciproce (și interferența cu funcționarea sistemului de
operare), este necesar un mecanism de protecție. Chiar dacă acest mecanism face parte din
echipament, el este controlat de sistemul de operare. Acest punct de vedere este legat de
problemele de gestionare și protecție a memoriei RAM. O altă problemă de memorie, dar nu
mai puțin importantă, este gestionarea spațiului de adrese al procesului. Fiecărui proces îi este
atribuit pentru utilizare un set continuu de celule de memorie cu adrese, de obicei de la zero la
maxim. În cel mai simplu caz, volumul maxim de locațiuni alocate unui proces este mai mic
decât capacitatea memoriei operative. Astfel, procesul își poate umple spațiul de adrese și
există suficient spațiu pentru plasarea sa în memoria RAM.
Cu toate acestea, multe computere folosesc adrese de 32 sau 64 biți, ceea ce permite să avem
un spațiu de adrese de 232, respectiv 264 octeți. Ce se întâmplă dacă spațiul de adrese al
procesului depășește capacitatea RAM a calculatorului, iar procesul trebuie să utilizeze întregul
său spațiu? În primele calculatoare acest lucru era imposibil. Astăzi există tehnologia, numită
memorie virtuală, în care sistemul de operare stochează unele fragmente ale spațiului de
adrese în RAM și altele pe disc, schimbându-și fragmentele în funcție de necesități. În esență,
sistemul de operare creează o abstractizare a spațiului de adrese ca un set de adrese la care
se poate referi procesul. Spațiul de adrese este separat de memoria fizică a mașinii și poate fi
mai mare decât încape în memoria fizică. Gestionarea spațiilor de adrese și a memoriei fizice
este o parte importantă a sistemului de operare.

1.5.3 Fișierele
Un alt concept cheie susținut de aproape toate sistemele de operare este sistemul de fișiere.
După cum a fost menționat anterior, funcția principală a sistemului de operare este de a
ascunde specificul discurilor și al altor dispozitive de intrare / ieșire și de a oferi programatorului
un model abstract convenabil și simplu de înțeles, format din fișiere independente de dispozitiv.
Evident, apelurile de sistem sunt necesare pentru a crea, șterge, citi și scrie fișiere. Înainte ca
fișierul să fie gata de citire, acesta trebuie să fie găsit pe disc și deschis, iar după citire, trebuie
să fie închis. Pentru aceste operațiuni, sunt furnizate apeluri de sistem.
Pentru a oferi un loc pentru stocarea fișierelor, multe sisteme de operare pentru calculatoarele
personale folosesc directorul ca o modalitate de a combina fișierele în grupuri. De exemplu, un
student poate avea un director pentru fiecare curs studiat (pentru programele necesare pentru
acest curs), un alt director pentru e-mail și un al treilea pentru pagina sa de pornire. Pentru
crearea și ștergerea directoarelor sunt necesare apeluri de sistem. Sunt necesare operații de
introducere sau de ștergere a unui fișier existent în/din director. În calitate de elementele ale
unui director pot fi fișierele sau alte directoare. Acest model a devenit prototipul structurii
ierarhice a sistemului de fișiere, una dintre variante fiind prezentată în fig. 1-14.
Fig. 1-14. Sistemul de fișiere pentru un departament universitar
Ambele ierarhii de procese și fișiere sunt organizate în arbori, dar asemănarea se oprește aici.
Ierarhiile de proces nu sunt de obicei foarte profunde (mai mult de trei niveluri întâlnindu-se
rar), în timp ce ierarhiile de fișiere sunt pe patru, cinci sau chiar mai multe niveluri. Ierarhiile de
proces sunt de obicei de scurtă durată, minute cel mult, în timp ce ierarhia de directoare poate
exista ani de zile. Proprietatea și protecția diferă, de asemenea, pentru procese și fișiere. De
obicei, doar un proces părinte poate controla sau chiar accesa un proces copil, dar aproape
întotdeauna există mecanisme care permit ca fișierele și directoarele să fie citite de un grup mai
larg decât doar de proprietar.
Fiecare fișier din ierarhia de directoare poate fi specificat folosind numele de cale (path name)
- calea care începe de la directorul rădăcină până la numele fișierului. Astfel de nume absolute
de cale sunt formate din lista de directoare care trebuie parcurse pornind de la directorul
rădăcină pentru a ajunge la fișier, cu bare oblice care separă componentele. În fig. 1-14, calea
pentru fișierul CS101 este /Faculty/Prof.Brown/Courses/CS101. Prima bară oblică indică faptul
că calea este absolută, adică începe de la directorul rădăcină. În Windows, caracterul “\” (bară
oblică inversă (backslash) este utilizat ca separator în loc de caracterul „/” (din motive istorice),
astfel că calea dată mai sus ar fi scrisă în Windows ca \Faculty\Prof.Brown\Cursuri\CS101. În
toată această carte vom folosi în general convenția UNIX pentru căi.
La fiecare moment de timp orice proces are un director de lucru (director curent), în care sunt
folosite c[i care nu încep cu o bară. De exemplu, în fig. 1-14, dacă /Faculty/Prof.Brown ar fi
directorul de lucru, utilizarea căii Cursuri/CS101 ar conduce la același fișier ca și calea absolută
de mai sus. Procesele își pot schimba directorul de lucru prin emiterea unui apel de sistem care
specifică noul director de lucru. Pentru a putea fi citit sau scris un fișier trebuie mai întâi
deschis, moment în care sunt verificate permisiunile. Dacă accesul este permis, sistemul
returnează un număr întreg numit descriptor de fișier pentru a fi utilizat în operațiunile
ulterioare. Dacă accesul este interzis, este returnat un cod de eroare.
Un alt concept important în UNIX este sistemul de fișiere montat. Majoritatea computerelor
desktop au una sau mai multe unități optice în care pot fi inserate CD-ROM-uri, DVD-uri sau
discuri Blu-ray. Acestea au aproape întotdeauna porturi USB, în care pot fi conectate stick-urile
de memorie USB (adevărate unități de disc solid), iar unele computere au dischete sau hard
diskuri externe. Pentru a oferi un mod elegant de a face față acestor suporturi amovibile UNIX
permite ca sistemul de fișiere de pe discul optic să fie atașat la arborele principal. Fie situația
din fig. 1-15 (a). Înainte de apelul de montare, sistemul de fișiere rădăcină, de pe hard disk și
un al doilea sistem de fișiere, pe un CD-ROM, sunt separate și nu au nici o legătură.
Sistemul de fișiere de pe CD-ROM nu poate fi utilizat, deoarece nu există nicio modalitate de a
specifica numele căilor pentru fișierele de pe acesta. UNIX nu permite ca numele de cale să fie
prefixate cu un nume sau număr de unitate; aceasta ar fi exact genul de dependență de
dispozitiv pe care sistemele de operare ar trebui să-l ocololească. Apelul de sistem montare
permite atașarea sistemului de fișiere de pe CD-ROM la sistemul de fișiere rădăcină.

Fig. 1-15. (a) Înainte de montare fișierele de pe CD-ROM nu sunt accesibile. (b) După montare
ele devin parte a ierarhiei de fișiere
În Fig. 1-15 (b) sistemul de fișiere de pe CD-ROM a fost montat pe directorul b, permițând astfel
accesul la fișiere /b/x și /b/y. Dacă directorul b ar fi conținut fișiere, acestea nu ar fi accesibile în
timp ce CD-ROM-ul a fost montat, deoarece /b s-ar referi la directorul rădăcină al CD-ROM-ului.
(A nu fi capabil să accesezi aceste fișiere nu este la fel de grav pe cât pare la început:
sistemele de fișiere sunt aproape întotdeauna montate pe directoare goale.) Dacă un sistem
conține mai multe discuri dure, acestea pot fi montate și într-un singur arbore.
Un alt concept important în UNIX este conceptul de fișier special. Fișierele speciale sunt
furnizate pentru ca dispozitivele de intrare/ieșire să pară fișiere. În acest fel, ele pot fi citite și
scrise folosind aceleași apeluri de sistem ca și cele utilizate pentru citirea și scrierea fișierelor.
Există două tipuri de fișiere speciale: fișiere speciale orientate pe blocuri și fișiere speciale
orientate pe caractere. Fișierele speciale bloc orientate sunt utilizate pentru modelarea
dispozitivelor care constau dintr-o colecție de blocuri adresabile aleatoriu, cum ar fi discurile. La
deschiderea unui fișier special bloc orientat care citește, să zicem, blocul 4, un program poate
accesa direct al patrulea bloc de pe dispozitiv, fără a ține cont de structura sistemului de fișiere.
În mod similar, fișierele speciale orientate pe caractere sunt utilizate pentru modelarea
imprimantelor, modemurilor și a altor dispozitive care acceptă sau produc un flux de caractere.
Prin convenție, fișierele speciale sunt păstrate în directorul /dev. De exemplu, /dev/lp ar putea fi
imprimanta (odată numită imprimantă linie). Ultima noțiune despre care vom discuta aici se
referă atât la procese, cât și la fișiere este noțiunea de conductă (pipe). O conductă este un fel
de pseudofișier care este utilizat pentru a conecta două procese (fig. 1-16).
Fig. 1-16. Două procese conectate prin intermediul unei conducte (pipe)
Dacă procesele A și B doresc să discute folosind o conductă, acestea trebuie să o configureze
din timp. Când procesul A dorește să trimită date pentru procesul B, acesta scrie pe conductă
ca și cum ar fi un fișier de ieșire. De fapt, implementarea unei conducte seamănă foarte mult cu
cea a unui fișier. Procesul B poate citi datele citind din conductă ca și cum ar fi un fișier de
intrare. Astfel, comunicarea între procesele din UNIX seamănă foarte mult ca un fișier obișnuit
în care un proces scrie, iar altul citește. Mai mult chiar: singurul mod prin care un proces poate
descoperi că fișierul de ieșire în care scrie nu este un fișier adevărat, ci o conductă, este
folosirea unui apel sistem special. Sistemele de fișiere sunt foarte importante. Vom avea mult
mai multe de spus despre ele în cap. 4 și, de asemenea, în capitolele 10 și 11.

1.5.4 Intrări/Ieșiri
Toate calculatoarele au dispozitive fizice de intrare și de ieșire. Există multe tipuri de dispozitive
de intrare și ieșire, inclusiv tastaturi, monitoare, imprimante etc. Este în sarcina sistemului de
operare să gestioneze aceste dispozitive. În acest scop orice sistem de operare are un
subsistem de intrare/ieșire (I/E) pentru gestionarea dispozitivelor sale. O parte din software-ul
I/E este independent de dispozitiv, adică este pentru mai multe sau chiar pentru toate
dispozitivele de intrare/ieșire. Alte părți ale acestui soft, cum ar fi driverele de dispozitiv, sunt
specifice anumitor dispozitive de I / O. În cap. 5 vom analiza software-ul I/E.

1.5.5 Protecția și securitatea


Calculatoarele conțin cantități mari de informații pe care utilizatorii doresc adesea să le
protejeze și să le păstreze confidențial. Aceste informații pot include mesaje de e-mail, planuri
de afaceri, declarații fiscale și multe altele. Sistemul de operare trebuie să asigure securitatea
sistemului, astfel încât fișierele, de exemplu, să fie accesibile numai utilizatorilor autorizați.
Un exemplu simplu din UNIX: în UNIX fișierele sunt protejate atribuind fiecăruia un cod de
protecție binară pe 9 biți. Codul de protecție este format din trei câmpuri pe 3 biți, unul pentru
proprietar, unul pentru ceilalți membri ai grupului proprietarului (utilizatorii sunt împărțiți în
grupuri de către administratorul de sistem) și unul pentru toți ceilalți. Fiecare câmp are un bit
pentru acces la citire, un bit pentru acces la scriere și un bit pentru acces la executare. Acești 3
biți sunt cunoscuți sub numele de biți rwx. De exemplu, codul de protecție rwxr-x - x înseamnă
că proprietarul poate citi, scrie sau executa fișierul, alți membri ai grupului pot citi sau executa
fișierul (dar nu pot scrie) și toți ceilalți pot doar executa (dar nu pot citi sau scrie) fișierul. Pentru
un director, x indică permisiunea de căutare. O liniuță înseamnă că permisiunea respectivă nu
există.
Pe lângă protecția fișierelor, există multe alte probleme de securitate. Protejarea sistemului
împotriva intrusilor nedoriți, atât umani, cât și non-umani (de exemplu, viruși) este una dintre
ele. Vom analiza diverse probleme de securitate din cap. 9.

1.5.6 Interpretorul de comenzi (shell)


Sistemul de operare este codul care efectuează apelurile de sistem. Editoarele, compilatoarele,
asambloarele, utilitarele sau interpretorul limbajului de comandă cu siguranță nu fac parte din
sistemul de operare, chiar dacă sunt importante și utile. Cu riscul de a confunda oarecum
lucrurile, în această secțiune vom analiza pe scurt interpretorul limbajului de comandă UNIX -
shell-ul. Deși nu face parte din sistemul de operare, acesta folosește în mod intensiv multe
funcții ale sistemului de operare și, astfel, servește ca un bun exemplu al modului în care sunt
utilizate apelurile de sistem. De asemenea, este interfața principală între un utilizator care stă la
terminalul său și sistemul de operare, cu excepția cazului în care utilizatorul utilizează o
interfață grafică cu utilizatorul. Există multe shell-uri, inclusiv sh, csh, ksh și bash. Toate
acceptă funcționalitatea descrisă mai jos, care derivă din shell-ul original (sh).
Când un utilizator se conectează, un shell este pornit. Acesta are terminalul ca intrare și ieșire
standard. Începe afișând invitația la lucru (promptul), un caracter cum ar fi un semn dolar, care
îi spune utilizatorului că shell-ul așteaptă introducerea unei comenzi. Dacă utilizatorul introduce
date
de exemplu, shell-ul creează un proces copil pentru a rula programul date. La terminare, shell-
ul afișează din nou promptul și așteaptă următoarea comandă.
Utilizatorul poate specifica faptul că ieșirea standard va fi redirecționată către un fișier, de
exemplu,
date > file
În mod similar, intrarea standard poate fi redirecționată, ca în
sort <file1>file2
care va lansa programul de sortare sort cu intrarea preluată din file1 și ieșire trimisă la file2.
Ieșirea unui program poate fi utilizată ca intrare pentru un alt program, conectându-le cu o
conductă. Prin urmare prin introducerea liniei
cat file1 file2 file3 | sort >/dev/lp
este lansat programul cat care va concatena trei fișiere și va trimite ieșirea programului sort
care va aranja toate liniile în ordine alfabetică. Rezultatul sortării este redirecționat către fișierul
special /dev/lp, care este de obicei imprimanta.
Dacă utilizatorul adaugă simbolul ampersand (&) după o comandă, shell-ul nu așteaptă să fie
terminată sortarea și afișează imediat promptul. Prin urmare,
cat file1 file2 file3 | sort >/dev/lp &
pornește sort ca o lucrare de fundal, permițând utilizatorului să continue să lucreze normal în
timp ce sortarea continuă. Shell-ul are o serie de alte caracteristici interesante, pe care nu
avem spațiu pentru a le discuta aici.
Majoritatea calculatoarelor personale în aceste zile folosesc interfața grafică (GUI). De fapt,
GUI este doar un program care rulează deasupra sistemului de operare, precum un shell. În
sistemele Linux, acest fapt este evident, deoarece utilizatorul are de ales din (cel puțin) două
interfețe grafice: Gnome și KDE sau deloc (folosind o fereastră de terminal pe X11). În
Windows, este de asemenea posibil să înlocuiți desktop-ul GUI standard (Windows Explorer)
cu un program diferit, schimbând unele valori din registru, deși puțini oameni fac acest lucru.

1.6 APELURI DE SISTEM


Am văzut că sistemele de operare au două funcții principale: furnizarea de abstractizări pentru
programatori și programele utilizatorilor și gestionarea resurselor computerului. În cea mai mare
parte, interacțiunea dintre programele utilizatorului și sistemul de operare se referă la prima
funcție, de exemplu, crearea, scrierea, citirea și ștergerea fișierelor. Partea de gestionare a
resurselor este în mare măsură transparentă pentru utilizatori și realizată automat. Astfel,
interfața dintre programele utilizatorului și sistemul de operare se referă în primul rând la
abordarea abstractizărilor. Pentru a înțelege cu adevărat ce fac sistemele de operare, trebuie
să examinăm îndeaproape această funcție de intermediar. Apelurile de sistem utilizate în acest
caz variază de la un sistem de operare la altul (deși conceptele de bază tind să fie similare).
Suntem astfel obligați să facem o alegere între (1) generalități vagi ('' sistemele de operare au
apeluri de sistem pentru citirea fișierelor '') și (2) unele sisteme specifice (“UNIX are un apel de
sistem read cu trei parametri: unul pentru specificarea fișierului, al doilea spune unde vor fi
puse datele și al treilea stabilește câți octeți vor fi citiți'').
Noi am ales ultima abordare. Vom avea mai mult de lucru mergând pe acesată cale, dar putem
afla mai multe informații despre ceea ce fac cu adevărat sistemele de operare. Deși această
discuție se referă în mod special la POSIX (Standardul Internațional 9945-1), deci și la UNIX,
System V, BSD, Linux, MINIX 3 și așa mai departe, majoritatea celorlalte sisteme de operare
moderne au apeluri de sistem care îndeplinesc aceleași funcții, chiar dacă detaliile diferă.
Deoarece mecanica efectivă a emiterii unui apel de sistem depinde în mare măsură de mașină
și deseori trebuie exprimată în cod de asamblare, o bibliotecă de proceduri este oferită pentru a
face posibilă efectuarea de apeluri de sistem din programe C și deseori și din alte limbi.
Este util să aveți în vedere următoarele. Orice computer cu un singur procesor poate executa o
singură instrucțiune la un moment de timp dat. Dacă un proces rulează un program de utilizator
în modul utilizator și are nevoie de un serviciu de sistem, cum ar fi citirea datelor dintr-un fișier,
acesta trebuie să execute o instrucțiune specială (TRAP) pentru a transfera controlul sistemului
de operare. Sistemul de operare află despre ce dorește procesul apelant prin inspectarea
parametrilor. Apoi execută apelul de sistem și returnează controlul procesului la instrucțiunea
imediat următoare apelului de sistem. Într-un anumit sens, a face un apel de sistem este ca și
cum ai face un tip special de apel de procedură, doar că apelurile de sistem intră în kernel, pe
când apelurile de procedură nu pot face acest lucru.
Pentru a clarifica mecanismul apelurilor de sistem, să aruncăm o privire rapidă la apelul de
sistem read. Așa cum am menționat mai sus, acesta are trei parametri: primul care specifică
fișierul din care vor fi citiți niște octeți, cel de-al doilea stabilește buferul folosit pentru citire, iar
al treilea indică câți octeți vor fi citiți. Ca aproape toate apelurile de sistem, reade ste invocat din
programele C, apelând la o procedură de bibliotecă cu același nume ca apelul de sistem: read.
Un apel de sistem într-un program C poate arăta astfel:
count = read (fd, buffer, nbytes);
Apelul de sistem (și procedura de bibliotecă) returnează numărul de octeți citiți efectiv în count.
Această valoare este în mod normal aceeași cu nbytes, dar poate fi mai mică, dacă, de
exemplu, în timpul lecturii este întâlnit sfârșitul fișierului.
Dacă apelul de sistem nu poate fi efectuat din cauza unui parametru nevalid sau a unei erori de
disc, count este setat în -1 și numărul erorii este introdus într-o variabilă globală, errno.
Programele ar trebui să verifice întotdeauna rezultatele unui apel de sistem pentru a vedea
dacă a apărut o eroare.
Apelurile de sistem sunt efectuate într-o serie de pași. Pentru a clarifica acest concept, să
examinăm apelul read discutat mai sus. Pregătindu-se pentru apelarea procedurii de bibliotecă
read, care efectiv va efectua apelul de sistem read, programul apelant introduce mai întâi
parametrii în stiva de execuție, așa cum se poate observa în etapele 1-3 din fig. 1-17.
Fig. 1.17. Cei 11 pași în realizarea apelului de sistem read(fd, buffer, nbytes).
Compilatoarele C și C ++ introduc parametrii în stivă în ordine inversă din motive istorice.
Primul și al treilea parametru sunt desemnați prin valoare, iar al doilea este desemnat prin
referință, ceea ce înseamnă că adresa buferului (indicată de &) este folosită, nu conținutul
acestuia. Apoi are loc apelul efectiv la procedura bibliotecii (pasul 4). Această este o
instrucțiune obișnuită de apel de procedură, folosită pentru apelarea tuturor procedurilor.
Procedura de bibliotecă, posibil scrisă în limbajul de asamblare, plasează de obicei numărul
apelului de sistem într-un loc în care sistemul de operare îl așteaptă, cum ar fi un registru
(pasul 5). Apoi, execută o instrucțiune TRAP pentru a trece de la modul utilizator la modul
kernel și începe executarea la o adresă fixă din kernel (pasul 6). Instrucțiunea TRAP este de
fapt destul de similară cu instrucțiunea de apelare a procedurii, doar că instrucțiunea care
urmează este preluată dintr-o locație la care are acces doar sistemul de operare, iar adresa de
retur este salvată pe stivă pentru a fi utilizată ulterior.
Instrucțiunea TRAP diferă, de asemenea, de instrucțiunea de apel procedură în două moduri
fundamentale. În primul rând, asigură trecerea în modul kernel. O instrucțiune obișnuită de apel
de procedură nu face acest lucru. În al doilea rând, în loc să dea o adresă relativă sau absolută
unde se află procedura, instrucțiunea TRAP nu poate trece la o adresă arbitrară. În funcție de
arhitectură, fie sare la o singură locație fixă, fie că există un câmp de 8 biți în instrucțiune -
indexul în tabelul din memorie care conține adrese de salt sau echivalente.
Codul kernel-ului care începe în urma TRAP-ului examinează numărul apelului de sistem și
apoi este trimis către handler-ul apelurilor de sistem, de obicei via un tabel de indicatoare către
manipulatoarele de apel sistem indexate pe numărul de apel de sistem (pasul 7). La pasul 8
este executat codul funcției solicitate a sistemului de operare. După ce-și încheie execuția,
controlul poate fi returnat procedurii de bibliotecă în spațiul utilizatorului la instrucțiunea
următoare instrucțiunii TRAP (pasul 9). Această procedură returnează apoi controlul
programului de utilizator în modul obișnuit (pasul 10).
Pentru a termina lucrarea, programul utilizator trebuie să curețe stiva, așa cum se întâmplă
după orice apel de procedură (pasul 11). Presupunând că stiva crește în jos, așa cum se
întâmplă adesea, codul compilat crește indicatorul stivei suficient de exact pentru a elimina
parametrii introduși în stivă înainte de apelul read. Programul va rula în continuare conmform
instrudțiunilor sale.
În pasul 9 de mai sus, am spus că „controlul poate fi returnat procedurii de bibliotecă în spațiul
utilizatorului”, pentru un motiv întemeiat. Apelul de sistem poate bloca apelantul, împiedicându-l
să continue. De exemplu, dacă încearcă să citească de pe tastatură și nu s-a tastat nimic,
apelantul trebuie blocat. În acest caz, sistemul de operare va stabili dacă nu cumva poate fi
rulat un alt proces. Mai târziu, când este disponibilă intrarea dorită, acest proces va atrage
atenția sistemului și va parcurge pașii 9–11.
În secțiunile următoare, vom examina unele dintre cele mai utilizate apeluri de sistem POSIX,
sau mai precis, procedurile de bibliotecă care efectuează aceste apeluri de sistem. POSIX are
aproximativ 100 de apeluri de procedură. Unele dintre cele mai importante sunt enumerate în
Fig. 1-18, grupate pentru comoditate în patru categorii.
În mare măsură, serviciile oferite de aceste apeluri determină cea mai mare parte a activității
sistemului de operare, deoarece gestionarea resurselor pe computerele personale este minimă
(cel puțin în comparație cu mașinile mari cu mai mulți utilizatori). Serviciile includ lucruri precum
crearea și încheierea proceselor, crearea, ștergerea, citirea și scrierea fișierelor, gestionarea
directoarelor și efectuarea intrării și ieșirilor.

1.6.1 Apeluri de sistem pentru administrarea proceselor


Primul grup de apeluri din Fig. 1-18 se ocupă de gestionarea proceselor.Este corect să
începem cu fork, care este singura modalitate de a crea un nou proces în POSIX. Această
funcție un duplicat exact al procesului părinte, inclusiv descriptorii de fișiere, registrele sunt la
fel. După execuția lui fork, procesul inițial și copia (părintele și copilul) merg pe căi separate.
Toate variabilele au valori identice în momentul fork, dar din moment ce datele părintelui sunt
copiate pentru a crea copilul, modificările ulterioare ale uneia dintre ele nu o afectează pe
cealaltă. (Textul programului, care nu poate fi modificat, este împărțit între părinte și copil.)
Apelul fork returnează o valoare, care este zero pentru procesul copil și egală cu PID (ID-ul de
proces) copilului la părinte. Folosind PID-ul returnat, cele două procese pot vedea care este
procesul părinte și care este procesul copil.
În cele mai multe cazuri, după un fork, copilul va trebui să execute un cod diferit față de părinte.
Fie ca exemplu cazul shell-ului. Este citită o comandă de la terminal, prin fork este creat un
proces copil, așteaptă ca să fie executată comanda (proceul copil), la încheierea procesului
copil este citită următoarea comandă. Pentru a aștepta finalizarea proceului copil, părintele
execută un apel de sistem waitpid, care îl face să aștepte terminarea procesului copil (orice
copil dacă există mai mult de unul). Apelul lui waitpid poate conduce la așteptarea unui anumit
proces copil sau oricărui proces copil, setând primul parametru la −1. Când waitpid se încheie,
adresa indicată de al doilea parametru, statloc, va fi setată la starea de ieșire a procesului copil
(valoarea normală sau anormală de ieșire). De asemenea, sunt oferite diferite opțiuni,
specificate de al treilea parametru. De exemplu, întoarcerea imediată dacă niciun copil nu a
ieșit deja.
Fig. 1-18. Câteva dintre cele mai importante apeluri de sistem POSIX
Să vedem cum fork este folosit de shell. Când o comandă este tastată, shell-ul crează un nou
proces. Acest proces copil trebuie să execute comanda utilizatorului. Face acest lucru folosind
apelul de sistem execve, ceea ce face ca întreaga sa imagine de bază să fie înlocuită cu fișierul
numit în primul său parametru. (De fapt, apelul de sistem propriu-zis este exec, dar mai multe
proceduri de bibliotecă îl apelează cu parametri diferiți și nume ușor diferite. Vom trata acesta
ca apeluri de sistem aici). Un shell extrem de simplificat, care ilustrează utilizarea lui fork,
waitpid, și a execve este arătată în fig. 1-19.
În cel mai general caz, execve are trei parametri: numele fișierului care urmează să fie lansat în
execuție, un pointer către tabloul de argumente și un pointer către tabloul de mediu. Există
diverse rutine de bibliotecă, inclusiv execl, execv, execle și execve, pentru a permite omiterea
sau specificarea parametrilor în diferite moduri. Noi vom folosi exec pentru a reprezenta apelul
de sistem invocat de toate acestea.
Să luăm în considerare cazul unei comenzi cum ar fi
cp file1 file2
utilizat pentru a copia file1 în file2. După ce shell-ul a fost solicitat, procesul copil localizează și
execută fișierul cp și îi transmite numele fișierelor sursă și țintă.
#define TRUE 1
while (TRUE) { /*ciclu infinit*/
type_promt(); /*afișarea promt-ului*/
read_command(command, parameters); /*citește de la terminal*/
if(fork()!=0) { /*a fost creat un process copil*/
/*Codul procesului părinte */
Waitpid(-1, &status, 0); /*a;teaptă terminarea pr.copil*/
} else {
/*Codul procesului copil */
Execve(command, parameters, 0); /*comanda de executare*/
}
}
Fig. 1-19. Cum funcționează un Shell
Programul principal al cp (și programul principal al majorității celorlalte programe C) conține
declarația
main(argc, argv, envp)
unde argc este numărul de elemente din linia de comandă, inclusiv numele programului. Pentru
exemplul de mai sus, argc este 3.
Al doilea parametru, argv, este un pointer către un tablou. Elementul i al acestui tablou este un
pointer către șirul cu numărul i din linia de comandă. În exemplul nostru, argv[0] punctează pe
șirul „cp”, argv[1] pe șirul „file1”, iar argv[2] pe „file2”.
Al treilea parametru al lui main, envp, este un pointer către mediul de execuție, un tablou de
șiruri care conține atribuiri de forma name = value folosite pentru a transmite programelor
informații precum tipul terminalului și numele directorului principal. Există proceduri de
bibliotecă la care programele pot apela pentru a obține variabile de mediu, care sunt adesea
utilizate pentru a personaliza modul în care un utilizator dorește să îndeplinească anumite
sarcini (de exemplu, imprimanta implicită pe care să o utilizeze). În fig. 1-19, niciun mediu nu
este trecut copilului, deci al treilea parametru al execve este 0.
Dacă exec vi se pare complicat, nu disperați; este (semantic) cel mai complicat dintre toate
apelurile de sistem POSIX. Toate celelalte sunt mult mai simple. De exemplu, apelul exit,
utilizat de procese atunci când își termină execuția. Are un singur parametru, starea de ieșire
(de la 0 la 255), care este returnat părintelui prin statloc în apelul sistem waitpid.
Spațiul de adrese al proceselor UNIX este împărțit în trei segmente: segmentul text (codul
programului), segmentul de date (variabilele) și segmentul de stivă. Segmentul de date crește
în sus și stiva crește în jos, așa cum vedem în fig. 1-20.
Între ele este spațiul de adrese neutilizat (gap). Stiva crește automat în gap, conform
necesităților, iar extinderea segmentului de date se face explicit folosind un apel de sistem, brk,
care specifică noua adresă unde urmează să se termine segmentul de date. Acest apel nu este
definit de standardul POSIX, deoarece programatorii sunt încurajați să utilizeze procedura de
bibliotecă malloc pentru alocarea dinamică a spațiului de stocare. Însă implementarea de bază
a malloc nu a fost creată a fi un subiect potrivit pentru standardizare, deoarece puțini
programatori o folosesc direct și este îndoielnic că oricine observă că brk nu se află în POSIX.
1.6.2 Apeluri de sistem pentru administrarea fisierelor
Multe apeluri de sistem se referă la gestiunea fișierelor. În această secțiune vom analiza
apelurile care operează pe fișiere individuale; în următoarea, vom examina cele care implică
directoare sau sistemul de fișiere în ansamblu.

Fig. 1-20. Spațiul de adrese al unui proces


Pentru citire sau scriere fișierul trebuie mai întâi deschis. Acest apel specifică numele fișierului
care trebuie deschis, fie folosind calea absolută sau în raport cu directorul curent, precum și
modul preconizat de lucru O_RDONLY, O_WRONLY sau O_RDWR, adică deschis pentru
citire, scriere sau ambele. Pentru a crea un fișier nou, este utilizat parametrul O_CREAT.
Descriptorul returnat al fișierului poate fi folosit pentru citire sau scriere. După, fișierul poate fi
închis prin close, iar descriptorul va fi disponibil pentru reutilizare la o deschidere ulterioară.
Cele mai utilizate apeluri sunt, fără îndoială, read și wrtite. Am discutat despre read mai
devreme, write are aceiași parametri.
Deși majoritatea programelor citesc și scriu fișierele în mod secvențial, pentru unele aplicații
programele trebuie să poată accesa oricare parte a unui fișier prin acces direct. Asociat cu
fiecare fișier este un indicator (un pointer) care indică poziția curentă din fișier. În cazul unui
acces secvențial, acesta indică următorul octet care trebuie citit/scris. Apelul lseek schimbă
valoarea indicatorlui de poziție, astfel încât apelurile ulterioare pentru citire sau scriere pot
începe oriunde în fișier.
Apelul lseek are trei parametri: primul este descriptorul fișierului, al doilea este o poziție în
fișierului, iar al treilea spune dacă poziția în fișier este relativă la începutul fișierului, la poziția
curentă sau la sfârșitul fișierului. Valoarea returnată de lseek este poziția absolută în fișier (în
octeți) după schimbarea indicatorului de poziție.
Pentru fiecare fișier, UNIX tipul fișierului (fișier obișnuit, fișier special, director și așa mai
departe), dimensiunea, timpul ultimei modificări și alte informații. Programele pot cere să vadă
aceste informații prin apelul sistemului stat. Primul parametru specifică fișierul care trebuie
inspectat; cel de-al doilea este un indicator către o structură în care trebuie introduse
informațiile. Apelurile fstat fac același lucru pentru un fișier deschis.

1.6.3 Apeluri de sistem pentru administrarea cataloagelor


În această secțiune vom analiza unele apeluri de sistem care se referă mai mult la directoare
sau la sistemul de fișiere în ansamblu. Primele două apeluri, mkdir și rmdir, sunt pentru crearea
și ;tergerea directoarelor goale. Următorul apel este link. Scopul său este de a permite același
fișier să apară sub două sau mai multe nume, adesea în directoare diferite. O utilizare tipică
este de a permite mai multor membri ai aceleiași echipe de programare să partajeze un fișier
comun, pentru fiecare membru fișierul să apară în propriul său director, eventual sub nume
diferite. Partajarea unui fișier nu este aceeași cu acordarea unei copii private fiecărui membru
al echipei; a avea un fișier partajat înseamnă că modificările pe care orice membru al echipei le
face sunt vizibile instantaneu celorlalți membri - există un singur fișier. Când se lucrează cu
copii ale unui fișier, modificările ulterioare aduse unei copii nu le afectează pe celelalte.
Pentru a vedea cum funcționează link, luați în considerare situația din fig. 1-21 (a). Există doi
utilizatori, ast și jim, fiecare având propriul său director cu unele fișiere. Dacă ast execută un
program care conține apelul de sistem
link("/usr/jim/memo", "/usr/ast/note");
fișierul memo din directorul lui jim este acum introdus în directorul ast sub numele note. După
asta, /usr/jim/memo și /usr/ast/note se referă la același fișier. Apropo, unde sunt păstrate
directoarele de utilizator (în /usr, /user, /home sau în altă parte) este o decizie luată de
administratorul local.

Fig. 1-21. Două directoare până la apelul link (a) și după (b)
Înțelegerea modului de funcționare a apelului link va clarifica ceea ce acesta face. Fiecare fișier
din UNIX are un număr unic, numele său intern i, care îl identifică. Acest număr i este un index
într-un tabel de i-noduri, unul per fișier, care spune cine deține fișierul, care sunt blocurile sale
de disc și așa mai departe. Un director este pur și simplu un fișier care conține un set de
perechi (i, nume ASCII). În primele versiuni aleSO UNIX, fiecare intrare de director era 16 octeți
- 2 octeți pentru numărul i și 14 octeți pentru nume. Acum este necesară o structură mai
complicată pentru a susține nume de fișiere lungi, dar conceptual un director este încă un set
de perechi (i, ASCII). În fig. 1-21, poștă are numărul i egal cu 16 și așa mai departe. Ceea ce
face link este pur și simplu să creeze o intrare de director nou cu un nume (eventual nou),
folosind numărul i al unui fișier existent. În fig. 1-21 (b), două intrări au același număr i (70) și se
referă astfel la același fișier. Dacă oricare dintre acestea este eliminat ulterior, folosind apelul
de sistem unlink, celălalt rămâne. Dacă ambele sunt eliminate, UNIX vede că nu există întrări la
fișier (un câmp din nodul i ține evidența numărului de intrări de directoare îndreptate către
fișier), și fișierul este eliminat de pe disc.
Așa cum am menționat anterior, apelul de sistem mount permite îmbinarea a două sisteme de
fișiere într-unul. O situație obișnuită este să ai sistemul de fișiere rădăcină, care să conțină
versiunile binare (executabile) ale comenzilor comune și alte fișiere folosite în mod intens, pe
un hard disk (sub)partiție și fișiere utilizator pe o altă (sub)partiție. Mai departe, utilizatorul poate
apoi să insereze un disc USB cu fișiere care să fie citite.
Prin executarea apelului de sistem de mount, sistemul de fișiere USB poate fi atașat la sistemul
de fișiere rădăcină, așa cum este arătat în fig. 1-22. Un operator tipic mount în C este
mount("/dev/sdb0", "/mnt", 0);
unde primul parametru este numele unui fișier special de bloc pentru unitatea USB 0, cel de-al
doilea parametru este locul din arborele unde urmează să fie montat, iar al treilea parametru
spune dacă sistemul de fișiere trebuie montat pentru citire și scriere sau doar pentru citire.
Figura 1-22. (a) Sistem de fișiere înainte de montare. (b) Sistem de fișiere după montare.
După apelul mount, un fișier de pe unitatea 0 poate fi accesat doar utilizând calea sa din
directorul rădăcină sau din directorul curent, fără a ține cont de unitatea inițială. Un al doilea, al
treilea sau al patrulea dispozitv poate fi, de asemenea, montat oriunde în arbore. Apelul mount
face posibilă integrarea suportului amovibil într-o singură ierarhie de fișiere integrate, fără a fi
nevoie să vă faceți griji cu privire la dispozitivul pe care este activat un fișier. Deși acest
exemplu se referea la CD-ROM-uri, porțiuni de hard disk-uri (adesea numite partiții sau
dispozitive secundare) pot fi, de asemenea, montate în acest fel, precum și hard disk-uri
externe și stick-uri USB. Când un sistem de fișiere nu mai este necesar, acesta poate fi
demontat cu apelul de sistem umount.

1.6.4 Alte apeluri de sistem


Există o varietate de alte apeluri de sistem. Vom analiza doar patru dintre ele aici. Apelul chdir
modifică directorul de lucru curent. După apelul
chdir(„/usr/ast/test”);
o deschidere a fișierului xyz va deschide /usr/ast/test/xyz. Conceptul de director de lucru
(director curent) ne eliberează de nevoia de a tasta tot timpul nume (lungi) absolute de cale.
În UNIX, pentru fiecare fișier există o modalitate folosită pentru protecție. În acest scop sunt
folosiți biții de citire-scriere-execuție pentru proprietar, grup și alții. Apelul sistemului chmod face
posibilă schimbarea modului de accesare a unui fișier. De exemplu, pentru a permite tuturor, cu
excepția proprietarului, accesul în regim read-only la un fișier introducem comanda
chmod(„fișier”, 0644);
Apelul de sistem kill este modul în care utilizatorii și procesele utilizatorului trimit semnale. Dacă
un proces este pregătit pentru a intercepta un anumit semnal, atunci când acesta este generat,
o procedură de tratare (un handler) va fi lansată în execuție. Dacă procesul nu este pregătit
pentru a gestiona semnalul, atunci sosirea lui “ucide” procesul (de unde și numele apelului).
POSIX definește o serie de proceduri pentru a duce evidența timpului. De exemplu, time
întoarce ora curentă în secunde, cu valoare 0 corespunzătoare datei de 1 ianuarie 1970 la
miezul nopții (la începutul zile). Pe computerele care folosesc cuvinte pe 32 de biți, valoarea
maximă a timpului este de 232 - 1 secunde. Această valoare este un pic mai mare de 136 ani.

1.6.5 Interfața pentru programarea aplicațiilor Windows Win32 API


Până acum ne-am concentrat în principal pe UNIX. Acum este timpul să privim un pic spre
Windows. Windows și UNIX diferă în mod fundamental în modelele de programare respective.
Un program UNIX constă dintr-un cod care face ceva sau altul, făcând apeluri de sistem pentru
a efectua anumite servicii. În schimb, un program Windows este condus în mod normal de
evenimente. Programul principal așteaptă să se întâmple un eveniment, apoi apelează la o
procedură (handler-ul) care să-l gestioneze. Evenimentele tipice sunt apăsarea tastelor sau a
butoanelor de mouse, deplasarea mouse-ului sau introducerea unei unități USB, etc.
Handlerele de evenimente sunt apelate să proceseze evenimentul, să actualizeze ecranul și să
actualizeze starea internă a programului. În total, acest lucru duce la un stil de programare
oarecum diferit decât în cazul SO UNIX, dar, întrucât această carte se concentrează asupra
funcției și structurii sistemului de operare, aceste modele de programare diferite nu ne vor
preocupa prea mult.
Desigur, Windows are și apeluri de sistem. Cu UNIX, există aproape o relație unu la unu între
apelurile de sistem (de exemplu, read) și procedurile de bibliotecă (de exemplu, read) utilizate
pentru a executa apelurile de sistem. Cu alte cuvinte, pentru fiecare apel de sistem, există
aproximativ o procedură de bibliotecă care este chemată să îl execute, așa cum este indicat în
fig. 1-17. Mai mult, POSIX include doar aproximativ 100 de apeluri de procedură.
Cu Windows, situația este radical diferită. Pentru început, apelurile din bibliotecă și apelurile
reale ale sistemului sunt foarte decuplate. Microsoft a definit un set de proceduri numite API
Win32 (Application Programming Interface) pe care programatorii trebuie să le utilizeze pentru
a obține serviciile sistemului de operare. Această interfață este (parțial) acceptată pe toate
versiunile Windows începând cu Windows 95. Prin decuplarea interfeței API de la apelurile
reale ale sistemului, Microsoft își păstrează posibilitatea de a schimba apelurile reale ale
sistemului la timp (chiar și de la lansare la lansare) fără a invalida programele existente. Ceea
ce constituie de fapt Win32 este, în principiu, ușor ambiguu, deoarece versiunile recente ale
Windows au numeroase apeluri noi care nu au fost disponibile anterior. În această secțiune,
Win32 înseamnă interfața acceptată de toate versiunile de Windows. Win32 oferă
compatibilitate între versiunile Windows.
Numărul de apeluri API Win32 este extrem de mare, de ordinul miilor. Mai mult, în timp ce
multe dintre apelurile API Win21 invocă apeluri de sistem, un număr substanțial este efectuat în
întregime în spațiul utilizatorului. În consecință, cu Windows este imposibil de văzut ce este un
apel de sistem (adică, efectuat de kernel) și ce este pur și simplu un apel de bibliotecă în
spațiul utilizatorului. De fapt, ceea ce este un apel de sistem într-o versiune de Windows se
poate face în spațiul utilizatorului într-o versiune diferită, și invers. Cand discutăm apelurile
sistemului Windows din această carte, vom folosi procedurile Win32 (acolo unde este cazul),
deoarece Microsoft garantează că acestea vor fi stabile în timp. Dar merită să ne amintim că nu
toate sunt adevărate apeluri de sistem (adică Traps (capcane) către kernel).
API-ul Win32 are un număr mare de apeluri pentru gestionarea ferestrelor, figurilor geometrice,
textului, fonturilor, barelor de defilare, casetelor de dialog, meniurilor și a altor caracteristici ale
GUI. În măsura în care subsistemul grafic rulează în kernel (adevărat pe unele versiuni de
Windows, dar nu pe toate), acestea sunt apeluri de sistem; altfel sunt doar apeluri la bibliotecă.
Ar trebui să discutăm sau nu aceste apeluri în această carte? Deoarece nu au legătură cu
funcția unui sistem de operare, am decis să nu o facem, chiar dacă acestea pot fi efectuate de
kernel. Cititorii interesați de API-ul Win32 ar trebui să consulte una dintre numeroasele cărți pe
această temă (de exemplu, Hart, 1997; Rector și nou-venit, 1997; și Simon, 1997).
Dacă introducerea tuturor apelurilor API Win32 nu se pune în discuție, ne vom limita la acele
apeluri care corespund aproximativ funcționalității apelurilor UNIX enumerate în fig. 1-18.
Acestea sunt enumerate în fig. 1-23. Să trecem acum pe scurt pe lista din fig. 1-23.
CreateProcess creează un nou proces. Face lucrul combinat al fork și execve din UNIX. Are
mulți parametri care specifică proprietățile procesului nou creat. Windows nu are o ierarhie de
procese așa cum este în UNIX, astfel nu există niciun concept de proces părinte și proces copil.
După crearea unui proces, descendentul și creatorul sunt egali. WaitForSingleObject este
folosit pentru a aștepta un eveniment. Multe evenimente posibile pot fi așteptate. Dacă
parametrul specifică un proces, atunci apelantul așteaptă ieșirea procesului specificat, care se
realizează folosind ExitProcess.
Următoarele șase apeluri operează cu fișierele și sunt asemănătoare funcțional cu omologii lor
UNIX, deși diferă în parametri și detalii. Totuși, fișierele pot fi deschise, închise, citite și scrise
aproape ca în UNIX. Apelurile SetFilePointer și GetFileAttributesEx setează poziția fișierului
sau obțin atribute ale fișierului.
Windows are directoare care sunt create cu apelând CreateDirectory și distruse apelând
RemoveDirector din Win32 API. Există, de asemenea, o noțiune de director curent, setată cu
ajutorul lui SetCurrentDirectory. Ora curentă a zilei este obținută folosind GetLocalTime.
Interfața Win32 nu are legături către fișiere, sisteme de fișiere montate, securitate sau semnale,
astfel încât apelurile corespunzătoare celor UNIX nu există. Desigur, Win32 are un număr
foarte mare de alte apeluri pe care UNIX nu le are, în special pentru gestionarea GUI. Ultimile
versiuni Windows Vista au un sistem puternic de securitate, acceptă legături de fișiere și multe
alte funcții și apeluri de sistem.

Fig. 1-23. Apelurile Win32 API, care corespund apelurilor UNIX din fi. 1-18.
O ultimă observație despre Win32 merită a fi făcută. Win32 nu este o interfață teribil de
uniformă sau consistentă. Principalul vinovat a fost nevoia de a fi compatibil cu interfața
anterioară pe 16 biți folosită în Windows 3.x.

1.7 STRUCTURA SISTEMELOR DE OPERARE


După ce am văzut cum arată sistemele de operare la exterior (adică la nivel de interfață a
programatorului), este timpul să aruncăm o privire în interior. În secțiunile următoare, vom
examina șase structuri diferite care au fost încercate, pentru a ne face o idee despre spectrul
posibilităților. Acestea nu sunt în niciun caz exhaustive, dar oferă o idee a unor modele care au
fost încercate în practică. Cele șase modele despre care vom discuta aici sunt sistemele
monolitice, sistemele stratificate, microkernelele, sistemele client-server, mașinile virtuale și
exokernelul.

1.7.1 Sisteme monolitice


Este de departe cea mai comună organizare. În demersul monolitic întregul sistem de operare
rulează ca un singur program în modul kernel. Sistemul de operare este scris ca o colecție de
proceduri, legate între ele într-un singur program binar executabil mare. Când se folosește
această tehnică, fiecare procedură din sistem este liberă să apeleze oricare alta, dacă aceasta
din urmă furnizează unele calcule utile de care are nevoie prima. Posibilitatea de a apela orice
procedură este foarte eficientă, dar a avea mii de proceduri care se pot apela reciproc fără
restricții poate duce la un sistem complicat și greu de înțeles. De asemenea, o cădere în oricare
dintre aceste proceduri va conduce la căderea întregului sistem de operare.
Pentru a construi modulul obiect al sistemului de operare atunci când este utilizată această
abordare, mai întâi trebuie compilate toate procedurile individuale (sau fișierele care conțin
procedurile) care apoi vor fi legate într-un singur fișier executabil utilizând link-erul de sistem. În
termeni de ascundere a informațiilor, ascunderea nu există - fiecare procedură este vizibilă
pentru orice altă procedură (spre deosebire de o structură cu module sau pachete, în care o
mare parte din informație este ascunsă în interiorul modulelor și numai punctele de intrare
desemnate oficial pot fi apelate din afara modulului).
Chiar și în sistemele monolitice este posibil să existe o anumită structură. Serviciile (apeluri de
sistem) furnizate de sistemul de operare sunt solicitate prin plasarea parametrilor într-un loc
bine definit (de exemplu, pe stivă) cu executarea unei instrucțiuni de interceptare. Această
instrucțiune trece execuția de la modul utilizator la modul kernel și transferă controlul la
sistemul de operare, ca pasul 6 din fig. 1-17. Apoi, sistemul de operare preia parametrii și
determină ce apel de sistem trebuie efectuat și lansează procedura care efectuează apelul de
sistem k (pasul 7 din fig. 1-17).
Această organizație sugerează următoarea structură pentru sistemul de operare:
1. Un program principal care invocă procedura de service solicitată.
2. Un set de proceduri de service care efectuează apelurile de sistem.
3. Un set de proceduri utilitare care ajută procedurile de servicii.
În acest model, pentru fiecare apel de sistem există o singură procedură de serviciu care are
grijă de ea și o execută. Procedurile de utilitate fac lucrurile necesare mai multor proceduri de
servicii, cum ar fi preluarea datelor din programele utilizatorului. Această divizare a procedurilor
în trei straturi este prezentată în fig. 1-24.
În plus față de sistemul de operare de bază care este încărcat la pornirea computerului, multe
sisteme de operare acceptă extensii care pot fi încărcate, cum ar fi driverele de dispozitive I/E
și sistemele de fișiere. Aceste componente sunt încărcate la cerere. În UNIX se numesc
Fig. 1-24. Structura unui sistem de operare monolitic
biblioteci partajate. În Windows se numesc DLL (Dynamic-Link Libraries), au extensia de fișier
.dll și în directorul C:\Windows\system32 din Windows pot fi găsite peste 1000.

1.7.2 Sisteme structurate pe nivele


O generalizare a abordării din fig. 1-24 este de a organiza sistemul de operare ca o ierarhie de
straturi, fiecare construit pe cel de sub el. Primul sistem construit în acest fel a fost sistemul
THE construit la Technische Hogeschool Eindhoven din Olanda de E. W. Dijkstra (1968) și
elevii săi, care era un sistem simplu, pentru un computer olandez, Electrologica X8, cu 32K de
cuvinte pe 27 de biți (biții erau scumpi pe atunci). Sistemul avea șase nivele, așa cum este
aratat în fig. 1-25. Stratul 0 era responsabil de alocarea procesorului - comutarea între procese
atunci când aveau loc întreruperi sau expira cuanta de alocare. Peste nivelul 0 erau procesele
secvențiale fiecare în spațiul propriu de memorie, mai multe procese rulau pe un singur
procesor. Cu alte cuvinte, nevelul 0 forma baza pentru multiprogramarea procesorului.

Fig. 1-25. Structura sistemului de operare THE


Nivelul 1 era responsabil de managementul memoriei. Aici era alocat spațiu pentru procesele
din memoria principală și pentru un tambur de 512K cuvinte folosit pentru păstrarea unor părți
ale proceselor (pagini) pentru care nu exista suficient spațiu în memoria principală. Mai sus de
nivelul 1, procesele nu trebuiau să se îngrijoreze dacă sunt în memorie sau în această memorie
tambur; software-ul stratului 1 asigura aducerea paginilor în memoria principală la necesitate
sau eliminarea lor atunci când nu era necesare.
Nivelul 2 era nivelul care gestiona comunicărilor între procese și consola operatorului. Acest
nivel punea la dispoziția fiecărui proces propria sa consolă operator. Nivelul 3 se ocupa de
gestiunea dispozitivelor I/O și de buferizarea fluxurilor de date către și de la acestea. Datorită
nivelului 3, fiecare proces trata dispozitive de I/O abstracte cu proprietăți frumoase, în loc de
dispozitive reale cu multe particularități.
Nivelul 4 era al programelor de utilizator. Utilizatorii nu aveau grijă de gestionarea proceselor,
memoriei, consolei sau I/O. Procesul operatorului de sistem era localizat în nivelul 5.
O nouă generalizare a conceptului de structură ultinivel a fost prezentă în sistemul MULTICS.
În loc de nivele, MULTICS avea o serie de inele concentrice, cele interioare fiind mai
privilegiate decât cele exterioare (ceea ce este efectiv același lucru). Când o procedură dintr-un
inel exterior dorea să apeleze la o procedură într-un inel interior, ea trebuia să facă echivalentul
unui apel de sistem, adică o instrucțiune TRAP care verifica cu atenție validitatea parametrilor
transmiși înainte ca apelul să poată continua. Deși întregul sistem de operare făcea parte din
spațiul de adrese al fiecărui proces de utilizator MULTICS, hardware-ul desemna procedurile
individual (segmente de memorie, de fapt) ca protejate împotriva citirii, scrierii sau executării.

1.7.3 Modelul microkernel


Prin abordarea multinivel designerii pot stabili unde să fie plasată granița user-kernel. În mod
tradițional, toate straturile intrau în kernel, dar acest lucru nu este necesar. De fapt, ar fi fost
mult mai binevenit să se pună cât mai puține obligațiuni nivelului kernel, deoarece bug-urile din
kernel pot conduce la căderea întregului sistem. În schimb, procesele utilizatorilor pot fi
configurate astfel încât să aibă mai puțină putere pentru ca erorile de aici să nu fie fatale.
Diversi cercetători au studiat în mod repetat statisticile referitoare la numărul de buguri la 1000
de linii de cod (de exemplu, Basilli și Perricone, 1984; și Ostrand și Weyuker, 2002). Densitatea
erorilor depinde de dimensiunea modulului, vârsta modulului și multe altele, dar o valoare
estimativă pentru sistemele industriale mari este cuprinsă între două și zece buguri la o mie de
linii de cod. Aceasta înseamnă că un sistem de operare monolitic cu cinci milioane de linii de
cod poate conține între 10.000 și 50.000 de erori de kernel. Nu toate acestea sunt fatale,
desigur, deoarece unele bug-uri pot fi lucruri precum emiterea unui mesaj de eroare incorect
într-o situație care apare rar. Cu toate acestea, sistemele de operare conțin suficiente bug-uri
încât producătorii de computere să pună butoane de resetare, ceva ce producătorii de
televizoare sau automobile nu fac, în ciuda cantității mari de software din acestea.
Ideea de bază din spatele proiectării microkernelului este de a obține o fiabilitate ridicată prin
împărțirea sistemului de operare în module mici, bine definite, doar unul dintre ele -
microkernelul - rulează în modul kernel, iar restul rulează ca procese de utilizator obișnuite. În
special, rulând fiecare driver de dispozitiv sau sistem de fișiere ca proces de utilizator separat,
o eroare într-unul dintre acestea poate bloca acea componentă, dar nu poate duce la căderea
întregului sistem. Astfel, o eroare a driverului audio va face ca sunetul să fie de calitate joasă
sau să se oprească, dar nu va bloca întregul calculator.
Pe parcursul anilor au fost realizate și implementate mai multe sisteme cu micronucleu (Haertig
și colab., 1997; Heiser și colab., 2006; Herder și colab., 2006; Hildebrand, 1992; Kirsch și
colab., 2005; Liedtke, 1993, 1995, 1996; Pike și colab., 1992; și Zuberi și colab., 1999). Cu
excepția OS X, care se bazează pe microkernel-ul Mach (Accetta și colab., 1986), sistemele de
operare obișnuite pentru desktop nu folosesc micro-nuclee. Cu toate acestea, ele sunt
dominante în aplicații de timp real, industrial, avionic și militar, critice pentru misiune și au
cerințe de fiabilitate foarte ridicate. Câteva dintre cele mai cunoscute microkerneluri includ
Integrity, K42, L4, PikeOS, QNX, Symbian și MINIX 3. Vom oferi în continuare o scurtă privire
de ansamblu asupra MINIX 3, care a dus ideea modularității la limită, împărțind cea mai mare
parte a sistemului de operare în mai multe procese independente în modul utilizator. MINIX 3
este un sistem open source conform POSIX, disponibil gratuit pe www.minix3.org (Giuffrida și
colab., 2012; Giuffrida și colab., 2013; Herder și colab., 2006; Herder și colab., 2009; și Hruby
et al., 2013).
Micronucleul MINIX 3 reprezintă aproximativ 12.000 de linii de cod C și aproximativ 1400 de linii
cod de asamblare pentru funcții de nivel foarte scăzut, cum ar fi ointerceptarea întreruperilor și
procesele de comutare. Codul C gestionează și planifică procesele, gestionează comunicarea
intre procese (prin mesaje) și oferă un set de aproximativ 40 de apeluri kernel pentru a permite
restului sistemului de operare să își facă treaba. Aceste apeluri îndeplinesc funcții precum
legarea handlerului de o întrerupere, mutarea datelor între spațiile de adrese și instalarea
hărților de memorie pentru procesele noi. Structura procesului MINIX 3 este prezentată în fig. 1-
26 în care apelul de kernel este etichetat cu Sys. Driverul dispozitivului pentru ceas este, de
asemenea, în kernel, deoarece planificatorul interacționează strâns cu acesta. Celelalte drivere
de dispozitiv rulează ca procese de utilizator separate.
În afara nucleului, sistemul este structurat pe trei nivele de procese, toate rulând în modul
utilizator. Cel mai jos nivel conține driverele de dispozitiv. Întrucât rulează în modul utilizator, nu
au acces fizic la spațiul portului I/O și nu pot emite comenzi I/O direct. În schimb, pentru a
programa un dispozitiv de I/O, driverul construiește o structură care spune ce valori trebuie să
scrie la care porturi I/O și face un apel al kernelului care îi spune kernelului să execute scrierea.
Această abordare înseamnă că nucleul poate verifica pentru a vedea dacă driverul scrie (sau
citește) cu I/O pe care este autorizat să o folosească. În consecință (și spre deosebire de un
design monolitic), un driver audio buggy nu poate scrie accidental pe disc.
Deasupra driverelor este un alt nivel de mod utilizator care conține servere, care realizează cea
mai mare parte a activității sistemului de operare. Unul sau mai multe servere de fișiere
gestionează sistemul (sistemele) de fișiere, managerul de procese creează, distruge și
gestionează procesele etc. Programele utilizator obțin servicii ale sistemului de operare prin
trimiterea de mesaje scurte către servere. De exemplu, un proces care trebuie să facă o citire
trimite un mesaj unuia dintre serverele de fișiere spunându-i ce să citească.
Un server interesant este serverul de reîncarnare, a cărui sarcină este să verifice dacă celelalte
servere și drivere funcționează corect. În cazul în care se detectează unul defect, acesta se
înlocuiește automat fără nicio intervenție a utilizatorului. În acest fel, sistemul se autotratează,
operație care sporește semnificativ fiabilitatea.
Sistemul are multe restricții care limitează posibilitățile fiecărui proces. Așa cum am menționat,
driverele pot atinge doar porturi de I/O autorizate, accesul la apelurile kernelului este controlat
pe bază de proces. Procesele pot acorda, de asemenea, permisiunea limitată pentru alte
procese pentru ca nucleul să aibă acces la spațiile de adrese. Ca exemplu, un sistem de fișiere
poate acorda permisiunea pentru driverul de disc să permită nucleului să pună un bloc de disc
nou citit la o adresă specifică din spațiul de adrese al sistemului de fișiere. Suma totală a tuturor
acestor restricții este aceea că fiecare driver și server are exact puterea de a-și face munca și
nimic mai mult, limitând astfel foarte mult daunele pe care le poate face o componentă buggy.
Un nucleu poate fi minimizat dacă va fi responsabil doar de mecanisme, nu și de politici. De
exemplu, dacă utilizăm priorități pentru procese poate fi creat un algoritm de planificare relativ
simplu. Nucleul va rula procesul cu prioritate maximă. Mecanismul - în kernel - este să caute
procesul cu prioritate cea mai înaltă și să îl execute. Politica, adică cum să fie alocate prioritățile
proceselor, - poate fi realizată prin procese executate în modul utilizator. În acest fel, politica și
mecanismul pot fi decuplate și nucleul poate fi redus.

1.7.4 Modelul client-server


O ușoară variație a ideii de microkernel este de a separa procesele în două clase: clasa
proceselor server, fiecare oferind un serviciu și clasa proceselor client care utilizează aceste
servicii. Acest model este cunoscut sub numele de modelul client-server. Adesea, cel mai jos
nivel este un microkernel, dar acest lucru nu este obligator. Esența constă în prezența
proceselor client și a proceselor server.
Comunicarea între clienți și servere se face adesea prin transmiterea mesajelor. Pentru a
obține un serviciu, un proces client construiește un mesaj care spune ce dorește și îl trimite
serverului corespunzător. Serverul tratează cererea și trimite înapoi răspunsul. Dacă procesele
client și server sunt executate pe aceeași mașină, sunt posibile anumite optimizări, dar
conceptual, rămânem pe aceeași lungume de undă.
O generalizare evidentă a acestei idei este ca clienții și serverele să fie rulate pe diferite
calculatoare, conectate printr-o rețea locală sau globală, așa cum este prezentat în fig. 1-27.
Deoarece clienții comunică cu serverele prin trimiterea de mesaje, clienții nu sunt obligați să
știe dacă mesajele sunt gestionate local pe propriile mașini sau dacă sunt trimise prin
intermediul unei rețele către servere de pe o mașină de la distanță. În ceea ce privește clientul,
același lucru se întâmplă în ambele cazuri: cererile sunt trimise și răspunsurile revin. Astfel,
modelul client-server este o abstractizare care poate fi folosită pentru o singură mașină sau
pentru o rețea de mașini.

Fig. 1-27. Modelul client-server pentru o rețea de calculatoare


Este modul de lucru modern în care tot mai mulți utilizatori, folosind calculatoarele lor de acasă
- clienți, solicită serviciile unor mașini mari care funcționează în altă parte - servere. De fapt, o
mare parte din Web funcționează astfel. Un computer trimite o solicitare pentru o pagină Web
către server și pagina web revine. Este utilizarea obișnuită a modelului client-server într-o rețea.

1.7.5 Masini virtuale


Versiunile inițiale ale sistemului de operare OS/360 erau pentru prelucrarea pe loturi. Cu toate
acestea, mulți de utilizatori de OS/360 doreau să poată lucra în mod interactiv la un terminal,
astfel încât diverse grupuri, atât din interiorul cât și din afara IBM, au decis să scrie sisteme cu
timp partajat în acest scop. Sistemul oficial IBM cu timesharing, TSS/360, a cam întârziat, iar
atunci când în sfârșit a fost prezentat, era de mare și încet încât puțini au fost cei care l-au
acceptat. Într-un final a fost abandonat, chiar dacă dezvoltarea sa a constituit aproximativ 50 de
milioane de dolari (Graham, 1970). Dar un grup de dezvoltatori de la IBM Scientific Center din
Cambridge, Massachusetts, a propus un sistem radical diferit, acceptat de IBM. Un descendent
al acestuia, numit z/VM, este acum utilizat pe scară largă pe mainframe-urile actuale IBM
zSeries, mainframe-uri foarte utilizate în centrele de date corporative mari, de exemplu, ca
servere de e-commerce care gestionează sute sau mii de tranzacții pe secundă și utilizează
baze de date cu dimensiuni de ordinul miilor de terabaiți.
VM/270
Acest sistem, denumit inițial CP/CMS și mai apoi redenumit VM/370 (Seawright și MacKinnon,
1979), s-a bazat pe o observație isteață: un sistem partajarea timpului oferă (1)
multiprogramare și (2) o mașină extinsă cu o interfață mai convenabilă decât hardware-ul gol.
Esența VM/370 este separarea completă a acestor două funcții.
Partea centrală a sistemului, cunoscută sub numele de monitorul mașinii virtuale, rulează pe
hardware-ul gol și realizează multiprogramarea, oferind nu una, ci mai multe mașini virtuale
pentru următorul nivel (fig. fig. 1-28). Cu toate acestea, spre deosebire de toate celelalte
sisteme de operare, aceste mașini virtuale nu sunt mașini extinse, cu sisteme de fișiere și alte
funcții frumoase. În schimb, acestea sunt copii exacte ale hardware-ului gol, inclusiv modul
kernel/utilizator, I/O, întreruperi și orice altceva ce are mașina reală.

Fig. 1-28. Structura VM/370 cu CMS


Deoarece fiecare mașină virtuală este identică cu hardware-ul adevărat, pe fiecare poate fi
instalat orice sistem de operare care va rula direct pe hardware-ul gol. Unii utiizatori IBM
VM/370 rulau OS/360, sisteme cu procesare pe loturi sau pentru procesarea tranzacțiilor, în
timp ce alții au rulau un sistem interactiv cu un singur utilizator, numit CMS (Conversational
Monitor System). Acesta din urmă era foarte popular în rândul programatorilor.
Când un program CMS executa un apel de sistem, apelul era interceptat de sistemul de
operare în propria sa mașină virtuală (trap), nu de VM/370. CMS emula instrucțiunile de I/O
hardware normale pentru citirea discului său virtual sau orice era necesar pentru a executa
apelul. Aceste instrucțiuni de I/O eau apoi preluate de VM/370, care le executa ca parte a
simulării hardware-ului real. Prin separarea completă a funcțiilor (multiprogramare și furnizarea
mașinii extinseI, fiecare dintre componente deveneau mai simple, mai flexibile și mult mai ușor
de întreținut.
În realizarea sa modernă, z/VM este utilizat pentru a rula mai multe sisteme de operare
complete, mult mai performante decât sistemele cu un singur utilizator cum ar fi CMS. De
exemplu, sistemul zSeries este capabil să ruleze una sau mai multe mașini virtuale Linux
împreună cu sistemele de operare tradiționale IBM.
Mașini virtuale redescoperite
Deși IBM a implementat mașina virtuală cu aproape 5 decenii în urmă, iar câteva alte companii,
inclusiv Oracle și Hewlett-Packard, au adăugat recent suport pentru mașini virtuale pe serverele
lor de înaltă performanță, ideea virtualizării a fost în mare parte ignorată în lumea PC-urilor. Dar
în ultimii ani, o combinație de necesități, tehnologii și software noi s-au combinat și au creat un
subiect fierbinte.
Mai întâi despre nevoi. Multe companii și-au rulat în mod tradițional serverele de poștă,
serverele Web, serverele FTP și alte servere pe computere separate, uneori cu sisteme de
operare diferite. Ei văd virtualizarea ca o modalitate de a le rula pe toate pe aceeași mașină
fără ca o cădere a unui server să codnucă la căderea celorlalte servere.
Virtualizarea este de asemenea populară în hosting-ul web. Fără virtualizare, clienții care au
nevoie de hosting web sunt nevoiți să aleagă între hosting partajat (care le oferă doar un cont
de autentificare pe un server Web, dar fără control asupra software-ului serverului) și hosting
dedicat (ceea ce le oferă propria mașină, care este foarte flexibilă, dar acest mod nu este
recomandat pentru site-urile mici până la mijlocii). Atunci când o companie de hosting web
oferă mașini virtuale de închiriat, o singură mașină fizică poate rula mai multe mașini virtuale,
fiecare dintre ele simulând o mașină separată. Clienții pot folosi orice sistem de operare sau
software aplicativ, achitând doar o parte din costul unui server dedicat.
O altă utilizare a virtualizării este pentru utilizatorii finali care doresc să poată rula două sau mai
multe sisteme de operare în același timp, deoarece unele dintre pachetele de aplicații preferate
rulează pe un sistem, iar altele - pe celălalt. Această situație este ilustrată în fig. 1-29 (a), unde
termenul “monitor de mașină virtuală” a fost redenumit în hipervizor de tip 1.

Fig. 1-29. (a) Hipervizor de tip 1. (b) Hipervizor pur de tip 2. (c) Hipervizor practic de tip 2
Deși astăzi nimeni nu contestă atractivitatea mașinilor virtuale, la momentul realizării ideii
problema era. Pentru a rula software-ul mașinii virtuale pe un computer, procesorul său trebuie
să fie virtualizabil (Popek și Goldberg, 1974) - aici este problema. Când un sistem de operare
care rulează pe o mașină virtuală (în modul utilizator) execută o instrucțiune privilegiată, cum ar
fi modificarea PSW sau efectuarea I/O, este esențial ca trap-ul hardware să fie monitorizat
pentru mașina virtuală, astfel încât instrucțiunea să poată fi emulată în software. Pe unele
procesoare - în special Pentium, predecesorii și clonele sale - încercările de a executa
instrucțiuni privilegiate în modul utilizator sunt doar ignorate. Această proprietate a făcut
imposibil existența de mașini virtuale pe acest hardware, ceea ce explică lipsa de interes pentru
lumea x86. Simulatoarele pentru Pentium, cum ar fi Bochs, care rulau pe Pentium aveau o
pierdere de performanță de la 10 până la o sută de ori și nu au fost utili pentru munci serioase.
Această situație s-a schimbat ca urmare a mai multor proiecte de cercetare academică din anii
1990 și primii ani ai acestui mileniu, în special Disco la Stanford (Bugnion și colab., 1997) și
Xen la Universitatea Cambridge (Barham și colab., 2003). Aceste lucrări de cercetare au dus la
mai multe produse comerciale (de exemplu, VMware Workstation și Xen) și la o renaștere a
interesului pentru mașinile virtuale. Pe lângă VMware și Xen, hipervizorii populari includ astăzi
KVM (pentru nucleul Linux), VirtualBox (de Oracle) și Hyper-V (de Microsoft).
Unele dintre aceste proiecte de cercetare timpurie au sporit performanța comparativ cu Bochs
prin compilarea blocurilor de cod „din zbor”, stocarea lor într-o memorie cache internă și apoi
reutilizarea lor (dacă nu au fost modificate). Acest lucru a îmbunătățit considerabil performanța
și a dus la ceea ce vom numi simulatoare de mașini (fig. 1-29 (b)). Cu toate acestea, deși
această tehnică, cunoscută sub numele de translatare binară, a ajutat la îmbunătățirea situației,
sistemele rezultate, deși sunt suficient de bune pentru a publica lucrări la conferințe, nu sunt
încă destul de rapide pentru a fi utilizate în medii comerciale unde performanța contează mult.
Următorul pas în îmbunătățirea performanței a fost adăugarea unui modul de kernel (fig. 1-29
(c)). În practică, toți hipervizorii comerciali, cum ar fi VMware Workstation, folosesc această
strategie hibridă (plus multe alte îmbunătățiri). Sunt numiți hipervizori de tip 2 de toată lumea,
așa că vom folosi această denumire în restul acestei cărți, chiar dacă am prefera să le numim
hipervizori de tip 1.7 pentru a reflecta faptul că nu sunt în întregime programe executate în mod
user. În cap. 7, vom descrie în detaliu cum funcționează VMware Workstation.
Mașina virtuală Java
Un alt domeniu în care sunt utilizate mașini virtuale, dar într-un mod oarecum diferit, este
rularea programelor Java. Când Sun Microsystems a inventat limbajul de programare Java, a
inventat și o mașină virtuală (adică, o arhitectură computerizată) numită JVM (Java Virtual
Machine). Compilatorul Java produce cod pentru JVM, care este de obicei executat de un
interpretor software JVM. Avantajul acestei abordări constă în faptul că codul JVM poate fi
expediat pe internet către orice computer care are un interpretor JVM și poate rula acolo. Dacă
compilatorul ar fi produs programe binare SPARC sau x86, de exemplu, acestea nu ar fi putut fi
expediate și rulate la fel de ușor. (Desigur, Sun ar fi putut propune un compilator care să
producă cod binar SPARC și apoi să distribuie un interpretor SPARC, dar JVM este o
arhitectură mult mai simplă de interpretat.) Un alt avantaj al utilizării JVM este că dacă
interpretorul este implementat corect, ceea ce nu este atât de simplu, programele JVM banale,
pot fi verificate pentru siguranță și apoi executate într-un mediu protejat, astfel încât acestea să
nu poată fura date sau să facă pagube.

1.7.6 Despre Exokernel


În locul clonării unei mașini, așa cum se face în cazul mașinilor virtuale, există o altă strategie
numită partajare, prin care fiecărui utilizator se pune la dispoziție un subset de resurse. Astfel, o
mașină virtuală poate primi blocuri de disc de la 0 până la 1023, următoarea ar putea primi
blocuri 1024 - 2047 și așa mai departe.
În nivelul de jos, care rulează în modul kernel, se află un program numit exokernel (Engler și
colab., 1995). Sarcina sa este să aloce resurse mașinilor virtuale și apoi să verifice încercările
de a le folosi pentru a se asigura că nici o mașină nu încearcă să utilizeze resursele altcuiva.
Fiecare mașină virtuală la nivel de utilizator poate rula propriul sistem de operare, ca pe
VM/370 și Pentium virtual 8086, cu excepția faptului că fiecare este restricționată să utilizeze
doar resursele solicitate după ce au fost alocate.
Avantajul schemei exokernel este că economisește un nivel de mapare. În celelalte modele,
fiecare mașină virtuală crede că are propriul său disc, cu blocuri care rulează de la 0 la un
maxim, astfel încât monitorul mașinii virtuale trebuie să mențină tabele pentru a remapa
adresele de disc (și toate celelalte resurse). Cu exokernel-ul, această remapare nu este
necesară. Exokernel-ul trebuie doar să țină evidența de la ce mașină virtuală și care resursă i-a
fost atribuită. Această metodă mai are avantajul de a separa multiprogramarea (în exokernel)
de codul sistemului de operare al utilizatorului (în spațiul utilizatorului).

1.8 DIN LUMEA LIMBAJULUI C


Sistemele de operare constă din programe mari în limbajul C (sau uneori C++), formate din
multe module, scrise de mulți programatori. Mediul utilizat pentru dezvoltarea sistemelor de
operare este foarte diferit de ceea ce obișnuiesc programatorii simpli (cum ar fi studenții) atunci
când scriu mici programe Java. Această secțiune este o încercare de a oferi o scurtă
introducere în lumea scrierii unui sistem de operare pentru programatorii Java sau Python.
1.8.1 Despre limbajul C
Acesta nu este un ghid pentru C, ci un scurt rezumat al unora dintre diferențele cheie între C și
limbbaje precum Python și în special Java. Java se bazează pe C, deci există multe asemănări
între cele două. Python este oarecum diferit, dar are și similitudini. Pentru comoditate, ne vom
concentra pe Java. Java, Python și C sunt limbaje imperative cu tipuri de date, variabile și
instrucțiuni de control. Tipurile de date primitive din C sunt numerele întregi (inclusiv cele scurte
și lungi), caractere și numere în virgulă flotantă. Tipurile de date compuse sunt tablourile,
structurile și uniunile. Instrucțiunile de control în C sunt similare cu cele din Java, inclusiv
instrucțiunile if, switch, for și while. Funcțiile și parametrii sunt aproximativ aceiași în ambele
limbaje.
O caracteristică prezentă în C și lipsă în Java și Python pointerii expliciți. Un pointer este o
variabilă care indică (conține adresa) unei alte variabile sau a unei structuri de date.
În fragmentul de cod
char c1, c2, *p;
c1 = „c”;
p = &c1;
c2 = *p;
c1 și c2 sunt declarate ca variabile de tip caracter, iar p ca o variabilă care indică, adică conține
adresa unui caracter. Prima atribuire pune codul ASCII al caracterului „c” în variabila c1. A doua
atribuie adresa lui c1 variabilei pointer p. A treilea alocă conținutul variabilei punctate de p
variabilei c2, astfel încât după executarea acestor declarații, c2 conține codul ASCII al lui „c”. În
teorie nu ar trebui să atribuiți adresa unui număr în virgulă flotantă unui indicator de caracter,
dar în practică compilatoarle acceptă astfel de atribuiri, deși uneori cu un avertisment. Pointerii
sunt o construcție foarte puternică, dar și o mare sursă de erori atunci când sunt folosiți cu
neglijență.
Limbajul C nu include șiruri încorporate, fire, pachete, clase, obiecte, type safety și colectarea
gunoiului. Ultima este un dezavantaj major pentru sistemele de operare. Toată stocarea în C
este sau statică sau alocare/eliberare explicită de programator, de obicei folosind funcțiile de
bibliotecă malloc și free. Programatorul are controlul total al memoriei - împreună cu pointerii
expliciți care îl fac limbajul C atractiv pentru scrierea sistemelor de operare. Sistemele de
operare sunt, în esență, sisteme de timp real, chiar și cele cu destinație generală. Când are loc
o întrerupere, sistemul de operare poate avea doar câteva microsecunde pentru a efectua
unele acțiuni sau pentru a pierde informații critice. Punerea la dispoziție a colectorului de gunoi
într-un moment arbitrar este intolerabilă.

1.8.2 Fișiere antet


Un proiect dedicat elaborării unui sistem de operare include, de obicei, câteva directoare,
fiecare conținând mai multe fișiere .c cu codul pentru o parte a sistemului, împreună cu unele
fișiere antet .h care conțin declarații și definiții utilizate de unul sau mai multe fișiere de cod.
Fișierele antet pot include, de asemenea, macro-uri simple, cum ar fi
#define BUFFER SIZE 4096
care permite programatorului să definească constante, astfel încât atunci când se utilizează
BUFFER SIZE în cod, acesta este înlocuit în timpul compilării cu valoarea 4096. O bună
practică de programare C este să numești fiecare constantă, cu excepția lui 0, 1 și −1, și uneori
chiar acestea. Macro-urile pot avea parametri, cum ar fi
#define max(a, b) (a > b? a : b)
ceea ce permite programatorului să scrie
i = max(j, k+1)
și să obțină
i = (j > k+1? j : k + 1)
pentru a alege valoarea mai mare dintre j și k+1 în i. Anteturile pot conține, de asemenea, o
compilare condiționată, de exemplu
#ifdef X86
intel_int_ack();
#endif
care se compilează într-un apel către funcția intel int_ack dacă macro-ul X86 este definit.
Compilarea condiționată este folosită pentru a izola codul dependent de arhitectură, astfel încât
un anumit cod este inserat doar atunci când sistemul este compilat pe X86, un alt cod este
inserat doar atunci când sistemul este compilat pe un SPARC și așa mai departe. Un fișier .c
poate include fizic zero sau mai multe fișiere antet folosind directiva #include. Există mai
multe fișiere antet comune pentru aproape toate fișierele .c stocate într-un director central.

1.8.3 Proiecte mari de programare


Pentru a construi un sistem de operare, fiecare modul sursă .c este compilat într-un fișier obiect
de către compilatorul C. Fișierele obiect, care au sufixul .o, conțin instrucțiuni binare pentru
mașina țintă. Ulterior vor fi direct executate de procesor. Nu există nimic ca codul de byte Java
sau codul de byte Python în lumea C.
Prima fază de compilare, numită preprocesare C, pe măsură ce citește fiecare fișier .c, de
fiecare dată când atinge o directivă #include, procesează fișierul antet din el, extindând macro-
urile, gestionând compilarea condiționată (și anumite alte lucruri) și trece rezultatele la
următoarea fază de compilare.
Având în vedere că sistemele de operare sunt foarte mari (de obicei, peste cinci milioane de
linii de cod), faptul că ar trebui să recompilăm totul de fiecare dată când un fișier este schimbat
ar fi insuportabil. Pe de altă parte, modificarea unui fișier antet inclus în mii de alte fișiere
necesită recompilarea acelor fișiere. Să urmărești care fișier obiect depinde de care fișier antet
este complet imposibil fără ajutor.
Din fericire, calculatoarele sunt foarte bune la acest gen de lucruri. Pe sistemele UNIX, există
un program numit make (cu numeroase variante precum gmake, pmake, etc.) care citește
Makefile, care îi spune ce fișiere depind de alte fișiere. Programul make știe ce fișiere obiect
sunt necesare pentru a construi codul binar al sistemului de operare și pentru fiecare, verifică
dacă vreunul dintre fișierele antet de care depinde fișierul obiect a fost modificat ulterior ultimei
compilări. Dacă da, fișierul obiect trebuie recompilat. Doar în acest caz make lansează
recompilarea, reducând astfel numărul de compilări la minim.
După crearea tuturor fișierelor .o, acestea sunt transmise unui program numit linker pentru a le
combina pe toate într-un singur fișier binar executabil. Funcțiile de bibliotecă apelate sunt, de
asemenea, incluse în acest moment, operație numită legarea externilor, referințele de
funcționare sunt rezolvate și adresele mașinii sunt relocate după caz. Când legarea externilor
este încheiată, rezultatul este un program executabil, denumit în mod tradițional a.out pe
sistemele UNIX. Diferitele componente ale acestui proces sunt ilustrate în fig. 1-30 pentru un
program cu trei fișiere C și două fișiere antet. Deși am discutat despre dezvoltarea sistemului
de operare aici, toate acestea se aplică dezvoltării oricărui program mare.

Fig. 1-30. Schema procesului de compilare


După ce modulul executabil a sistemului de operare a fost obținut, computerul poate fi repornit
și noul sistem de operare lansat. Odată lansat, acesta poate încărca în mod dinamic
componente care nu au fost incluse static în fișierul binar, cum ar fi driverele de dispozitiv și
sistemele de fișiere. În timpul funcționării, sistemul de operare poate consta din mai multe
segmente: segmentul de cod, de date și stivă. Segmentul de cod este în mod normal imuabil,
nu se schimbă în timpul executării. Segmentul de date începe cu o anumită dimensiune și este
inițializat cu anumite valori, dar poate crește la nevoie. Stiva este inițial goală, dar crește și se
micșorează pe măsură ce funcțiile sunt apelate și returnate. Adesea, segmentul de cod este
plasat aproape de începutul memoriei, segmentul de date chiar deasupra acestuia, cu
capacitatea de a crește în sus, iar segmentul de stivă la o adresă virtuală mare, cu posibilitatea
de a crește în jos.
În toate cazurile, codul sistemului de operare este executat direct de hardware, fără niciun
interpretor și fără o compilare just-in-time, așa cum este cazul Java.

1.9 Cercetări în domeniul sistemelor de operare


Informatica este un domeniu care avansează rapid și este greu de făcut prognoze. Cercetătorii
universităților și laboratoarelor de cercetare industrială se gândesc în mod constant la idei noi,
unele dintre care nu duc nicăieri, dar unele dintre ele devin piatra de temelie a produselor
viitoare și au un impact masiv asupra industriei și a utilizatorilor. Separarea grâului de pleavă
este deosebit de dificilă, deoarece deseori durează 20-30 de ani de la idee la impact.
De exemplu, când în 1958 președintele Eisenhower a înființat Agenția de Proiecte Avansate de
Cercetare (ARPA) a Departamentului Apărării, el pornea de la necesitatea sporirii capacităților
de apărare, nici gând să inventeze Internetul. ARPA a finanțat cercetări universitare în cadrul
cărora a fost dezvoltat conceptul, nu prea înțeles atunci, de comutare a pachetelor, concept
care a condus la prima rețea experimentală de calculatoare cu comutare a pachetelor,
ARPANET, pusă în funcțiune în 1969. Multe alte rețele de cercetare finanțate de ARPA au fost
conectate la ARPANET și Internetul s-a născut. Internetul a fost apoi folosit cu succes de
cercetătorii academici pentru a-și trimite e-mail-uri timp de 20 de ani. La începutul anilor 1990,
Tim Berners-Lee de la laboratorul de cercetare CERN din Geneva a inventat World Wide Web,
iar Marc Andreesen de la Universitatea din Illinois a scris un browser grafic pentru acesta.
Imediat, Internetul s-a umplut de adolescenți pe Twitter. Președintelui Eisenhower nu-i vine să
creadă ce a lansat atunci.
Cercetarea în sistemele de operare a dus, de asemenea, la schimbări dramatice în sistemele
aplicative. Cum am subliniat anmterior, primele sisteme informatice comerciale erau pentru
tratarea pe loturi, până când M.I.T. la începutul anilor 1960, a venit cu idea de partajare a
timpului. Interfața utilizatorului era una bazată pe text până când Doug Engelbart a inventat
mouse-ul și interfața grafică de utilizator la Stanford Research Institute la sfârșitul anilor 1960.
Cine știe ce va urma?
În acest compartiment și în altele asemănătoare, vom arunca o scurtă privire asupra unor
cercetări în domeniul sistemelor de operare, doar pentru a oferi o aromă a ceea ce ar putea fi la
orizont. Această introducere nu este cu siguranță exhaustică. Se bazează în mare parte pe
lucrări care au fost publicate în cadrul conferințelor de cercetare de top, deoarece aceste idei
au supraviețuit cel puțin unui proces riguros de evaluare pentru a fi publicate. Rețineți că, în
informatică - spre deosebire de alte domenii științifice, majoritatea cercetărilor sunt publicate în
conferințe, nu în reviste. Cele mai multe dintre lucrările citate în secțiunile de cercetare au fost
publicate de ACM, IEEE Computer Society sau USENIX și sunt disponibile pe internet
membrilor acestor organizații. Pentru mai multe informații despre aceste organizații și
bibliotecile lor digitale, consultați
ACM https://www.acm.org/
IEEE Computer Society https://www.computer.org/
USENIX https://www.usenix.org/
Practic, toți cercetătorii din domeniul sistemelor de operare își dau seama că sistemele de
operare actuale sunt masive, inflexibile, nesigure, nefiabile și pline de bug-uri. În consecință,
există o mulțime de cercetări cu privire la modul de construire a unor sisteme de operare mai
bune. Au fost publicate lucrări despre bug-uri și depanare (Renzelmann și colab., 2012; și Zhou
și colab., 2012), reacția la accidente (Correia și colab., 2012; Ma et al., 2013; Ongaro și colab.,
2011; și Yeh și Cheng, 2012), managementul energiei (Pathak și colab., 2012; Petrucci și
Loques, 2012; și Shen și colab., 2013), sisteme de fișiere și stocare (Elnably și Wang, 2012;
Nightingale și colab., 2012 ; și Zhang și colab., 2013a), I/O de înaltă performanță (De Bruijn și
colab., 2011; Li și colab., 2013a; și Rizzo, 2012), hipertratare și multitratare (Liu și colab.,
2011), actualizare live (Giuffrida și colab., 2013), gestionarea GPU-urilor (Rossbach și colab.,
2011), gestionarea memoriei (Jantz și colab., 2013; și Jeong et al., 2013), sisteme de operare
multicore (Baumann și colab., 2009; Kapritsos, 2012; Lachaize și colab., 2012; și Wentzlaff et
al., 2012), corectitudinea sistemului de operare (Elphinstone et al., 2007; Yang și colab., 2006;
și Klein și colab., 2009), fiabilitatea sistemului (Hruby și colab., 2012; Ryzhyk și colab., 2009,
2011 și Zheng și colab., 2012 ), confidențialitate și securitate (Dunn și colab., 2012; Giuffrida și
colab., 2012; Li și colab., 2013b; Lorch și colab., 2013; Ortolani și Crispo, 2012; Slowinska și
colab., 2012; și Ur și colab., 2012), monitorizarea utilizării și performanței (Harter și colab.,
2012; și Ravindranath et al., 2012) și virtualizare (Agesen și colab., 2012; Ben-Yehuda și
colab., 2010; Colp și colab., 2011; Dai și colab., 2013; Tarasov și colab., 2013; și Williams și
colab., 2012) printre multe alte subiecte.

1.10 Rezumat
Sistemele de operare pot fi privite din două puncte de vedere: ca manager de resurse și mașină
extinsă. În viziunea managerului de resurse, sarcina sistemului de operare este de a gestiona
în mod eficient diferite părți ale sistemului. Din punctul de vedere al mașinii extinse, fincția
sistemului este de a oferi utilizatorilor abstracții care sunt mai convenabile de utilizat decât
mașina fizică, abstracții denumite procese, spații de adrese sau fișiere. Sistemele de operare
au o istorie lungă, începând din zilele în care au înlocuit operatorul, până la sisteme moderne
cu multiprogramare.
Deoarece sistemele de operare interacționează strâns cu hardware-ul, unele cunoștințe despre
hardware-ul computerului sunt utile pentru înțelegerea acestora. Calculatoarele sunt construite
din procesoare, memorii și dispozitive I/O. Aceste piese sunt conectate prin magistrale de date.
Conceptele în baza cărora sunt construite toate sistemele de operare sunt procesele,
managementul memoriei, managementul I/O, sistemul de fișiere și securitatea. Fiecare dintre
acestea va fi tratat într-un capitol următor.
Partea principală a oricărui sistem de operare este setul de apeluri de sistem pe care le poate
gestiona. Acestea stabilesc ce face de fapt sistemul de operare. Pentru UNIX, am analizat
patru grupuri de apeluri de sistem. Primul grup de apeluri de sistem se referă la crearea și
terminarea proceselor. Al doilea grup este pentru citirea și scrierea fișierelor. Al treilea grup
este destinat managementului directorului. Al patrulea grup conține apeluri diverse.
Sistemele de operare pot fi structurate în mai multe moduri. Mai frecvente structuri sunt
sistemele monolitice, sistemele multinivel, microkernel, client-server, mașină virtuală sau
exokernel.
Probleme
1. Care sunt cele două funcții principale ale unui sistem de operare?
2. În secțiunea 1.4, sunt descrise nouă tipuri diferite de sisteme de operare. Prezentați o
listă de aplicații pentru fiecare din aceste sisteme (una pentru fiecare tip).
3. Care este diferența dintre sistemele cu partajare a timpului și multiprogramarea?
4. Pentru a utiliza memoria cache, memoria principală este împărțită în linii de cache, de
obicei 32 sau 64 de octeți. O linie cache este o zonă de memorie tratată ca un tot întreg.
Care este avantajul de a memora în cache o linie întreagă în loc de un singur octet sau
un cuvânt la un moment dat?
5. Pe computerele timpurii, fiecare octet de date citite sau scrise era gestionat de procesor
(adică nu exista DMA). Ce implicații are aceasta pentru multiprogramare?
6. Instrucțiunile legate de accesarea dispozitivelor I/O sunt de obicei instrucțiuni
privilegiate, adică sunt executate în modul kernel, nu în modul utilizator. Indicați un motiv
pentru care aceste instrucțiuni trebuie să fie privilegiate.
7. Ideea familiei de computere a fost introdusă în anii 1960 pornind cu linia IBM
System/360. Această idee este astăzi uitată sau trăiește?
8. Unul dintre motivele pentru care GUI a fost adoptată greu a fost costul hardware-ului
necesar pentru susținerea ei. Câtă memorie RAM video este necesară pentru a susține
un ecran monocrom cu 25 de linii × 80 rânduri de caractere? Dar pentru o imagine
bitmap culor de 1200 × 900 pixeli pe 24 de biți? Care era fost costul acestei RAM la
prețurile din 1980 (5 USD/KB)? Care este prețul de azi?
9. Există mai multe obiective care trebuie urmărite la proiectarea și construirea unui sistem
de operare, de exemplu, utilizarea resurselor, actualitatea, robustețea și așa mai
departe. Dati un exemplu de doua obiective de proiectare care se pot contrazice.
10. Care este diferența dintre modul kernel și modul utilizator? Explicați modul în care
daceste ouă moduri distincte vin în ajutorul dezvoltatorilor de sisteme de operare.
11. Un disc de 255 GB are 65.536 cilindri cu 255 de sectoare pe pistă fiecare sector este de
512 octeți. Câte platouri și capete are acest disc? Presupunând un timp mediu de
căutare a cilindrului de 11 ms, o întârziere de rotație medie de 7 msec și o rată de citire
de 100 MB/sec, calculați timpul mediu necesar pentru a citi 400 KB dintr-un sector.
12. Care dintre următoarele instrucțiuni ar trebui să fie permise doar în modul kernel?
 Dezactivați toate întreruperile.
 Citiți indicațiile ceasului.
 Setați ceasul.
 Schimbați harta memoriei.
13. Luați în considerare un sistem care are două CPU, fiecare procesor având două fire
(hiperthreading). Să presupunem că trei programe, P0, P1 și P2, sunt pornite cu
perioade de rulare de 5, 10 și, respectiv, 20 msec. Cât timp va dura până la finalizarea
executării acestor programe? Presupunem că toate cele trei programe sunt 100% legate
de procesor, nu se blochează în timpul execuției și nu cedează procesoarele odată
alocate.
14. Un computer are o conductă cu patru faze. Fiecare fază are același timp pentru a-și face
munca, și anume, 1 nsec. Câte instrucțiuni pe secundă poate executa această mașină?
15. Luați în considerare un sistem de computer care are memorie cache, memorie principală
și disc și un sistem de operare care utilizează memorie virtuală. Este nevoie de 1 nsec
pentru a accesa un cuvânt din cache, 10 nsec pentru a accesa un cuvânt din memoria
RAM și 10 ms pentru a accesa un cuvânt de pe disc. Dacă rata de accesare a memoriei
cache este de 95% și rata de accesare a memoriei principale (după un insucces la
memoria cache) este de 99%, care este timpul mediu pentru a accesa un cuvânt?
16. Atunci când un program de utilizator efectuează un apel de sistem pentru a citi sau scrie
un fișier pe disc, acesta indică numele fișierului, un ppointer către bufferul de date și
numărul de simboluri. Controlul este apoi transferat sistemului de operare, care apelează
driverul corespunzător. Să presupunem că driverul pornește discul și oprește când apare
întreruperea respectivă. În cazul citirii de pe disc, evident că apelantul va trebui să fie
blocat (așteaptă datelesolicitate). Dar cazul scrierii pe disc? Este necesar ca apelantul
să fie blocat pentru a finaliza transferul de disc?
17. Ce este o instrucțiune capcană (trap)? Explicați utilizarea sa în sistemele de operare.
18. De ce este necesară un tabel al proceselor într-un sistem cu partajarea timpului? Este
acest tabel și în sistemele de operare ale calculatoarelor personale care rulează UNIX
sau Windows cu un singur utilizator?
19. Există vreun motiv pentru care s-ar putea să doriți să montați un sistem de fișiere într-un
director nonempty? Dacă da, care este?
20. Pentru fiecare dintre următoarele apeluri de sistem, dați o condiție care face ca acesta
să eșueze: fork, exec și unlink.
21. Ce tip de multiplexare (în timp, în spațiu sau ambele) pot fi utilizate pentru partajarea
următoarelor resurse: procesor, memorie, disc, placă de rețea, imprimantă, tastatură și
display?
22. Poate oare
count = write(fd, buffer, nbytes);
atribui lui count o altă valoare decât nbytes? Dacă da, de ce?
23. Un fișier al cărui descriptor este fd conține următoarea secvență de octeți: 3, 1, 4, 1, 5, 9,
2, 6, 5, 3, 5. Sunt efectuate următoarele apeluri de sistem:
lseek(fd, 3, SEEK_SET);
read(fd, &buffer, 4);
unde apelul lseek face o căutare pentru octetul 3 din fișier. Ce conține buferul după ce
s-a finalizat citirea?
24. Să presupunem că un fișier de 10 MB este stocat pe un disc pista 50 în sectoare
consecutive. Bratul de disc este situat în prezent peste pista 100. Cât timp va dura citirea
acestui fișier de pe disc? Presupunem că este nevoie de aproximativ 1 ms pentru a muta
brațul de la un cilindru la altul și aproximativ 5 ms pentru a ajunge la sectorul în care
începutul fișierului este stocat (rotirea discului). Presupunem că citirea se face cu viteza
de 200 MB/s.
25. Care este diferența esențială între un fișier special pentru dispozitivele bloc orientate și
un fișier special pentru dispozitivele orientate pe caractere?
26. În exemplul din fig. 1-17, procedura de bibliotecă se numește read, iar apelul de sistem
este la fel read. Este esențial ca ambele să aibă același nume? Dacă nu, care este mai
important?
27. Sistemele de operare moderne decuplează spațiul de adrese al procesului de memoria
fizică a mașinii. Enumerați două avantaje ale acestui design.
28. Pentru un programator, un apel de sistem arată ca orice alt apel către o procedură de
bibliotecă. Este important ca un programator să știe ce proceduri de bibliotecă au ca
rezultat apeluri de sistem? În ce circumstanțe și de ce?
29. Figura 1-23 arată că o serie de apeluri de sistem UNIX nu au echivalente Win32 API.
Pentru fiecare dintre apelurile enumerate ca neavând un echivalent Win32, care sunt
consecințele pentru un programator convertirea unui program UNIX pentru a rula sub
Windows?
30. Un sistem de operare portabil este unul care poate fi portat de la o arhitectură de sistem
la alta, fără nicio modificare. Explicați de ce nu este fezabil să construiți un sistem de
operare complet portabil. Descrieți cele două hi-level necesare pentru proiectarea unui
sistem de operare extrem de portabil.
31. Explicați modul în care separarea politicilor și a mecanismelor ajută la construirea
sistemelor de operare bazate pe microkernel.

2. PROCESE SI FIRE DE EXECUTIE


În acest capitol vom realiza o analiză detaliată a modului de implementare a sistemelor de
operare. Conceptul de bază în orice sistem de operare este procesul: o abstractizare care
descrie un program care rulează. Orice altceva depinde de acest concept, de aceea este
imperativ ca proiectanții de sisteme de operare (precum și studenții) să înțeleagă complet
conceptul de proces cât mai devreme. Procesele sunt una dintre cele mai vechi și mai
importante abstractizări ale sistemului de operare. Ele susțin capacitatea de a efectua
operațiuni (pseudo) paralele chiar și cu un singur procesor. Acestea transformă un procesor
central în mai multe virtuale. Fără această abstractizare, denumită proces, calculul modern pur
și simplu nu poate exista. În acest capitol, abordăm multe dintre detaliile referitoare la procese
și rudele lor apropiate, firele de execuție.
2.1 Procese
Calculatoarele moderne de obicei execută mai multe lucruri simultan. Deoarece oamenii
obișnuiți să lucreze cu computerele nu înțeleg pe deplin acest fapt, vom lua în considerare o
serie de exemple. Să ne imaginăm mai întâi un server web. Acesta primește solicitări de
pretutindeni pentru furnizarea de pagini web. Când apare o solicitare, serverul verifică dacă
pagina dorită este în cache. Dacă este acolo, trimite pagina respectivă; dacă nu este acolo, va
fi căutată pe disc. Dar, din punctul de vedere al procesorului, este nevoie de o întreagă veșnicie
ca să obții informații de pe disc. În timpul așteptării rezultatelor unei cereri de informații de pe
disc, pot veni multe alte solicitări. Dacă pe sistem sunt instalate mai multe discuri, unele sau
toate cererile noi pot fi direcționate către alte discuri cu mult înainte ca prima solicitare să fie
satisfăcută. Este clar că e nevoie de o anumită modalitate de modelare și gestionare a acestor
lucrări paralele. Procesele (și în special firele) ajută în acest sens.
Într-un calculator personal, la pronirea sistemului sunt lansate multe procese de care utilizatorul
de multe ori nici măcar nu știe. De exemplu, un proces ar putea fi lansat pentru e-mailuri. Un alt
proces ar putea aparține unui program anti-virus și poate fi proiectat pentru a verifica periodic
apariția unor viruși noi. Pe lângă aceasta, procesele pot fi lansate explicit de către utilizator -
tipărirea fișierelor sau trimiterea fotografiilor utilizatorului pe o unitate USB, toate rulând
simultan cu browserul cu care utilizatorul navighează pe Internet. Toată aceste lucrări trebuie
să fie gestionate, deci, este util un sistem multitasking care acceptă mai multe procese.
În orice sistem multitasking, procesorul central este comutat rapid între procese, oferind
fiecăruia dintre ele zeci sau sute de milisecunde. În același timp, deși în orice moment
procesorul central execută un singur proces, în 1 secundă poate reuși să funcționeze cu mai
multe dintre ele, creând iluzia unei execuții paralele. Uneori, în acest caz, se vorbește de
pseudo-paralelism, spre deosebire de paralelismul hardware real în sistemele multiprocesorale
(care au cel puțin două unități centrale de procesare care utilizează aceeași memorie fizică).
Este destul de dificil pentru oameni să țină evidența mai multor activități care se desfășoară în
paralel. În acest scop, creatorii de sisteme de operare de-a lungul anilor au creat un model
conceptual de procese secvențiale care facilitează lucrul cu calcul paralel. Acest model,
aplicarea sa și unele dintre consecințele aplicării sale vor fi subiectul acestui capitol.

2.1.1 Modelul de proces


În acest model, toate softurile care rulează pe un computer, inclusiv uneori sistemul de operare,
sunt reduse la o serie de procese secvențiale sau simplu procese. Un proces este pur și simplu
o instanță a unui program executabil, incluzând valorile curente ale contorului de instrucțiuni,
registrele și variabilele. Conceptual, fiecare proces are propriul procesor central virtual. Desigur
în realitate, procesorul trece în mod constant între procese, dar pentru a înțelege sistemul, este
mult mai ușor să te gândești la un set de procese care se desfășoară în modul (pseudo) paralel
decât să încerci să urmărești modul în care CPU comută între programe. Această comutare
constantă între procese, după cum am aflat în capitolul 1, se numește multiprogramare sau
modul de operare multitasking.
În fig. 2.1, a arată un computer care rulează în modul multitasking și are patru programe în
memorie. În fig. 2.1, b prezintă patru procese, fiecare dintre ele având propriul algoritm de
control (adică propriul contor de comenzi logice) și funcționează independent de toate celelalte.
Este clar că există de fapt un singur contor de comenzi fizice, așa că atunci când începe fiecare
proces, contorul său de comenzi logice este încărcat în contorul real. Când lucrul cu un proces
este oprit o perioadă, valoarea contorului de comenzi fizice este stocată într-un contor de
comenzi logice alocat de proces în memorie. În fig. 2.1, se arată că, pe o perioadă destul de
lungă de observare, toate procesele au avansat, dar la fiecare moment luat separat, un singur
proces funcționează de fapt.

Fig. 2-1. (a) Multiprogramarea pentru patru procese. (b) Modelul conceptual: patru procese
secvențiale independente. (c) Doar un singur program este executat la un moment de timp
În acest capitol vom presupune că avem la dispoziție o singură unitate centrală de procesare.
Deși din ce în ce mai des, astfel de presupuneri sunt contrare adevărului, deoarece noile
cristale sunt adesea multicore, având două, patru sau mai multe nuclee. Capitolul 8 se va
concentra în principal pe cristale multicore și multiprocesoare, dar acum ne este mai ușor să
credem că doar un singur procesor central rulează simultan. Prin urmare, când spunem că
procesorul central este de fapt capabil să execute un singur proces la un moment de timp dat,
atunci dacă are două nuclee (sau procesoare centrale), un singur proces poate fi lansat pe
fiecare dintre ele la un moment dat.
Odată ce procesorul este comutat între procese, viteza cu care un proces își execută calculul
nu va fi uniformă și, probabil, nici măcar reproductibilă, dacă aceleași procese vor fi rulate din
nou. Astfel, procesele nu trebuie să fie gândite în termeni preconcepuți referitor la durată. Un
exemplu ilustrativ ar să comparați un proces audio care redă muzică pentru a însoți un
videoclip de înaltă calitate rulat de un alt dispozitiv. Deoarece sunetul ar trebui să înceapă puțin
mai târziu decât videoclipul, va fi semnalat serverul video să înceapă redarea, apoi va trebui
introdusă o buclă de întârziere folosind un cronometru înainte de a reda audio. Totul merge
bine, dacă cronometrul este de încredere, dar dacă CPU decide să treacă la un alt proces în
timpul buclei, procesul audio poate să nu mai ruleze la momentul necesar contentului video –
vom avea un mod enervant din sincronizare. Atunci când un proces are cerințe critice în timp
real ca în acest exemplu, adică, evenimente particulare trebuie să aibă loc într-un număr
specificat de milisecunde, trebuie luate măsuri speciale pentru a se asigura că acestea apar. În
mod normal, majoritatea proceselor nu sunt afectate de multiprogramarea de bază a
procesorului sau de viteza relativă a diferitelor procese.
Diferența dintre un proces și un program este subtilă, dar absolut crucială. O analogie vă poate
ajuta aici. Luați în considerare un culinar care gătește un tort pentru fiica sa cea mică de ziua ei
de naștere. Are o rețetă de tort pentru acest eveniment și o bucătărie bine aprovizionată cu tot
necesarul: făină, ouă, zahăr, extract de vanilie și așa mai departe. În această analogie, rețeta
este programul, adică un algoritm exprimat într-o notare adecvată, culinarul este procesorul, iar
ingredientele tortului sunt datele de intrare. Procesul este activitatea constând în citirea rețetei,
preluarea și amestecarea ingredientelor și coacerea tortului.
Acum imaginați-vă că fiul culinarului vine plângând, spunând că a fost înțepat de o albină.
Culinarul fixează locul în care era în rețetă (starea procesului curent este salvat), scoate o carte
de prim ajutor și începe să urmeze indicațiile din ea. Aici vedem că procesorul trece de la un
proces (coacere) la un proces cu prioritate mai mare (administrarea de îngrijiri medicale),
fiecare având un program diferit. Când acordarea ajutorului medical se incheie, culinarul se
întoarce la tortul său, continuând de la punctul în care a fost întrerupt.
Ideea cheie aici este că un proces este o activitate de un anumit fel. Are un program, intrare,
ieșire și o stare. Un singur procesor poate fi împărțit între mai multe procese, un anumit
algoritm de planificare fiind obișnuit să determine când să oprească lucrul la un proces și să îl
servească pe altul. În schimb, un program este ceva ce poate fi stocat pe disc, nu face nimic.
Trebuie remarcat că, dacă un program rulează de două ori, vor avea loc două două procese
distincte. De exemplu, este posibil să porniți un procesor de text de două ori sau să imprimați
două fișiere în același timp, dacă sunt disponibile două imprimante. Faptul că două procese se
execută cu același program nu contează; ele sunt procese distincte. Este posibil ca sistemul de
operare să poată împărtăși codul între ele, astfel încât o singură copie să fie în memorie, dar
acesta este un detaliu tehnic care nu schimbă situația conceptuală a două procese rulate.

2.1.2 Crearea proceselor


Sistemele de operare sunt responsabile de crearea proceselor. În sistemele foarte simple sau
în sistemele proiectate pentru a rula o singură aplicație (de exemplu, regulatorul într-un cuptor
cu microunde), poate fi posibil ca toate procesele care vor fi necesare să fie prezente atunci
când sistemul este pronit. În sistemele cu destinație generală, totuși, este nevoie de o anumită
modalitate de a crea și de a termina procesele conform necesităților reale. Vom analiza în
continuare câteva aspecte legate de crearea și încheierea proceselor.
Evenimentele principale care determină crearea de procese sunt:
1. Initializarea sistemului.
2. Crearea unui proces la solicitarea unui proces curent.
3. O solicitare a utilizatorului pentru a crea un nou proces.
4. Lansarea unei lucrări dintr-un lot .
La lansarea unui sistem de operare, de obicei se creeate numeroase procese. Unele dintre
acestea sunt procese primare, adică procese care interacționează cu utilizatorii (umani) și
efectuează munca pentru aceștia. Altele rulează în fundal și nu sunt asociați cu anumiți
utilizatori, ci au o funcție specifică. De exemplu, un proces de fundal poate fi proiectat pentru a
accepta e-mailurile primite, “dormind” în cea mai mare parte a zilei, dar brusc „trezindu-se”
când soseste un e-mail. Un alt proces de fundal poate fi proiectat pentru a accepta solicitări
pentru paginile Web găzduite de calculatorul respectiv, trezindu-se la sosirea unei cereri care
solicită un serviciu. Procesele care sunt executate în fundal pentru a gestiona o anumită
activitate, cum ar fi e-mail, pagini Web, știri, etc. sunt numite demoni. Sistemele mari au de
obicei zeci de astfel de procese. În UNIX, programul ps poate fi utilizat pentru a lista procesele
rulate. În Windows, managerul de sarcini (task manager) poate fi utilizat în acest scop.
Suplimentar proceselor create la momentul pornirii SO, pot fi create ulterior procese noi.
Adesea, un proces care rulează va emite apeluri de sistem pentru a crea unul sau mai multe
procese noi care să-l ajute să își facă treaba. Crearea de procese noi este deosebit de utilă
atunci când sarcina poate fi formulată în termeni de mai multe procese care interacționează,
dar, care pot fi independente. De exemplu, dacă o cantitate mare de date este preluată printr-o
rețea pentru prelucrarea ulterioară, poate fi convenabil să se creeze un singur proces pentru a
obține datele și a le pune într-un tampon partajat în timp ce un al doilea proces prelucrează
datele. Într-un sistem multiprocesoral, punând la dispoziția fiecărui proces un procesor propriu,
calculele pot fi execitate mai repede.
În sistemele interactive, utilizatorii pot porni un program tastând o comandă sau făcând dublu
clic pe o pictogramă. Va începe un nou proces și va rula programul selectat în el. În sistemele
UNIX noul proces preia fereastra în care a fost pornit. În Windows, atunci când un proces este
început, acesta nu are o fereastră, dar poate crea una (sau mai multe). În ambele sisteme,
utilizatorii pot avea mai multe ferestre deschise simultan, fiecare executând un proces. Utilizând
mouse-ul, utilizatorul poate selecta o fereastră ca să interacționeze cu procesul, de exemplu,
oferind input atunci când este nevoie.
Ultima situație în care sunt create procesele se aplică numai sistemelor de tratare pe loturi,
existente încă în mainframuri. Gândiți-vă la gestionarea stocurilor la sfârșitul unei zile la un lanț
de magazine. Aici utilizatorii pot trimite job-uri în sistem (posibil de la distanță). Când sistemul
de operare decide că are resurse pentru a rula o altă lucrare, creează un nou proces și execută
următoarea lucrare.
Tehnic, în toate aceste cazuri, un nou proces este creat prin faptul că un proces existent
execută un apel de sistem de creare a proceselor. Acest proces poate fi un proces de utilizator,
un proces de sistem invocat de la tastatură sau mouse, sau un proces generat de managerul
lucrărilor în tratarea pe loturi. Ceea ce face acest proces este să execute un apel de sistem
pentru a crea un noul proces. Acest apel de sistem spune sistemului de operare să creeze un
nou proces și indică, direct sau indirect, programul care va rula în el.
În UNIX, există un singur apel de sistem pentru a crea un nou proces: fork. Acest apel creează
o clonă exactă a procesului apelant. După fork, cele două procese, părintele și copilul, au
aceeași imagine în memorie, aceleași variabile de mediu și aceleași fișiere deschise. De obicei,
procesul copilul execută apoi execve sau un apel similar de sistem pentru a-și schimba
imaginea din memorie și a rula un program nou. De exemplu, atunci când un utilizator tastează
o comandă, să zicem, sort, fork-ul generează un proces copil și copilul execută sortarea.
Motivul acestui proces în doi pași este de a permite procesului copil să-și manipuleze
descriptorii de fișiere după fork, dar înainte de execve, pentru a asigura redirecționarea intrării
standard, a ieșirii standard și a erorii standard.
În schimb, în Windows, un singur apel funcțional Win32, CreateProcess, gestionează atât
crearea procesului, cât și încărcarea programului corect în noul proces. Acest apel are 10
parametri, care includ programul care trebuie executat, parametrii liniei de comandă pentru
alimentarea acelui program, diverse atribute de securitate, biți care controlează dacă fișierele
deschise sunt moștenite, informații despre prioritate, o specificație a ferestrei care trebuie
creată pentru proces (dacă există) și un pointer către o structură în care informațiile despre
procesul recent creat sunt returnate apelantului. În afară de CreateProcess, Win32 mai are
aproximativ 100 de funcții pentru gestionarea și sincronizarea proceselor și subiectelor conexe.
În ambele sisteme UNIX și Windows, după crearea unui proces, părintele și copilul au propriile
spații de adresă distincte. Dacă oricare proces schimbă un cuvânt în spațiul său de adrese,
modificarea nu este vizibilă pentru celălalt proces. În UNIX, spațiul de adresă inițial al copilului
este o copie a părintelui, dar există cu siguranță două spații de adresă distincte implicate; nicio
memorie scrisă nu este partajată. Unele implementări UNIX partajează textul programului între
cele două procese, deoarece acesta nu poate fi modificat. În mod alternativ, copilul poate
partaja toată memoria părintelui, dar în acest caz memoria este partajată copy-on-write, ceea
ce înseamnă că, de fiecare dată când oricare dintre cei doi dorește să modifice o parte din
memorie, acea bucată de memorie este copiată în primul rând în mod explicit pentru a se
asigura că modificarea are loc într-o zonă de memorie privată. În Windows, spațiile de adresă
ale părinților și ale copilului sunt diferite de la început.

2.1.3 Terminarea proceselor


Nimic nu este veșnic, nici măcar procesele. Mai devreme sau mai târziu, oricare proces se va
încheia, de obicei în una dintre următoarele situații:
1. Terminare normală (voluntară).
2. Terminare eronată (voluntară).
3. Eroare fatală (involuntară).
4. Terminat de un alt proces (involuntar).
Majoritatea proceselor se termină pentru că și-au “făcut munca”. Când un compilator a compilat
programul care i-a fost dat, compilatorul execută un apel de sistem pentru a spune sistemului
de operare că a încheiat lucrul. Acest apel este exit în UNIX și ExitProcess în Windows.
Programele care au o interfață grafică acceptă, de asemenea, încetarea voluntară.
Procesoarele de text, browserele de internet și programe similare au întotdeauna o pictogramă
sau un element de meniu pe care utilizatorul poate face clic pentru a spune procesului să
elimine orice fișiere temporare pe care le are deschise și apoi să se încheie.
Al doilea motiv pentru terminare este atunci când procesul descoperă o eroare. De exemplu,
dacă un utilizator tastează comanda
cc foo.c
pentru a compila programul foo.c și nu există un astfel de fișier, compilatorul anunță pur și
simplu acest fapt și se oprește. Procesele interactive orientate pe ecran nu ies, în general, când
sunt introduși parametri incorecți. De obicei apare o casetă de dialog care solicită utilizatorului
să încerce din nou.
Al treilea motiv pentru terminare este o eroare cauzată de proces, adesea datorată unei erori
de program. Exemplele includ executarea unei instrucțiuni ilegale, referirea la memoria
inexistentă sau divizarea la zero. În unele sisteme (de exemplu, UNIX), un proces poate spune
sistemului de operare că dorește să se ocupe de anumite erori, caz în care procesul este
întrerupt în loc să se termine când apare una dintre erori.
Al patrulea motiv pentru care un proces s-ar putea încheia este acela că este executat un apel
de sistem care spune sistemului de operare să distrugă acest proces. În UNIX acest apel este
kill. Funcția Win32 corespunzătoare este TerminateProcess. În ambele cazuri, kill-erul trebuie
să posede autorizația necesară pentru a termina procesul victimă. În unele sisteme, când un
proces se încheie, voluntar sau altfel, toate procesele create de acesta sunt imediat terminate.
Însă nici UNIX, nici Windows nu procedează astfel.

2.1.4 Ierarhii de procese


În unele sisteme, când un proces creează un alt proces, procesul părinte și procesul copil
continuă să fie asociate în anumite moduri. Procesul copil poate crea el însuși mai multe
procese, formând o ierarhie de procese. Rețineți că, spre deosebire de plante și animale care
folosesc reproducerea sexuală, un proces are un singur părinte (dar zero, unul, doi sau mai
mulți copii). Deci, un proces este mai mult ca o hidră decât ca, să zicem, o vacă.
În UNIX, un proces și toți copiii și urmașii săi împreună formează un grup de procese. Când un
utilizator trimite un semnal de la tastatură, semnalul este livrat tuturor membrilor grupului de
procese asociate în prezent cu tastatura (de obicei toate procesele active care au fost create în
fereastra curentă). Individual, fiecare proces poate intercepta semnalul, ignora semnalul sau
poate lua măsuri implicite, cum ar fi terminare generată de acest semnal.
În calitate de alt exemplu când ierarhia de procese joacă un rol cheie să analizăm modul cum
are loc inițializarea SO UNIX la pornirea calculatorului. Un proces special, numit init, este
prezent în boot image. Când acesta își începe lucrul, el citește un fișier care spune câte
terminale există. Apoi, creează câte un nou proces pentru fiecare terminal. Aceste procese
așteaptă ca cineva să se conecteze. Dacă o autentificare are succes, procesul de autentificare
execută un shell pentru a accepta comenzile. Aceste comenzi pot porni mai multe procese și
așa mai departe. Astfel, toate procesele din întregul sistem aparțin unui singur arbore, cu init la
rădăcină.
În Windows nu există ierarhie de procese. Toate procesele sunt egale. Singurul indiciu al
ierarhiei unui proces este că atunci când este creat, părintelui i se oferă un jeton special (numit
handle) pe care îl poate folosi pentru a controla procesul copil. Însă, este permisă trecerea
acestui jeton la un alt proces, invalidând astfel ierarhia. Procesele din UNIX nu își pot
dezmoșteni copiii.

2.1.5 Starile unui proces


Deși fiecare proces este o entitate independentă, cu contorul de instrucțiuni propriu al
programului și starea internă, procesele trebuie adesea să interacționeze cu alte procese. Un
proces poate genera o ieșire pe care un alt proces o folosește ca intrare. În comanda shell
cat capitol1 capitol2 capitol3 | grep tree
primul proces, care apare în rezultatul lansării în execuție a programului cat, execută
concatenarea și extragerea a trei fișiere. Al doilea proces - grep, selectează toate liniile din
fișierul-rezultat, care conțin cuvântul '' tree ''. În funcție de viteza relativă a celor două procese
(care depinde atât de complexitatea relativă a programelor, cât și de timpul de procesare al
fiecăruia), se poate întâmpla ca grep să fie gata să funcționeze, dar nu există o intrare care să
poată fi tratată. În acest caz grep tebuie să se blocheze până când va fi disponibilă o intrare.
Un proces se blochează pentru că, logic nu poate continua, de obicei din cauza că așteaptă o
intrare care nu este încă disponibilă. De asemenea, este posibil ca un proces, în principiu gata
pentru a fi executat, să fie stopat de sistemul de operare care a decis să aloce procesorul unui
alt proces pentru o animtă perioadă de timp. Aceste două condiții sunt complet diferite. În
primul caz, suspendarea este inerentă problemei (nu puteți prelucra linia de comandă a
utilizatorului până când nu a fost tastată). Al doilea caz este legat de cadrul tehnic - nu sunt
suficiente procesoare pentru a oferi fiecărui proces propriul procesor. În Fig. 2-2 este
prezentată diagrama stărilor cu trei stări în care un proces se poate afla:
1. Running (utilizând procesorul).
2. Ready (rulare; oprit temporar pentru că procesorul este ocupat de un alt proces).
3. Blocked (incapabil să ruleze până când se întâmplă un eveniment extern).
Fig. 2-2. Diagrama stărilor
Logic, primele două stări sunt similare. În ambele cazuri, procesul este dispus să funcționeze,
doar că în cel de-al doilea, nu există un procesor disponibil. Starea a treia este fundamental
diferită de primele două, prin faptul că procesul nu poate rula, chiar dacă procesorul este liber.
Între aceste trei stări sunt posibile patru tranziții sunt posibile. Tranziția 1 are loc atunci când
sistemul de operare descoperă că un proces nu poate continua. În unele sisteme, procesul
poate executa un apel de sistem, cum ar fi pause, pentru a intra în starea blocat. În alte
sisteme, inclusiv UNIX, când un proces citește dintr-o conductă sau un fișier special (de
exemplu, un terminal) și nu există o intrare disponibilă, procesul este blocat automat.
Tranzițiile 2 și 3 sunt cauzate de planificator - o componentă a sistemului de operare. Tranziția
2 are loc atunci când planificatorul decide că procesul ș-a consumat cuanta de alocare a
procesorului și este timpul să permită unui alt proces să primească timp procesor. Tranziția 3
are loc atunci când toate celelalte procese au avut partea lor corectă de timp de procesor și
este timpul ca următorul proces să obțină procesorul din nou. Planificatorul, adică componenta
care decide ce proces ar trebui să ruleze, când și pentru cât timp, este unul important; o vom
analiza mai târziu în acest capitol. Pentru a răspunde la cerințele concurente de eficiență la
nivel de sistem și asigurarea corectitudinii proceselor individuale au fost elaborați diferiți
algoritmi, unii dintre care vor fi studiați mai târziu în acest capitol.
Tranziția 4 are loc atunci când se produce un eveniment extern așteptat de un proces anterior
blocat. Dacă nu are loc execuția unui alt proces în acel moment, tranziția 3 va fi declanșată și
procesul va începe să funcționeze. În caz contrar, este posibil să fie nevoie să aștepte în starea
Ready un timp până când procesorul va deveni disponibil.
Folosind modelul stărilor, devine mult mai ușor de înțeles ce se întâmplă în interiorul sistemului.
Unele dintre procese rulează programe care efectuează comenzi introduse de un utilizator. Alte
procese fac parte din sistem și gestionează sarcini, cum ar fi efectuarea de solicitări pentru
servicii de fișiere sau gestionarea detaliilor privind rularea unui disc sau a unei unități de
memorie flash. Atunci când are loc o întrerupere a discului, sistemul ia o decizie de a opri
rularea procesului curent și de a rula procesul de disc, care a fost blocat în așteptarea acestei
întreruperi. Astfel, în loc să ne gândim la întreruperi, ne putem gândi la procesele utilizatorilor,
procesele de disc, procesele terminalelor și așa mai departe, care se blochează atunci când
așteaptă să se întâmple ceva. Când operația de citire de pe disc se încheie procesul care
aștepta această terminare va fi deblocat și va putea să ruleze din nou. Această vedere creează
modelul prezentat în Fig. 2-3.
Fig. 2-3. Cel mai inferior nivel al unui sistem de operare structurat pe procese.
Toate detaliile legate de întreruperi, generarea, lansarea în execuție, oprirea proceselor sunt
ascunse în ceea ce este numit aici planificator, care de fapt nu este un fragment prea mare de
cod. Restul sistemului de operare este frumos structurat sub formă de proces. În realitate însă,
puține sisteme sunt la fel de bine structurate ca acesta.

2.1.6 Implementarea proceselor


Pentru a implementa modelul proceselor, sistemul de operare întreține un tabel (o serie de
structuri, înregitrări), numit tabelul proceselor, cu câte o intrare pentru fiecare proces. (Unii
autori numesc o astfel de înregistrare bloc de control al procesului.) Această intrare conține
informații importante despre starea procesului, inclusiv contorul instrucțiunilor, indicatorul stivei,
alocarea memoriei, starea fișierelor deschise, informații statistice și de programare, și orice
altceva despre proces, informații care trebuie salvate atunci când procesul trece de la starea
Running la starea Ready sau Blocked, astfel încât să poată fi repornit mai târziu, ca și cum nu
ar fi fost niciodată oprit.
Gestiunea proceselor Gestiunea memoriei Gestiunea fișierelor
Regiștrii Pointer la segmentul de cod Catalogul rădăcină
Contorul ordinal Pointer la segmentul de date Catalogul curent
PSW Pointer la segmentul stivei Discriptori de fișier
Pointer la stivă ID utilizatorului
Starea procesului ID grupului
Prioritatea
Parametri de planificare
ID procesului
Procesul părinte
Grupul procesului
Semnale
Ora lansării în execuție
Timp de procesor utilizat
Timp de procesor al proceselor
descendente
Timpul următoarei alarme
Fig. 2-4. Câteva din câmpurile unei intrări din tabelul proceselor
Figura 2-4 prezintă câteva dintre câmpurile cheie dintr-un sistem tipic. Câmpurile din prima
coloană se referă la gestionarea proceselor. Celelalte două se referă la gestionarea memoriei și
respectiv la gestionarea fișierelor. Trebuie remarcat: câmpurile pe care le are tabelul proceselor
depinde de sistem, iar această figură oferă o idee generală a tipurilor de informații necesare.
Acum că am analizat tabelul de procese, este posibil să explicăm un pic mai multe despre
modul în care se menține iluzia mai multor procese secvențiale pe un singur procesor. Asociat
cu fiecare clasă de I / O este o locație (de obicei o locație fixă la baza memoriei) numită vector
de întrerupere. Conține adresa procedurii de tratare a întreruperii. Să presupunem că procesul
utilizatorului 3 se execută atunci când se produce o întrerupere a discului. Contorul ordinal al
utilizatorului 3, cuvântul de stare al programului și uneori unul sau mai mulți regiștri sunt puși în
stiva (curentă) de hardware-ul întrerupt. Apoi computerul sare la adresa specificată în vectorul
de întrerupere. Aceasta este tot ce face hardware-ul. De aici încolo totul revine software-ului.
Toate întreruperile încep prin salvarea regiștrilor, adesea în linia tabelului proceselor, care
corespunde procesului curent. Apoi informația pusă pe stivă de întrerupere este eliminată și
indicatorul stivei este setat să indice spre o stivă temporară folosită de managerul proceselor.
Acțiuni precum salvarea regiștrilor și setarea indicelui stivei nu pot fi exprimate în limbaje de
nivel înalt, cum ar fi C, astfel acestea sunt efectuate printr-o rutină în limbaj de asamblare, de
obicei aceeași pentru toate întreruperile, deoarece salvarea regiștrilor este identică, indiferent
care este cauza întreruperii.
Când această rutină este încheiată, este apelată o procedură C pentru a face restul lucrărilor
pentru tipul concret de întrerupere. (Presupunem că sistemul de operare este scris în C,
alegerea obișnuită pentru toate sistemele de operare reale.) Când și-a făcut treaba, eventual
pregătind un proces nou, programatorul este chemat să vadă cine va rula mai departe. După
aceea, controlul este returnat la codul limbajului de asamblare pentru a încărca regiștrii și harta
de memorie pentru procesul curent și pentru a începe execuția acestuia. Tratarea întreruperii și
planificarea sunt rezumate în fig. 2-5. Detaliile diferă de la sistem la sistem.
1. Hardware-ul salvează în stivă contorul ordinal, etc.
2. Hardware încarcă noul contor ordinal din vectorul de întrerupere.
3. Procedura în limbaj de asamblare salvează regiștrii.
4. Procedura în limbajul de asamblare stabilește noua stivă.
5. Serviciul (în C) de tratare a întreruperii (operații de I/E cu bufferul).
6. Planificatorul decide ce proces va urma.
7. Procedura C returnează controlul codului de asamblare.
8. Procedura în limbajul de asamblare pornește noul proces curent.
Fig. 2-5. Acțiunile nivelului inferior al sistemului de operare la apariția unei întreruperi.
Un proces poate fi întrerupt de mii de ori în timpul executării sale, dar ideea cheie este că după
fiecare întrerupere, procesul întrerupt revine la aceeași stare în care se afla înainte de sosirea
întreruperii.

2.1.7 Modelarea multiprogramării


Multiprogramarea asigură sporirea ratei de utilizare a procesorului. De exemplu, dacă un
proces folosește procesorul în medie 20% din timpul în care se află în memorie, atunci având în
memorie simultan cinci procese, procesorul ar trebui să fie ocupat tot timpul. Acest model
optimist este însă nerealist, deoarece presupune în mod tacit că toate procesele nu au operații
de I/E simultane.
Un model mai bun este să analizezi utilizarea procesorului din punct de vedere probabilistic. Să
presupunem că un proces petrece o fracțiune p din timpul său așteptând finalizarea unei
operații de I/E. Cu n procese în memorie simultan, probabilitatea ca toate n procese să aștepte
operații de I/E (caz în care procesorul va fi inactiv) este pn. Utilizarea procesorului este dată de
formula
ρ = 1 - pn
În fig. 2-6 este prezentată dependența ratei de utilizare a procesorului de n, numită gradul de
multiprogramare.
Fig. 2-6. Rata de utilizare a procesorului în funcție de procese în memorie
Din figura 2-6 rezultă clar că, dacă procesele petrec 80% din timp în așteptarea I/E, cel puțin 10
procese trebuie să fie în memorie simultan pentru a obține rata de inactivitate a procesorului
sub 10%. Când vă dați seama că un proces interactiv care așteaptă ca un utilizator să tasteze
ceva la un terminal (sau faceți clic pe o pictogramă) este în stare de așteptare I/O, ar trebui să
fie clar că timpii de așteptare de I/O de 80% și mai mult nu sunt neobișnuiți . Dar chiar și pe
servere, procesele care fac multe operații de I/O de disc vor avea deseori acest procent sau
mai mult.
Din motive de acuratețe, trebuie subliniat faptul că modelul probabilistic tocmai descris este
doar o aproximare. Presupune implicit că toate cele n procese sunt independente, ceea ce
înseamnă că este destul de acceptabil ca un sistem cu cinci procese în memorie să aibă trei
procese în starea de execuție și două în așteptare. Cu un singur procesor, nu putem avea trei
procese care rulează simultan, astfel că un proces care este gata în timp ce procesorul este
ocupat, va trebui să aștepte. Astfel, procesele nu sunt independente. Un model mai precis
poate fi construit folosind teoria sistemelor cu fire de așteptare, dar ideea introdusă –
multiprogramarea, permite proceselor să utilizeze procesorul când acesta este liber - este,
desigur, încă valabilă, chiar dacă adevăratele curbe sunt ușor diferite de cele prezentate în
figura 2-6.
Chiar dacă modelul din Fig. 2-6 este unul puternic simplificat, acesta poate fi totuși utilizat
pentru a face predicții specifice, deși aproximative, despre performanța procesorului. Să
presupunem, de exemplu, că un computer are 8 GB memorie, sistemul de operare și tabelele
sale ocupând 2 GB și fiecare program de utilizator preia de asemenea 2 GB.
Aceste dimensiuni permit trei programe de utilizator să fie în memorie simultan. Cu o așteptare
medie de 80% I/O, avem o utilizare a procesorului (ignorând sistemul de operare) de 1 - 0.83
sau aproximativ 49%. Adăugarea altor 8 GB de memorie permite sistemului să treacă de la
multiprogramarea cu trei procese simultane la multiprogramarea cu șapte procese, ridicând
astfel utilizarea procesorului la 79%. Cu alte cuvinte, cei 8 GB suplimentari vor crește
randamentul cu 30%.
Adăugarea încă 8 GB ar crește utilizarea procesorului doar de la 79% la 91%, crescând astfel
volumul cu doar 12%. Folosind acest model, proprietarul computerului ar putea decide că prima
adăugare a fost o investiție bună, iar a doua nu prea.

2.2 Fire de executie


În sistemele de operare tradiționale fiecare proces are un spațiu de adrese și un singur fir de
control. De fapt, aceasta este aproape definiția noțiunii de proces. Cu toate acestea, în multe
situații, este de dorit să existe mai multe fire de control în același spațiu de adrese care rulează
în cvasi-paralel, de parcă ar fi (aproape) procese separate (cu excepția spațiului de adrese
partajat). În secțiunile următoare vom discuta despre aceste situații și implicațiile acestora.

2.2.1 Utilizarea firelor de executie


De ce ar vrea cineva să aibă un fel de proces în cadrul unui proces? Se pare că există mai
multe motive pentru a avea aceste miniprocese, numite fire. Să examinăm acum unele dintre
ele. Motivul principal pentru a avea fire este că, în multe aplicații, mai multe activități se
desfășoară simultan. Unele dintre acestea se pot bloca din când în când. Prin descompunerea
unei astfel de aplicații în mai multe fire secvențiale care rulează cvasi-paralel, modelul de
programare devine mai simplu.
Am discutat anterior despre acest argument când am motivat necesitatea de a avea procese. În
loc să avem grijă de întreruperi, cronometre și comutarea contextului, folosim procese paralele.
Firele ne permit să adăugăm un element nou: capacitatea entităților paralele de a foloai același
spațiu de adrese și toate datele procesului. Această abilitate este esențială pentru anumite
aplicații, iar soluția de a avea mai multe procese (cu spații de adrese separate) nu este cea mai
bună.
Un al doilea argument pentru a avea fire este că, deoarece acestea au o greutate mai mică
decât procesele, sunt mai ușor de creat și de distrus decât procesele. În multe sisteme, crearea
unui fir este de 10–100 de ori mai rapidă decât crearea unui proces. Când numărul de fire
necesare se schimbă dinamic și rapid, această proprietate este foarte utilă.
Un al treilea motiv pentru a avea fire este performanța. Firele nu aduc câștig de performanță
atunci când au nevoie doar de procesor. Însă, atunci când există (în afara calculelor) I/E
substanțiale, firele pot permite dezlegarea în spațiu a activităților (unele folosesc procesorul,
altele – dispozitivele de I/E, simultan), iar în consecință crește viteza de execuție a aplicației.
În cele din urmă, firele sunt utile în sistemele multiprocesorale, unde este posibil un paralelism
real. Vom reveni la această problemă în cap. 8.
Vom încerca să exemplificăm utilitatea firelor prin câteva exemple concrete. Ca un prim
exemplu fie un procesor de texte. Procesoarele de text afișează, de obicei, documentul creat
pe ecran formatat exact așa cum va apărea pe pagina tipărită. În special, toate spațiile și
paginile sunt în pozițiile corecte și finale, astfel încât utilizatorul să le poată inspecta și să
schimbe documentul, dacă este necesar.
Să presupunem că utilizatorul scrie o carte. Din punctul de vedere al autorului, este mai ușor să
păstrezi întreaga carte ca un singur fișier pentru a ușura căutarea de subiecte, a efectua
substituții globale etc. În mod alternativ, fiecare capitol poate fi un fișier separat. Cu toate
acestea, a avea fiecare secțiune și subsecțiune ca fișier separat este o adevărată problemă
atunci când trebuie efectuate modificări globale pentru întreaga carte. De exemplu, în cazul în
care propunerea standard xxxx este aprobată chiar înainte de apariția cărții, toate aparițiile din
„Proiectul standard xxxx” vor fi schimbate în „Standard xxxx” în ultima clipă. Dacă întreaga
carte este un singur fișier, de obicei o singură comandă poate efectua toate înlocuirile. În
schimb, dacă cartea este distribuită pe 300 de fișiere, fiecare trebuie editat separat.
Să vedem ce se întâmplă când utilizatorul șterge o propoziție de pe pagina 1 a unei cărți de
800 de pagini. După ce a verificat corectitudinea paginii schimbate, el dorește acum să facă o
altă modificare la pagina 600 și introduce o comandă care spune procesorului de texte să
meargă la pagina respectivă (eventual căutând o frază care apare doar acolo). Procesorul de
texte este acum obligat să reformateze întreaga carte până la pagina 600, deoarece nu știe
care va fi prima linie a paginii 600 până când nu va prelucra toate paginile anterioare. Poate
exista o întârziere substanțială înainte de afișarea paginii 600.
Firele ne pot ajuta în acest caz. Să presupunem că procesorul de texte este scris ca un
program cu două fire. Un fir interacționează cu utilizatorul, iar celălalt se ocupă cu reformatarea
în fundal. De îndată ce propoziția este ștersă de la pagina 1, firul interactiv spune firului de
reformatare să reformateze întreaga carte. Între timp, firul interactiv continuă să asculte
tastatura și mouse-ul și răspunde la comenzi simple precum derularea paginii 1, în timp ce
celălalt fir își face lucrul în fundal. Cu puțin noroc, reformatarea va fi finalizată înainte ca
utilizatorul să solicite să vadă pagina 600, astfel încât să poată fi afișată instantaneu.
În timp ce suntem la ea, de ce să nu adăugăm un al treilea fir? Multe procesoare de texte pot
salva automat întregul fișier pe disc la fiecare câteva minute pentru a proteja utilizatorul
împotriva pierderii muncii unei zile în caz de probleme cu programul, blocare a sistemului sau
accident. Al treilea fir poate gestiona copiile de rezervă de pe disc fără a interfera cu celelalte
două. Situația cu trei fire este prezentată în Fig. 2-7.

Fig. 2-7. Un procesor de texte cu trei fire de execuție.


Dacă programul ar fi cu un singur fir, de fiecare dată când fișierul este salvat pe disc, tastatura
și mouse-ul vor fi blocate până la finalizarea operației de scriere pe disc. Utilizatorul ar percepe
cu siguranță acest lucru ca fiind diminuare substanțială a performanței aplicației. În principiu,
evenimentele de la tastatură sau mouse ar putea întrerupe operația de scriere pe disc, sporind
performanța, dar ajungem la un model de programare pilotat de întreruperi. Cu trei fire, modelul
de programare este mult mai simplu. Primul fir interacționează doar cu utilizatorul. Al doilea fir
reformatează documentul atunci când este necesar. Al treilea fir scrie periodic conținutul
fișierului din memoria operativă pe disc. Firele partajează memoria comună și astfel toate au
acces la documentul care este editat. Cu trei procese, acest lucru ar fi imposibil.
Situații analogice pot fi prezente în multe alte programe interactive. De exemplu, o foaie de
calcul electronică este un program care permite utilizatorului să mențină o matrice, dintre care
unele elemente sunt date furnizate de utilizator. Alte elemente sunt calculate pe baza datelor de
intrare folosind formule complexe. Când un utilizator schimbă un element, este posibil să fie
recalculate multe alte elemente. Având un fir de fundal pentru a recalcula, firul interactiv poate
permite utilizatorului să facă modificări suplimentare în timp ce au loc calculele. În mod similar,
un al treilea thread poate gestiona backup-uri periodice pe disc pe cont propriu.
Un alt exemplu de folosire a firelor: un server Web. Pentru majoritatea site-urilor Web este
caracteristic că unele pagini sunt mai frecvent accesate decât altele. De exemplu, pagina
principală a companiei Sony este accesată cu mult mai des decât o pagină din partea de jos a
arborelui site-ului, pagină care conține specificațiile tehnice ale unei anumite camere foto.
Serverele web folosesc acest fapt pentru a îmbunătăți performanța, menținând un set de pagini
intens utilizate în memoria principală pentru a elimina nevoia de a apela de fiecare dată discul
pentru a le obține. O astfel de colecție se numește cache și este folosită și în multe alte
contexte. Am văzut memorii cash ale procesorului în capitolul 1, de exemplu.
Un mod de organizare a serverului Web este prezentat în Fig. 2-8 (a). Aici, un fir, dispecerul,
citește cererile venite din rețea. După examinarea cererii, alege un fir de lucru inactiv (adică,
blocat) și transmite cererea. Dispeceratul apoi trezește “lucrătorul adormit”, transferându-l din
starea Blocat în stare Ready.

Fig. 2-8. Server Web cu mai multe fire


Firul ales pentru a acorda serviciul solicitat, verifică dacă cererea poate fi satisfăcută din
memoria cache a paginii Web, la care au acces toate firele. Dacă nu, începe o operație de citire
pentru a extrage pagina de pe disc și se blochează până când operația de disc se încheie.
Când firul este blocat de operația de citire de pe disc, este lansat în execuție un alt fir, eventual
dispecerul.
Acest model permite serverului să fie scris ca o colecție de fire secvențiale. Programul
dispecerului constă dintr-o buclă infinită care primește solicitări și le transmite unui fir. Codul
fiecărui fir constă dintr-o buclă infinită care acceptă solicitări de la dispecer și verifică cache-ul
Web pentru a vedea dacă pagina solicitată este prezentă. Dacă da, aceasta este returnată
clientului, iar firul se blochează în așteptarea unei noi solicitări. Dacă nu, preia pagina de pe
disc, o transmite clientului și se blochează în așteptarea unei noi solicitări.
În fig. 2-9 este prezentată o schemă aproximativă a codului. Aici, la fel ca în restul acestei cărți,
valoarea de adevăr TRUE este reprezentată prin constanta 1. De asemenea, buf și page sunt
structuri adecvate pentru o solicitare și, respectiv, o pagină Web.
while(TRUE) { while(TRUE) {
get_next_request(&buf); wait_for_work(&buf)
handoff_work(&buf); look_for_page_in_cache(&buf, &page);
} if(page_not_in_cache(&page))
read_page(&page);
}
(a) (b)
Fig. 2-9. Prezentarea schematică a codului (a) firului dispecerului, (b) firului de execuție
Cum ar putea fi compus codul unui server Web în absența firelor? O posibilitate ar fi ca acesta
să fie conceput cu un singur fir. Bucla principală a serverului Web primește o solicitare, o
examinează și o tratează până la finalizare înainte de a primi următoarea solicitare. În timpul
operațiilor de disc serverul este inactiv și nu procesează alte cereri sosite ulterior. Dacă
serverul Web rulează pe o mașină dedicată, așa cum se întâmplă în mod obișnuit, procesorul
este pur și simplu inactiv în timp ce procesul serverului Web așteaptă informația de pe disc. În
rezultat vor fi procesate mult mai puține cereri într-o unitate de timp. Introducerea firelor
conduce la sporirea considerabilă a vitezei de deservire a cererilor, chiar dacă fiecare fir este
executat secvențial, adică în mod obișnuit.
Am discutat până aici despre două modele posibile: un server Web cu mai multe fire și altul cu
un singur fir. Să presupunem că firele nu sunt disponibile, dar proiectanții de sistem nu acceptă
ideea că pierderea de performanță este din cauza unui singur fir. Dacă există o versiune de
apeli de sistem read fără blocare este posibilă o a treia soluție. Când vine o solicitare, singurul
fir o examinează. Dacă se poate satisface din cache, bine, dar dacă nu, o operație nonblocantă
de lucru cu discul începe.
Serverul înregistrează starea cererii curente într-un tabel și trece la următorul eveniment.
Următorul eveniment poate fi fie o solicitare pentru o nouă lucrare, fie o replică de la disc
despre o operațiune anterioară. Dacă este o lucrare nouă, acea lucrare este începută. Dacă
este o replică de pe disc, informațiile relevante sunt preluate din tabel și replica este procesată.
Cu operații de I/E cu discul fără blocare, replica poate fi de forma unui semnal.
Într-o astfel de abordare modelul „proces secvențial” din primele două cazuri este pierdut.
Starea calculelor trebuie salvată și restabilită în mod explicit în tabel de fiecare dată când
serverul trece de la o cerere la alta. De fapt, în așa mod noi simulăm firele și stivele lor într-o
manieră destul de complicată. Un astfel de model, în care fiecare calcul are o stare salvată și
există un set de evenimente care pot apărea pentru a schimba starea, se numește automat
finit. Acest concept este utilizat pe scară largă în informatică.
Acum ar trebui să fie clar ce ne oferă firele. Acestea fac posibilă păstrarea ideii de procese
secvențiale cu apeluri blocante (de exemplu, pentru operații de I/E cu discul) și permit
realizarea paralelismului. Apelurile de sistem cu blocare facilitează programarea, iar
paralelismul îmbunătățește performanța. Serverul cu un singur fir păstrează simplitatea
apelurilor de sistem cu blocare și sporesc performanța. A treia abordare realizează performanțe
ridicate prin paralelism, dar folosește apeluri neblocante și întreruperi fiind mai greu de
programat. Aceste modele sunt rezumate în Fig. 2-10.
Model Caracteristici
Fire de execuție Paralelism, apeluri de sistem cu blocare
Procese cu un cingur fir de execuție Fără paralelism, apeluri de sistem cu blocare
Automate finite Paralelism, apeluri de sistem fără blocare, întreruperi
Fig. 2-10. Trei posibilități de construire a unui server
Un al treilea exemplu în care firele sunt utile este cazul aplicațiilor care trebuie să proceseze
cantități foarte mari de date. Abordarea normală este să le copiați într-un bloc de date, să le
prelucrați, apoi să le scrieți din nou pe disc. Problema aici este că, dacă sunt disponibile doar
apeluri de sistem cu blocare, procesul se blochează în timpul ce datele sunt citite sau scrise.
Faptul că procesorul nu funcționează (este inactiv) atunci când există o mulțime de calcule de
făcut ar trebui evitat dacă este posibil.
Firele oferă o soluție. Procesul ar putea fi structurat cu un fir de intrare, un fir de procesare și un
fir de ieșire. Firul de intrare citește datele într-un tampon de intrare. Firul de procesare scoate
datele din tamponul de intrare, le prelucrează și pune rezultatele într-un tampon de ieșire.
Tamponul de ieșire scrie aceste rezultate înapoi pe disc. În acest fel, intrarea, ieșirea și
procesarea pot continua simultan. Evident, acest model funcționează numai dacă un apel de
sistem blochează doar firul de apelare, nu întregul proces.

2.2.2 Modelul clasic al firelor


Am aflat când firele ar putea fi utile și cum pot fi utilizate. Să investigăm ideea un pic mai
îndeaproape. Modelul procesului se bazează pe două concepte independente: gruparea
resurselor și execuția. Uneori este util să separați aceste concepte; aici intră firele. Mai întâi
vom studia modelul clasic al firelor; după aceea vom examina modelul firelor din Linux, care
voalează linia dintre procese și thread-uri.
Un proces poate fi privit ca o modalitate de a grupa resursele aferente. Un proces are un spațiu
de adrese care conține codul și datele programului, precum și alte resurse. Aceste resurse pot
include fișiere deschise, procese descendent, semnale în așteptare, informații statistice și multe
altele. Punându-le împreună sub forma unui proces, ele pot fi gestionate mai ușor.
Din alt punct de vedere procesul este un fir de execuție. Firul are un contor ordinal care ține
evidența instrucțiunilor de executat. Are registre, cu variabilele sale de lucru actuale. Are o
stivă, care conține istoricul execuției, cu un cadru (mediul curent) pentru fiecare procedură
apelată, dar încă nereturnată. Deși un fir trebuie executat într-un anumit proces, firul și procesul
sunt concepte diferite și pot fi tratate separat. Procesele sunt utilizate pentru a grupa resursele,
firele sunt entități programate pentru execuția pe procesor.
Firele adaugă modelului de proces posibilitatea de a permite efectuarea mai multor execuții în
același mediu al procesului, execuții cu un mare grad de independență. A avea mai multe fire
care rulează în paralel într-un singur proces este analog cu a avea mai multe procese care
rulează în paralel într-un singur computer. În primul caz, firele partajează un spațiu de adrese și
alte resurse. În ultimul caz, procesele partajează memorie fizică, discuri, imprimante și alte
resurse. Deoarece firele au unele dintre proprietățile proceselor, ele sunt uneori numite procese
ușoare. Termenul multitreading este de asemenea utilizat pentru a descrie situația de a permite
mai multe fire în același proces. După cum am văzut în cap. 1, unele procesoare au suport
hardware direct pentru multitreading și permit comutarea firelor în câteva nanosecunde.
În Fig. 2-11 (a) sunt prezentate trei procese tradiționale. Fiecare proces are propriul spațiu de
adrese și un singur fir de control. În schimb, în Fig. 2-11 (b) vedem un singur proces cu trei fire
de control. Deși în ambele cazuri avem trei fire, în Fig. 2-11 (a) fiecare dintre ele operează într-
un spațiu de adrese diferit, în timp ce în Fig. 2-11 (b) toate trei au același spațiu de adrese.

Fig. 2-11. (a) Trei procese fiecare cu câte un fir. (b) Un proces cu trei fire
Când un proces cu mai multe fire este executat pe un sistem cu un singur procesor, firele sunt
executate pe rând. În Fig. 2-1, am văzut cum funcționează multiprogramarea proceselor.
Comutând procesorul între mai multe procese, sistemul oferă iluzia de procese secvențiale
separate care rulează în paralel. Multithreading-ul funcționează la fel. Procesorul comută rapid
înainte și înapoi printre fire, ceea ce oferă iluzia că firele rulează în paralel, doar că pe un
procesor mai lent decât cel real. Cu trei fire într-un proces, firele ar părea să funcționeze în
paralel, fiecare pe un procesor cu o treime din viteza procesorului real.
Firele unui proces nu sunt la fel de independente ca și procesele. Toate firele au exact același
spațiu de adrese, ceea ce înseamnă că, de asemenea, au aceleași variabile globale. Deoarece
fiecare fir poate accesa orice adresă de memorie din spațiul de adrese al procesului, un fir
poate citi, scrie sau chiar șterge stiva unui alt fir. Nu există nicio protecție între fire, deoarece (1)
este imposibil și (2) nu ar trebui să fie necesar. Spre deosebire de procese, care pot fi de la
utilizatori diferiți și care pot fi ostili unele față de altele, un proces este întotdeauna deținut de un
singur utilizator, care, probabil, a creat mai multe fire, astfel încât să poată coopera, nu să lupte.
În afară de partajarea unui spațiu de adrese, toate firele pot partaja același set de fișiere
deschise, procese descendent, alarme și semnale (fig. 2-12). Astfel, organizarea din fig. 2-11
(a) ar fi folosită atunci când cele trei procese sunt în esență fără nici o legătură, în timp ce fig. 2-
11 (b) ar fi adecvată atunci când cele trei fire fac parte din aceeași lucrare și sunt în mod activ
și cooperant strâns legate între ele.
Entități per proces Entități per fir
Spațiul de adrese Contor ordinal
Variabilele globale Regiștri
Fișierele deschise Stiva
Fișierele descendent Starea
Alarme în așteptare
Semnale și handlere de semnal
Informații statistice
Fig. 2-12. (a) Entități comune pentru toate firele unui proces. (b) Entități private ale unui fir
Elementele din prima coloană sunt proprietăți ale procesului, nu proprietăți ale unui fir. Pentru
exemplu, dacă un fir deschide un fișier, acel fișier este vizibil pentru celelalte fire din proces și
pot citi din și scrie în fișier. Acest lucru este logic, deoarece procesul este unitatea de
gestionare a resurselor, nu firul. Dacă fiecare fir ar avea propriul spațiu de adrese, fișiere
deschise, alarme în așteptare și așa mai departe, ar fi un proces separat. Ceea ce încercăm să
obținem cu conceptul fir este posibilitatea ca mai multe fire de execuție să partajeze un set de
resurse, astfel încât să poată colabora strâns pentru a îndeplini o anumită sarcină.
Ca și în cazul unui proces tradițional (de exemplu, un proces cu un singur fir), un fir poate fi în
oricare din mai multe stări: exe, wait, ready sau terminate. Un fir care rulează are în prezent
procesorul și este activ. În schimb, un fir blocat așteaptă un eveniment care să îl deblocheze.
De exemplu, când un fir efectuează un apel de sistem pentru a citi de la tastatură, acesta este
blocat până este introdusă intrarea. Un fir se poate bloca în așteptarea unui eveniment extern
sau un alt fir îl va debloca. Un fir în starea ready este planificat să ruleze și va rula imediat ce îi
va veni rândul. Tranzițiile dintre stările firelor sunt aceleași cu cele dintre stările proceselor și
sunt ilustrate în fig. 2-2.
Este important să memorizăm că fiecare fir are propria sa stivă, așa cum este ilustrat în fig. 2-
13. Stiva fiecărui fir conține cadrul/mediul procedurii curente. Acest cadru conține variabilele
locale ale procedurii și adresa de retur pe care trebuie să o folosească la finalizarea apelului de
procedură. De exemplu, dacă procedura X apelează procedura Y și Y apelează procedura Z,
atunci în timp ce Z se execută, cadrele pentru X, Y și Z vor fi pe stivă. În general, fiecare fir va
apela proceduri diferite și va avea astfel un istoric de execuție diferit. Acesta este motivul pentru
care fiecare fir are nevoie de propria stivă.

Fig. 2-13. Fiecare fir are propria stivă


Când există mai multe fire, procesele încep de obicei cu un singur fir. Acest fir are capacitatea
de a crea noi fire apelând la o procedură de bibliotecă, cum ar fi thread_create. Un parametru
al procedurii thread_create specifică numele unei proceduri responsabile de crearea noului fir.
Nu este necesar (nici posibil) să fie specificat ceva despre spațiul de adrese al noului fir, acesta
rulează automat în spațiul de adrese al firului de creare. Uneori, firele sunt ierarhice, cu o relație
părinte-copil, dar deseori nu există o astfel de relație, toate firele fiind egale. Cu sau fără o
relație ierarhică, firul de creare returnează de obicei identificatorul noului fir.
Când un fir își încheie activitatea, el poate ieși apelând o procedură de bibliotecă, să zicem,
thread_exit. Apoi dispare și nu mai este planificabil. În unele sisteme, un fir poate aștepta
ieșirea unui alt fir (specific) apelând o procedură, de exemplu, thread_join. Această procedură
blochează firul apelant până când fir respectiv a ieșit. În această privință, crearea și terminarea
firului seamănă foarte mult cu crearea și terminarea procesului.
Un alt apel comun de fir este thread_yield, care permite unui fir să renunțe în mod voluntar la
procesor pentru a permite unui alt fir să ruleze. Un astfel de apel este important, deoarece nu
există o întrerupere de ceas pentru a impune efectiv multiprogramarea, așa cum există în cazul
proceselor. Prin urmare, este important ca firele să fie politicoase și să predea voluntar
procesorul din când în când pentru a oferi altor fire șanse să ruleze. Există apeluri care permit
unui fir să aștepte ca un alt fir să termine unele lucrări sau permit să anunțe că un fir a terminat
unele lucrări, etc.
În timp ce firele sunt adesea utile, ele introduc și o serie de complicații în programarea practică.
Vom începe cu apelul de sistem fork dinUNIX. Dacă procesul părinte are mai multe fire,
procesul copil ar trebui să le aibă și el? Dacă nu, procesul poate să nu funcționeze corect,
deoarece acest moment poate fi esențial. Dacă procesul copil primește la fel de multe fire ca și
procesul părinte, ce se întâmplă dacă un fir din procesul părinte era fost blocat la un apel read,
să zicem, de la tastatură? Acum sunt blocate două fire la tastatură, unul la părinte și unul la
copil? Când se introduce o linie, ambele fire primesc o copie a acesteia? Doar părintele? Numai
copilul? Aceeași problemă există cu conexiunile de rețea deschise.
O altă clasă de probleme este legată de faptul că firele partajază multe structuri de date. Ce se
întâmplă dacă un fir închide un fișier în timp ce altul încă citește din el? Să presupunem că un
fir observă că există prea puțină memorie și începe să aloce memorie suplimentară. Tot atunci,
un nou fir este creat și acesta la fel observă că există prea puțină memorie și începe și el să
aloce mai multă memorie. Memoria va fi probabil alocată de două ori. Aceste probleme pot fi
rezolvate cu ceva efort, dar este nevoie de o gândire atentă și de proiectare corectă pentru ca
programele multifir să funcționeze corect.

2.2.3 Fire POSIX


Pentru a face posibilă elaborarea programelor portabile cu fire, IEEE a introdus un standard
pentru fire în standardul IEEE 1003.1c. Pachetul firelor definit se numește Pthreads și este
acceptat de majoritatea sistemelor UNIX. Standardul definește peste 60 de apeluri funcționale,
care sunt mult prea multe pentru a le studia aici. Vom descrie doar câteva dintre cele mai
importante pentru a ne crea o idee despre cum funcționează. Apelurile pe care le vom descrie
mai jos sunt enumerate în fig. 2-14.
Toate firele Pthreads au anumite proprietăți. Fiecare are un identificator, un set de registre
(inclusiv contorul ordinal) și un set de atribute, care sunt stocate într-o structură. Atributele
includ dimensiunea stivei, parametrii de planificare și alte elemente necesare pentru fir.

Apelul firului Descrierea


Pthread_create Crearea unui fir nou
Pthread_exit Terminarea apelului firului
Pthread_join Așteptare pentru pentru ca un fir specific să se termine
Pthread_yield Eliberează procesorul pentru ca un alt fir să fie executat
Pthread_attr_init Crearea și inițializarea unei structuri pentru atributele firului
Pthread_attr_destroy Ștergerea unei structuri ale atributele firului
Fig. 2-14. Câteva apeluri Pthread.
Un nou thread este creat folosind apelul pthread_create. Identificatorul firului nou creat este
returnat ca valoare a funcției. Acest apel este în mod intenționat asemănător cu apelul de
sistem fork (cu excepția parametrilor), identificatorul de fir jucând rolul PID, în mare parte pentru
identificarea firelor referite în alte apeluri.
Atunci când un fir termină lucrul care i-a fost încredințat, acesta se poate încheia apelând
pthread_exit: firul este oprit și stiva acestuia eliberată.
Adesea, un fir trebuie să aștepte un alt fir să-și termina activitatea și să plece înainte ca primul
să poată continua. Firul care trebuie să aștepte apelează pthread_join pentru a aștepta
terminarea firului specificat. Identificatorul firului care trebuie așteptat este dat ca parametru.
Uneori se întâmplă ca un fir nu ar trebui să fie blocat logic, dar dacă a rulat suficient de mult
poate ceda procesorul unui alt fir. În acest scop este folosit apelul pthread_yield. Nu există o
astfel de apel pentru procese, deoarece se presupune că fiecare proces dorește tot timpul
procesorului pe care îl poate obține. Cu toate acestea, din moment ce firele unui proces
lucrează împreună, iar codul lor este scris de același programator, uneori programatorul
dorește ca firele să-și ofere reciproc o altă șansă.
Următoarele două apeluri thread sunt pentru atribute. Pthread_attr_init creează structura pentru
atributele asociate unui fir și o inițializează la valorile implicite. Aceste valori (cum ar fi
prioritatea) pot fi modificate prin manipularea câmpurilor din structura atributelor.
În sfârșit, pthread_attr_destroy elimină structura pentru atributele unui fir, eliberând memoria.
Nu afectează firele care o folosesc la moment; ele continuă să existe.
Pentru a înțelege mai bine cum funcționează Pthreads, urmează un exemplu simplu (fig. 2-15).
Aici, programul principal creează în mod iterativ (de NUMBER_OF_THREADS ori), un fir nou la
fiecare iterație. Dacă crearea firului nu reușește, este tipărit un mesaj de eroare și procesul se
încheie. După crearea tuturor firelor, programul principal se încheie.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUMBER_OF_THREADS 10
void *print_hello_world(void *tid)
{
/Această funcție tipărește identificatorul firului și se oprește.*/
printf(“Hello World. Salutări de la fire %d\n”, tid);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
/* În programul principal sunt create 10 fire după care exit.*/
pthread_t threads[NUMBER_OF_THREADS];
int status, i;
for(i=0; i < NUMBER_OF_THREADS; i++) {
printf(„Programul principal. Creare fire %d\n”, i);
status = pthread_create(&threads[i], NULL, print_hello_world, (void *)i);
if(status != 0) {
printf(“Atenție! Eroare pthread_create %d\n”, status);
exit(-1);
}
}
exit(NULL);
}

Fig. 2-15. Exemplu de program care folosește fire


În acest program, când este creat un fir, acesta tipărește un mesaj pe o singură linie prin care
anunță că a fost creat, apoi iese. Ordinea în care se intercalează mesajele nu este stabilită și
poate varia de la o execuție la alta a programului.
Apelurile Pthreads descrise mai sus nu sunt singure. Vom examina alte apeluri după ce vom
discuta despre sincronizarea proceselor și a firelor.

2.2.4 Implementarea firelor în spațiul utilizatorului


Există două metode de implementare a firelor: în spațiul utilizatorului și în nucleu. Alegerea este
un pic controversată și este posibilă și o implementare hibridă. Vom descrie în continuare
aceste metode, împreună cu avantajele și dezavantajele lor.
Prima metodă este de a pune setul de fire complet în spațiul utilizatorului. Nucleul nu știe nimic
despre fire. În ceea ce privește nucleul, acesta gestionează procese obișnuite, cu un singur fir.
Primul și cel mai evident avantaj este că un set de fire la nivel de utilizator poate fi implementat
pe un sistem de operare care nu lucrează cu firele. Anterior toate sistemele de operare se
încadrau în această categorie, și chiar și acuma unele au rămas acolo. Cu această abordare,
firele sunt implementate de o bibliotecă.
Toate aceste implementări au aceeași structură generală, ilustrată în fig. 2-16 (a). Firele
rulează deasupra unui sistem format dintr-o colecție de proceduri care gestionează firele. Am
văzut deja patru dintre acestea: pthread_create, pthread_exit, pthread_join și pthread_yield, dar
de obicei sunt mai multe.

Fig. 2-16. (a) Set de fire la nivelul user. (b) Set de fire gestionat de kernel
Când firele sunt gestionate în spațiul utilizatorului, fiecare proces are nevoie de propriul tabel de
fire pentru a ține evidența firelor. Acest tabel este analog cu tabelul proceselor al kernel-ului, cu
excepția faptului că ține evidența numai a proprietăților per-fir, cum ar fi contorul ordinal al
fiecărui fir, indicatorul stivei, regiștrii, starea și altele. Tabelul firelor este gestionat de sistemul
de rulare. Când un fir este trecut în stare ready sau wait, informațiile necesare pentru a-l reporni
sunt stocate în tabelul firului, exact la fel cum nucleul salvează informații despre procese din
tabelul proceselor.
Când un fir face ceva care îl poate determina să fie blocat local, de exemplu, în așteptarea unui
alt fir din același proces, este apelată o procedură a sistemului. Această procedură verifică
dacă firul trebuie pus în stare blocat. Dacă da, sunt salvați regiștrii firelor în tabelul firelor, caută
în tabel un fir pregătit pentru a fi rulat și reîncărcă regiștrii cu valorile salvate anterior ale noului
fir. După ce indicatorul stivei și contorul ordinal sunt comutate, noul fir revine la viață automat.
În cazul în care calculatorul are o instrucțiune pentru a salva toate registrele și alta pentru a le
încărca, comutarea firelor poate fi realizată doar prin câteva instrucțiuni. Efectuarea comutării
firelor în acest mod este mult mai rapidă decât apelarea la serviciile nucleului - argument
puternic în favoarea firelor la nivel de utilizator.
Cu toate acestea, există o diferență cheie în raport cu procesele. Atunci când un fir se oprește
pentru un moment, de exemplu, atunci când apelează thread_yield, codul lui thread_yield poate
salva informațiile firului în tabelul propriu-zis al firelor. Mai mult, poate apela apoi planificatorul
firelor pentru a alege un alt fir care să fie rulat. Procedura care salvează starea firului și
planificatorul sunt doar proceduri locale, astfel încât invocarea acestora este mult mai eficientă
decât efectuarea unui apel la kernel. Printre alte probleme, nu este necesară o operație trap, nu
este nevoie de un comutator de context, nu este necesară vidarea memoriei cache și așa mai
departe. Toate acestea fac planificarea firelor foarte rapidă.
Firele la nivel de utilizator au și alte avantaje. Ele permit fiecărui proces să aibă propriul
algoritm personalizat de planificare. Pentru unele aplicații, de exemplu, cele cu fir pentru
garbage-collector, nu trebuie să vă faceți griji cu privire la oprirea unui fir într-un moment. De
asemenea, obținem o scalabilitate mai bună, deoarece firele în nucleu necesită un anumit
spațiu pentru tabele și stivă, ceea ce poate fi o problemă dacă există un număr mare de fire.
Cu performanțe mai bune, firele la nivel de utilizator au, totuși, unele probleme majore. Prima
este problema modului în care sunt implementate apelurile blocante de sistem. Să presupunem
că un fir citește de la tastatură la apăsarea unei taste. Să pemitem acestui fir să execute efectiv
apelul de sistem este inacceptabil, deoarece acest lucru va opri toate celelalte fire. Unul dintre
obiectivele principale referitor la fire era să se permită fiecăruia să utilizeze apelurile de blocare,
dar să împiedice un fir blocat să-i afecteze pe ceilalți. Cu apelurile de sistem blocante este greu
de atins acest obiectiv.
Apelurile de sistem ar putea fi schimbate pentru a fi non-blocante (de exemplu, un read de la
tastatură ar returna doar 0 octeți dacă nu au fost introduse caractere în bufer), însă numărul de
modificări care trebuie intrpduse în sistemul de operare este prea mare. În plus, un argument
pentru ca firele să fie la nivel de utilizator a fost tocmai faptul că acestea să poată fi rulate cu
sistemele de operare existente. În plus, schimbarea semanticii operației de citire va necesita
modificări la multe programe de utilizator.
O altă alternativă este disponibilă în cazul în care este posibil să se anunțe în avans dacă un
apel va conduce la blocare. În majoritatea versiunilor UNIX, există apelul de sistem select, care
spune apelantului dacă o citire viitoare se va bloca. Când este prezent acest apel, procedura de
citire din bibliotecă poate fi înlocuită cu una nouă, care face mai întâi un apel select după care
va urma apelul read doar dacă acesta nu se va bloca. Dacă apelul read se va bloca, el nu va fi
efectuat. În schimb, va fi rulat un alt fir. Încercarea de citire va fi repetată ulterior. Această
abordare necesită rescrierea unor părți ale bibliotecii de apeluri de sistem și este ineficientă și
lipsită de elegantă, însă nu există prea multe opțiuni. Codul plasat în jurul apelului de sistem
pentru a testa la blocare se numește sacou sau înveliș.
Oarecum analog cu problema apelurilor de sistem blocante este problema erorilor din pagină.
Vom studia acestea în cap. 3. Pentru moment, este suficient să spunem că calculatoarele pot fi
configurate astfel încât nu toate programele să fie în memoria principală simultan. Dacă
programul apelează sau sare la o instrucțiune care nu este în memorie, apare o eroare de
pagină și sistemul de operare va căuta pagina cu instrucțiunea lipsă pe disc. Aceasta se
numește o eroare a paginii sau excepție de tip pagină lipsă. Procesul este blocat în timp ce
pagina cu instrucțiunea necesară este localizată și citită. Dacă un fir provoacă o defecțiune de
acest tip, kernelul, neștiind de existența firelor, blochează întregul proces până când operația
de I/E este finalizată, chiar dacă alte fire ar putea fi rulate.
O altă problemă a firelor la nivel de utilizator este că, dacă începe execuția unui fir, niciun alt fir
din acest proces nu va rula decât dacă primul fir va renunța voluntar la procesor. În cadrul unui
singur proces, nu există întreruperi de ceas, ceea ce face imposibilă programarea proceselor în
mod round-robin.
O posibilă soluție a problemei firelor „veșnice” este ca sistemul să solicite un semnal de ceas
(întrerupere) o dată pe secundă pentru a controla firul, dar aceasta nu este ușor de programat.
Întreruperile periodice de ceas la o frecvență mare nu sunt întotdeauna posibile și, chiar dacă
sunt, cheltuielile suplimentare pot fi substanțiale. În plus, un fir poate avea nevoie de o
întrerupere de ceas, care interferează cu utilizarea ceasului în timpul execuției.
Un alt argument, cel mai devastator, argument împotriva firelor la nivel de utilizator este că
programatorii doresc, de obicei, folosirea firelor exact în aplicațiile în care firele se blochează
frecvent, cum ar fi, de exemplu, un server Web multithreaded. Aceste fire efectuează în mod
constant apeluri de sistem. Odată ce s-a produs o adresare la kernel pentru a efectua apelul de
sistem, este greu și cu cheltuieli suplimentare substanțiale kernel schimbă firele dacă un alt fir
era blocat anterior. Dacă kernel-ul face acest lucru este eliminată nevoia de a efectua în mod
constant apeluri select pentru a verifica dacă apelurile read sunt în siguranță. În cazul
aplicațiilor care execută doar calcule și rareori se blochează nu există nici un motiv să folosim
fire. Nimeni nu și-ar propune în mod serios să calculeze primele n numere prime sau să joace
șah folosind fire, deoarece nu se poate câștiga nimic în acest fel.

2.2.5 Implementarea firelor în nucleu


În cazul acestui model kernelul știe despre fire și le gestionează. Nu este necesar un sistem
run-time pentru fiecare proces, așa cum se arată în Fig. 2-16 (b). De asemenea, nu există nici
un tabel al firelor în fiecare proces. În schimb, kernelul are un tabel care ține evidența tuturor
firelor din sistem. Când un fir dorește să creeze un fir nou sau să distrugă unul existent, acesta
face un apel al kernel-ului, care apoi creează sau distruge prin actualizarea tabelului firelor.
Tabelul firelor nucleului conține regisștrii, starea și alte informații ale fiecărui fir. Informațiile sunt
aceleași ca și în cazul firelor la nivel de utilizator, dar acum sunt păstrate în kernel în loc de
spațiul utilizatorului (în interiorul lui run-time system). Această informație este un subset al
informațiilor pe care nucleele tradiționale le păstrează despre procesele cu un singur fir, adică
starea procesului. În plus, nucleul menține, de asemenea, tabelul tradițional al proceselor
pentru a ține evidența proceselor.
Toate apelurile care ar putea bloca un fir sunt implementate ca apeluri de sistem, cu un cost
considerabil mai mare decât un apel către o procedură de sistem. Când un fir se blochează,
nucleul, la alegerea sa, poate rula fie un alt fir din același proces (dacă unul este gata), fie un fir
dintr-un proces diferit. Cu ajutorul firelor la nivel de utilizator, sistemul menține rularea firelor
dintr-un proces până când kernel-ul retrage proccesorul de la acesta (sau nu mai există fire
pregătite pentru a fi executate).
Datorită costului relativ mare al aperațiilor de creare și distrugere a firelor în spațiul nucleului,
unele sisteme adoptă o abordare “ecologică” și își reciclează firele. Când un fir este distrus,
acesta este marcat ca neexecutabil, dar structurile sale de date nu sunt afectate. Mai târziu,
când trebuie creat un fir nou, un fir vechi este reactivat, minimizând cheltuielile. Reciclarea
firelor este de asemenea posibilă la nivel de utilizator, dar din moment ce cheltuielile pentru
gestionarea firelor sunt mult mai mici, există un stimulent mai mic în acest sens.
Firele de kernel nu necesită noi apeluri de sistem non-blocante. În plus, dacă un fir dintr-un
proces provoacă o excepție de tip pagină lipsă, kernel-ul poate verifica cu ușurință pentru a
vedea dacă procesul are alte fire ready și, dacă da, lansează execuția unuia dintre ele în timpul
așteptării încheierii operației de citire a paginii lipsă. Dezavantajul principal al acestora este că
costul unui apel de sistem este mare, astfel operațiunile cu firele (creare, terminare etc.) vor
genera mult mai multe cheltuieli.
Firele în spațiul nucleului rezolvă unele probleme, dar nu toate. De exemplu, ce se întâmplă
când un proces multifir execută fork? Noul proces are la fel de multe fire ca cel vechi, sau are
doar unul? În multe cazuri, cea mai bună alegere depinde de ce intenționează să facă procesul
în continuare. Dacă va apela exec pentru a porni un program nou, probabil că un singur fir este
alegerea corectă, dar, dacă acesta continuă execuția, reproducerea tuturor firelor este probabil
cea mai bună alegere.
O altă problemă sunt semnalele. Nu uitați că semnalele sunt trimise la procese, nu la fire, cel
puțin în modelul clasic. Când vine un semnal, ce fir trebuie să-l gestioneze? Unele fire ar putea
să-și înregistreze interesul pentru anumite semnale, astfel încât, atunci când vine un semnal,
acesta să fie dat firului care a spus că îl dorește. Dar ce se întâmplă dacă două sau mai multe
fire se înregistrează pentru același semnal? Acestea sunt doar două dintre problemele pe care
le introduc firele. În realitate sunt mult mai multe.

2.2.6 Implementări hibride


Au fost cercetate diverse moduri pentru a încerca îmbinarea avantajelor firelor la nivel de
utilizator cu firele la nivel de nucleu. Una din modalități este să se pornească cu utilizarea firelor
la nivel de kernel și apoi să se treacă la firele la nivel de utilizator, multiplexân, la necesitate
unele sau toate (fig. 2-17).
În cazul acestei abordări, programatorul poate determina câte fire de nucleu să folosească și
câte de nivel de utilizator să fie multiplexate. Acest model oferă flexibilitate înaltă.
Cu această abordare, nucleul este responsabil doar de firele de la nivelul său. Unele dintre
aceste fire pot avea mai multe fire corespondente la nivel de utilizator. Firele de la nivelul de
utilizator sunt create, distruse și programate la fel ca firele la nivel de utilizator într-un proces
care rulează pe un sistem de operare fără capacitate de multithreading. În acest model, fiecare
fir la nivel de nucleu are un set coresondent de fire la nivel de utilizator.
Fig. 2-17. Multiplexarea firelor

2.2.7 Activizarea planficatorului


Deși la anumiți indicatori cheie firele la nivelul nucleului întrec firele de la nivel de utilizator, ele
sunt fără discuții, mult mai lente. Din această cauză cercetătorii au căutat modalități de a
îmbunătăți situația fără a pierde proprietățile lor bune. În continuare vom descrie o abordare
concepută de Anderson și colab. (1992), numită activizarea planficatorului. Acest moment este
analizat în Edler și colab (1988) și Scott și colab. (1990).
Obiectivele operațiilor de activizare a planificatorului sunt de a imita posibilitățile funcționale ale
firelor de la nivelul nucleului, dar cu o mai mare productivitate și flexibilitate, caracteristică
pachetelor de fire implementate în spațiul utilizatorului. În particular, firele de utilizator nu
trebuie să efectueze apeluri speciale nonblocante de sistem sau să verifice în avans dacă este
sigur să efectueze un anumit apel de sistem. Cu toate acestea, atunci când un fir se blochează
pe un apel de sistem sau pe o eroare de pagină, trebuie să existe posibilitatea de a executa un
alt fir în cadrul aceluiași proces, dacă există cel puțin un fir în starea ready.
Eficiența se obține prin evitarea tranzițiilor inutile între spațiile utilizator și user. De exemplu,
dacă un fir se blochează în așteptarea anumitor acțiuni ale unui alt fir, adresarea la nucleu nu
are sens și sunt posibile economii prin neexecutarea tranziției kernel-user. Sistemul de rulare
(run-time) din spațiul utilizatorului poate bloca firul de sincronizare și poate planifica executarea
unui alt fir.
Când se utilizează activizarea planificatorului, kernelul alocă un anumit număr de procesoare
virtuale fiecărui proces, iar sistemului de rulare din spațiul utilizatorului i se permite să
repartizeze fire procesoarelor. Acest mecanism poate fi utilizat și în cazul unui sistem
multiprocesoral în care procesoarele virtuale sunt procesoare reale. Numărul procesoarelor
virtuale alocate unui proces este inițial unul, dar procesul poate solicita mai multe și poate
returna procesoare de care nu mai are nevoie. Nucleul de asemenea poate prelua
procesoarele virtuale alocate deja pentru a le atribui proceselor mai nevoiașe.
Operabilitatea acestei scheme este determinată de următoarea idee de bază: atunci când
nucleul știe că un fir este blocat (de exemplu, din cauza unui apel de sistem de blocare sau a
apărut o eroare la accesarea unei pagini inexistente), el notifică sistemul de rulare care aparține
procesului, transmițând prin stivă ca parametri numărul acestui fir și descrierea evenimentului
care a avut loc. Notificarea se produce prin faptul că nucleul activizează sistemul de rulare de la
o adresă de de start cunoscută, aproximativ analog cu semnalele din UNIX. Acest mecanism se
numește upcall (apelare sus).
Activat în așa mod, sistemul de rulare își poate replanifica firele, de obicei prin trecerea firulului
curent în starea blocat și alocarea procesorului unui un alt fir din lista firelor ready, setarea
valorilor regiștrilor și repornirea acestuia. Mai târziu, când nucleul află că firul inițial poate rula
din nou (de exemplu, conducta (pipe) din care încerca să citească conține deja datele așteptate
sau pagina lipsă a fost adusă de pe disc), el va face un alt apel upcall în adresa sistem de
rulare pentru a-l informa despre acest eveniment. Sistemul de rulare poate reporni imediat firul
blocat sau îl poate introduce în lista firelor ready pentru a fi rulat ulterior.
Când se produce o întrerupere hardware în timpul rulării unui fir de utilizator, procesorul
întrerupt trece în modul kernel. Dacă întreruperea este cauzată de un eveniment care nu
interesează procesul întrerupt, cum ar fi finalizarea unei operații de I/E a unui alt proces, atunci
la terminarea tratării întreruperii, handlerul de întrerupere va restabili firul întrerupt în starea în
care se afla înainte de întrerupere. Dacă, însă, procesul este interesat în această întrerupere,
cum ar fi sosirea unei pagini necesare unuia dintre firele procesului, firul întrerupt nu este
repornit. În acest caz firul întrerupt este suspendat, iar pe acel proces este pornit sistemul de
rulare cu starea firului întrerupt pe stivă. Apoi, sistemul de rulare trebuie să decidă ce fir să
planifice pe acel procesor: cel întrerupt, cel nou pregătit sau o a treia alegere.
O obiecție față de metoda de activizare a planificatorului este dependența totală a acestei
tehnologii de apelurile upcall, concept care încalcă structura caracteristică oricărui sistem
multinivel. În mod normal, nivelul n oferă anumite servicii la care poate apela nivelul n + 1, dar
nu este permis ca nivelul n să apeleze la procedurile nivelului n + 1. Upcall-urile nu respectă
acest principiu fundamental.

2.2.8 Fire de executie de tip pop-up


Firle sunt frecvent utilizate în sistemele distribuite. Un exemplu bun în acest sens poate fi
tratarea mesajelor de intrare, cum ar fi cererile de servire. Abordarea tradițională este utilizarea
unui proces sau fir, pe care apelul de sistem receive îl va fi blocat în așteptarea mesajului de
intrare. În momentul în care mesajul sosește, procesul/firul îl acceptă, îl despachetează, îi
verifică conținutul și efectuează procesări ulterioare.
Este posibilă și o abordare complet diferită, în care sosirea unui mesaj obligă sistemul să
creeze un fir nou pentru a-l prelucra. Un astfel de flux (figura 2.18) este numit flux pop-up.
Principalul avantaj al fluxurilor pop-up este că sunt create de la zero și nu au istorie - nici
regiștri, nici stivă și orice altceva care ar trebuie restabilit. Fiecare dintre aceste fire începe de la
zero și firele sunt identice. Aceasta permite crearea unor astfel de fire destul de rapid. Un nou
fir preia un mesaj pentru procesarea ulterioară. Prin utilizarea fluxurilor pop-up, întârzierea
dintre sosire și începutul procesării mesajelor poate fi redusă la minimum.
Atunci când sunt utilizate fluxuri pop-up este necesară o planificare prealabilă. De exemplu,
apare întrebarea: în care proces să ruleze firul? Dacă sistemul acceptă fire care rulează în
spațiul kernelului, un fir poate rula în acel spațiu (motiv pentru care kernelul nu este prezent în
figura 2.18). În general, este mai ușor și mai rapid să fie lansat un fir pop-up în spațiul kernel-
ului decât în spațiul utilizatorului. În plus, firul pop-up în spațiul kernelului are acces ușor la
toate tabelele nucleului și la dispozitivele de I/E de care ar putea avea nevoie pentru a gestiona
întreruperea. În același timp, un fir defect în spațiul kernelului poate face mai multe daune decât
același fir în spațiul utilizatorului. De exemplu, dacă execuția luidurează prea mult și nu există
nicio modalitate de retragere a procesorului, datele de intrare pot fi pierdute pentru totdeauna.
Fig. 2-18. Crearea firului nou la sosirea unui mesaj.
(a) Înaintea sosirii mesajului. (b) După sosirea mesajului.

2.2.9 Transformarea codului monofir în cod multifir


Multe dintre programele existente au fost create pentru procese cu un singur fir. Transformarea
lor în programe cu mai multe fire este mult mai dificilă decât ar putea părea la prima vedere.
Mai jos vom analiza doar câteva dintre capcanele existente.
Vom începe cu faptul că, codul unui fir, la fel ca și codul unui proces, este compus de obicei din
mai multe proceduri, care pot avea variabile și parametri locali și globali. Cei locali nu creează
probleme, apar probleme cu acele variabile care sunt de natură globală pentru fir, dar nu și
pentru întregul program. Globalitatea acestor variabile este aceea că sunt folosite de mai multe
proceduri în interiorul unui fir (deoarece procedurele pot utiliza orice variabilă globală), dar din
punct de vedere logic alte fire ar trebui să le lase în pace.
Să luăm ca exemplu variabila erno din UNIX. Când un proces (sau un fir) face un apel de
sistem care eșuează, un cod de eroare este plasat în errno. În fig. 2.19 firul 1 execută apelul de
sistem access pentru a determina dacă este permis accesul la un anumit fișier.

Fig. 2-19. Conflicte între fire la utilizarea unei variabile globale.


Sistemul de operare returnează răspunsul în variabila globală errno. După returnarea
controlului la firul 1, dar înainte de a citi valoarea errno, planificatorul decide că firul 1 a folosit
suficient timp de procesor în acest moment și procesrul ar trebui să treacă la firul 2. Firul 2 face
un apel open, care suferă eșec, ceea ce face ca în erno să fie pusă o altă valoare. Ca rezultat,
codul access al primului fir este pierdut pentru totdeauna. Când un pic mai târziu firul 1 își reia
execuția, valoarea din erno pentru acest fir va fi incorectă, deci și firul se va comporta incorect.
Există diferite modalități de a rezolva această problemă. Puteți dezactiva în totalitate utilizarea
variabilelor globale. Oricât de tentantă este această idee, ea intră în conflict cu multe programe
existente. O altă modalitate este de a atribui fiecărui fir propriile variabile globale (figura 2.20).
În acest caz, pentru a evita conflictele, fiecare fir are o copie proprie (copie privată) a variabilei
errno și pentru alte variabile globale. Ca urmare, este creat un nou nivel de domeniu (scoping
level), în care variabilele sunt vizibile pentru toate procedurile dintr-un fir (dar nu sunt vizibile
pentru alte fire), pe lângă nivelul domeniul deja existent, unde variabilele sunt vizibile doar
pentru o procedură și unde variabilele sunt vizibile de oriunde în program.

Fig. 2-20. Fire cu variabile globale proprii


Accesarea variabilelor globale private este un pic cam complicată, deoarece majoritatea
limbajelor de programare au modalități de declarare a variabilelor locale și a variabilelor
globale, dar nu și forme intermediare. Este posibilă alocarea unei zone de memorie pentru
variabilele globale și de o pasat fiecărei proceduri a firului ca parametru suplimentar. Nu prea
elegant, dar funcționează.
În mod alternativ, pot fi introduse noi proceduri de bibliotecă pentru a crea, seta și citi din fire
aceste variabile globale. Primul apel ar putea arăta astfel:
create global ("bufptr");
Este alocat spațiu pentru stocarea unui indicator numit bufptr pe heap sau într-o zonă
specială de stocare rezervată firului de apelare. Indiferent unde este alocată memoria, numai
firul apelant are acces la variabila globală. Dacă un alt fir creează o variabilă globală cu același
nume, acesta primește o locație de stocare diferită care nu intră în conflict cu cea existentă.
Două apeluri sunt necesare pentru a accesa variabile globale: unul pentru scriere și altul pentru
citire. Pentru scriere va fi ceva de genul set global("bufptr", &buf); care salvează
valoarea unui indicator în locația creată de apelul anterior. Pentru a citi o variabilă globală,
apelul ar putea arăta astfel: bufptr = read global ("bufptr"); care returnează
adresa salvată în variabila globală, astfel încât datele sale să poată fi accesate.
Următoarea problemă în transformarea unui program cu un singur fir într-unul multithreaded
este faptul că multe proceduri de bibliotecă nu sunt reentrante. Adică nu au fost concepute
pentru a efectua un al doilea apel la vreo procedură inițială, în timp ce un apel anterior nu s-a
încheiat încă. De exemplu, trimiterea unui mesaj prin rețea poate consta din asamblarea lui într-
un tampon fix din bibliotecă, apoi adresarea către kernel pentru a-l expedia. Ce se întâmplă
dacă un fir și-a asamblat mesajul în buffer, iar o întrerupere de ceas forțează trecerea la un al
doilea fir care suprascrie imediat bufferul cu propriul mesaj?
În mod similar, procedurile de alocare a memoriei, cum ar fi malloc în UNIX, mențin cele mai
importante tabele despre utilizarea memoriei, de exemplu, sub forma unor liste legate de zone
de memorie disponibile. În timp ce malloc este ocupat cu actualizarea acestor liste, acestea pot
fi temporar într-o stare inconsistentă, cu indicatoare care nu indică nicăieri. Dacă o comutare de
fir are loc în timp ce tabelele sunt inconsistente și un nou apel vine dintr-un fir diferit, poate ave
loci folosirea unui pointer invalid, ceea ce duce la o blocare a programului. Rezolvarea eficientă
a tuturor acestor probleme înseamnă rescrierea întregii biblioteci. Nu este o activitate simplă cu
multe posibilități reale de a introduce erori suplimentare.
O altă soluție este de a oferi fiecărei proceduri un jacket care setează un bit pentru a marca
faptul că biblioteca este în utilizare. Orice încercare pentru un alt fir de a utiliza o procedură de
bibliotecă în timp ce un apel anterior nu a fost încă finalizat este blocat. Deși această abordare
ar putea fi realizată, în acest caz va fi diminuat substanțial paralelismul potențial.
În continuare vom analiza semnalele. Unele semnale sunt în mod logic specifice firelor, în timp
ce altele nu. De exemplu, dacă un fir apelează apelul de sistem alarm, are sens ca semnalul
rezultat să meargă la firul care a făcut apelul. Cu toate acestea, atunci când firele sunt
implementate în întregime în spațiul utilizatorului, nucleul nici măcar nu știe despre fire și cu
greu poate direcționa semnalul către firul potrivit. O complicație suplimentară apare dacă un
proces poate avea o singură alarmă în așteptare la un moment dat și mai multe fire apelează
alarma independent.
Alte semnale, cum ar fi întreruperile de la tastatură, nu sunt asociate logic cu firele. Cine ar
trebui să le intercepteze? Un fir special desemnat? Toate firele? Sau poate un fir pop-up nou
creat? Mai mult, ce se întâmplă dacă un fir schimbă handlerele de semnal fără a informa
despre aceasta alte fire? Și ce se întâmplă dacă un fir vrea să intercepteze un anumit semnal
(să zicem, utilizatorul care introduce CTRL-C), iar un alt fir dorește ca acest semnal să încheie
procesul? Această situație poate apărea dacă unul sau mai multe fire execută proceduri de
bibliotecă standard, iar altele sunt scrise de utilizator. Evident, sunt dorințe incompatibile. În
general, semnalele sunt destul de dificil de gestionat într-un mediu cu un singur fir. Trecerea
într-un mediu multithreaded nu le face mai ușor de gestionat.
O ultimă problemă introdusă de thread este gestionarea stivelor. În multe sisteme, când stiva
unui proces devine plină, nucleul oferă în mod automat mai multă memorie. Când un proces
are mai multe fire, trebuie să aibă, de asemenea, mai multe stive. Dacă nucleul nu știe de toate
aceste stive, nu le poate crește automat atunci când este necesar. De fapt, nucleul poate nici
nu-și dă seama că eroarea de memorie este legată de creșterea stivei unui fir.
Aceste probleme cu siguranță nu sunt insurmontabile, dar arată că introducerea firelor într-un
sistem existent fără un redesign substanțial al sistemului nu va merge. Minimum semantica
apelurilor de sistem ar trebui să fie redefinită și bibliotecile rescrise. Și toate acestea trebuie
făcute astfel încât să fie asigurată compatibilea cu programele existente. Pentru informații
suplimentare despre fire, consultați Hauser și colab. (1993), Marsh și colab. (1991) și
Rodrigues et al. (2010).
2.3 Comunicarea între procese
Procesele trebuie să comunice frecvent cu alte procese. De exemplu, într-o conductă de shell,
ieșirea primului proces trebuie legată de intrarea la al doilea proces, și așa mai departe în linie.
Astfel, este nevoie de comunicare între procese, de preferință într-un mod bine structurat, fără
folosirea întreruperilor. În secțiunile următoare vom analiza unele dintre problemele legate de
această comunicare între procese (InterProces Communication sau IPC).
Vom studia trei momente. Primul este modul în care un proces transmite informația către alt
proces. Al doilea este asigurarea lucrului în comun a două sau mai multe procese fără a crea
impedimente reciproce, de exemplu, două procese dintr-un sistem de rezervare a biletelor de
avion, fiecare proces încercând să prindă ultimul loc dintr-un avion pentru un clienți diferiți. Al
treilea se referă la secvențierea corfectă atunci când există interdependențe: dacă procesul A
produce date și procesul B le tipărește, B trebuie să aștepte până când A finalizat lucrarea
înainte de a începe tipărirea. Vom examina toate aceste trei probleme începând din secțiunea
următoare.
Este important de menționat că două dintre aceste momente sunt prezente și în cazul firelor.
Primul - transmitereainformației – în cazul firelor are o soluție mult mai simplă în cazul firelor,
deoarece acestea împărtășesc un spațiu de adrese comun (firele din spații de adrese diferite
care trebuie să comunice se încadrează la categoria comunicarea proceselor). Însă, celelalte
două – excluderea impedimentelor reciproce și secvențierea corectă – sunt exact în aceeași
situație ca și în cazul proceselor - aceleași probleme și aceleași soluții. În continuare
problemele vor fi analizate în contextul proceselor, dar trebuie să rețineți că aceleași probleme
și soluții se aplică și referitor la fire.

2.3.1 Conditii de concurență


În unele sisteme de operare, procesele care lucrează în comun pot utiliza un depozit comun de
stocare a datelor partajate, accesibil pentru fiecare proces pentru citire și scriere. Acest spațiu
de stocare partajat poate fi localizat în memoria operativă (posibil într-o structură de date a
kernelului) sau poate fi reprezentat printr-un fișier partajat. Locația memoriei partajate nu
schimbă natura interacțiunii sau problemele care apar. Pentru a vedea cum interacționează
procesele în practică, să analizăm un exemplu simplu, binecunoscut - un spooler de imprimare.
Când un proces trebuie să imprime un fișier, acesta pune numele acelui fișier într-un director
special din spooler.
Un alt proces numit demonul imprimantei verifică periodic fișierele de imprimat și, dacă este
cazul, tipărește câte unul și îl elimină din director. Imaginați-vă că directorul spoolerului are un
număr mare de zone de memorie numerotate 0, 1, 2 ..., fiecare putând stoca un nume de fișier.
De asemenea, imaginați-vă că există două variabile comune: out, care indică următorul fișier
care va fi tipărit și in, care indică următoarea zonă liberă din director. Aceste două variabile ar
putea fi stocate într-un fișier cu două cuvinte, fișier accesibil tuturor proceselor. La un moment
dat, zonele de la 0 la 3 sunt goale (fișierele au fost deja tipărite). Aproape simultan, procesele A
și B decid că trebuie să introducă în firul de așteptare la printer numele fișier pentru imprimare.
Într-un cadru legal în care sunt aplicabile legile lui Murphy, se poate întâmpla următoarea
situație (fig. 2-21).
Fig. 2-21, Două procese care vor să acceseze memoria partajată în același timp
Procesul A citește valoarea in și stochează valoarea 7 într-o variabilă a sa locală numită
next_free_slot. Imediat după aceasta, are loc o întrerupere de ceas, procesorul central decide
aloce procesorul procesului B. Procesul B citește și el valoarea variabilei in și primește același
valoare 7. O stochează în variabila sa locală next_ free_slot. În acest moment, ambele procese
presupun că următoarea zonă disponibilă va fi 7. Procesul B continuă să ruleze. Își stochează
numele de fișier în zona 7 și atribuie variabilei in valoarea actualizată 8. Apoi continuă să facă
altceva. După un timp, executarea procesului A se reia din locul în care a fost oprit. Citește
valoarea variabilei next_ free_slot, vede numărul 7 acolo și scrie numele fișierului său în zona
7, rescrie numele fișierului care tocmai a fost plasat în el de procesul B. Apoi calculează next_
free_slot + 1, atribuie valoarea 8 variabilei in. Nu există nicio contradicție internă în directorul
spooler, demonul de tipărire nu va observa nicio inconsecvență, dar procesul B nu va primi
niciodată printerul.
Utilizatorul B va aștepta imprimantă ani de zile, sperând obțină odată și odată fișierul imprimat.
O situație de genul acesta în care două sau mai multe procese citesc sau scriu unele date
partajate, iar rezultatul final depinde de ce proces se execută și când, se numește condiție de
cocurență (race condition). Depanarea programelor în care există race conditions nu aduce
prea multă bucurie. Rezultatele majorității rulărilor pot fi destul de acceptabile, dar până la un
anumit moment, până se va produce acel foarte rar caz când se întâmplă ceva misterios și
inexplicabil. Din păcate, odată cu creșterea nivelului de paralelism din cauza mai multor nuclee,
situațiile asemănătoare sunt tot mai frecvente.

2.3.2 Secțiuni critice


Cum evităm condițiile de concurență? Cheia pentru prevenirea problemelor aici și în multe alte
situații care implică memorie partajată, fișiere partajate și orice altceva este să găsești o
modalitate de a interzice mai multor procese să citească și să scrie simultan date partajate. Cu
alte cuvinte, este necesară o excludere reciprocă, adică un fel de a ne asigura că dacă un
proces folosește o variabilă sau un fișier partajat, celelalte procese vor fi excluse de la acel
obiect. Dificultatea de mai sus s-a produs deoarece procesul B a început să folosească una
dintre variabilele partajate înainte ca procesul A să fi finalizat lucrul cu aceasta. Alegerea
operațiilor primitive adecvate pentru obținerea excluderii reciproce este o problemă majoră de
proiectare în orice sistem de operare și un subiect pe care îl vom examina în detaliu în
secțiunile următoare.
Problema evitării condițiilor de concurență poate fi, de asemenea, formulată într-un mod
abstract. O parte din timp, un proces este ocupat făcând calcule interne și alte lucruri care nu
duc la condiții de concurență. Însă uneori, un proces trebuie să acceseze memoria, fișiere
partajate sau să facă alte lucruri critice care pot duce la concurență. Acea parte a programului
în care este accesată memoria partajată se numește regiune critică sau secțiune critică. Dacă
am putea aranja problemele astfel încât să nu existe simultan două procese în regiunile lor
critice, am putea evita cursele.
Deși această cerință evită condițiile de concurență, nu este suficient ca procesele paralele să
coopereze corect și eficient folosind date partajate. O soluție bună corfectă asigură respectarea
următoarelor patru condiții:
1. Nu se pot afla simultan două procese în regiunile lor critice.
2. Nu se pot face ipoteze referitor la viteza sau numărul procesoarelor.
3. Nici un proces care se execută în afara regiunii sale critice nu poate bloca alt proces.
4. Niciun proces nu trebuie să aștepte la infinit pentru a intra în regiunea sa critică.

Fig. 2-22. Excluderea mutuală folosind regiuni critice


În sens abstract, comportamentul pe care îl dorim este prezentat în fig. 2-22. Aici procesul A
intră în regiunea sa critică la momentul T1. Puțin mai târziu, la momentul T2, procesul B
încearcă să intre în regiunea sa critică, dar nu reușește, deoarece un alt proces (A) se află deja
în regiunea sa critică și este permis doar cel mult unui proces să se afle în secțiunea sa critică.
În consecință, B este suspendat temporar până la momentul T3, când A părăsește regiunea sa
critică, permițând lui B să intre imediat. Până la urmă B părăsește secțiunea critică (la T4) și ne
întoarcem la situația inițială fără procese în regiunile lor critice.

2.3.3 Excluderea mutuală folosind asteptarea activă


În această secțiune vom discuta diverse sugestii pentru realizarea excluderii reciproce, în care,
atâta timp cât un proces este actualizează memoria partajată și se află în secțiunea sa critică,
niciun alt proces nu va putea intra în regiunea sa critică creând probleme.
Dezactivarea întreruperilor
În sistemele uniprocesorale cea mai simplă soluție este de a dezactiva toate întreruperile unui
proces imediat după intrarea în regiunea sa critică și de a le activa imediat după ieșirea din
secțiunea critică. Când întreruperile sunt dezactivate, nu se pot produce întreruperi de ceas.
Deoarece procesorul central trece de la un proces la altul ca urmare a temporizării sau a
oricărei alte întreruperi, acesta nu va putea trece la un alt proces atunci când întreruperile sunt
dezactivate. Deoarece procesul a dezactivat întreruperile, acesta poate examina și actualiza
memoria partajată, fără teamă de interferențe din partea altor procese.
Însă această abordare nu este foarte atractivă, deoarece este complet incorect să acordăm
proceselor utilizatorilor autoritatea de a dezactiva toate întreruperile. Imaginați-vă ce se va
întâmpla dacă unul dintre proces a dezactivat și apoi nu a reactivat întreruperile? Acest lucru
poate conduce la prăbușirea întregului sistem. Mai mult, dacă avem de-a face cu un sistem
multiprocesoral (cu două sau, poate, mai multe procesoare centrale), interdicția de întrerupere
afectează doar procesorul central pe care este executată instrucțiunea prohibitivă. Toate
celelalte procesoare vor continua să funcționeze și vor putea accesa memoria partajată.
În același timp, dezactivarea întreruperilor pentru doar câteva instrucțiuni este adesea un
instrument foarte convenabil pentru kernel atunci când actualizează variabile sau liste. De
exemplu, când o întrerupere se produce în momentul modificării stării listei proceselor ready
poate apărea o situație concurență. Concluzia aici este următoarea: dezactivarea întreruperilor
este, în mare parte, o tehnologie utilă în interiorul sistemului de operare, dar nu este
recomandată ca mecanism universal de blocare reciprocă pentru procesele utilizatorilor.
Odată cu creșterea numărului de procesoare multi-core, chiar și pe computere personale cu
costuri reduse, posibilitatea realizării excluderii reciproce prin dezactivarea întreruperilor chiar și
în interiorul kernelului se restrânge. Procesoarele cu două nuclee devin obișnuite, multe mașini
au patru nuclee, iar procesoarele cu 8, 16 sau 32 de nuclee nu mai sunt o raritate. Interzicerea
întreruperilor pe o unitate de procesare centrală în sisteme cu mai multe nuclee nu împiedică
alte unități centrale să interfereze cu operațiile efectuate de prima unitate de procesare
centrală. În consecință, este nevoie de scheme mai complexe.
Variabile de blocare
O altă soluție, software, utilizează o singură variabilă partajată (de blocare) a cărei valoare
inițială este zero. Când un proces trebuie să intre în regiunea sa critică, acesta verifică mai întâi
valoarea variabilei de blocare. Dacă este 0, procesul schimbă valoarea în 1 și intră în regiunea
critică. Dacă valoarea este deja 1, procesul așteaptă până va fi 0. Astfel, valoarea 0 indică
faptul că nici un dintre procese nu se află în regiunea sa critică, iar 1 - că un proces se află în
zona sa critică.
Din păcate, punerea în aplicare a acestei idei duce la exact același rezultat fatal pe care l-am
văzut deja în exemplul cu directorul spooler-ului. Să presupunem că un proces citeste valoarea
variabile de blocare și vede că este 0. Inainte ca acesta să seteze valoarea 1, planificatorul
începe un alt proces care stabilește valoarea 1 pentru această variabilă. Când primul proces
reia execuția, va seta și el valoarea variabilei de blocare în 1, iar cele două procese se vor
regăsi simultan în regiunile lor critice.
Aparent problema ar putea fi rezolvată citind mai întâi valoarea variabilei de blocare și apoi
verificând valoarea acesteia înainte de a stoca noua valoare în ea, dar asta nu este așa. O
cursă va avea loc dacă al doilea proces schimbă valoarea variabilei de blocare imediat după ce
primul proces finalizează re-verificarea valorii acesteia.
Alternanță strictă
O a treia abordare a problemei excluderii reciproce este prezentată în Fig. 2-23. Acest fragment
de cod, ca aproape toate celelalte din această carte, este scris în C. C a fost ales aici,
deoarece sistemele de operare reale sunt practic întotdeauna scrise în C (sau C ++), dar
aproape niciodată în Java, Python sau Haskell . C este puternic, eficient și previzibil,
caracteristici critice pentru scrierea sistemelor de operare. Java, de exemplu, nu este previzibil,
deoarece s-ar putea să rămână fără memorie liberă într-un moment critic și trebuie să invoce
colectorul de gunoi pentru a recupera memoria în cel mai nepotrivit moment. Acest lucru nu se
poate întâmpla în C, deoarece nu există o colectare a gunoiului în C. O comparație cantitativă
de C, C ++, Java și alte patru limbaje este dată de Prechelt (2000).
Procesul 0 Procesul 1
while(TRUE) { while(TRUE) {
while(turn != 0) /*ciclu*/ while(turn != 1) /*ciclu*/
critical_region(); critical_region();
turn = 1; turn = 0;
noncritical_region(); noncritical_region();
} }
Fig. 2-23. Soluție posibilă pentru problema excluderii reciproce
În Fig. 2-23, variabilă întreagă turn, cu valoarea inițială 0, urmărește al cui este rândul să intre
în secțiunea critică și să verifice sau să actualizeze memoria partajată. Inițial, procesul 0
verifică valoarea lui turn, determină că este 0 și intră în regiunea sa critică. Procesul 1, de
asemenea, determină că este 0 și, prin urmare, se află într-o buclă scurtă, testarea continuă,
până când turn devine 1. Testarea continuă a unei variabile până când apare o anumită valoare
se numește așteptare activă. De obicei, aceste așteptări trebuie evitate, deoarece este cheltuit
timpul procesorului. Se recomandă să folosim metoda așteptării active numai atunci când există
o presupunere rezonabilă că așteptarea va fi scurtă. Un lacăt care folosește așteptarea activă
se numește blocare-spin (turnichet).
Când procesul 0 părăsește regiunea critică, seteză valoarea lui turn în 1, pentru a permite
procesului 1 să intre în regiunea sa critică. Să presupunem că procesul 1 își termină regiunea
critică rapid, astfel încât ambele procese se află în regiunile lor necritice, cu turn = 0. Acum,
procesul 0 își execută rapid bucla întreagă, ieșind din regiunea critică și setând rândul la 1. În
acest moment rândul său este 1 și ambele procese se execută în regiunile lor necritice.
Presupunem că, brusc, procesul 0 își termină regiunea necritică și se întoarce în vârful buclei
sale. Din păcate, nu este permisă intrarea în regiunea sa critică acum, deoarece turn are
valoarea 1 iar procesul 1 se află în afara regiunii sale critice. Procesul 0 se blochează în bucla
sa scurtă până când procesul 1 va transforma turn în 0. Cu alte cuvinte, soluția nu este tocmai
bună atunci când unul dintre procese este mult mai lent decât celălalt.
Această situație încalcă condiția 3 expusă mai sus: procesul 0 este blocat de un proces care nu
se află în regiunea sa critică. Revenind la directorul spooler-ului discutat mai sus, dacă acum
asociem regiunea critică la citirea și scrierea directorului spooler, procesul 0 nu ar avea voie să
imprime un alt fișier, deoarece procesul 1 face altceva.
De fapt, această soluție necesită ca cele două procese să alterneze strict pentru a intra în
regiunile lor critice, de exemplu, în fișierele de spooling. Niciunuia nu i s-ar permite să introducă
în spooler două fișiere la rând. Deși acest algoritm evită toate race condition, nu poate fi un
candidat serios ca soluție, deoarece încalcă condiția 3.
Algoritmul lui Peterson
Combinând ideea alternanței stricte cu ideea de variabilă de blocare și avertizare, T. Dekker, un
matematician olandez, a fost primul care a propus o soluție software la problema excluderii
reciproce care nu solicită o alternanță strictă. Pentru o discuție despre algoritmul lui Dekker,
consultați Dijkstra (1965).
În anul 1981, G. L. Peterson a descoperit o modalitate mult mai simplă de a realiza excluderea
mutuală, făcând astfel soluția lui Dekker învechită. Algoritmul lui Peterson este prezentat în fig.
2-24. Acest algoritm constă din două proceduri scrise în ANSI C, ceea ce înseamnă că
prototipurile de funcții ar trebui furnizate pentru toate funcțiile definite și utilizate. Cu toate
acestea, pentru a economisi spațiu, nu vom arăta prototipurile aici și nici mai târziu.
#define FALSE 0
#define TRUE 1
#define N 2 /* numărul de procese */
int turn; /* al cui este rândul? */
int interested[N]; /* valorile inițiale 0 (FALSE) */
void enter_region(int process); /* procesul are valoarea 0 sau 1 */
{
int other; /* numărul celuilalt process */
other = 1 – process; /* celălalt process*/
interested[process] = TRUE; /* demonstrează cointeresare */
turn = process /* setarea flagului */
while(turn==process&&interested[other]==TRUE /*ciclu fără corp*/
}
void leave_region(int process) /* process: cine pleacă */
{
interested[process] = FALSE; /* ieșirea din secțiunea critică */
}
Fig. 2-24. Algoritmul lui Peterson
Înainte de a utiliza variabilele partajate (adică, înainte de a intra în regiunea sa critică), fiecare
proces apelează funcția enter_region cu propriul număr de proces, 0 sau 1, ca parametru.
Acest apel îl va determina să aștepte, dacă este nevoie, până când îi va fi permis să intre.
După ce termină lucrul cu variabilele partajate, procesul apelează funcția leave_region pentru a
indica acest fapt și pentru a permite celuilalt proces să intre, dacă dorește acest lucru.
Să vedem cum funcționează acest algoritm. Inițial niciun proces nu se află în regiunea sa
critică. Apoi procesul 0 apelează enter_region. Își indică interesul prin setarea elementului său
de matrice și atribuind variabilei turn valoarea 0. Deoarece procesul 1 nu și-a arătat interesul de
a intra în secțiunea sa critică, funcția enter_region returnează imediat controlul. Dacă procesul
1 va face acum un apel pentru a intra în regiunea sa critică, acesta va aștepta la intrare până
când elementul interested[0] va lua valoarea FALSE, eveniment care se întâmplă doar
atunci când procesul 0 apelează funcția leave_region pentru a ieși din secțiunea sa critică.
Considerăm acum cazul în care ambele procese apelează funcția enter_region aproape
simultan. Ambele procese vor stoca numărul său în turn. Contează ultima valoare stocată
deoarece prima este suprascrisă și pierdută. Să presupunem că procesul 1 stochează ultimul,
deci turn = 1. Când ambele procese ajung la instrucțiunea while, procesul 0 îl execută de zero
ori și intră în regiunea sa critică. Procesul 1 va intra în ciclu și nu va putea intră în regiunea sa
critică până când procesul 0 nu va ieși din regiunea sa critică.
Isntrucțiunea TSL
În continuare va urma o soluție care presupune un pic de ajutor din partea resurselor tehnice.
Unele calculatoare, în special cele proiectate pentru a lucra cu mai multe procesoare, posedă
instrucțiunea
TSL RX, LOCK
(TSL însemnănd Testează și Setează Lacătul), care funcționează astfel. Această instrucțiune
citește conținutul cuvântului de memorie LOCK în registrul RX și apoi la adresa memoriei
rezervată pentru LOCK stochează o valoare diferită de zero. Operațiile de citire și stocare sunt
garantate ca fiind indivizibile - niciun alt procesor nu poate accesa cuvântul de memorie până
când instrucțiunea nu este terminată. Procesorul care execută instrucțiunea TSL blochează
magistrala memoriei pentru a interzice accesul altor procesoare la memorie până la terminartea
acestei instrucțiuni.
Este important să rețineți că blocarea magistralei de memorie diferă substanțial de dezactivarea
întreruperilor. Dacă la efectuarea unei citiri a unui cuvânt de memorie urmat de o scriere în acel
cuvânt vor fi interzise întreruperile, nimic nu împiedică un al doilea procesor central, conectat la
magistrala memoriei să acceseze cuvântul între operațiile de citire și scriere. De fapt,
dezactivarea întreruperilor procesorului 1 nu are niciun efect asupra procesorului 2. Singura
modalitate de a împiedica procesorul 2 să acceseze memoria până la terminarea procesorului 1
este de a bloca magistrala, ceea ce presupune o facilitate hardware specială (practic, o linie a
magistralei, semnalul căreia blochează magistrala interzicând accesul la ea pentru toate
procesoarele, altele decât cel care a blocat-o).
Pentru a utiliza instrucțiunea TSL, vom folosi o variabilă partajată de blocare cu ajutorul căreia
vom coordona accesul la memoria partajată. Când valoarea lui lock este 0, orice proces îl poate
seta în 1 folosind instrucțiunea TSL, apoi poate citi sau scrie din/în memoria partajată. La
terminare procesul setează lock în 0 folosind o instrucțiune obișnuită move.
Cum se poate folosi această instrucțiune pentru a împiedica două procese să intre simultan în
regiunile lor critice? Soluția este dată în Fig. 2-25. Există o subrutină cu patru instrucțiuni într-un
limbaj de asamblare fictiv (dar tipic). Prima instrucțiune copiază vechea valoare lock în registru
și setează lock în 1. Apoi, vechea valoare este comparată cu 0. Dacă este diferită de zero,
înseamnă că lock a fost deja setat, așa că programul revine la început și testează din nou. Mai
devreme sau mai târziu această valoare va deveni 0 (când procesul care se află în regiunea
critică va finaliza lucrul său și va avea loc ieșirea din subrutină cu setarea blocării). Deblocarea
este foarte simplă: este suficient ca programul să pună valoarea lui lock în 0. Nu sunt necesare
instrucțiuni speciale de sincronizare.
Esența acestei soluții a problemei regiunilor critice este acum clară. Înainte de a intra în
regiunea sa critică, un proces apelează funcția enter_region, care asigură intrarea în ciclul de
așteptare activă până când se va produce deblocarea; apoi această funcție setează starea
blocat și întoarce controlul. Procesul intră în secțiunea sa critică, iar la finalizarea activităților din
cadrul secțiunii critice apelează funcția leave_region, care atribuie variabilei lock valoarea zero.
Ca și în cazul tuturor soluțiilor problemei secțiunilor critice, pentru ca această metodă să
funcționeze, procesele trebuie să apeleze exact atunci când este necesar funcțiile enter_region
și leave_region. Dacă careva dintre procese nu va respecta această condiție, excluderea
reciprocă va eșua. Cu alte cuvinte, regiunile critice funcționează numai dacă procesele
cooperează.
enter region:
TSL REGISTER, LOCK | copierea valorii lui lock în registru cu setare în 1
CMP REGISTER, #0 | era oare valoare lui lock zero?
JNE enter region | dacă valoarea era diferită de 0 înseamnă că blocarea
| era setată și trebuie de intrat în ciclul de așteptare
RET | controlul este returnat programului apelant;
| are loc intrarea în secțiunea critică

leave_region:
MOVE LOCK, #0 | variabilei lock se atribuie valoarea zero
RET | controlul este returnat programului apelant;
Fig. 2-25. Intrarea în și ieșirea din secțiunea critică folosind instrucțiunea TSL
O alternativă la instrucțiunea TSL este instrucțiunea XCHG, care schimbă în mod atomar
conținutul a două zone de memorie, de exemplu un registru și un cuvânt de memorie. Se poate
de observat că, codul din Listingul 2.4 este în esență același ca și soluția TSL. Comanda XCHG
este utilizată pentru sincronizarea la nivel scăzut în toate procesoarele Intel x86.
enter_region:
MOVE REGISTER,#1 | încărcarea valorii 1 în registru
XCHG REGISTER,LOCK | schimbarea conținutului registrului cu variabila lock
CMP REGISTER,#0 | era oare valoare lui lock zero?
JNE enter_region | dacă valoarea era diferită de 0 înseamnă că blocarea
| era setată și trebuie de intrat în ciclul de așteptare
RET | controlul este returnat programului apelant;
| are loc intrarea în secțiunea critică
leave region:
MOVE LOCK, #0 | variabilei lock se atribuie valoarea zero
RET | controlul este returnat programului apelant;
Fig. 2-26. Intrarea în și ieșirea din secțiunea critică folosind instrucțiunea XCHG

2.3.4 Suspendarea și activarea


Atât algoritmul Peterson, cât și instrucțiunile TSL sau XCHG sunt suficient de viabile, dar au un
dezavantaj - necesitatea de aflare în așteptare activî. În esență, aceste soluții se reduc la
următoarele: atunci când un proces trebuie să intre într-o zonă critică, verifică dacă această
intrare este permisă. Dacă intrarea este refuzată, procesul intră pur și simplu într-o buclă
scurtă, așteptând permisiunea. Soluțiile date, în afară pierderilor de timp a procesorului, poate
avea și efecte complet neașteptate. Pentru exemplificare să presupunem că avem un calculator
cu două procesoare, H cu un grad ridicat de prioritate și L cu un grad scăzut de prioritate.
Regulele de planificare a activității lor prevăd că H este executat imediat după intrarea în starea
ready. La un moment dat, când L se află în regiunea sa critică, H trece în starea ready (de
exemplu, după finalizarea unei operații de I/E), imediat intră în modul de așteptare activ, adică îi
este alocat procesorul. Deoarece are loc executarea procesului H, L nu poate fi trecut în starea
exe, respectiv nu are nicio șansă să părăsească regiunea sa critică. Încă o dată: H execută o
buclă infinită, din cauza că L nu poate ieși din secțiunea critică ca să permită intrarea lui H.
Această situație este uneori numită problema inversării priorităților.
Vom analiza în continuare unele primitive folosite la comunicarea între procese, care blochează
procesele până când li se permite să intre în secțiunea critică, fără a pierde timpul procesorului.
Perechea sleep și wakeup este cea mai simplă pereche de astfel de primitive. Apelul de sistem
sleep blochează procesul apelant, care este pus în așteptare până când un alt proces îl va
debloca. Apelul de activizare wakeup are un singur argument, procesul care va fi deblocat. În
plus, atât sleep, cât și wakeup folosesc un alt argument - adresa de memorie folosită pentru a
coordona apelurile sleep cu apelurile wakeup.
Problema producător-consumator
Ca un exemplu al modului în care aceste primitive pot fi utilizate, vom luăm în considerare
problema producător-consumator (cunoscută și sub denumirea de problema tamponului limitat).
Două procese partajează un tampon comun, cu un număr fix de casete. Unul dintre procese,
producătorul, introduce informații în buffer, iar celălalt, consumatorul, preia. (Este posibilă
generalizarea problemei pentru m producători și n consumatori, dar, pentru simplitate, vom
analiza doar cazul unui producător și al unui consumator.)
Probleme apar atunci când producătorul dorește să introducă un element nou în buffer, dar
acesta este deja plin. Soluția este ca producătorul să fie trecut în starea blocat, din care să fie
deblocat atunci când consumatorul a preluat unul sau mai multe elemente din bufer. În mod
similar, dacă consumatorul dorește să preia un element din tampon și vede că tamponul este
vid, se va bloca până când producătorul introduce un element în tampon și îl deblochează.
Abordarea pare destul de simplă, dar generează aceleași probleme legate asociate condițiilor
de cursă ca și cazul cu directorul spooler-ului. Pentru a ține evidența numărului de elemnte
prezente în buffer, vom folosi variabila count. Dacă buferul are N casete, procesul producător
va verifica mai întâi dacă nu cumva toate casetele sunt pline (count = N). Dacă da, producătorul
va fi pus în așteptare; în caz contrar, producătorul va introduce un nou element în bufer și va
incrementa valoarea lui count.
Codul procesului consumator este similar: mai întâi va verifica dacă în bufer sunt elemente (are
ce consuma). Dacă count este 0, va trece în starea blocat, în caz contrar va prelua un element
din bufer și va decrementa valoarea lui count. Fiecare proces verifică, de asemenea, dacă nu
cumva celălalt proces ar trebui să fie activizat și, dacă da, îl deblochează. Codul procesului
producător, cât și codul consumatorui sunt prezentate în fig. 2-27.
Pentru a exprima apelurile de sistem sleep și wakeup în C, le vom prezenta ca apeluri către
rutine de bibliotecă. Acestea nu fac parte din biblioteca standard C, dar, probabil, ar fi
disponibile pentru orice sistem care are aceste apeluri de sistem. Procedurile insert_item și
remove_item, care nu sunt prezentate aici, gestionează contabilitatea introducerii elementelor
în tampon și preluarea elementelor din tampon.
Acum să revenim la condiția de cursă. Aceasta poate să apară din cauza că accesul la count
nu este restricționat. În consecință, poate apărea următoarea situație: buferul este vid și
consumatorul citește valoarea lui count pentru a vedea dacă este 0. În mod normal
consumatorul ar trebui să fie blocat. Dar, în acest moment, planificatorul decide să rettragă
procesorul de la procesul consumator și să-l aloce procesului producător. Producătorul
introduce un element în buffer, incrementează count și informează că valoarea lui este acum 1.
În rezultat, numai ce valoarea lui count era 0 și consumatorul ar fi trebuit să fie blocat, dar
procesul producător apelează wakeup ca să lanseze consumatorul.
#define N 100 /* numărul de casete în bufer */
int count = 0; /* number of items in the buffer */
void producer(void)
{
int item;
while(TRUE) { /* repeat forever */
item = produce item( ); /* generate next item */
if (count == N) sleep( ); /* if buffer is full, go to sleep */
insert item(item); /* put item in buffer */
count = count + 1; /* increment count of items in buffer */
if (count == 1) wakeup(consumer); /* was buffer empty? */
}
}
void consumer(void)
{
int item;
while (TRUE) { /* repeat forever */
if (count == 0) sleep( ); /* if buffer is empty, got to sleep */
item = remove item( ); /* take item out of buffer */
count = count −1; /* decrement count of items in buffer */
if (count == N − 1) wakeup(producer); /* was buffer full? */
consume item(item); /* pr int item */
}
}
Fig. 2-27. Modelul producător-consumator cu condiție de cursă fatală
Dar, diin păcate, fiindcă consumatorul nu era încă logic blocat, semnalul de deblocare se va
pierde. La realocarea procesrului procesului consumator, acesta va continua cu valoare 0 a lui
count și se va bloca. Mai devreme sau mai târziu, procesul producător va umple buferul și, de
asemenea, se va bloca. Ambele procese vor intra într-un “somn de veci”.
Esența problemei este că un semnal de deblocare trimis unui proces care nu este (încă) blocat
se va pierde. Dacă nu s-ar pierde, totul ar funcționa. O soluție rapidă ar fi să modificăm regulile
prin introducerea unui bit de așteptare pentru semnal. Când semnalul de deblocare este trimis
unui proces care nu este încă blocat, acest bit este setat. Mai târziu, când procesul încearcă să
se blocheze, dacă bitul de așteptare este activat, acesta va fi oprit rămânând neblocat. Bitul de
așteptare este un fel de depozit pentru stocarea semnalelor de deblocare. Procesul consumator
va șterge bitul de așteptare la fiecare iterație a buclei.
Bitul de așteptare este o soluție pentru exemplul nostru simplu, însă există situații cu trei sau
mai multe procese în care un singur bit de așteptare este insuficient. Există posibilitatea de a
adăuga un al doilea bit de așteptare, sau poate 8 sau 32, dar, în principiu, problema rămâne.

2.3.5 Semaforul
Aceasta era situația din 1965, când E. W. Dijkstra (1965) a sugerat utilizarea unei variabile
întregi pentru a ține evidența numărului de deblocări salvate pentru utilizării viitoare. Dijkstra a
propus să fie introdus un nou tip de variabilă, pe care a numit-o semafor. Valoarea zero a
semaforului indica faptul că nu a fost salvată nici o deblocare, iar o valoarea pozitivă – câte
operații de deblocare sunt în așteptare.
Dijkstra a propus ca semaforul să aibă două operații, de obicei numite acum down și up
(generalizare pentru sleep și wakeup). Operația down pe un semafor verifică dacă valoarea lui
este mai mare ca 0. Dacă da, din valoarea curentă se scade 1 (ceea ce ar însemna că este
folosită o deblocare stocată) și procesul continuă. Dacă valoarea este 0, procesul este blocat
fără a executa down pentru moment. Verificarea valorii, modificarea ei și, eventual, blocarea,
toate se fac ca o singură acțiune atomică, indivizibilă. Se garantează că, odată ce o operațiune
de semafor a început, niciun alt proces nu poate accesa semaforul până când operațiunea nu a
fost finalizată sau blocată. Această atomicitate este esențială pentru rezolvarea problemelor de
sincronizare și evitarea condițiilor de cursă. Acțiunile atomice, în care un grup de operații
conexe sunt fie toate efectuate fără întrerupere, fie deloc efectuate, sunt extrem de importante
și în multe alte domenii ale informaticii.
Operația up crește valoarea variabile de semafor. Dacă unul sau mai multe procese erau
blocate pe acel semafor în imposibilitatea de a finaliza o operație down anterioară, unul dintre
ele este ales de sistem (de exemplu, la întâmplare) și i se permite termine execuția operației
down. Astfel, după o operație up pe un semafor cu procese blocate, semaforul va fi totuși 0, dar
va exista cu un proces blocat mai puțin pe el. Operația de incrementare a semaforului și de
deblocare a unui proces este, de asemenea, indivizibilă. Niciun proces nu blochează vreodată
o operație up, la fel cum niciun proces nu blochează o operație wakeup în modelul precedent.
În lucrarea originală Dijkstra a denumit operațiile de semafor P și V în loc de down și, respectiv,
up. Deoarece acestea nu au o semnificație mnemotică pentru persoanele care nu vorbesc
olandeză și doar o semnificație nesemnificativă pentru cei care o fac - Proberen (încercați) și
Verhogen (ridicați, crește) - vom folosi termenii down și up, introduși pentru prima dată în
limbajul de programare Algol 68.
Rezolvarea problemei producător-consumator folosind semafoare
Semafoarele rezolvă problema pierderii operației wakeup, așa cum este arătat în codul din fig.
2-28. Pentru a le face să funcționeze corect, este esențial ca acestea să fie implementate
respectând condiția de indivizibilitatea. Modul normal este de a implementa up și down sub
formă de apeluri de sistem, sistemul de operare dezactivând pentru o perioadă scurtă toate
întreruperile în timpul testării și actualizării semaforului, blocând procesul, dacă este necesar.
Deoarece aceste acțiuni iau doar câteva instrucțiuni, nu apar probleme legate de dezactivarea
întreruperilor. Dacă se folosesc mai multe procesoare, fiecare semafor ar trebui să fie protejat
de o variabilă de blocare, cu instrucțiunile TSL sau XCHG utilizate pentru a vă asigura că doar
un singur procesor examinează semaforul.
Trebuie să înțelegeți că utilizarea TSL sau XCHG pentru a împiedica accesul simultan a mai
multor procesoare diferă de situația cu un proces (producător sau consumator) nevoit să
aștepte ca celălalt să elibereze sau să alimenteze o casetă a buferului. Operațiunea de semafor
va dura doar câteva microsecunde, iar timp ce operația cu buferul ar putea dura foarte mult
timp.

#define N 100 /* numărul de de casete în bufer */


typedef int semaphore; /* semaforul este un tip special de int */
semaphore mutex = 1; /* controleză accesul în secțiunea critică */
semaphore empty = N; /* toate casetele sunt goale */
semaphore full = 0; /* toate casetele sunt pline */

void producer(void)
{
int item;

while (TRUE) { /* TRUE este constanta 1 */


item = produce item( ); /* produce un element pentru a-l pune în bufer */
down(&empty); /* decrementarea contorului casetelor goale */
down(&mutex); /* intrare în secțiunea critică */
insert item(item); /* introduce un element în bufer */
up(&mutex); /* părăsește secțiunea critică */
up(&full); /* incrementează contorul casetelor pline */
}
}

void consumer(void)
{
int item;

while (TRUE) { /* ciclu infinit */


down(&full); /* decrementarea contorului casetelor pline */
down(&mutex); /* intrare în secțiunea critică */
item = remove item( ); /* preia un element din bufer */
up(&mutex); /* părăsește secțiunea critică */
up(&empty); /* incrementează contorul casetelor goale */
consume item(item); /* consumă elemental preluat din bufer */
}
}

Fig. 2-28. Soluționarea problemei producător-consumator utlizând semafoare


În această soluție sunt folosite trei semafoare: unul numit full pentru evidența numărului de
casete pline, unul numit empty pentru evidența numărului de casete goale și unul numit mutex
pentru a nu permite accesarea simultană a buferului de către producător și consumator.
Valoarea inițială a lui full este 0, valoarea inițială a lui empty este egală cu numărul de casete în
bufer, iar a lui mutex este 1. Semafoarele inițializate în 1 și folosite de două sau mai multe
procese pentru a se asigura că doar unul dintre ele poate intra în secțiunea sa critică în același
timp se numesc semafoare binare. Dacă fiecare proces face o operație down chiar înainte de
intrarea în regiunea sa critică și una up imediat după părăsirea acesteia, excluderea reciprocă
este garantată.
Acum, când avem la dispoziție o comunicare inter procese bună, să privim încă o dată pașii de
tratare a unei întreruperi din figura 2-5. Într-un sistem care folosește semafoare, modul natural
de a ascunde întreruperile este de a avea un semafor, inițial setat pe 0, asociat cu fiecare
dispozitiv de I/E. Imediat după pornirea unui dispozitiv de I/E, procesul de gestionare execută o
operație down pe semaforul asociat, astfel blocându-se imediat. Atunci când o întrerupe
sosește, manipulatorul întreruperii execută o operație up pe semaforului asociat, ceea ce face
ca procesul relevant să fie trecut în starea ready. În acest model, pasul 5 din fig. 2-5 constă în
exectarea unui up pe semaforul dispozitivului, astfel încât în pasul 6 planificatorul va putea rula
managerul dispozitivului. Desigur, dacă mai multe procese sunt acum gata, planificatorul poate
alege să ruleze un proces și mai important. Vom analiza unii dintre algoritmii folosiți pentru
planificare mai târziu în acest capitol.
În exemplul din fig. 2-28, am folosit de fapt semafoare în două moduri diferite. Această diferență
este de o importanț majoră pentru a fi explicată. Semaforul mutex este folosit pentru excluderea
reciprocă. Este conceput pentru a garanta că doar un singur proces va executa operații de citire
sau scriere cu buferului și cu variabilele asociate. Excluderea reciprocă permite prevenirea
haosului. Vom studia excluderea mutuală și realizarea ei în secțiunea următoare.
Cealaltă utilizare a semafoarelor este pentru sincronizare. Semfoarele full și empty sunt
necesare pentru a garanta că anumite secvențe de evenimente se produc sau nu se produc. În
acest caz, ele asigură blocarea procesului producător când buferul este plin și, respectiv,
procesul consumator este blocat dacă buferul este gol. Această utilizare este diferită de mutex.

2.3.6 Mutex
Când nu nu sunt necesare toate abilitățile unui semafor poate fi folosita o versiune simplificată
a acestuia, numită mutex. Mutex-urile sunt doar pentru a gestiona excluderea reciprocă a unor
resurse sau fragmente de cod partajate. Implementarea lor este simplă și eficientă, ceea ce le
face deosebit de utile în cazul firelor de execuție, implementate integral în spațiul utilizatorului.
Un mutex este o variabilă partajată care poate fi într-una din două stări: încuiat sau descuiat. În
consecință, este necesar doar un bit pentru a-l reprezenta, dar în practică este folosit adesea
un număr întreg, pentru care 0 are semnificația deescuiat și toate celelalte valori - încuiat. Două
proceduri sunt prezente aici. Când un fir (sau un proces) are nevoie de acces la o regiune
critică, apelează la operația de încuiere. Dacă mutex-ul este deescuiat la acel moment (ceea ce
înseamnă că regiunea critică este disponibilă), apelul se termină cu succe și firul apelant este
liber să intre în regiunea critică.
Pe de altă parte, dacă mutexul este deja încuiat, firul apelant este blocat până când firul din
regiunea critică își termină lucrul și apelează descuierea lui mutex. Dacă mai multe fire sunt
blocate pe același mutex, unuia dintre (ales la întâmplare) i se permite să execute încuierea.
Simplitatea mutex-urilor permite implementarea lor în spațiul utilizatorului, cu condiția să existe
o instrucțiune TSL sau XCHG. Codul unui mutex utilizat cu un set de fire la nivel de utilizator
sunt prezentate în fig. 2-29. Soluția cu XCHG este în esență aceeași.
mutex lock:
TSL REGISTER,MUTEX | copie mutex în registu și seatză mutex în 1
CMP REGISTER,#0 | mutex este 0?
JZE ok | dacă da (mutex era descuiat), salt la ok: return
CALL thread yield | mutex este încuiat (ocupat); planifică un alt fir
JMP mutex lock | mai încearcă o dată
ok: RET | retur la apelant; intrare în secțiunea critică

mutex unlock:
MOVE MUTEX,#0 | pune 0 în mutex
RET | retur la apelant
Fig. 2-29. Realizarea mutex_lock și mutex_unlock
Codul de blocare mutex este similar cu codul enter_region din fig. 2-25, dar cu o diferență
crucială. Când enter_region nu permite intrarea în regiunea critică, aceasta continuă testarea
lacătului în mod repetat (așteptare activă). Eventual, procesorul poate fi alocat unui alt proces.
Mai devreme sau mai târziu din cauza timesharring-ului, procesul care a ocupat lacătul va primi
procesorul și va elibera lacătul.
Cu fire (de utilizator), situația este diferită, deoarece în acest caz nu există nici-un ceas care să
retragă procesul unui fir căruia i-a expirat cuanta de timp de procesor. În consecință, un fir care
încearcă să folosească un lacăt prin metoda așteptării active va intra într-un ciclu infinit și nu va
primi niciodată lacătul, deoarece acest fir nu permite unui alt fir să treacă în starea de execuție
și să elibereze lacătul.
Aceasta este diferența dintre enter_region și mutex_lock. Când enter_region nu reușește să
obțină un lacăt, el apelează thread_yield pentru alocarea procesorului unui alt fir. În consecință,
nu există nicio așteptare activă. Când firul va rula data viitoare, el va testa din nou lacătul.
Deoarece thread_yield este doar un apel către planificatorul firelor în spațiul utilizatorului,
acesta este foarte rapid. În consecință, nici mutex_lock, nici mutex_unlock nu au nevoie de
apeluri la kernel. Folosindu-le, firele la nivel de utilizator se pot sincroniza în întregime în spațiul
utilizatorului, utilizând proceduri care necesită doar o serie de instrucțiuni.
Sistemul mutex pe care l-am descris mai sus este fundamentul unui set de apeluri. Însă
resursele software întotdeauna au nevoie de ceva suplimentar, iar primitivele de sincronizare
nu fac excepție. De exemplu, uneori, un set de fire poate apela procedura mutex_trylock care
obține lacătul (blocarea) sau returnează un cod în caz de eșec, dar fără blocare. Acest apel
permite firului să decidă (flexibilitate!) ce să facă în continuare dacă există alternative unei
așteptări simple.
Până la acest moment a existat o problemă subtilă pe care nu am punctat, dar care merită
măcar să fie amintită. Atâta timp cât discutăm doar despre fire în spațiul utilizatorului nu există
nicio problemă legată de accesul comun al mai multor fire la unul și același mutex, deoarece
toate firele funcționează într-un spațiu de adrese comun. Cu toate acestea, în majoritatea
soluțiilor anterioare, cum ar fi algoritmul lui Peterson și semafoarele, există o presupunere
nerostită că mai multe procese au acces la un spațiu de memorie partajată, poate doar un
singur cuvânt. Dacă procesele au spații disjuncte de adrese, așa cum am spus în mod
constant, cum pot partaja variabila turn în algoritmul lui Peterson sau semafoarele sau un
tampon comun? Există două răspunsuri. Mai întâi, unele dintre structurile de date partajate,
cum ar fi semafoarele, pot fi stocate în kernel și accesate numai prin apeluri de sistem. Această
abordare rezolvă problema. În al doilea rând, cele mai moderne sisteme de operare (inclusiv
UNIX și Windows) oferă o modalitate pentru procese de a partaja o parte din spațiul de adrese
cu alte procese. În acest fel, tampoanele și alte structuri de date sunt partajate. În cel mai rău
caz, când nimic altceva nu este posibil, se poate utiliza un fișier partajat.
Dacă două sau mai multe procese partajează cea mai mare parte sau toate spațiile de adrese,
distincția dintre procese și fire devine oarecum vagă, dar este totuși prezentă. Două procese
care partajează un spațiu de adrese comun, pot avea fișiere deschise diferite, cronometre de
alarmă și alte proprietăți per-proces, în timp ce firele din cadrul unui singur proces nu pot. Și
este întotdeauna adevărat că mai multe procese care partajează un spațiu comun de adrese nu
au niciodată eficiența firelor la nivel de utilizator, deoarece nucleul este profund implicat în
gestionarea proceselor.
Fiutex
Pe măsura creșterii nivelului de paralelism sincronizarea eficientă și blocarea devin foarte
importante pentru performanță. Turnichetele sunt rapide în cazul unor așteptări scurte, dar dacă
așteptarea durează prea mult, prea mari devin pierderile din timpul de procesor. În cazul unei
concurențe ridicate, o soluție mai eficiente va fi blocarea procesului și să de permită deblocarea
de către nucleu numai atunci când lacătul este liber. Din păcate, apare o problema opusă: totul
funcționează foarte bine într-un mediu extrem de competitiv, dar dacă concurența este scăzută
din start, trecerile frecvente la modul kernel devin prea scumpe. Și mai rău, prognozarea
numărului de blocări concurente poate fi dificilă.
O soluție interesantă care încearcă să îmbine tot ce este mai bun în ambele soluții este așa-
numitul fiutex sau fast user space mutex - mutex rapid în spațiul utilizatorului. Fiutex se referă la
o proprietate a SO Linux care implementează blocarea de bază (la fel ca mutexul), dar evită să
treacă în modul kernel până la momentul când apare o nevoie rezonabilă. Deoarece comutarea
la și de la modul kernel este prea scumpă, această tehnologie poate îmbunătăți semnificativ
performanțele. Un fiutex are două componente: un serviciu de kernel și o bibliotecă pentru
utilizatori. Serviciul kernel furnizează o „coadă de așteptare” care permite mai multor procese
să aștepte eliminarea blocării. Procesele nu vor rula până când nucleul nu le va debloca
explicit. Pentru ca un proces să intre în coada de așteptare este nevoie de un apel (destul de
scump) de sistem, apel care ar trebui evitat. În schimb, în absența concurenței, fiutex-ul
funcționează completamente în spațiul utilizatorului. Mai precis, procesele partajează o
variabilă de blocare comună, care este un nume fictiv pentru o valoare intreagă pe 32 de biți
folosită pentru blocare.
Să presupunem că valoarea inițială de blocare este 1, ceea ce înseamnă că blocarea este
liberă. Firul capturează blocarea prin efectuarea „decrementării și testării” atomice (funcțiile
atomice în Linux sunt compuse din cod de asamblare incorporat, inclus în funcțiile C și sunt
definite în fișierele antet). Firul analizează apoi rezultatul pentru a vedea dacă blocarea a fost
liberă. Dacă era într-o stare deblocată, totul merge bine și firul obține blocarea. Dar dacă
blocarea este ținută de un alt fir, firul nostru este obligat să aștepte. Într-un astfel de caz,
biblioteca fiutex nu se adresează către turnichet, ci folosește un apel de sistem pentru a pune
firul în coadă de așteptare în spațiul kernel-ului. Există speranța că costul trecerii la modul
kernel este acum justificat, deoarece tot una firul ar fi fost blocat. Când firul care a achiziționat
blocarea își finalizează sarcina, acesta va elibera blocarea prin execuția atomară a
incrementării cu o unitate, testarea și verificarea rezultatului pentru a vedea dacă există
procese blocate în coada de așteptare din spațiul kernel-ului. Dacă este cazul, va anunța
nucleul că poate debloca unul sau mai multe dintre aceste procese. Dacă concurență este
lipsă, nucleul nu va fi implicat defel.
Mutexes în pachetul Pthreads
Pachetul Pthreads oferă o serie de funcții care pot fi utilizate pentru sincronizarea firelor.
Mecanismul principal este utilizarea variabilelor - mutex, destinate pentru a proteja fiecare zonă
critică. Fiecare astfel de variabilă poate fi în stare blocat sau deblocat. Un fir care trebuie să
intre într-o regiune critică încearcă mai întâi să blocheze mutex-ul corespunzător. Dacă mutexul
nu este blocat, un fir poate intra în regiunea critică fără obstacole și poate bloca mutexul printr-
o acțiune indivizibilă, împiedicând alte fire să intre în secțiunea critică. Dacă mutexul este deja
blocat, firul apelant se va bloca până la deblocarea mutexului. Dacă mai multe fire așteaptă
deblocarea aceluiași mutex, doar unuia dintre ele i se permite să-și reia execuția și să
reblocheze mutex-ul. Aceste blocări nu sunt obligatorii. De respectarea ordinii utlizării blocărilor
de către fire este în totalitate resposnabil programatorul.
Principalele apeluri asociate cu mutex sunt afișate în fig. 2-30. Așa cum era de așteptat, mutex
pot fi create și distruse. Apelurile care efectuează aceste operațiuni sunt numite
pthread_mutex_init și, respectiv, pthread_mutex_destroy. Mutex pot fi de asemenea blocate
apelând pthread_mutex_lock, care încearcă să obțină blocarea și blochează un fir dacă mutex
era deja blocat. Există, de asemenea, un apel folosit pentru a încerca blocarea mutex și
returnarea unui cod de eroare în caz de eșec (dacă mutex era deja blocat). Acest apel se
numește pthread_mutex_trylock. El permite firului să organizeze așteptarea activă eficientă în
caz de necesitate. Și, în sfârșit, apelarea pthread_mutex_unlock deblochează un mutex și reia
un singur fir dacă există unul sau mai multe fire care așteaptă să fie deblocate. Mutex pot avea
și atribute, care sunt folosite doar pentru soluționarea unor probleme speciale.
Apel Descriere
pthread_mutex_init Creare mutex
pthread_mutex_destroy Dsitrugerea unui mutex existent
pthread_mutex_lock Capturarea blocării sau blocarea unui fir
pthread_mutex_trylock Capturarea blocării sau ieșire în caz de eroare
pthread_mutex_unlock Deblocare
Fig. 2.30. Câteva apeluri din pachetul Pthreads, legate de mutex
Pe lângă mutex, Pthreads oferă un al doilea mecanism de sincronizare - variabilele de tip
condiție. Mutex sunt bune pentru a permite sau interzice accesul în zona critică. Variabilele de
tip condiție permit blocarea unui fir până la îndeplinirea unor condiții specifice. Aceste două
metode sunt aproape întotdeauna folosite împreună. Acum să aruncăm o privire mai atentă la
interacțiunile dintre fire, mutex și variabilele de tip condiție.
Ca un exemplu simplu vom lua în considerare modelul producător-consumator: un fir pune un
element într-o casetă a buferului, iar celălalt preia un element din bufer. Dacă producătorul
detectează că nu există casete libere, el este obligat să se blocheze până apare una. Mutex-ul
oferă posibilitatea de a efectua verificarea în mod atomar, fără interferențe din alte fire, dar
constatând că tamponul este plin, producătorul are nevoie de o modalitate de a se bloca și de a
se debloca mai târziu. Anume aceasta oferă variabilele de tip condiție.
Cele mai importante apeluri asociate variabilelor de tip condiție sunt prezentate în fig. 2.31.
După cum probabil era de așteptat, sunt apeluri destinate creerii și distrugerii variabilelor de tip
condiție. Pot avea atribute și există o serie de alte apeluri pentru gestionarea lor (care nu sunt
prezentate aici). Operațiile principale cu variabilele de tip condiție sunt pthread_cond_wait și
pthread_cond_signal. Prima operație blochează firul apelant până când un oarecare alt fir
(folosind apelul pthread_cond_signal) îi trimite un semnal. Desigur, motivele pentru blocare și
așteptare nu fac parte din protocolul de așteptare și semnalizare. Un fir blocat așteaptă adesea
ca firul de semnalizare să facă unele lucrări, să elibereze unele resurse sau să efectueze alte
acțiuni. Abia după aceea, firul blocat își continuă activitatea. Variabilele de tip condiție permit ca
așteptarea și blocarea să fie efectuate ca operații indivizibile. Apelul pthread_cond_broadcast
este utilizat atunci când există o oportunitate potențială pentru mai multe fire să fie în stare
blocat și să aștepte același semnal.
Apel din fir Descriere
pthread_cond_init Crearea unei variabile de tip condiție
pthread_cond_destroy Dsitrugerea unei de tip condiție
pthread_cond_wait Blocare în așteptarea unui semnal
pthread_cond_signal Semnalizarea și deblocarea unui alt fir
pthread_cond_broadcast Semnalizarea și deblocarea unor mai multe fire
Fig. 2-31. Câteva apeluri din pachetul Pthreads pentru variabilele de tip condiție
Variabilele de tip condiție și mutexurile sunt întotdeauna utilizate împreună. Schema pentru un
fir constă în “încuierea” mutex-ului după care va urma o așteptare conform variabilei de tip
condiție, dacă firul nu poate obține obiectul necesar. Cu timpul, un alt fir îl va executa opera’ia
de semnalizare și firul anterior blocat va putea continua execuția. Apelul pthread_cond_wait
este îndeplinit în mod atomar și execută “descuierea” mutex-ului printr-o operație indivizibilă și
neîntreruptă. Din acest motiv, mutexul este unul dintre parametrii acestui apel.
De asemenea, este de remarcat faptul că variabilele de tip condiție (spre deosebire de
semafoare) nu pot fi memorizate. Dacă un semnal este trimis către o variabilă de tip condiție, în
coada de așteptare a căreia nu există niciun fir, semnalul este pierdut. Programatorii trebuie să
fie atenți să nu piardă semnalele.
Ca un exemplu al modului în care sunt utilizate mutexurile și variabilele de condiție, în listingul
din fig. 2-32 este prezentată o problemă foarte simplă din categoria producător-consumator cu
un singur tampon.
#include <stdio.h>
#include <pthread.h>

#define MAX 1000000000 /* numărul elemente produse */


Pthread_mutex_t the_mutex;
Pthread_cond_t condc, condp; /* utilizat pentru semnalizare */
int buffer = 0; /* bufer utilizat */

void *producer(void *ptr) /* procesul producător produce date */


{ int i;

for (i= 1; i <= MAX; i++) {


pthread_mutex_lock(&the_mutex); /* acordă acces exclusiv la bufer */
while (buffer != 0) pthread_cond_wait(&condp, &the_mutex);
buffer = i; /* introduce un element în bufer */
pthread_cond_signal(&condc); /* deblochează consumatorul */
pthread_mutex_unlock(&the mutex); /* acordă acces la bufer */
}
Pthread_exit(0);
}

void *consumer(void *ptr) /* consumă elemente din bufer */


{ int i;
for (i = 1; i <= MAX; i++) {
pthread_mutex_lock(&the_mutex); /* acordă acces exclusiv la bufer */
while (buffer ==0 ) pthread_cond_wait(&condc, &the mutex);
buffer = 0; /* take item out of buffer */
pthread_cond_signal(&condp); /* deblochează producătorul */
pthread_mutex_unlock(&the mutex); /* acordă acces la bufer */
}
Pthread_exit(0);
}

int main(int argc, char **argv)


{
Pthread_t pro, con;
Pthread_mutex_init(&the mutex, 0);
Pthread_cond_init(&condc, 0);
Pthread_cond_init(&condp, 0);
Pthread_create(&con, 0, consumer, 0);
Pthread_create(&pro, 0, producer, 0);
Pthread_join(pro, 0);
Pthread_join(con, 0);
Pthread_cond_destroy(&condc);
Pthread_cond_destroy(&condp);
Pthread_mutex_destroy(&the mutex);
}
Fig. 2-32. Folosirea firelor pentru rezolvarea problemei producător-consumator.
Când tamponul devine plin, procesul producător trebuie să aștepte până când consumatorul va
goli cel puțin o casetă înainte de a produce următorul element. În mod similar, atunci când
consumatorul preia ultimul element din bufer, el va trebui să aștepte până când producătorul va
produce și va introduce îb bufer unul nou. Deși este foarte simplu, acest exemplu ilustrează
cum funcționează mecanismele de bază. Instrucțiunea din linia de cod în care este suspendată
execuția unui fir, înainte de a continua execuția acestuia, trebuie să se convingă de îndeplinirea
condiției, deoarece firul ar fi putea fi deblocat din alte cauze.

2.3.7 Monitoarele
Cu semafoare și mutexe, comunicarea între procese pare simplă, nu? Uită de ele. Privește
atent la ordinea operațiilor down înainte de a insera sau prelua elemente din tampon în fig. 2-
28. Să presupunem că cele două down din codul producătorului sunt puse în ordine inversă,
astfel încât mutex a fost decrementat înainte de empty. Dacă tamponul ar fi fost plin,
producătorul s-ar bloca, cu mutex-ul setat la 0. În consecință, următoarea dată când
consumatorul va încerca să acceseze tamponul, va face un down pe mutex-ul, care este acum
0, și se va bloca și el. Ambele procese vor rămâne blocate pentru totdeauna. Această situație
nefericită este numită impas (dead-lock). Vom studia în detaliu impasurile în capitolul 6.
Am punctat pe această problemă pentru a sublinia cât de atent trebuie să fiți atunci când
utilizați semafoarele. O eroare subtilă și totul se oprește. Este ca programarea în limbajul de
asamblare, chiar mai rău, deoarece erorile pot veni de la condițiile de cursă sau de la blocaje și
alte forme de comportament imprevizibil și ireproductibil. Pentru a facilita scrierea programelor
corecte, Brinch Hansen (1973) și Hoare (1974) au propus o primitivă de sincronizare la nivel
superior, numită monitor. Propunerile lor diferă ușor, așa cum este descris mai jos. Un monitor
este o colecție de proceduri, variabile și structuri de date care sunt toate grupate într-un fel
special de modul sau pachet. Procesele pot apela procedurile unui monitor ori de câte ori
doresc, dar nu pot accesa direct structurile de date interne ale monitorului din procedurile
declarate în afara monitorului. Figura 2-33 ilustrează un monitor scris într-un limbaj imaginar,
Pidgin Pascal. C nu poate fi folosit, deoarece monitoarele sunt un concept de limbaj lipsă în C.
monitor example
integer i;
condition c;

procedure producer( );
.
.
.
end;

procedure consumer( );
.
.
.
end;
end monitor;
Fig. 2-33. Prezentarea schematică a monitorului.
Monitoarele au o proprietate importantă care le face utile pentru rezolvarea problemei excluderii
reciproce: un singur proces poate fi activ pe un monitor la un moment de timp dat. Monitoarele
sunt o construcție-limbaj de programare, astfel încât compilatorul știe că sunt obiecte speciale
și pot gestiona apelurile pentru a monitoriza procedurile într-un mod diferit de monitorizarea
altor proceduri. În mod obișnuit, atunci când un proces apelează la o procedură de monitor,
primele câteva instrucțiuni ale procedurii vor verifica dacă există vreun alt proces în prezent
activ pe monitor. Dacă da, procesul de apelare va fi suspendat până când celălalt proces va
părăsi monitorul. Dacă niciun alt proces nu utilizează monitorul, procesul de apelare poate intra.
Implementarea excluderii reciproce folosind intrările monitorului este în sarcina compilatorului,
dar o modalitate obișnuită este utilizarea unui mutex sau a unui semafor binar. Deoarece
compilatorul, nu programatorul, rezolvă problema excluderii reciproce, este mult mai puțin
probabil ca ceva să nu meargă. În orice caz, persoana care scrie monitorul nu trebuie să fie
conștientă de modul în care compilatorul se configurează pentru excluderea reciprocă. Este
suficient să știm că lăsând regiunile critice în seama procedurilor de monitor, nu vor exista
procese care își vor executa simultan regiunile critice.
Deși monitoarele oferă o modalitate ușoară de a asigura excluderea reciprocă, mai este
necesară o modalitate de a bloca procesele atunci când acestea nu pot continua. În cazul
problemei producător-consumator, este simplu să punem toate verificările de tipul buffer-plin
sau buffer-gol în seama procedurilor de monitor, dar cum se va bloca producătorul sau
consumatorul când găsește tamponul plin, respectiv gol?
Soluția este să introducem variabile de tip condiție, cu două operații pe acestea, wait și
signal. Procesele vor apela proceduri externe ale monitorului pentru ași rezolva problema
excluderii mutuale. Când un poces apelează o procedură de monitor și aceasta consideră că
procesul nu poate continua (de exemplu, producătorul găsește tamponul plin), o operație wait
pe o anumită variabilă de tip condiție a monitorului, să zicem, condiția full (wait(full)), va fi
executată. Această operație va bloca procesul apelant. De asemenea, apelarea de către un alt
proces (procesul consumator) a unei proceduri externe a monitorului poate conduce la
executarea operației singnal (full) (procesul consumator a preluat un element din bufer și
acesta nu mai este plin), operație care va debloca procesul producător anterior blocat. Pentru
a evita să avem două procese active în monitor în același timp, avem nevoie de o regulă care
să spună ce se întâmplă la emiterea unui semnal. Hoare a propus ca să fie executat procesul
nou-deblocat, iar cellălalt proces să fie suspendat. Brinch Hansen a propus o soluție mai
radicală: procesul care a apelat o procedură de monitor în care este executată operația
signal(cond) trebuie să părăsească imediat monitorul. Cu alte cuvinte, o operație signal(cond)
ar trebui să fie în linia finală a procedurii de monitor. Vom folosi propunerea lui Brinch Hansen,
deoarece este mai simplă și mai ușor de implementat. Dacă o operație signal efectuată pe o
variabilă de tip condiție este așteaptată de mai multe procese, doar unul dintre procese,
determinat de planificatorul sistemului, este deblocat.
Există și o a treia soluție, care nu a fost propusă nici de Hoare, nici de Brinch Hansen. Această
constă în a permite “semnalizatorului” să-și continue execuția, iar procesului deblocat să-i fie
alocat procesorul după ce “semnalizatorul” a părăsit monitorul.
Variabilele de tip condiție nu sunt contoare. Acestea nu acumulează semnale pentru utilizări
ulterioare cum fac semafoarele. Astfel, dacă o operație signal este executată pe o condiție care
nu este așteptată de nici un proces, semnalul se pierde pentru totdeauna. Cu alte cuvinte, wait
trebuie să se producă înainte signal. Această regulă face implementarea mult mai simplă. În
practică, este ușor de urmărit starea fiecărui proces cu variabile de tip condiție.
Schematic soluția problemei producător-consumator cu monitoare este prezentată în fig. 2-34
din nou în Pidgin Pascal. Avantajul folosirii Pidgin Pascal constă în simplitate și urmează exact
modelul Hoare / Brinch Hansen.
monitor ProducerConsumer procedure producer;
condition full, empty; begin
integer count; while true do
begin
procedure insert(item: integer);
begin item = produce item;
if count=N then wait(full); ProducerConsumer.insert(item)
end
insert item(item);
end;
count := count + 1;
if count=1 then signal(empty)
procedure consumer;
end;
begin
function remove: integer; while true do
begin begin
if count = 0 then wait(empty); item = ProducerConsumer.remove;
remove = remove item; consume item(item)
count := count − 1; end
if count=N−1 then signal(full) end;
end;
count := 0;
end monitor;
Fig. 2-34. Folosirea monitoarelor în problema producător-consumator.
S-ar putea să vă gândiți că operațiile wait și signal sunt similare operațiilor cu sleep și wakeup,
care așa cum am văzut anterior pot genera condiții de cursă fatale. Întradevăr sunt foarte
asemănătoare, dar cu o diferență crucială: sleep și wakeup au condus la eșec, deoarece în
timp ce un proces încerca să execute sleep, celălalt însista să-l trezească prin wakeup.
Monitoarele nu permit să se întâmple așa ceva. Realizarea excluderii reciproce automată cu
ajutorul procedurilor de monitor garantează că, dacă, să spunem, producătorul din cadrul unei
proceduri de monitorizare descoperă că tamponul este plin, acesta va putea finaliza operația
wait fără a fi nevoie să vă faceți griji cu privire la posibilitatea că programatorul ar apela la
consumator înainte de finalizarea lui wait. Consumatorul nu va fi lăsat nici măcar să intre în
monitor până nu va fi terminată operația wait și producătorul să fie marcat că nu poate continua
execuția.
Deși Pidgin Pascal este un limbaj imaginar, unele limbaje de programare acceptă monitoarele,
deși nu întotdeauna în forma proiectată de Hoare și Brinch Hansen. Un astfel de limbaj este
Java. Java este un limbaj orientat pe obiecte care acceptă fire la nivel de utilizator și permite
gruparea metodelor (procedurilor) în clase. Adăugând la declararea unei metode cuvântul cheie
synchronized, Java garantează următoarele: imediat cum un fir începe execuția acestei
metode, niciunui alt fir nu i se va permite să înceapă execuția oricărei alte metode a obiectului
cu cuvântul cheie synchronized. Fără synchronized, nu există garanții cu privire la alternanță.
O soluție a problemei producător-consumator folosind monitoare în Java este prezentată în fig.
2-35. Soluția noastră are patru clase. Clasa externă, ProducerConsumer, creează și începe
execuția a două fire, p și c. Clasele a doua și a treia, respectiv producer și consumer, conțin
codul pentru producător și consumator. În cele din urmă, clasa our_monitor este monitorul.
Conține două fire sincronizate care sunt utilizate pentru inserarea elementelor în bufferul
partajat și extragerea lor. Spre deosebire de exemplele anterioare, aici avem codul complet
pentru insert și remove.
public class ProducerConsumer {
static final int N = 100 // dimensiunea buferului
static producer p = new producer( ); // instanțierea unui nou fir producător
static consumer c = new consumer( ); // instanțierea unui nou fir consumator
static our_monitor mon = new our_monitor( ); // instanțierea unui nou monitor

public static void main(String args[]) {


p.start(); // startul firului producător
c.start(); // startul firului consumator
}
static class producer extends Thread {
public void run() { // metoda run conține codul firului
int item;
while (true) { // ciclul producătorului
item = produce item();
mon.insert(item);
}
}
private int produce item( ) { ... } // producere actuală
}
static class consumer extends Thread {
public void run( ) { // metoda run conține codul firului
int item;
while (true) { // ciclul consumatorului
item = mon.remove( );
consume item (item);
}
}
private void consume_item(int item) { ... } // consum actual
}
static class our_monitor { // acesta este un monitor
private int buffer[ ] = new int[N];
private int count = 0, lo = 0, hi = 0; // contoare ți indicatori

public synchronized void insert(int val) {


if (count == N) go_to_sleep(); // dacă buferul este plin, marș la culcare
buffer [hi] = val; // inserează un item in bufer
hi = (hi + 1) % N; // caseta pentru plasarea următorului
count = count + 1; // cu un item mai mult în bufer acuma
if (count == 1) notify(); // dacă consumatorul dormea, trezirea
}

public synchronized int remove() {


int val;
if (count == 0) go_to_sleep(); // dacă buferul este gol, marș la culcare
val = buffer [lo]; // preluarea unui element din bufer
lo = (lo + 1) % N; // caseta din care va fi preluat următorul
count = count − 1; // cu un item mai puțin în bufer acuma
if (count == N − 1) notify(); // dacă producătorul dormea, trezirea
return val;
}
private void go_to_sleep(){try{wait();}catch(InterruptedException
exc) {};}
}
}
Fig. 2-35. Soluția problemei producător-consumator în Java
Din punct de vedere funcțional firele producător și consumator sunt identice cu omologii lor din
toate exemplele noastre anterioare. Producătorul are o buclă infinită în care sunt generate date
și introduse în bufferul comun. Consumatorul are o buclă la fel infinită, cu care preia date din
bufferul comun și face ceva ele.
Partea interesantă a acestui program este clasa our_monitor, care deține bufferul, variabilele
de administrare și două metode sincronizate. Când producătorul este activ în interiorul metodei
insert, el este sigur că consumatorul nu poate fi activ în interiorul metodei remove, ceea ce îi
permite să actualizeze variabilele și tamponul, fiind sigur că nu vor apărea condițiile de cursă.
Variabila count ține evidența numărului de casete ocupate. Poate fi orice valoare de la 0 până
la N - 1. Variabila lo punctează pe caseta de unde urmează să fie preluat următorul element. În
mod similar, hi punctează pe caseta unde urmează să fie plasat următorul articol. Se poate
întâmpla ca lo = hi, ceea ce înseamnă că fie buferul este gol sau buferul este plin. Valoarea lui
count concretizează care caz are loc.
Metodele sincronizate din Java diferă de monitoarele clasice într-un mod esențial: Java nu are
variabile de tip condiție încorporate. În locul lor, Java oferă două proceduri, wait și notify, care
sunt echivalentul lui sleep și wakeup, cu excepția că atunci când sunt utilizate în cadrul
metodelor sincronizate ele nu pot genera condiții de cursă. În teorie, metoda wait poate fi
întreruptă, asta este destinați codulului care o înconjoară. Java obligă ca tratarea excepțiilor să
fie explicită. În cazul nostru trebuie doar să ne imaginăm că utilizarea metodei go_to_sleep este
doar o modalitate de a suspenda execuția.
Datorită automatizării excluderii reciproce a intrării în regiunile critice, monitoarele, spre
deosebire de semafoare, permit diminuarea numărului de erori în programarea paralelă. Cu
toate acestea, monitoarele au și unele dezavantaje. Nu întâmplător cele două exemple de
monitoare au fost în Pidgin Pascal și nu în C, cum este pentru celelalte exemple din această
carte. Cum am spus anterior, monitorul este noțiune a limbajului de programare. Compilatorul
trebuie să le recunoască și, într-un fel sau altul, să asigure excluderea reciprocă. C, Pascal și
majoritatea altor limbaje nu au monitoare. Din această cauză nu are sens să ne așteptăm de la
compilatoarele lor realizarea unor reguli de excludere reciprocă. Întradevăr, de unde ar putea
compilatorul să afle care proceduri erau în monitoare și care nu?
În aceste limbaje nu există nici semafore, dar adăugarea lor este simplă: trebuie doar să
suplinim biblioteca cu două subrutine scurte scfrise în limbaj de asamblare pentru a obține
apelurile de sistem up și down. Compilatoarele pot să nu știe de existența lor. Desigur, sistemul
de operare trebuie să știe despre semafoare, dar în orice caz, dacă aveți un sistem de operare
care utilizează semafoare, puteți scrie aplicații în C sau C ++ (sau chiar în limbaj de asamblare
dacă nu aveți de făcut altceva mai bun). Pentru utilizarea monitoarelor avem nevoie de un
limbaj de programare în care acestea să fie integrate.
O altă caracteristică inerentă monitoarelor și semafoarelor este că acestea au fost proiectate
pentru rezolvarea problemei de excludere reciprocă pentru un procesor central sau mai multe,
toate partajând o memorie comună. Prin introducerea semafoarelor în memoria partajată și
protejarea lor cu instrucțiuni TSL sau XCHG, putem evita cursele. Când trecem la un sistem
distribuit cu mai multe procesoare, fiecare cu propria memorie privată și conectate printr-o rețea
locală, aceste primitive devin inaplicabile. Concluzia este că semafoarele sunt de nivel prea jos,
iar monitoarele nu sunt utilizate decât într-un număr mic de limbaje de programare. Unde mai
pui că niciuna dintre primitive nu permite schimbul de informații între calculatoare. Avem nevoie
de altceva aici.

2.3.8 Transmiterea mesajelor


În acest scop este folosită mesageria (transmiterea mesajelor). Această metodă de comunicare
între procese folosește două primitive, send și receive, care, asemeni semafoarelor și spre
deosebire de monitoare, sunt apeluri de sistem, nu construcții ale limbajului de programare. Ca
atare, ele pot fi ușor plasate în rutine de bibliotecă, de exemplu:
send(destination, &message);
sau
receive(suurce, &message);
Primul apel trimite mesajul destinatarului specificat, iar cel de-al doilea primește mesajul de la
sursa specificată (sau de la orice sursă dacă destinatarului nu-i pasă). Dacă nu există mesaje
disponibile, destinatarul poate fi blocat până la sosirea lor. Sau poate returna controlul imediat
cu un cod de eroare.
Probleme de proiectare a sistemelor de mesagerie
Sistemele de mesagerie au multe probleme complicate de proiectare, care nu apar dacă
folosim semafoare sau cu monitoare, mai ales dacă procesele de comunicare sunt pe diferite
mașini conectate în rețea. De exemplu, mesajele pot fi pierdute la transmiterea prin rețea.
Pentru a evita pierderea mesajelor, expeditorul și receptorul pot să se înțeleagă că, de îndată
ce un mesaj este primit, receptorul va trimite înapoi un mesaj special de confirmare. Dacă
expeditorul nu primește confirmarea într-un anumit interval de timp, acesta retransmite mesajul.
Există posibilitatea ca mesajul să recepționat corect, dar confirmarea să fie pierdută. În acest
caz expeditorul va retransmite mesajul, astfel că receptorul îl va primi de două ori. Este
important ca receptorul să poată face distincția între mesajul transmis și cel retransmis. De
obicei, această problemă este rezolvată prin introducerea unor numere succesive consecutive
în fiecare mesaj original. Dacă receptorul primește un mesaj care poartă același număr de
secvență ca mesajul anterior, știe că acest mesaj este un duplicat care poate fi ignorat.
Comunicarea cu succes într-o mesagerie nesigură este o parte importantă a studiului rețelelor
de calculatoare. Pentru mai multe informații, consultați Tanenbaum și Wetherall (2010).
Sistemele de mesaje trebuie să se ocupe de asemenea despre modul în care sunt numite
procesele, astfel încât procesul specificat într-un apel de trimitere sau recepție să fie lipsit de
ambiguitate.
Autentificarea este, de asemenea, o problemă în sistemele de mesagerie: cum poate clientul să
spună că comunică cu un server anume de fișiere, și nu cu un impostor?
La celălalt capăt al spectrului, există probleme de proiectare importante atunci când expeditorul
și receptorul se află pe aceeași mașină. Una dintre acestea este performanța. Copierea
mesajelor de la un proces la altul este întotdeauna mai lentă decât efectuarea unei operațiuni
de semafor sau intrarea într-un monitor. Multe cercetări au fost consacrate eficientizării
transmiterii mesajelor.
Problema producător-consumator folosind transmiterea mesajelor
Să vedem acum cum poate fi rezolvată problema producător-consumator folosind mesageria și
fără memorie partajată. O soluție este dată în fig. 2-36. Presupunem că toate mesajele au
aceeași dimensiune fixă și că mesajele trimise, dar care nu au fost încă primite, sunt protejate
automat de sistemul de operare. În această soluție, se utilizează un total de N mesaje, analog
cu cele N casete dintr-un tampon în memoria partajată. Consumatorul începe prin a trimite N
mesaje vide către producător. De fiecare dată când producătorul vrea să ofere un element
consumatorului, acesta ia un mesaj gol, introduce elementul și trimite înapoi mesajul. În acest
fel, numărul total de mesaje din sistem rămâne constant în timp, astfel încât acestea pot fi
stocate într-o anumită zonă de memorie cunoscută dinainte.
În cazul în care producătorul lucrează mai repede decât consumatorul, toate mesajele goale se
vor umple cu elemente, iar producătorul se va bloca așteptând până consumatorul va prelua un
mesaj. Dacă consumatorul lucrează mai repede, totul se va întâmpla invers: toate mesajele vor
deveni goale în așteptarea ca producătorul să le completeze; consumatorul va fi blocat,
așteptând un mesaj plin.
Sunt posibile mai multe variante de mesagerie. Pentru început să facem cunoștință cu modul
de adresare a mesajelor. O modalitate ar fi să atribuim fiecărui proces o adresă unică și să
trimitem mesajele către procese. Putem, de asemenea, inventa o nouă structură de date,
numită cutie poștală. O cutie poștală este un loc pentru a buferiza un anumit număr de
mesaje, specificat de obicei la crearea cutiei poștale. Când se folosesc cutiele poștale, în
calitate de parametri de adresă din apelurile send și receive sunt folosite cutiele poștale, nu
procesele. Când un proces încearcă să trimită la o cutie poștală plină, acesta este blocat până
când un mesaj este extras din acea cutie poștală, făcând loc pentru unul nou.
Pentru soluționarea problemei producător-consumator, atât producătorul, cât și consumatorul
creaază cutii poștale suficient de mari pentru a stoca N mesaje. Producătorul va trimite mesaje
care conțin date reale în cutia poștală a consumatorului, iar consumatorul va trimite mesaje
goale către cutia poștală a producătorului. Când sunt folosite cutiele poștale, mecanismul de
buferizare este destul de simplu: cutia poștală a destinatarului conține mesajele trimise
procesului destinatarului, care încă nu au fost preluate de acesta.
#define N 100 /* number of slots in the buffer */
void producer(void)
{
int item;
message m; /* message buffer */
while (TRUE) {
item = produce item( ); /* generate something to put in buffer */
receive(consumer,&m); /* wait for an empty to arrive */
build_message(&m, item); /* constr uct a message to send */
send(consumer,&m); /* send item to consumer */
}
}
void consumer(void)
{
int item, i;
message m;

for(i=0; i<N; i++) send(producer, &m); /* send N empties */


while (TRUE) {
(producer,&m); /* get message containing item */
item = extract item(&m); /* extract item from message */
(producer,&m); /* send back empty reply */
consume_item(item); /* do something with the item */
}
}
Fig. 2-36. Modelul producător-consumator cu N mesaje.
O altă extremă, atunci când folosim cutiile poștale, este să refuzăm total la buferizare. În acest
caz, dacă operația send are loc înaintea operației receive, procesul-emițător este blocat până la
finalizarea operației receive, în acest timp mesajul poate fi copiat direct de la expeditor la
receptor, fără buferizarea intermediară. În mod similar, dacă operația receive se produce mai
întâi, receptorul este blocat până când are loc o operație send. Această strategie este de obicei
numită randez-vous. Este mai simplu de realizat decât o schemă de mesaje buferizate, dar
este mai puțin flexibilă, deoarece expeditorul și receptorul sunt obligați să lucreze într-un mod
strict predefinit.
Mesageria este frecvent utilizată în programarea paralelă. Un sistem cunoscut de transmitere a
mesajelor, de exemplu, este MPI (Message-Passing Interface). Este utilizat pe scară largă în
calculul științific. Pentru mai multe informații, consultați de exemplu Gropp și colab. (1994) și
Snir și colab. (1996).

2.3.9 Bariere
Ultimul nostru mecanism de sincronizare este destinat grupurilor de procese, spre deosebire de
situațiile de tipul producător-consumator în care sunt prezente două procese. Unele aplicații
sunt împărțite în faze și au regula ca niciun proces să nu poată continua în faza următoare
până când toate procesele nu vor fi gata să treacă la următoarea fază. Acest comportament
poate fi realizat prin plasarea unei bariere la sfârșitul fiecărei faze. Când un proces ajunge la
barieră, acesta este blocat până în momentul în care toate procesele ajunng la barieră. Aceasta
permite sincronizarea grupurilor de procese. Funcționarea barierei este ilustrată în Fig. 2-37.
Fig. 2-37. Utilizarea barierei. (a) Procesele se spropie de barieră. (b) În afara unui proces, toate
celelalte au ajuns la barieră. (c) Când sosește ultimul proces, toate merg mai departe.
În fig. 2-37 (a) sunt prezentate patru procese care se apropie de o barieră: au loc calcule și nici
un proces nu a ajuns încă la sfârșitul fazei curente. După un timp, primul proces termină toate
calculele primei faze, după care execută primitiva barrier, apelând la o procedură de bibliotecă.
Procesul este apoi suspendat. Puțin mai târziu, al doilea și apoi al treilea proces termină prima
fază și execută, de asemenea, primitiva barrier. Această situație este ilustrată în Fig. 2-37 (b).
În sfârșit, când ultimul proces, C, execută barrier, toate procesele sunt eliberate, așa cum se
arată în fig. 2-37 (c).
Ca exemplu de problemă în care avem nevoie de bariere, luăm în considerare o problemă din
fizică sau inginerie. Avem o foaie de metal, încălzită de la o flacără care acționează pe unul
dintre colțurile ei. Presupunem că trebuie să calculăm cum are loc propagarea temperaturii
pentru anumite puncte ale foii, puncte care formează o matrice bidimensională.
Începând cu valorile actuale, reieșind din legile termodinamicii, elementele matricei inițiale vor
recalculate și vom obține valorile noi ale temperaturii fiecărui punct după un interval de timp
egal cu ∆T (a doua versiune a matricei). Apoi, procesul se repetă, obținând temperaturile în
punctele de probă în funcție de timp, pe măsură ce foaia se încălzește. Obținem secvențe
temporale de matrici, fiecare pentru un moment de timp dat.
Presupunem că matricea este foarte mare (de exemplu, 106x106), astfel încât este nevoie de
procese paralele (eventual un sistem multiprocesoral) pentru a accelera calculele. Diferite
procese lucrează cu diferite elemente ale matricei, calculând noile elemente din cele vechi
conform legilor fizicii. Dar, nici un proces nu poate începe iterația n + 1 până când iterația n nu
este încheiată, adică până când toate procesele nu-și termină activitatea curentă. Putem atinge
scopul urmărit obligând fiecare proces să execute operația barrier în momentul în care încheie
operațiile iterației curente. Când toate procesele își vor finaliza operațiile iterației curente va fi
obținută noua matrice (intrarea pentru următoarea iterație) și tuturor proceselor li se va permite
să înceapă următoarea iterație.

2.3.10 Evitarea blocajelor: citire-copiere-actualizare


Cel mai rapide lacăte sunt lacătele lipsă. Întrebarea este dacă poate fi permis accesul simultan
fără blocare pentru citire și scriere la structuri de date partajate. În general, răspunsul este
negativ. Presupunem că procesul A sortează un tablou numeric, în timp ce B calculează media.
Întrucât A deplasează elementele tabloului, procesul B poate lucra cu unele elemente de mai
multe ori, iar cu altele deloc. Rezultatul poate fi oricare, dar cel mai probabil va fi unul greșit.
Cu toate acestea, în unele cazuri, este posibil să se permită procesului de scriere să
actualizeze datele, chiar dacă alte procese le folosesc în acest timp. Este important de asigurat
ca fiecare cititor să citească sau versiunea veche sau cea nouă a datelor, nu și o combinație de
versiuni. Ca exemplu, fie arborele prezentat în fig. 2.19.

Fig. 2-38. Read-Copy-Update: inserarea unui nod, eliminarea unei ramuri – totul fără blocare.
Cititorii parcurg arborele de la rădăcină spre frunze. În jumătatea superioară a figurii, a fost
adăugat un nou nod X. Înainte de a face vizibil acest nod în arbore, el trebuie pregătit: sunt
inițializate toate valorile din nodul X, inclusiv indicatorii descendenților. Apoi cu ajutorul unei
operații atomare de scriere îl facem pe X descendentl al nodului A. Versiunea inconsistentă nu
poate fi citită de niciun cititor. În partea de jos a figurii, nodurile B și D sunt șterse secvențial. În
primul rând, redirecționăm indicatorul descendent stâng al nodului A către nodul C. Toți cititorii
care apasă A vor continua să citească din nodul C și nu vor vedea niciodată nodul B sau D. Cu
alte cuvinte, vor vedea doar versiune noua. În același timp, toți cititorii aflați în prezent în
nodurile B sau D vor continua să citească urmând indicatorii structurii originale de date și vor
vedea versiunea veche. Totul va fi bine și nu este nevoie de blocare. Ștergerea nodurilor B și D
funcționează fără a bloca structura de date, în principal, deoarece RCU (Citire - Copiere -
Actualizare) separă faza de ștergere de faza de recuperare a actualizării.
Dar există și o problemă. Până când nu suntem siguri că nu există cititori în B sau D, nu putem
scăpa de ei. Dar cât timp ar trebui să așteptăm? Un minut? Zece minute? Trebuie să așteptăm
până când ultimul cititor părăsește aceste noduri. RCU definește în mod obligator timpul maxim
în care cititorul poate ține o legătură la structura de date. După expirarea acestei perioade,
memoria poate fi eliberată. Mai precis, cititorilor li se acordă acces la structura de date
cunoscută sub denumirea secțiune critică pentru citire (read-side critical section), care poate
conține orice cod întrucât ea nu este blocată sau în regim de așteptare. În acest caz este
cunoscut timpul maxim de așteptare forțată. Anume, este definită o perioadă de grație (grace
period) sub forma oricărei perioade de timp în care se știe că fiecare fir va fi în afara secțiunii
critice pentru citire cel puțin o dată. Dacă perioada de așteptare înainte de recuperare este cel
puțin egală cu așteptarea forțată, totul va fi bine. Întrucât codul din secțiunea critică nu poate fi
blocat sau pus în așteptare, un simplu criteriu este să se aștepte până când toate firele vor
executa comutarea contextului.

2.4 Planificarea
În cazul multiprogramării există în mod frecvent mai multe procese sau fire concurente la
procesor. Această situație apare ori de câte ori două sau mai multe procese/fire sunt simultan
în starea ready. Dacă există un singur procesor, trebuie să se aleagă cărui proces va fi alocat
acesta. Partea sistemului de operare care face alegerea se numește planificator, iar algoritmul
pe care îl folosește se numește algoritm de planificare. Aceste subiecte fac obiectul
următoarelor secțiuni.
Multe din soluțiile elaborate pentru planificarea proceselor se aplicabile și în planificarea firelor,
unele însă sunt diferite. Când nucleul SO gestionează firele, planificarea se face de obicei pe
fir, fără a ține cont, parțial sau chiar defel, de procesul de care aparține firul. Inițial ne vom
concentra pe planificare în cazul momentelor caracteristice atât proceselor, cât și firelor. Mai
târziu vom analiza în mod explicit planificarea firelor și unele momente singulare. Vom discuta
despre procesoarele cu mai multe nuclee în capitolul 8.

2.4.1 Introducere în planificare


Dacă să ne întoarcem la vremea sistemelor cu tratarea pe loturi în care introducerea datelor
era sub formă de imaginilor cartelelor perforate, transpuse pe o bandă magnetică, algoritmul de
planificare era relativ simplu: trebuia doar de lansat în execuție următoarea lucrare. Odată cu
implementarea multiprogramării, algoritmul de planificare a devenit mai complex, deoarece
există mai mulți utilizatori care așteaptă serviciul. Unele calcultoare mari încă mai combină și
azi tratarea pe loturi cu regimul de partajare a timpului. În acest caz planificatorul decide care
va fi următoarea lucrare - de lot sau de la un utilizator interactiv. (Printre altele, o lucrare de lot
poate fi o solicitare pentru a lansa mai multe programe succesive, dar pentru această secțiune,
vom presupune doar că este o solicitare pentru a lansa un singur program.) Deoarece timpul de
procesor este o resursă deficitară pentru aceste calculatoare, un planificator bun poate face
diferența în ceea ce privește performanța percepută și satisfacția utilizatorului. Din această
cauză s-a muncit mult pentru conceperea unor algoritmi de planificare inteligibili și eficienți.
Odată cu apariția computerelor personale, situația s-a schimbat în două moduri. În primul rând,
de cele mai multe ori există un singur proces activ. Este puțin probabil ca un utilizator care
introduce un document pe un procesor de texte să compileze simultan un program în fundal.
Atunci când utilizatorul tastează o comandă la procesorul de texte, planificatorul nu trebuie să
depună mare efort pentru a afla care proces să lucreze în continuare - procesorul de texte este
singurul candidat.
În al doilea rând, calculatoarele au devenit atât de rapide, încât procesorul nu mai este o
resursă deficitară. Majoritatea programelor pentru calculatoarele personale sunt limitate de
viteza cu care utilizatorul poate prezenta informația de intrare (tastând sau făcând clic), nu de
viteza de lucru a procesorului. Chiar și compilarea, în trecutul apropiat beneficiarul principal al
timpului de procesor, durează astăzi doar câteva secunde în majoritatea cazurilor. Chiar și
atunci când două programe rulează efectiv simultan, cum ar fi un procesor de texte și o foaie de
calcul, nu prea contează care merge mai întâi - utilizatorul așteaptă ca ambele să se termine.
În consecință, planificarea nu contează mult în cazul PC-urilor simple. Desigur, există aplicații
foarte “pofticioase” care cheltuie practic tot timpul de procesor. De exemplu, redarea unei ore
de video de înaltă rezoluție cu reglarea culorilor în fiecare dintre cele 107 892 de cadre (în
NTSC) sau 90 000 de cadre (în PAL) necesită putere de calcul cu rezistență industrială. Dar,
aplicații similare sunt mai degrabă excepții decât regulă.
Când însă apelăm la servicii de rețea, situația se schimbă considerabil. Aici, mai multe procese
concurează pentru procesor, deci planificarea devine din nou importantă. De exemplu, atunci
când planificatorul trebuie să aleagă între a aloca procesorul unui proces care adună statistici
zilnice sau unui proces care tratează solicitările utilizatorilor, utilizatorii vor fi mult mai mulțumiți
dacă prioritate vor avea solicitările utilizatorilor. Multe din dispozitivele mobile, cum ar fi
smartphoanele (cu excepția probabil a celor mai puternice modele) și nodurile din rețelele de
senzori nu se pot “lăuda” cu „abundență de resurse”. În acest caz procesorul poate fi slab și
memoria mică. Mai mult, întrucât durata de viață a bateriei este una dintre cele mai importante
constrângeri, unele planificatoare încearcă să optimizeze consumul de energie.
Pe lângă alegerea procesului potrivit pentru a fi rulat, planificatorul trebuie să se preocupe și de
utilizarea eficientă a procesorului, deoarece comutarea procesului este costisitoare. Mai întâi
trebuie să se producă trecerea de la modul utilizator la modul kernel. Apoi, starea procesului
curent trebuie salvată pentru a-l putea relua ulterior. În unele sisteme, trebuie salvată și harta
de memorie (de exemplu, biți de referință la pagini). În continuare, este executat algoritmul de
planificare pentru a selecta procesul căruia îi va fi alocat procesorul. După aceea, unitatea de
gestionare a memoriei (MMU) trebuie reîncărcată cu harta de memorie a noului proces. În cele
din urmă, noul proces trebuie lansat în execuție. Suplimentar la toate cele enunțate, comutarea
proceselor invalidează toată memoria cache și tabelele aferente, forțându-o să fie reîncărcată
dinamic din memoria principală de două ori (la intrarea în nmucleu și la ieșirea de acolo). În
rezultat, prea multe comutări pot cheltui o cantitate substanțială de timp de procesor, așa că
este recomandată prudență.
Comportamentul procesului
Aproape toate procesele alternează “exploziile” de calcul cu solicitări de I/E (disc sau rețea),
așa cum arată fig. 2-39. Adesea, procesorul rulează o perioadă fără oprire, apoi se face un apel
de sistem pentru a citi dintr-un fișier sau a scrie într-un fișier. Când apelul de sistem se încheie,
procesorul „muncește” din nou până când are nevoie sau trebuie să scrie noi date, etc. Unele
activități de I/E contează la fel ca și calculele. De exemplu, când procesorul copiază biți pe o
memorie RAM video pentru a actualiza ecranul, acesta calculează - este ocupat, nu execută
I/E. O operație de I/E „curată” este atunci când un proces este trecut în starea blocat așteptând
ca un dispozitiv extern să-și finalizeze lucrul.
Fig. 2-39. Utilizarea procesorului (CPU burst) alternează cu perioade de așteptarea a I/E.
(a) Proces limitat de viteza de calcul. (b) Proces limitat de viteza dispozitivelor de I/E.
Observăm că unele procese (fig. 2-39 (a)), își petrec cea mai mare parte din timp calculând, în
timp ce alte procese (fig. 2 -39 (b)), își petrec cea mai mare parte a timpului așteptând I/E.
Primele sunt numite procese limitate de viteza calcul sau legate de procesor (compute-bound
sau CPU-bound); cele din urmă se numesc procese limitate de operațiile de I/E (I/O bound).
Procesele CPU-bound au, de obicei, intervale lungi de operații de procesor cu așteptări rare și
scurte pentru I/E, în timp ce procesele legate de I/E au perioade scurte de timp de procesor și
așteptări frecvente pentru operațiile de I/E. Trebuie de sbliniat că factorul cheie este durata
CPU burst, nu cea a I/E burst. Procesele I/O bound se numesc așa din cauza că calculele nu
durează mult (CPU burst este scurt), nu pentru că au cereri de I/E lungi. Este nevoie de același
interval de timp pentru a emite cererea hardware pentru a citi un bloc de disc, indiferent cât de
mult sau cât de puțin timp necesită procesarea datelor după sosirea lor.
Este demn de remarcat faptul că, pe măsură ce procesoarele devin mai rapide, procesele tind
să devină tot mai I/E bound. Acest efect are loc deoarece performanțele procesoarelor cresc
mult mai repede decât ale discurilor. În consecință, planificarea proceselor I/O bound este
probabil să devină un subiect tot mai important în viitor. Aici ideea de bază este că, dacă un
proces I/O bound dorește să ruleze, acesta ar trebui să aibă o șansă rapidă, astfel încât să
poată emite cererea de disc și să țină discul ocupat. Așa cum am văzut în fig. 2-6, când
procesele sunt de tipul I/O bound, este nevoie de câteva procese pentru a menține procesorul
complet ocupat.
Când planificăm
Problema cheie a planificării este momentul luării deciziei. Există o varietate de situații care
solicită implicarea planificatorului. În primul rând, atunci când este creat un nou proces, trebuie
luată o decizie care proces să fie executat în continuare - procesul părinte sau procesul copil.
Deoarece ambele procese sunt în starea ready, este o decizie normală de planificare, iar
planificatorul va alege să rulat fie părintele, fie copilul.
În al doilea rând, o decizie de planificare trebuie luată la terminarea unui proces. Acest proces
nu mai poate fi executat (deoarece nu mai există), deci un alt proces trebuie ales din setul de
procese gata de execuție. Dacă niciun proces nu este gata, un proces inactiv, furnizat de
sistem, este rulat în mod normal.
În al treilea rând, când un proces se blochează pe o operație de I/E, pe un semafor sau pentru
un alt motiv, trebuie selectat un alt proces pentru a fi rulat. Uneori, motivul blocării poate juca un
rol în alegere. De exemplu, dacă A este un proces important care așteaptă ca B să părăsească
regiunea sa critică, planificatorul îl va lăsând-l pe B să funcționeze în continuare îi va permite să
părăsească regiunea sa critică și astfel să lase A să continue. Problema este însă că, în
general, plaificatorul nu are informațiile necesare pentru a ține cont de această dependență.
În al patrulea rând, atunci când se produce o întrerupere de I/E, poate fi luată o decizie de
planificare. Dacă întreruperea provine de la un dispozitiv de I/E care și-a încheiat acum lucrul,
un proces care era blocat în așteptarea finalizării acestei I/E poate fi gata să fie rulat. Depinde
de planificator să decidă dacă să execute un proces ready, procesul care era executat în
momentul întreruperii sau un al treilea proces.
Dacă un ceas hardware generează întreruperi periodice la 50 sau 60 Hz sau o altă frecvență,
se poate lua o decizie de planificare la fiecare întrerupere sau la fiecare kilo-întrerupere de
ceas. Algoritmii de planificare pot fi împărțiți în două categorii în dependență de modul în care
se acționează cu întreruperile ceasului. Un algoritm de planificare nonpreemptiv alege un
proces de rulat și apoi îl lasă să ruleze până când se blochează (fie pe I/E, fie în așteptarea
unui alt proces), fie eliberează voluntar procesorul. Chiar dacă durează multe ore, nu-i poate fi
retras în mod forțat procesorul. De fapt, nu se iau decizii de planificare în timpul întreruperilor
ceasului. După ce procesarea întreruperii ceasului a fost terminată, procesul care a fost
executat înainte de întrerupere este reluat, cu excepția cazului în care un proces cu prioritate
mai mare aștepta procesorul.
În schimb, un algoritm de planificare preventivă (preemptiv) alege un proces și îi permite să
ruleze pentru o cuantă fixă de timp. Dacă cuanta expiră și procesul nu s-a terminat, acesta este
suspendat și planificatorul alege un alt proces pentru a fi rulat (dacă există procese ready).
Efectuarea unei planificări preventive necesită o întrerupere de ceas la sfârșitul cuantei de timp
pentru a retirna controlul procesorului la planificator. Dacă nu este disponibil niciun ceas,
planificare nonpreemptiv este singura opțiune.
Categoriile algoritmilor de planificare
Nu este surprinzător că, în medii diferite, este nevoie de algoritmi de planificare diferiți. Această
situație apare pentru că diferite domenii de aplicare (și diferite tipuri de sisteme de operare) au
obiective diferite. Cu alte cuvinte, obiectivele urmărite de optimizare sunt diferite. Trebuie de
remarcat trei medii de execuție a programelor:
1. mediul tratării pe loturi,
2. mediul interactiv,
3. mediul tratării în timp real.
Sistemele cu tratarea pe loturi sunt încă utilizate pe scară largă în lumea afacerilor pentru
efectuarea salarizării, inventarului, creanțelor, conturilor plătibile, calculului dobânzii (la bănci),
procesării creanțelor (la companiile de asigurări) și alte activități periodice. Aici nu există
utilizatori care așteaptă cu nerăbdare la terminalele lor un răspuns rapid la o solicitare scurtă. În
consecință, algoritmi nonpreemptivi sau algoritmi preventivi cu perioade lungi de timp pentru
fiecare proces, sunt adesea acceptabili. Această abordare reduce numărul de comutări ale
proceselor sporind performanța. Algoritmii de planificare utilizați în sistemele cu tratarea pe
loturi au un caracter relativ universal și pot fi utilizați adesea și în alte situații, motiv din care
merită să fie studiați chiar și de persoanele departe de calculul corporativ cu utilizarea
calculatoarelor universale mari.
Într-un mediu cu utilizatori interactivi pentru a nu permite unui proces să pună stăpânire pe
procesor pentru o perioadă prea lungă de timp lăsând celelalte procese fără timp de procesor,
prioritizarea este esențială. Chiar dacă niciun proces nu este conceput să ocupe procesorul
pentru totdeauna, un proces ar putea închide tuturor celorlalte procese accesul la procesor la
nesfârșit din cauza unei erori a programului. Este necesară prioritizarea pentru a preveni acest
comportament. În această categorie se încadrează serverele, deoarece normal, servesc mai
mulți utilizatori (la distanță), toți fiind grăbiți. Utilizatorii calculatoarelor se grăbesc întotdeauna.
În sistemele cu restricții în timp real, cât nu ar părea de straniu prioritizarea uneori nu este
necesară, deoarece procesele sunt concepute să funcționeze pentru perioade scurte de timp și,
de obicei, își fac munca rapid după care se blochează. Spre deosebire de sistemele interactive
în sistemele de timp real sunt lansate numai acele programe care sunt destinate să participe la
soluționarea unei anumite probleme aplicative. Sistemele interactive au un caracter general și
pot lansa în execuție programe arbitrare care nu sunt cooperante și chiar posibil dăunătoare.
Obiectivele unui algoritm de planificare
Pentru a crea un algoritm de planificare, este necesar să aveți o idee despre ce ar trebui să
facă un algoritm bun. Unele obiective depind de mediul de execuție (tratarea pe loturi, sistem
interactiv sau în timp real), dar altele sunt urmărite în toate cazurile. În fig. 2-40 sunt enumerate
câteva obiective. Vom discuta despre ele în continuare.
 Toate sistemele:
 Echitate – oferirea unei cote corecte de procesor fiecărui proces;
 Respectarea politicii – monitorizarea respectării politicii adoptate;
 Echilibru - menținerea unui anume nivel de ocupare a componentelor sistemului.
 Sistemele pentru tratarea pe loturi
 Productivitate - numărul de lucrări executate într-o unitate de timp;
 Timp total de așteptare – intervalul de timp dintre trimiterea și încheierea lucrării;
 Rata de utilizare a procesorului – raportul dintre menținerea procesorului ocupat tot
timpul.
 Sistemele interactive
 timpul de răspuns - răspuns rapid la solicitări;
 proporționalitate - satisfacerea așteptărilor utilizatorilor.
 Sistemele în timp real
 respectarea termenelor limită - evitarea pierderilor datelor;
 previzibilitate - evitarea degradării calității în sistemele multimedia.
Figura 2-40. Câteva obiective ale algoritmului de planificare în diferite circumstanțe.
Corectitudinea (echitatea) este importantă în toate circumstanțele. Procesele comparabile ar
trebui să obțină servicii comparabile. Acordând unui proces mult mai mult timp de procesor
decât unui alt proces echivalent nu este echitabil. Desigur, diferite categorii de procese pot fi
tratate diferit. Comparați asigurarea securității cu calculele contabile la centrul de calcul al unui
reactor nuclear.
Legată de echitate este și implementarea politicilor sistemului. Dacă politica locală este aceea
că procesele de control al siguranței trebuie să fie lansate imediat ori de câte ori doresc acest
lucru, chiar dacă înseamnă că plata salariilor întârzie cu 30 de secunde, planificatorul trebuie să
se asigure că această politică este respectată.
Un alt obiectiv general este de a menține toate părțile din sistem ocupate atunci când este
posibil. Dacă procesorul și toate dispozitivele de I/E pot fi funcționale tot timpul, productivitatea
va fi mai mare decât dacă unele dintre componente pot fi în stare de repaus. Într-un sistem cu
tratarea pe loturi, de exemplu, planificatorul controlulează lucrările aduse în memorie pentru a fi
rulate. A avea în memorie împreună unele procese CPU-bound și unele procese I/O-bound
este o idee mai bună decât să fie încărcate mai întâi toate procesele CPU-bound și doar după
terminarea lor să fie încărcate procesele I/O-bound. Dacă este utilizată ultima strategie,
procesele CPU-bound, atunci când are loc execuția lor, vor lupta pentru procesor iar discul va fi
inactiv. Mai târziu, când vor veni lucrările legate de procesele I/O-bound, acestea vor lupta
pentru disc, iar procesorul va fi inactiv. Mai corect este să se mențină în funcțiune simultan
întregul sistem printr-un amestec atent de procese.
Managerii marilor centre de calcul în care sunt tratate numeroase loturi de lucrări, de obicei,
atunci când estimează performanța sistemului iau în calcul trei parametri: productivitatea, timpul
de tratare și rata de utilizare a procesorului. Productivitatea este definită ca numărul de lucrări
îndeplinite într-o oră. Având în vedere toate circumstanțele, executarea a 50 de lucrări pe oră
corespunde unei productivități mai buneă decât 40. Timpul de tratare este timpul mediu statistic
din momentul în care o lucrare este trasmisă pentru execuție până la momentul finalizării
execuției. Măsoară cât timp este nevoit să aștepte rezultatele un utilizator mediu (statistic
vorbind). Aici regula este: cu cât mai puțin cu atât mai bine.
Un algoritm de planificare care maximizează productivitatea poate să nu minimizeze în mod
obligator timpul de tratare. De exemplu, având în vedere un amestec de lucrări scurte și lucrări
lungi, un planificator care a rulat întotdeauna lucrări scurte, dar care nu a lucrat niciodată pentru
lucrări lungi ar putea obține o productivitate excelentă (multe lucrări scurte pe oră), dar în
detrimentul unui timp de tratare teribil pentru lucrările lungi. Dacă rata de apariție a lucrărilor
scurte este constant înaltă, lucrările lungi ar putea să nu fie lansate niciodată, ceea ce ar face
ca timpul mediu de tratare să fie infinit în timp ce se obține o productivitate ridicată.
Rata de încărcare a procesorului este adesea utilizată ca metrică în sistemele de tratare pe
loturi, dar nu este o metrică prea bună. Ceea ce contează cu adevărat este numărul de lucrări
care ies din sistem într-o oră (productivitatea) și cât timp este necesar pentru a obține
rezultatele (timpul de tratare). Utilizarea ratei de încărcare a procesorului ca metrică ne
amintește de compararea mașinilor electrice bazată pe numărul de rotații într-o unitate de timp.
Totuși, să știi când rata de încărcare a procesorului este aproape de 100% este util pentru
decide că a venit timpul să aveți mai multă putere de calcul.
Pentru sistemele interactive se aplică obiective diferite. Cel mai important este minimalizarea
timpului de răspuns, adică timpul dintre emiterea unei cereri și obținerea rezultatului. Pe un
computer personal unde sunt executate procese de fundal (de exemplu, citirea și stocarea e-
mailului din rețea), o solicitare a utilizatorului pentru a porni un program sau a deschide un fișier
trebuie să aibă prioritate. Dacă toate solicitările interactive au prioritate aceasta va fi perceput
ca un nivel bun al serviciului.
O problemă oarecum legată este ceea ce s-ar putea numi proporționalitate. Utilizatorilor le
este caracteristic să facă estimări inerente (dar adesea incorecte) a duratei evenimentelor.
Când o solicitare pe care utilizatorul o consideră complexă durează mult timp, utilizatorii
acceptă asta, dar când o solicitare care este percepută ca simplă durează mult timp, utilizatorii
se irită. De exemplu, dacă faceți clic pe o pictogramă care lansează încărcarea unui clip video
de 500 de MB pe un server cloud și procesul de încărcare durează 60 de secunde, utilizatorul
va accepta probabil asta ca fapt de viață, deoarece nu se aștepta ca transmiterea fișierului să
dureze 5 secunde. El știe că pentru aceasta este nevoie de timp.
Pe de altă parte, când utilizatorul face clic pe pictograma care întrerupe conexiunea cu serverul
cloud după încărcarea videoclipului, el are așteptări diferite. Dacă operația nu s-a finalizat după
30 de secunde, utilizatorul puțin probabil că va sta liniștit, iar după 60 de secunde își va ieși din
fire. Acest comportament se datorează percepției obișnuite a utilizatorului potrivit căreia
trimiterea multor date se prezumă că trebuie să dureze mult mai mult decât ruperea conexiunii.
În unele cazuri (ca și în acesta), planificatorul nu poate influența timpul de răspuns, dar în alte
cazuri, poate, mai ales atunci când întârzierea se datorează unei alegeri proaste a ordinii de
execuție a proceselor.
Spre deosebire de sistele interactive, sistemele în timp real au alte proprietăți și, prin urmare,
obiective diferite de planificare. Pentru aceste sisteme este caracteristică existența termenelor
limită care trebuie respectate. De exemplu, dacă un computer controlează un dispozitiv care
produce date în mod regulat, eșecul de a executa la timp procesul de colectare a datelor poate
duce la pierderea datelor. Astfel, cel mai important obiectiv într-un sistem de timp real este
respectarea tuturor (sau a celor mai multe) termene.
În unele sisteme de timp real, în special în sistemele multimedia, predictibilitatea este la fel de
importantă. Cazurile rare de nerespectare a termenelor nu sunt fatală, dar dacă procesul audio
rulează prea eratic, calitatea sunetului se va deteriora brusc. Este valabil și pentru procesele
video, dar urechea omului este mult mai sensibilă la denaturări aleatoare decât ochiul. Pentru a
evita această problemă, planificarea proceselor trebuie să fie extrem de previzibilă și constantă.
Vom studia algoritmii de planificare pentru tratarea pe loturi lot și sistemele interactive în acest
capitol. Planificarea în timp real nu este prezentă în această carte, ci în materialul suplimentar
pentru sistemele de operare multimedia de pe site-ul web al cărții.

2.4.2 Planificarea în sistemele cu prelucrare pe loturi


Acum este timpul să trecem de la problemele generale de planificare la algoritmi de planificare
specifici. În această secțiune vom analiza algoritmii folosiți în sistemele cu tratare pe loturi. Vom
examina apoi sistemele interactive și în timp real. Este de remarcat faptul că unii algoritmi sunt
folosiți atât în sistemele cu tratarea pe loturi, cât și în sisteme interactive. Le vom studia mai
târziu.
Prim-sosit, prim-servit
Probabil cel mai simplu dintre toți algoritmii de planificare concepute vreodată este algoritmul
prim-sosit, prim-servit (PSPS). Procesorul este atribuit proceselor în ordinea în care acestea
sunt introduse în firul de așteptare (coada) al proceselor ready. Practic, există o singură coadă
de procese ready. Prima lucrare care intră în sistem este pornită imediat și este lăsată să
funcționeze atât timp cât are nevoie. Nu este întreruptă dacă durează prea mult. Pe măsură ce
apar alte lucrări, acestea sunt puse în coada firului de așteptare. Când procesul de execuție se
blochează, procesorul este alocat primului proces din topul firului de așteptare. Când un proces
blocat anterior devine gata, acesta este pus în coada firului, ca și orice proces nou sosit, în
spatele tuturor proceselor care așteaptă procesorul.
Avantajul mare al acestui algoritm este simplitatea, iar ca și consecință, este ușor de înțeles și
la fel de ușor de programat. Echitatea lui este din aceeași categorie cu echitatea repartizării
biletelor deficitare la spectacole sportive și concerte sau locurilor în coada de așteptare pentru
procurarea iPhone-urilor nou-nouțe persoanelor care stau în rând începând cu ora 2 de noapte.
În cadrul acestui algoritm toate procesele așteaptă procesorul în unicul fir al proceselor ready.
Procesul ales pentru ai fi alocat procesorul este extras din topul cozii proceselor ready. O nouă
lucrare sau un proces deblocat este inserat la sfârșitul cozii. Ce ar putea fi mai simplu de
înțeles și de implementat?
Din păcate, algoritmul prim-sosit, prim-servit are și un dezavantaj puternic. Să presupunem că
există un singur proces de tipul CPU-bound care este executat de fiecare dată pentru o
secundă și o mulțime de procese legate I/O-bound care utilizează puțin timp de procesor, dar
fiecare trebuie să efectueze 1000 de operații de citire de pe disc înainte de a fi finalizate.
Procesul CPU-bound este executat timp de 1 sec și trece la citirea unui bloc de date de pe disc.
Apoi sunt lansate toate procesele de I/E care execută operații de citire de pe disc. Când
procesul CPU-bound obține blocul de disc solicitat, el este executat încă o secundă, după care
urmează din nou procesele I/O-bound ș.a.m.d.
În rezultat, fiecare proces I/O-bound citește 1 bloc pe secundă și vor fi necesare 1000 sec
pentru a ajunge la finiș. Dacă algoritmul de planificare peste fiecare 10 msec ar retrage
procesorul de la procesul CPU-bound, procesele I/O-bound s-ar încheia în 10 sec în loc de
1000 sec și fără a diminua substanțial viteza de execuție a procesului CPU-bound.
Cererea cea mai scurtă servită prima
Este un algoritm fără priorități pentru tratarea pe loturi care presupune cunoașterea apriori a
timpului de procesor necesar pentru executarea unei lucrări, numit cererea cea mai scurtă
servită prima (CSSP). Vom începe cu un exemplu. Într-o companie de asigurări oamenii pot
prezice cu relativă exactitate cât timp va dura execuția unui lot de 1000 de cereri, deoarece se
lucrează similar în fiecare zi. Dacă mai multe lucrări la fel de importante stau în coada
proceselor ready, planificatorul alocă procesorul celei mai scurte lucrări. În fig. 2-41 sunt
prezentate patru lucrări A, B, C și D cu timpii de execuție 8, 4, 4 și, respectiv, 4 minute.
Executând lucrările în această ordine, timpul de aflare în sistem pentru A este de 8 minute,
pentru B este de 12 minute, pentru C este de 16 minute, iar pentru D este de 20 de minute cu o
medie generală de 14 minute.

Fig. 2-40. Exemplu planificare CSSP. (a) Ordinea inițială de execuție a ucrărilor. (b) Ordinea
disctată de algoritmul CSSP
Acum să analizăm executarea acestor patru lucrări lansând mai întâi cea mai scurtă lucrare(fig.
2-41 (b)). Timpul de aflare în sistem este acum de 4, 8, 12 și 20 de minute pentru fiecare
lucrare cu o medie de 11 minute. Poate fi demonstrat că algoritmul care alege pentru execuție
lucrarea cea mai scurtă este optimal din punctul de vedere al timpului mediu de aflare în
sistem. Fie cazul a patru lucrări, cu timpi de execuție de a, b, c și, respectiv, d. Prima lucrare se
termină după a unități de timp, a doua după a + b și așa mai departe. Timpul mediu de aflare în
sistem este (4a + 3b + 2c + d) / 4. Este clar că prima lucrare contribuie cel mai mult la medie,
deci ar trebui să fie aleasă cea mai scurtă lucrare. Vor urma în ordine crescătoare b, apoi c, și
în final d ca cel mai lung, deoarece afectează doar timpul său de tratare. Raționamentul este
ușor extins pentru orice număr de lucrări.
Trebuie de remarcat că algoritmul este optim numai atunci când toate lucrările sunt disponibile
simultan. Ca un contraexemplu, luați în considerare cinci lucrări, de la A până la E, cu timpii de
execuție de 2, 4, 1, 1 și 1, respectiv. Momentu de sosire a acestora este 0, 0, 3, 3 și 3. Inițial, se
pot alege doar A sau B, din moment ce celelalte trei lucrări nu au sosit încă. Folosind regula
mai întâi cea mai scurtă lucrare, vom rula lucrările în ordinea A, B, C, D, E cu o așteptare medie
de 4,6. Executarea în ordinea B, C, D, E, A ar permite o așteptare medie de 4,4.
Prioritate procesului cu cel mai scurt timp rămas
O modificare a algoritmului precedent este algoritmul în care se acordă prioritate procesului
care are nevoie de cel mai puțin timp pentru a fi încheiat. În acest caz planificatorul alege
întotdeauna procesul al cărui timp de execuție rămas este cel mai scurt. Ca și până acum,
timpul de execuție trebuie cunoscut apriori. La sosirea unei noi lucrări, timpul său total este
comparat cu timpul rămas al procesului curent. Dacă noua lucrare are nevoie de mai puțin timp
pentru a fi finalizată, procesului curent i se retrage procesorul și începe execuția lucrării noi.
Această schemă permite servirea rapidă a lucrărilor noi.

2.4.3 Planificarea în sistemele interactive


Vom analiza în continuare unii dintre algoritmii folosiți în sistemele interactive. Acești algoritmi
sunt utilizați frecvent pe computerele personale, servere și alte tipuri de sisteme.
Algoritmul Round-Robin
Unul dintre algoritmii cei mai vechi, mai simpli, mai echitabili și mai des folosiți este algoritmul
Round-Robin. Fiecărui proces i se atribuie un interval de timp, numit cuanta procesului - este
timpul în care procesului îi este permisă execuția. Dacă procesul se încheie până la expirarea
cuantei, procesorul este retras de la procesul curent și alocat unui alt proces. Dacă procesul se
blochează sau se termină înainte de expirarea cuantei, procesorul este alocat unui alt proces,
desigur. Algoritmul Round-Robin este ușor de implementat. Tot ce trebuie să facă planificatorul
este să mențină o listă de procese ready (fig. 2-42 (a)). La expirarea cuantei de timp, procesul
care nu și-a încheiat activoitatea este plasat în coada listei (fig. 2-42 (b)).

Fig. 2-42. Algortimul Round-robin. (a) Lista proceselor ready. (b) Lista proceselor ready după
expirarea cuantei de execuție a procesului B.
Unicul moment care prezintă interes aici este mărimea cuantei. Trecerea de la un proces la
altul necesită o anumită perioadă de timp pentru activități de administrare: salvarea contextului
procesului curent și încărcarea regiștrilor și hărților de memorie, actualizarea diferitelor tabele și
liste, descărcarea pe disc și reîncărcarea memoriei cache, etc. Să presupunem că această
comutare de proces sau a contextului procesorului, cum este numit uneori, durează 1 msec,
inclusiv comutarea hărților de memorie, descărcarea și reîncărcarea cache-ului, etc. De
asemenea, să presupunem că valoarea cuantei este setată la 4 msec. Cu acești parametri,
după efectuarea a 4 msec de muncă utilă, procesorul central va trebui să cheltuiască (adică, să
piardă) 1 msec pentru comutarea procesorului. Astfel, 20% din timpul de procesor va fi risipit pe
probleme administrative (de întreținere, de regie) ceea ce, evident, este prea mult.
Pentru a îmbunătăți eficiența utilizării procesorului central, am putea seta cuanta la 100 msec.
Acum, se va pierde doar 1% din timpul de procesor. Dar să vedem ce se va întâmpla într-un
sistem server dacă 50 de solicitări vin într-un interval de timp foarte scurt și cu cerințe de
procesare variate. Cincizeci de procese ready vor fi introduse în lista proceselor ready. Dacă
procesorul era inactiv, primul proces va fi lansat imediat, iar cel de-al doilea poate să nu
pornească decât după 100 msec, ș.a.m.d. Dacă presupunem că toate procesele au nevoie de
întreaga cuantă, ultimul proces poate să fie nevoit aștepte 5 secunde înainte de a avea o
șansă. Majoritatea utilizatorilor vor percepe un timp de răspuns de 5 secunde la o comandă
scurtă ca fiind prea mare. Situația poate deveni și mai deplorabilă dacă unele dintre solicitările
de la sfârșitul cozii necesită doar câteva milisecunde de timp de procesor. Cu o valoare mică a
cuantei am fi obținut un serviciu mai bun.
Pe de altă parte, dacă cuanta este setată la o valoare mai mare decât timpul mediu de utilizare
a procesorului, comutarea proceselor se va produce mai rar. În schimb, majoritatea proceselor
vor efectua o operație de blocare înainte de epuizarea cuantei, care provoacă comutarea
procesului. Eliminarea întreruperilor forțate sporește performanța, deoarece comutările se
întâmplă doar atunci când sunt necesare logic, adică atunci când un proces se blochează.
Concluzia poate fi formulată după cum urmează: setarea unei valori prea mici a cuantei
generează prea multe comutări de proces și scade eficiența procesorului, dar setarea unei
valori prea mari poate provoca un timp de răspuns mare la solicitări interactive scurte. Valoarea
cuantei de 20–50 msec este adesea un compromis rezonabil.
Planificarea cu priorități
Planificarea round-robin presupune implicit că toate procesele sunt la fel de importante. Însă,
persoanele care dețin și operează computere multiuser sunt de o altă părere. La o universitate,
de exemplu, ierarhia priorităților pornește de la rector, vin apoi decanii facultății, șefii de
departamente, profesorii, secretarii și în final studenții. Necesitatea de a ține cont de factorii
externi duce la planificarea prioritară. Ideea de bază este simplă: fiecărui proces i se atribuie
o prioritate, iar procesorul este alocat procesului cu cea mai mare prioritate.
Chiar și pe un computer cu un singur proprietar, pot exista mai multe procese, unele dintre ele
mai importante decât altele. De exemplu, unui proces demon care trimite e-mailuri electronice
în fundal ar trebui să i se atribuie o prioritate mai mică decât unui proces care afișează în timp
real un film video pe ecran.
Pentru a împiedica procesele cu prioritate înaltă să abuzeze de procesor, planificatorul poate
micșora prioritatea procesului în curs de desfășurare la fiecare semnal de ceas (adică la fiecare
întrerupere a ceasului). Dacă această acțiune face ca prioritatea procesului curent să devină
mai mică decât cea a primului proces ready va avea loc comutarea proceselor. Există și o altă
alternativă: fiecărui proces i se poate atribui o cuantă maximă de timp în care îi este permis să
o ruleze. Când expiră această cuantă, procesul cu cea mai înaltă prioritate va avea șansa să-i
fie alocat procesorul.
Prioritățile pot fi statice sau dinamice. Pe un computer militar, procesele lansate de generali ar
putea începe cu prioritatea 100, procesele începute de colonei - 90, maiorii - 80, căpitanii - 70,
locotenenții - 60 și așa mai departe conform ierarhiei. În mod alternativ, la un centru informatic
comercial, lucrările cu prioritate ridicată pot costa 100 USD pe oră, cele cu prioritate medie - 75
USD pe oră și cu prioritate mică - 50 USD pe oră. Sistemul UNIX are o comandă, drăguță, care
permite utilizatorului să reducă în mod voluntar prioritatea procesului său, pentru a face plăcere
altor utilizatori. Nimeni nu o folosește.
De asemenea, prioritățile pot fi atribuite de către sistem în mod dinamic pentru a atinge anumite
obiective ale sistemului. De exemplu, procesele I/O-bound își petrec cea mai mare parte a
timpului așteptând finalizarea operațiilor de I/E. Ori de câte ori un astfel de proces are nevoie
de procesor, ar trebui să i se dea imediat procesorul, pentru a-i permite să înceapă tratarea
următoarei cereri de I/E, care poate continua în paralel cu un alt proces CPU-bound. Obligând
un proces I/O-bound să aștepte prea mult procesorul ajungem la utilizarea ineficientă a
memoriei. Un algoritm simplu pentru a oferi un serviciu bun proceselor I/O-bound presupune
setarea valorii priorității în 1/f, unde f este partea din ultima cuantă de timp, pe care a folosit-o
procesul. Un proces care a utilizat doar 1 msec din cuanta sa de 50 msec ar avea prioritatea
50, în timp ce un proces care a rulat 25 msec înainte de blocare - 2, iar un proces care a utilizat
întrega cuantă - prioritatea 1.
Adesea este convenabil ca procesele să fie împărțite în clase conform priorității și să fie folosită
planificarea prioritară pentru clase, iar în cadrul fiecărei clase să utilizăm planificarea round-
robin. În figura 2-43 este prezentat un sistem cu patru clase prioritare. Algoritmul de planificare
este după cum urmează: atâta timp cât există procese pentru executare în clasa cu prioritatea
4, pur și simplu va fi executat fiecare proces pe durata unei cuante conform algoritmului round-
robin, lucra cu procesele din clase cu prioritate mai mică. Dacă în clasa de prioritate 4 nu mia
sunt procese, se va trece la clasa 3. Dacă clasele 4 și 3 sunt ambele goale, atunci se merge la
clasa 2 și așa mai departe. Dacă prioritățile nu sunt ajustate adecvat, clasele cu prioritate mică
“pot muri de foame”.

Fig. 2-43. Algoritm de planificare cu 4 clase de prioritate.


Cozi multiple
Unul dintre primele sisteme cu planificare prioritară a fost CTSS (Compatible Time Sharing
System), folosit pe calculatorul IBM 7094 (Corbato et al., 1962). CTSS avea o problemă cu
comutarea proceselor care era lentă, deoarece calculatorul IBM 7094 putea lucra doar cu un
singur proces în memorie. Fiecare comutare însemna descărcarea (swapping-ul) procesului
curent pe disc și încărcarea unui proces nou de pe disc. Proiectanții CTSS și-au dat repede
seama că, pentru a reduce timpul cheltuit pentru swapping, este mai eficient ca proceselor
CPU-bound să se ofere cuante mari de timp (poate mai rar în timp), decât cuante mici cu o
frevență mai mare. Pe de altă parte, acordarea unor cuante mari tuturor proceselor ar însemna
un timp de răspuns inacceptabil, așa cum am văzut mai sus. Soluția a fost să se creeze clase
prioritare. Procesele din clasa cu prioritatea cea mai înaltă erau executate timp de o cuantă.
Procesele din clasa următoare (prioritate mai mică) – timp de două cuante. Procesele din clasa
3 (prioritate și mai mică) – timp de patru cuante, ș.a.m.d.
De exemplu, fie un proces care are nevoie de 100 cuante pentru a fi finalizat. Inițial, i se va
aloca procesorul pentru o cuantă după care procesul va fi descărcat pe disc. Data următoare
procesul va primi două quante înainte de a-i fi retras procesorul. În continuare acesta ar primi 4,
8, 16, 32 și 64 de quante, deși va folosi doar 37 din 64 quante finale pentru a-și încheia
activitatea. Doar 7 comutări sunt necesare (inclusiv încărcarea inițială) în loc de 100 în cazul
algoritmului round-robin. Mai mult ca atât, pe măsură ce procesul coboră tot mai adânc în cozile
prioritare, acesta va fi rulat din ce în ce mai rar, punând procesorul la dispoziția proceselor
interactive și scurte.
Pot exista situații în care un proces la prima lansare estre de lungă durată, dar apoi devine
interactiv. Pentru a nu penaliza în mod continuu astfel de procese a fost propusă următoarea
politică: la apăsarea tastei Enter, procesul care aparținea terminalului era mutat în clasa cu
prioritate mai mare, presupunând că acesta urma să devină interactiv. Într-o zi frumoasă, un
utilizator cu un proces CPU-bound a descoperit că doar stând la terminal și la fiecare câteva
secunde tastând ceva la întâmplare după care apăsând pe Enter, putea face minuni pentru
timpul său de răspuns. Le-a povestit prietenilor săi. Aceștia la rândul lor - tuturor prietenilor lor.
Morala acestei istorii este: în practică este adesea mai complicat să înțelegi ce se va obține din
ceea ce a fost conceput, decât să asimilezi însuși principiul ideii.
Procesul cel mai scurt - următorul
Deoarece în sistemele de tratare pe loturi acordând prioritate lucrărilor celor mai scurte obținem
minimizarea timpului mediu de răspuns, probabil că nu ar fi rău de utilizat acest principiu și
pentru procesele interactive. Într-o anumită măsură, așa este. De obicei, procele interactive
urmează, în general, schema în care este așteptată o comandă, după care comanda este
executată, apoi este așteptată o altă comendă, urmează executarea, și tot așa. Dacă
considerăm execuția fiecărei comenzi ca o ''lucrare separată '', atunci putem reduce timpul de
răspuns general lansând prima comanda care are nevoie de cel mai mic timp pentru execuție
(procesul “cel mai scurt”). Problema este să știm care dintre procese este cel mai scurt.
O metodă constă în a folosi informații din comportamentul anterior: procesorul să fie acrodat
procesului cu cel mai mic timp de procesor cheltuit până la momentul dat. Presupunem că
pentru un anumit terminal timpul estimat per comandă pentru un proces este T0. Acum să
presupunem că următoarea estimare va fi T1. Am putea actualiza estimarea noastră luând o
sumă ponderată a acestor două numere, adică aT0 + (1 - a)T1. Prin alegerea valorii lui a putem
decide dacă estimare procesului să uite repede vechile execuții sau să le “țină minte” pentru
mult timp. Cu a = 1/2, obținem următoarele estimări succesive:
T0, T0/2 + T1/2, T0/4 + T1/4 + T2/2, T0/8 + T1/8 + T2/4 + T3/2
După trei noi rulări, ponderea lui T0 în noua estimare a scăzut la 1/8.
Tehnica de calculare a următoarei valori dintr-o serie folosind suma ponderată a valorii curent
măsurate cu valorile obținute din calculele anterioare este uneori denumită distribuție de
vârstă. Este utilizată în multe situații în care trebuie să faceți unele predicții pe baza valorilor
anterioare. Distribuția de vârstă este deosebit de ușor de implementat atunci când a = 1/2. Tot
ce trebuie de făcut este de adăugat noua valoare la estimarea curentă și de împărțit suma la 2
(deplasând un bit la dreapta).
Planificare garantată
O abordare complet diferită a planificării este de a face promisiuni reale utilizatorilor cu privire la
performanță și apoi să fiți în conformitate cu aceste promisiuni. O promisiune realistă și ușor de
respectat constă în următoarele: dacă în sistem sunt înregistrați n utilizatori, fiecare va avea la
dispoziție aproximativ 1/n din puterea procesorului. În mod similar, pe un sistem monoutilizator
cu n procese rulate, cu alte condiții egale, fiecare proces ar trebui să obțină 1/n din ciclurile
procesorului. Pare echitabil.
Pentru a îndeplini această promisiune, sistemul trebuie să țină evidența timpului total de
procesor cheltuit de fiecare proces de la crearea sa. Sistemul va calcula cantitatea de timp de
procesor la care fiecare avea dreptul, și anume timpul de la creare împărțit la n. Deoarece este
cunoscută valoarea timpului total de procesor deja consumat pentru fiecare proces, este destul
de simplu să calculăm raportul dintre timpul total efectiv consumat și timpul la care procesul
avea dreptul. Un raport de 0,5 înseamnă că un proces a avut doar jumătate din ceea i s-ar fi
cuvenit, iar un raport de 2,0 înseamnă că un proces a avut de două ori mai mult decât avea
dreptul. Algoritmul alocă procesorul procesului cu cel mai mic raport până când raportul său
devine mai mare decât al celui mai apropiat concurent. Apoi, acest concurent este ales pentru a
rula în continuare.
Planificarede tip loterie
Deși metoda anterioară nu este rea, este mai complicat de pus în aplicare. Un alt algoritm
poate fi utilizat pentru obține rezultate similare la fel de previzibile, dar cu o implementare mult
mai simplă. Se numește planificarea tip loterie (Waldspurger și Weihl, 1994).
Ideea de bază este să se ofere proceselor bilete de loterie pentru diverse resurse de sistem,
cum ar fi timpul de procesor. Ori de câte ori trebuie luată o decizie de planificare, se alege un
bilet de loterie la întâmplare, iar procesul care deține acest bilet primește resursa. Referitor la
planificarea timpului de procesor, sistemul ar putea organiza trageri de 50 de ori pe secundă,
fiecare câștigător obținând ca premiu 20 msec timp de procesor.
Parafrazând pe George Orwell: “Toate procesele sunt egale, dar unele procese sunt mai
egale”. Proceselor mai importante pot fi acordate bilete suplimentare, pentru a crește șansele
de câștig. Dacă sunt 100 de bilete nefolosite și un proces deține 20 dintre ele, el va avea șansa
de 20% de a câștiga la fiecare tragere. Pe termen lung, va obține aproximativ 20% din timpul
de procesor. Spre deosebire de un planificator cu prioritate, unde este foarte greu de precizat
ce înseamnă de fapt o prioritate cu valoarea 40, aici regula este clară: un proces care deține o
fracție f din bilete va obține aproximativ o fracție f din resursa în cauză.
Programarea de tip loterie posedă o serie de proprietăți interesante. De exemplu, dacă apare
un nou proces și i se acordă câteva bilete, la următoarea tragere va avea șansa de a câștiga
proporțional cu numărul biletelor pe care le deține. Cu alte cuvinte, planificarea de tip loterie
reacționează foarte repede la schimbarea situației.
Procesele de cooperare pot schimba bilete dacă doresc. De exemplu, atunci când un proces
client trimite un mesaj către un proces server și apoi se blochează, acesta poate da toate
biletele sale procesului server, pentru a crește șansa ca următorul proces căruia îi va fi alocat
procesorul să fie procesul server. Când procesul server se termină, el returnează biletele, astfel
încât clientul să poată rula din nou. De fapt, în lipsa clienților, serverele nu au nevoie de bilete.
Planificarea de tip loterie poate fi folosită pentru a rezolva probleme dificil de soluționat folosind
alte metode. În calitate de exemplu poate fi un server video în care mai multe procese pun la
dispoziția clienților fluxuri video, cu frecvențe diferite a cadrelor. Să presupunem că procesele
au nevoie de frecvențe 10, 20 și 25 de cadre/sec. Repartizând acestor procese 10, 20 și,
respectiv, 25 de bilete, acestea vor împărți automat procesorul aproximativ în proporție corectă,
adică 10:20:25.
Planificare echitabilă
Până acum am presupus că fiecare proces este planificat de unul singur, fără a ține cont de
proprietar. În consecință, dacă utilizatorul 1 pornește nouă procese și utilizatorul 2 pornește un
proces, pentru cazul round-robin sau algoritmul cu priorități egale, utilizatorul 1 va obține 90%
din timpul de procesor, iar utilizatorul 2 doar 10%.
Pentru a preveni această situație, unele sisteme înaintea planificării iau în calcul cine este
proprietarul procesului. În acest model, fiecărui utilizator i se repartizează o parte din timpul de
procesor, iar planificatorul alege procesele respectând această repartiție. Astfel, dacă există doi
utilizatori și fiecăruia i s-a promis 50% din timpul de procesor, fiecare va primi asta, indiferent
de câte procese are.
Ca exemplu, avem un sistem cu doi utilizatori, fiecare cu promisiunea de 50% din timpul de
procesor. Utilizatorul 1 are patru procese, A, B, C și D, iar utilizatorul 2 are un singur proces, E.
Dacă se utilizează planificarea round-robin, o posibilă secvență de planificare care îndeplinește
toate constrângerile este următoarea:
AEBECEDEAEBECEDE ...
Însă, dacă utilizatorului 1 i se acordă de două ori mai mult timp de procesor decât utilizatorului
2, am putea primi următoarea secvență:
ABECDEABECDE ...
Evident, există numeroase alte posibilități exploatate în funcție de noțiunea cum înțelegem
noțiunea de echitate.

2.4.4 Planificarea în sistemele de timp real


Un sistem în timp real este unul în care timpul joacă un rol esențial. De obicei, unul sau mai
multe dispozitive fizice externe calculatorului generează semnale de intrare, iar calculatorul
trebuie să reacționeze corespunzător la acestea într-un interval de timp fix. De exemplu,
computerul unui CD player primește biți de la dispozitivul de acționare și trebuie să le
transforme în muzică într-un interval de timp foarte mic. Dacă calculul durează prea mult,
muzica va părea ciudată. În calitate de alte exemple pot fi aduse sistemele de monitorizare a
pacienților într-o unitate de terapie intensivă, pilotul automat într-o aeronavă și dispozitivul de
cmandă a roboților industriali într-o fabrică automatizată. În toate aceste cazuri, a avea un
rezultat corect, dar cu întârziere este adesea la fel de inacceptabil ca și să nu-l ai deloc.
Sistemele de timp real sunt clasificate în general în sisteme de timp real dur, în care întârzierele
sunt interzise (există termene absolute care trebuie îndeplinite) și sisteme de timp real flexibile
(isteme de timp real suple), în care nerespectarea iregulată a întârzierilor nu este de dorit, dar
poate fi acceptată. În ambele cazuri, regimul de timp real se realizează prin divizarea
programului într-un număr de procese, fiecare cu un comportament previzibil și cunoscut în
prealabil. Aceste procese au, în general, o durată scurtă de viață și pot finaliza lucrul în mai
puțin de o secundă. Când este detectat un eveniment extern, planificatorul trebuie să planifice
procesele astfel încât să fie respectate toate termenele.
Evenimentele la care ar trebui să răspundă un sistem de timp real pot fi clasificate ca periodice
(se produc la intervale regulate) sau aperiodice (se produc într-un mod imprevizibil). Este
posibil ca un sistem să fie nevoit să răspundă la mai multe fluxuri de evenimente periodice. În
funcție de cât timp este necesar pentru procesarea fiecărui eveniment, este posibil ca sistemul
să nu reușească să trateze toate evenimentele. De exemplu, dacă există m evenimente
periodice și evenimentul i are loc cu perioada Pi și are nevoie de Ci sec timp de procesor pentru
a gestiona fiecare eveniment, atunci informația poate fi procesată doar dacă este respectată
condiția:
∑𝑚
𝑖=0(𝐶𝑖/𝑃𝑖) ≤ 1

Sistemul de timp real care respectă acest criteriu se numește planificabil. Aceasta înseamnă că
poate fi realizat. Un proces care nu îndeplinește acest test nu poate fi planificat, deoarece
cantitatea totală de timp de procesor de care are nevoie acest proces este mai mare decât
procesorul central poate furniza.
Ca exemplu, vom analiza un sistem flexibil de timp real cu trei evenimente periodice, cu
perioade de 100, 200 și respectiv 500 msec. Dacă aceste evenimente necesită respectiv 50, 30
și 100 msec de timp de procesor, sistemul este programabil deoarece 0.5 + 0.15 + 0.2 < 1.
Dacă se adaugă un al patrulea eveniment cu perioada de 1 sec, sistemul va rămâne
programabil atâta timp cât acest eveniment nu are nevoie de mai mult de 150 msec de timp de
procesor. Am presupus că timpul pentru comutarea contextului poate fi ignorat.
Algoritmii de planificare din sistemele de timp real pot fi statici sau dinamici. Primii iau deciziile
de planificare înainte ca sistemul să înceapă să funcționeze. Ultimii urmă iau deciziile de
planificare în timp real, după ce execuția a început. Planificarea statică funcționează numai
atunci când există informații perfecte disponibile în avans despre activități și termenele care
trebuie îndeplinite. Algoritmii dinamici de planificare nu au aceste restricții.

2.4.5 Politici versus mecanisme


Până acum, am presupus tacit că toate procesele din sistem aparțin unor utilizatori diferiți și
concurează astfel pentru procesor. Deși acest lucru este adesea adevărat, uneori se întâmplă
ca un proces să aibă multe procese descendent lansate sub controlul său. De exemplu, un
proces de sistem care gestionează o bază de date poate avea mulți descendenți. Fiecare
descendent poate lucra la o cerere proprie sau să aibă o funcție specifică de îndeplinit
(analizarea interogărilor, acces la disc etc.). Este posibil ca procesul părinte știe la perfecție
care dintre descendenții săi este cel mai important (sau critic d.p.d.v. al timpului de execuție) și
care este mai puțin important. Din păcate, nici unul dintre planificatorii discutați mai sus nu
acceptă nici un fel de date de intrare de la procesele utilizatorilor cu privire la deciziile de
planificare. Drept urmare, planificatorul rareori face cele mai bune alegeri.
Soluția la această problemă constă în separarea mecanismului de planificare de politica de
planificare (Levin și colab., 1975). Aceasta înseamnă că există o anumită modalitate de a
parametriza algoritmul de planificare, care prevede posibilitatea de extindere a parametrilor de
către procesele utilizatorului. Să revenim la exemplul cu baza de date. Presupunem că nucleul
folosește algoritmul de planificare cu priorități, dar oferă un apel de sistem prin care un proces
poate stabili (și modifica) prioritățile descendenților săi. În acest fel, părintele poate controla din
toate punctele de vedere modul în care sunt planificați copiii săi, chiar dacă nu se ocupă de
planificare. În acest caz mecanismul este în nucleu, iar politica este stabilită de un proces de
utilizator. Ideea cheie aici constă în separarea politicii de mecanism.

2.4.6 Planificarea firelor de executie


Când există mai multe procese și fiecare proces are mai multe fire, sunt prezente două niveluri
de paralelism: procese și fire. Planificarea în acest caz depinde în mare măsură de nivelul la
care sunt acceptate firele - la nivel de utilizator, la nivel de nucleu sau la ambele nivele.
Să începem mai întâi cu fire la nivel de utilizator. Deoarece nucleul nu „știe” de existența firelor,
acesta funcționează ca întotdeauna - alege un proces, să zicem, A și îi transmite controlul
pentru o quană de timp. Planificatorul de fire în interiorul procesului A decide ce fir să fie lansat
în execuție, fie A1. Deoarece nu există întreruperi de ceas pentru fire multiprogram, acest fir
poate continua să funcționeze atât timp cât are nevoie. Dacă utilizează întreaga quantă a
procesului, nucleul va selecta un alt proces care va fi rulat.
Când procesul A va fi în sfârșit lansat din nou, firul A1 va relua rularea. Va continua să consume
tot timpul A până când se va termina. Însă comportamentul său “antisocial” nu va afecta alte
procese. Acestia vor obține ceea ce planificatorul va considera parte a lor, indiferent de ceea ce
se întâmplă în interiorul procesului A.
Acum vom analiza cazul în care firele proceslui A îndeplinesc un lucru de scurtă durată în
comparație quanta de timp care i se cuvine procesului, de exemplu, 5 msec de lucru dintr-o
quantă de 50 msec. Deci, fiecare fir rulează un timp scurt după care cedează procesorul
planificatorului firelor. În acest caz, înainte ca nucleul să treacă la procesul B, se poate produce
următoarea secvență: A1, A2, A3, A1, A2, A3, A1, A2, A3, A1. Această situație este prezentată în
fig. 2-44 (a).

Fig. 2-44. (a) Planificare posibilă a firelor de la nivelul utilizatorului cu o cuantă de timp de 50
msec și fire cu CPU-burst de 5 msec. (b) Planificare posibilă a firelor de la nivelul nucleului cu
aceleași caracteristici ca și în (a).
Algoritmul de planificare folosit de sistemul de asistență run-time poate fi oricare dintre cele
descrise mai sus. În practică, planificarea round-robin și planificarea cu priorități sunt cele mai
frecvente. Singura constrângere este absența unui ceas pentru a întrerupe un fir care rulează
prea mult. Din moment ce firele cooperează, aceasta nu este de obicei o problemă.
Acum, luați în considerare situația cu fire de la nivel de nucleu. Aici nucleul alege un anumit fir
pentru ai aloca procesorul. Nu trebuie să țină seama de care proces aparține firul, dar poate,
dacă este necesar. Firului îi este alocat procesorul pentru o quantă de timp, care este retras
forțat dacă cuanta expiră. Cu o cuantă de 50 msec și fire care se blochează după 5 msec,
ordinea firelor pentru o perioadă de 30 msec ar putea fi A1, B1, A2, B2, A3, B3, ceva ce nu este
posibil cu acești parametri și fire la nivel de utilizator. Această situație este parțial descrisă în
Fig. 2-44 (b).
O diferență majoră între firele la nivel de utilizator și cele la nivel de nucleu este performanța.
Comutarea firelor, realizate la nivel de utilizator, se face doar print-un număr mic de instrucțiuni
de calculator, iar pentru comutarea firelor, realizate la nivel de nucleu este necesară comutarea
totală a contextului, schimbarea mapei memoriei, anularea memoriei cache, ceea ce se
produce de câteva zeci de ori mai lent. Pe de altă parte, un fir la nivel de nucleu, blocat pe o
operație de I/E nu suspendă întregul proces, așa cum se întâmplă cu firele la nivel de utilizator.
Din moment ce nucleul știe că trecerea de la un fir din procesul A la un fir din procesul B costă
mai scump decât rularea unui al doilea fir al procesului A (datorită faptului că trebuie să schimbi
harta de memorie și ca memoria cache să fie anulată), nucleul poate folosi aceste informații la
adoptarea deciziei. De exemplu, în cazul a două fire care sunt la fel de importante, doar că
primul aparține aceluiași proces un fir din care tocmai a fost blocat și al doilea aparține unui alt
proces, ar putea fi preferat primul proces.
Un alt moment important este că firele la nivel de utilizator pot utiliza un planificator de fire
specific aplicației. De exemplu, serverul Web din fig. 2-8. Să presupunem că un fir tocmai s-a
blocat, iar firul dispecer și două fire de lucru sunt gata de execuție. Care fir ar trebui să fie
următorul? Planificatorul, știind destinația firelor, poate alege firul dispecerului să ruleze, astfel
încât acesta să poată porni un alt fir. Această strategie maximizează cantitatea de paralelism
într-un mediu în care firele de execuție se blochează frecvent pe operații de I/E. Cu fire la nivel
de kernel, nucleul nu știe care este destinația unui fir (deși li s-ar putea atribui priorități diferite).
În general, însă, planificatoarele firelor, care iau în considerare momentele specifice ale
aplicațiilor se pot adapta mai bine la aplicație decât nucleul.

2.5 Probleme clasice ale comunicării între procese


Literatura sistemelor de operare este plină de probleme interesante, care au fost discutate pe
larg și analizate folosind o varietate de metode de sincronizare. În secțiunile următoare vom
examina trei dintre problemele mai cunoscute.

2.5.1 Problema Cina filozofilor


În 1965, Dijkstra a formulat și apoi a rezolvat o problemă de sincronizare pe care a numit-o
Cina filozofilor. De atunci, toată lumea care inventează o primitivă de sincronizare s-a simțit
obligată să demonstreze cât de minunată este noua primitivă pe exemplul rezolvării problemei
filozofilor din sala de mese. Esența probleme este relativ simplă. Cinci filozofi sunt așezați în
jurul unei mese rotunde. Fiecare filosof are o farfurie cu spaghete. Spaghetele sunt atât de
alunecoase încât pot fi mâncate doar folosind două furculițe. Între fiecare pereche de farfurii
este câte o furculiță. Schematic situația este ilustrată în fig. 2-45.

Viața unui filozof constă din alternarea perioadelor cand acesta mănâncă și respectiv gândește.
(Este evident o abstractizare, chiar și pentru filozofi, dar celelalte activități sunt irelevante aici.)
Când unui filozof i se face foame, el încearcă să ia furculița din dreapta și furculița din stânga.
Daca reușește, mănâncă o perioadă apoi pune furculițele jos și continuă să gândească.
Întrebarea cheie este: ar fi posibil de scris un program pentru fiecare filozof care acționează în
modul de mai sus și nu se blochează niciodată? (Putem observa că, cerința referitoare la două
furculițe este oarecum artificială; poate că ar trebui să trecem de la bucătăria italiană la cea
chinezească, înlocuind spaghetele cu orez și bețișoare în loc de furculițe.)
În listingul din fig. 2-46 este prezentată soluția cea mai simplă a problemei. Procedura take_fork
așteaptă până când furculița specificată va deveni disponibilă și apoi o ia. Din păcate, soluția
este greșită. Să presupunem că toți filozofii își iau furculițele din stânga simultan. În acest caz
nimeni nu va putea să-și ia furculița din dreapta și se va intra în blocare reciprocă (impas).
#define N 5 /* numărul de filosofi */
void philosopher(int i) /* i: numărul filosofului (de la 0 la 4) */
{
while (TRUE) {
think( ); /* filosoful gândește */
take_fork(i); /* ia furculița stângă */
take_fork((i+1) % N); /* ia furculița dreaptă; */
/* % - împărțirea în modul N */
eat(); /* mănâncă */
put_fork(i); /* pune pe masă furculița stângă */
put_fork((i+1) % N); /* pune pe masă furculița dreaptă */
}
}
Fără prea mare efort am putea modifica programul astfel încât după luarea furculiței din stânga,
programul să verifice dacă este disponibilă furculița din dreapta. Dacă această furculiță nu este
disponibilă, filosoful pune înapoi furculița din stânga, așteaptă ceva timp și apoi repetă întregul
proces. Dar și această soluție este greșită, dar dintr-un alt motiv. Cu un pic de ghinion, toți
filozofii ar putea începe algoritmul simultan, să ia furculițele din stânga, să observe că furculițele
din dreapta nu sunt disponibile, să punâ pe masă furculițele din stânga, să aștepte, să ia din
nou simultan furculițele din stânga, și tot așa pentru totdeauna. O situație de genul acesta, în
care toate programele continuă să funcționeze la nesfârșit, dar nu reușesc să progreseze, este
numită înfometare. (Se numește înfometare chiar și atunci când problema apare în afara unui
restaurant italian sau chinezesc.)
S-ar putea să presupuneți că este suficient să schimbăm timpul de așteptare după prima
încercare nereușită, care era același pentru toți filozofii, cu o valoare aleatoare și probabilitatea
că filozofii vor bate pasul pe loc va fi foarte mică. Această observație este bună și, în aproape
toate aplicațiile, o nouă încercare după un interval de timp ales la întâmplare nu este o
problemă. De exemplu, într-o rețea locală Ethernet, dacă două computere trimit un pachet în
același timp, fiecare așteaptă un timp aleatoriu și încearcă din nou; în practică această soluție
funcționează bine. Însă, în unele aplicații, poate fi necesară o soluție care funcționează
întotdeauna și nu poate eșua din cauza unei serii improbabile de numere aleatorii. Gândiți-vă la
controlul securității într-o centrală nucleară.
Programul prezentat în listingul 2-46 poate fi îmbunătățit pentru a evita blocajele reciproce și
înfometarea. În acest scop trebuie protejați cu ajutorul unui semafor binar cinci operatori care
urmează după apelul think. Înainte să ia furculițe, filozoful i poate apela funcția down pe un
semafor mutex. După ce mănâncă și pune furculițele pe masă, apelează funcția up pe mutex.
Din punct de vedere teoretic, soluția este bună, dar practic, are un bug de performanță: un
singur filosof poate mânca la un moment de timp dat. Însă, cu cinci furculițe la disponiziție, ar
trebui să poată mânca simultan doi filosofi.
Soluția prezentată în fig. 2-47 nu generează blocări reciproce și permite paralelismul maxim
pentru un număr arbitrar de filozofi. Aici este introdus un tablou state, care memorează starea
fiecărui filozof: gândește, îi este foame, mănâncă. Un filozof poate trece în starea mănâncă
numai dacă niciun vecin nu este în această stare. Vecinii filozofului cu numărul i sunt definiți
prin macrocomenzile LEFT și RIGHT. Deci, dacă i este 2, LEFT este 1, iar RIGHT este 3.
#define N 5 /* numărul de filozofi */
#define LEFT (i+N−1) %N /* numărul vecinului din stânga */
#define RIGHT (i+1) %N /* numărul vecinului din dreapta */
#define THINKING 0 /* filozoful gândește */
#define HUNGRY 1 /* filozoful flămânzește */
#define EATING 2 /* filozoful mănâncă */
typedef int semaphore; /* semaforul – tip întreg special */
int state[N]; /* memorează starea fiecărui filozof */
semaphore mutex = 1; /* excludere reciprocă pentru regiuni critice */
semaphore s[N]; /* un semafor per filozof */

void philosopher(int i) /* i – numărul filozofului (de la 0 la N−1) */


{
while (TRUE) { /* ciclu infinit */
think(); /* filozoful gândește */
take_forks(i); /* filozoful ia două furculițe sau se blochează */
eat(); /* filozoful mănâncă */
put_forks(i); /* filozoful pune ambele furculițe pe masă */
}
}

void take_forks(int i) /* i – numărul filozofului (de la 0 la N−1) */


{
down(&mutex); /* intrare în secțiunea critică */
state[i] = HUNGRY; /* înregistrarea faptului că filozoful vrea să mănânce */
test(i); /* încercarea de a lua două furculițe */
up(&mutex); /* ieșirea din secțiunea critică */
down(&s[i]); /* blocare, dacă nu a reușit să ia două furculițe */
}

void put_forks(i) /* i – numărul filozofului (de la 0 la N−1) */


{
down(&mutex); /* intrare în secțiunea critică */
state[i] = THINKING; /* filozoful a mâncat */
test(LEFT); /* verificare dacă vecinul din stânga vrea să mănânce */
test(RIGHT); /* verificare dacă vecinul din dreapăta vrea să mănânce */
up(&mutex); /* ieșirea din secțiunea critică */
}

void test(i) /* i – numărul filozofului (de la 0 la N−1) */


{
if (state[i]==HUNGRY&&state[LEFT]!=EATING&&state[RIGHT]!=EATING) {
state[i] = EATING;
up(&s[i]);
}
}
Fig. 2-47. O soluție a problemei Cina filozofilor
Programul folosește un masiv de semafoare, una per filozof, astfel încât un filozof înfometat se
va bloca dacă furculița necesară este ocupată. Rețineți că fiecare proces în calitate de program
principal lansează procedura philosopher, dar celelalte proceduri: take_forks, put_forks și test
sunt proceduri obișnuite, nu procese separate.

2.5.2 Problema Cititorilor si Scriitorilor


Problema Cina filosofilor este utilă pentru modelarea proceselor care concurează pentru
accesul exclusiv la un număr limitat de resurse, cum ar fi dispozitivele de I/E. O altă problemă
faimoasă este problema cititorilor și scriitorilor (Courtois și colab., 1971), care modelează
accesul la o bază de date. Să ne imaginăm, de exemplu, un sistem de rezervare a biletelor de
avion, cu multe procese concurente care se adresează la BD pentru citire sau scriere. Este
acceptabil să existe mai multe procese care citesc simultan din baza de date, dar dacă un
proces actualizează (scrie în) baza de date, niciun alt proces nu poate avea acces la baza de
date, nici măcar pentru citire. Întrebarea este cum să scriem un program care să modeleze
situația? O soluție este prezentată în fig. 2-48.
typedef int semaphore; /* exploatați imaginația proprie */
semaphore mutex = 1; /* controleză accesul la rc */
semaphore db = 1; /* controleză accesul la BD */
int rc = 0; /* # numărul de cititori sau în așteptare la citire */
void reader(void)
{
while (TRUE) { /* ciclu infinit */
down(&mutex); /* obținerea accesulu exclusiv la rc */
rc = rc + 1; /* cu un cititor mai mult acuma */
if (rc == 1) down(&db); /* dacă este primul cititor ... */
up(&mutex); /* încheierea accesului exclusiv la rc */
read_data_base( ); /* acces la date */
down(&mutex); /* obținerea accesulu exclusiv la rc */
rc = rc −1; /* cu un cititor mai puțin acuma */
if (rc == 0) up(&db); /* dacă este ultimul cititor ... */
up(&mutex); /* încheierea accesului exclusiv la rc */
use_data_read( ); /* regiune noncritică */
}
}
void writer(void)
{
while (TRUE) { /* ciclu infint */
think_up_data( ); /* regiune noncritică */
down(&db); /* obținerea accesulu exclusiv */
write_data_base( ); /* actualizarea datelor */
up(&db); /* încheierea accesului exclusiv */
}
}
În această soluție, primul cititor care obține acces la baza de date efectuează o operație down
pe semaforul db. Cititorii următori nu fac decât să incrementeze cu 1 valoarea contorului rc. Pe
măsură ce cititorii pleacă, ei decrementează contorul, iar ultimul care pleacă face o operație up
pe semafor, permițând unui scriitor blocat, dacă există, să intre.
Soluția prezentată aici conține implicit o decizie subtilă demnă de remarcat. Să presupunem că
în timp ce un cititor folosește baza de date, vine un alt cititor. Deoarece a avea doi cititori în
același timp nu este o problemă, al doilea cititor este admis. Cititorii suplimentari pot fi, de
asemenea, admiși dacă vin.
Să presupunem că apare un scriitor. Scriitorul nu poate fi admis în baza de date, deoarece
scriitorii trebuie să aibă acces exclusiv, deci scriitorul este suspendat. Mai târziu, apar cititori
suplimentari. Atâta timp cât cel puțin un cititor este încă activ, cititorii următori sunt admiși. Ca
urmare a acestei strategii, atâta timp cât există o ofertă constantă de cititori, toți vor intra
imediat ce apar. Scriitorul va fi ținut suspendat până când nu vor pleca toți cititorii. Dacă un
cititor nou sosește, să zicem, la fiecare 2 secunde și fiecare cititor are nevoie de 5 secunde
pentru a-și face munca, scriitorul nu va intra niciodată.
Pentru a evita această situație, programul trebuie un pic modificat: dacă în momentul sosirii
unui cititor există deja un scriitor în așteptare, cititorul nu are acces imediat, ci își suspendă
lucrul și este pus în spatele scriitorului în așteptare. Astfel, un scriitor trebuie să aștepte cititorii
activi să termine, dar nu și cititorii care au venit după el. Dezavantajul acestei soluții este că
diminuează nivelul paralelismului și conduce la performanțe mai scăzute. Courtois și colab. au
propus o soluție care acordă prioritate scriitorilor. Pentru detalii, consultați lucrarea.

2.6 Cercetări asupra proceselor și firelor de execuție


În cap. 1, am analizat unele dintre cercetările actuale consacrate structurii sistemului de
operare. Aici, dar și în următoarele capitole, vom face cunoștință cu cercetări mai înguste, care
încep cu procesele. Cu timpul va deveni clar, unii subiecți au fost mult mai bine cercetați decât
alții. Majoritatea cercetărilor sunt de obicei focalizate pe subiecte noi și nu au tangență cu
temele care există de zeci de ani.
Conceptul proces este un exemplu de temă bine pusă la punct. Practic în fiecare sistem de
operare este utilizată noțiunea de proces ca un container destinat grupării resurselor conexe,
cum ar fi un spațiu de adrese, fire de execuție, fișiere deschise, permisiuni de accesare la o
resursă protejată și așa mai departe. Gruparea diferă de la sistem la sistem, însă acestea sunt
doar diferențe de inginerie. Ideea de bază nu mai stârnește discuții și există foarte puține
cercetări noi pe tema proceselor.
Firele sunt o idee mai nouă decât procesele, dar și ele sunt cercetate relativ fundamental.
Totuși, de la caz la caz apar articole consacrate firelor, de exemplu, clasterizația firelor în
sistemele multiprocesorale (Tam și colab., 2007) sau despre cât de scalabile sunt sistemele de
operare moderne, precum Linux, dacă există multe fire și multe nuclee (Boyd-Wickizer, 2010).
Un domeniu de cercetare deosebit de activ este cel al înregistrării și reproducerii (replaying)
execuției unui proces (Viennot și colab., 2013). Reproducerea îi ajută pe dezvoltatori să
găsească erorile greu de depistat, iar pe experții în securitate - la investigarea incidentelor.
În mod similar, multe cercetări în comunitatea sistemelor de operare astăzi se concentrează pe
probleme de securitate. Numeroase incidente au demonstrat că utilizatorii au nevoie de o
protecție mai bună împotriva atacatorilor (și, uneori, de la ei înșiși). O abordare constă în
urmărirea și restricționarea cu atenție a fluxurilor de informații dintr-un sistem de operare (Giffin
și colab., 2012).
Planificarea (atât uniprocesor, cât și multiprocesor) este încă un subiect apropiat și drag inimii
unora dintre cercetători. Unele dintre subiectele cercetate includ planificarea eficientă din punct
de vedere energetic pe dispozitivele mobile (Yuan și Nahrstedt, 2006), planificarea ținând cont
de hiperfire (hyperthreading-aware, Bulpin și Pratt, 2005) și planificarea cu offset cunoscut
(Koufaty, 2010). Odată cu sporirea cantității de calcule executate pe smartphone-urile cu
capacitate redusă de baterie, unii cercetători propun ca de fiecare dată când este comod,
procesul de calcul să migreze către un server mai puternic din cloud (Gordon et al., 2012). Cu
toate acestea, puțini dezvoltatori de sisteme moderne rătăcesc fără rost timp de zile întregi,
ridicându-și mâinile din cauza lipsei unui algoritm adecvat de planificare a firelor, astfel că acest
tip de cercetare este împins mai mult de cercetători decât condus de intensitatea cererii. În
general, procesele, firele și planificarea nu mai sunt, ca pe vremuri, subiecte de cercetare
relevante. Zilele cercetărilor active au trecut, iar cercetătorii s-au redirecționat la subiecte
precum managementul puterii, virtualizare, cloud computing și securitate.

2.7 Rezumat
Un proces poate fi într-o stare de execuție, gata de executat sau blocat și poate schimba starea
atunci când el însuși sau alte procese folosesc una dintre primitivele de comunicare între
procese. Interacțiunea fluxurilor este de natură similară.

Pentru a ascunde efectele întreruperilor, sistemele de operare oferă modelul conceptual al


proceselor secvențiale, care se desfășoară în paralel. Procesele pot fi create și distruse
dinamic. Fiecare proces are propriul spațiu de adrese.
Pentru unele aplicații este util să avem mai multe fire de execuție (de control) în cadrul unui
singur proces. Aceste fire au o planificare independentă și fiecare are propria stivă, dar toate
firele care aparțin aceluiași proces împărtășesc un spațiu de adrese comun. Firele pot fi
implementate în spațiul utilizatorului sau în nucleul sistemului de operare.
Procesele pot comunica între ele folosind primitive de comunicare interproces, de exemplu,
semafoare, monitoare sau mesaje. Aceste primitive sunt utilizate pentru a exclude prezența
simultană a două procese în zonele lor critice, adică pentru a preveni situația care duce la
haos. Un proces poate fi într-o stare de execuție (exe), gata de executat (ready) sau blocat
(ready) și poate schimba starea atunci când el însuși sau alte procese folosesc una dintre
primitivele de comunicare între procese. Comunicarea între fire este similară.
Primitivele de comunicare între procese pot fi utilizate pentru a rezolva probleme precum
producător-consumator, cina filozofilor și cititor-scriitor. Chiar și atunci când utilizați primitivele,
trebuie să aveți grijă specială pentru a evita erorile și blocajele.
Au fost studiați o mulțime de algoritmi de planificare. Unii algoritli, cum ar fi cererea cea mai
scurtă servită prima, sunt utilizați în primul rând în sistemele de tratare pe loturi. Alți algoritmi
sunt folosiți atât în sistemele de tratare pe loturi, cât și în cele interactive. Acești algoritmi includ
planificarea round-robin și prioritară, fire de așteptare cu niveluri, planificarea garantată sau tip
loterie și planificarea exchitabilă. În unele sisteme este trasată o linie clară între mecanismul de
planificare și politica de planificare, ceea ce permite utilizatorilor să dețină controlul algoritmului
de planificare.
Au fost studiați foarte mulți algoritmi de planificare. Unele dintre acestea sunt utilizate în
principal pentru sisteme de loturi, cum ar fi programarea cea mai scurtă pentru prima muncă.
Altele sunt comune atât în sistemele de lot, cât și în cele interactive. Acești algoritmi includ
rundă rotundă, planificare de prioritate, cozi pe mai multe niveluri, programare garantată,
programare de loterie și planificare de partajare corectă. Unele sisteme fac o separare curată
între mecanismul de planificare și politica de programare, ceea ce permite utilizatorilor să
dețină controlul asupra algoritmului de planificare.
Probleme
1. În fig. 2-2, sunt prezentate trei stări de proces. În teorie, cu trei stări, ar putea exista șase
tranziții, câte două pentru fiecare stare. Cu toate acestea, sunt prezentate doar patru tranziții.
Există circumstanțe în care ar putea apărea oricare sau ambele tranziții lipsă?
2. Să presupunem că ar trebui să proiectați o arhitectură de computer nouă care în loc să
utilizeze întreruperi execută comutarea hardware a proceselor. De care informații ar avea
nevoie procesorul central? Descrieți un posibil dispoztiv de comutare hardware a proceselor.
3. Pentru toate computerele actuale, cel puțin o parte din procedurile de tratare a întreruperilor
sunt scrise în limbajul de asamblare. De ce?
4. Atunci când în rezultatul unei întreruperi sau a unui apel de sistem controlul este transmis
sistemului de operare, este utilizează de obicei o zonă de stivă a nucleului separată de stiva
procesului întrerupt. De ce?
5. Un calculator are suficient spațiu în memoria sa principală pentru a păstra cinci programe.
Aceste programe sunt în așteptare în așteptarea operațiilor de I/E jumătate din timp. Ce fracție
din timpul procesorului este pierdut?
6. Un computer are 4 GB RAM din care sistemul de operare ocupă 512 MB. Procesele sunt
toate de 256 MB (pentru simplitate) și au aceleași caracteristici. Dacă obiectivul este utilizarea
procesorului la 99%, care este timpul maxim de așteptare a operațiilor de I/E care poate fi
tolerat?
7. Mai multe lucrări pot rula în paralel și se pot termina mai repede decât dacă ar fi rulat
secvențial. Să presupunem că două lucrări, fiecare având nevoie de 20 de minute timp de
procesor, încep simultan. Cât timp va dura ultimul până se va finaliza dacă vor rula secvențial?
Cât timp dacă rulează în paralel? Presupunem că în așteptarea finalizării operațiilor de I/E se
cheltuie 50% din timp.
8. Fie un sistem cu gradul de multiprogramare 6 (are simultan în memorie șase programe).
Presupunem că fiecare proces petrece 40% din timpul său așteptând I/E. Care va fi rata de
utilizare a procesorului central?
9. Presupunem că încercați să descărcați un fișier de 2 GB de pe Internet. Fișierul este
disponibil dintr-un set de servere oglindă, fiecare putând livra un subset de octeți ai fișierului;
presupunem că o solicitare este specificată prin octeții de început și de încheiere a fișierului.
Explicați cum puteți utiliza firle pentru a diminua timpul de descărcare.
10. În textul capitolului s-a afirmat că modelul din fig. 2-8,a nu este bun pentru un server de
fișiere, care utilizează cache în memoria operativă. De ce nu? Ar putea avea fiecare proces
propriul cache?
11. La ramificarea unui proces multifir, apare o problemă atunci când în procesul descendent
sunt copiate toate firele procesului părinte. Să presupunem că unul dintre firele originale era în
starea de așteptare a introducerii de la tastatură. Acuma vor fi în așteptarea intrării de la
tastatură două fire, câte unul pentru fiecare proces. Poate oare această problemă să apară
vreodată în procesele cu un singur fir?
12. În fig. 2-8 este prezentat un server Web multifir. Dacă singura modalitate de a citi dintr-un
fișier este apelul obișnuit blocant read, care tip de fire este utilizat pentru serverul Web - la nivel
de utilizator sau la nivel de nucleu? De ce?
13. În textul capitolului, a fost descris un server Web multifir, arătând de ce este mai bun decât
un server cu un singur fir și un server de de tip automat finit. Există circumstanțe în care un
server cu un singur fir ar putea fi mai bun? Dă un exemplu.
14. În fig. 2-12 setul de regiștri este listat ca un element caracteristic fiecărui fir (per-thread), nu
fiecărui proces. De ce? Doar mașina are un singur set de regiștri.
15. De ce ar putea renunța vreodată benevol un fir la procesorul central apelând procedura
thread_yield? Doar în lipsa întreruperilor periodice de ceas, există posibilitatea să nu mai obțină
niciodată procesorul.
16. Poate fi vreodată oprit un fir de o întrerupere a ceasului? Dacă da, în ce circumstanțe?
Dacă nu, de ce nu?
17. În această problemă, trebuie să comparați citirea unui fișier folosind un server de fișiere cu
un singur fir și un server multithreaded. Este nevoie de 12 msec pentru a obține o solicitare de
lucru, a o expedia și a face restul procesării, presupunând că datele necesare sunt în memoria
cache a blocului. Dacă este necesară o operație de disc, ceea ce se întâmplă în o treime din
cazuri, este necesar un plus de 75 msec, timp în care firul este suspendat. Câte cereri per sec
poate suporta serverul dacă are un singur fir? Dar dacă este multithreaded?
18. Care este cel mai mare avantaj al implementării thread-urilor în spațiul utilizatorului? Care
este cel mai mare dezavantaj?
19. În fig. 2-15 procesele de creare a firelor și mesajele extrase de fire alternează la întâmplare.
Există oare o modalitate de a stabili forțat o ordine strictă: firul 1 a fost creat, firul 1 afișază
mesajul despre existența firului 1, analogic firul 2 ș.a.m.d. Dacă da, cum poate fi realizată?
Dacă nu, de ce nu?
20. În discuția despre foloisrea variabilele globale în fire, a fost folosită o procedură
create_global pentru a aloca o zonă de memorie, dar nu pentru însăși variabila, ci pentru a
stoca un pointer la variabilă. Este oare obligatorie această acțiune sau procedurile pot funcționa
la fel de bine cu valorile propriu-zise ale variabilelor?
21. Fie un sistem în care fierle sunt implementate în întregime în spațiul utilizatorului, iar
sistemul de rulare obține o întrerupe de ceas o dată pe secundă. Să presupunem că are loc o
întrerupere de ceas în timp ce se execută un fir în sistemul de rulare. Ce problemă ar putea
apărea? Puteți sugera o modalitate de a o rezolva?
22. Presupunem că sistemul de operare nu are ceva de genul apelului de sistem select pentru
a vedea în avans dacă este sigur să citiți dintr-un fișier, conductă (pipe) sau dispozitiv, dar
permite setarea alarmei de ceas care va întrerupe apelurile de sistem blocante. Este oare
posibil să fie implementat un set de fire în spațiul utilizatorului în aceste condiții? Discutați.
23. Funcționează oare metoda așteptării active care folosește variabila turn (fig. 2-23) atunci
când cele două procese rulează pe un multiprocesor cu memorie partajată, adică două
procesoare centrale care partajează o memorie comună?
24. Va funcționa oare algoritmul lui Peterson pentru excluderea reciprocă, prezentat în fig. 2-24
atunci când planificarea procesului este preemptivă? Dar atunci când este nonpreemptivă?
25. Poate oare problema inversării priorităților, discutată în sec. 2.3.4, să apară în firele
realizate la nivel de utilizator? De ce da sau de ce nu?
26. În sec. 2.3.4, a fost descrisă o situație cu un proces cu prioritate ridicată, H și un proces cu
prioritate scăzută, L, care coduce la ciclarea lui H pentru totdeauna. Poate oare să apară
aceeași problemă dacă se folosește programarea round-robin în loc de planificarea cu
prioritate? Discutați.
27. Ce fel de stive există în sistemele cu fire realizate la nivel de utlizator: câte o stivă pe fir
sau câte o stivă pe proces? Dar când se folosesc fire la nivel de nucleu? Motivați răspunsul.
28. La crearea unui calculator nou, funcționarea lui este de obicei simulată mai întâi pe un
program care execută instrucțiunile strict secvențial. Chiar și sistemele multiprocesorale sunt
simulate în acest mod. Este oare posibil ca o condiție de cursă să apară dacă nu există
evenimente simultane, ca în acest caz?
29. Problema producător-consumator poate fi extinsă la un sistem cu mai mulți producători și
consumatori care introduc datele în sau extrag datele dintr-o bufer comun. Presupunem că
fiecare producător și consumator rulează în propriul său fir. Soluția prezentată în fig. 2-28,
folosind semafoare, va funcționa oare în acest caz?
30. Analizați următoarea soluție a problemei de excludere reciprocă care implică două procese
P0 și P1. Presupunem că variabila turn este inițializată în 0. Codul procesului P0 este prezentat
mai jos.
/ * Alt cod * /
while (turn! = 0) {} /* Nu faceți nimic și așteptați. */
Secțiunea critică / *. . . * /
tur n = 0;
/* Alt cod */
Pentru procesul P1, înlocuiți 0 cu 1 în codul de mai sus. Determinați dacă soluția îndeplinește
toate condițiile necesare pentru o soluție corectă de excludere reciprocă.
31. Cum ar fi posibil ca într-un sistem de operare care poate dezactiva întreruperile să fie
implementate semafoarele?
32. Arătați cum semafoarele counting (adică semafoare care pot deține o valoare arbitrară) pot
fi implementate folosind doar semafoare binare și instrucțiuni obișnuite ale mașinii.
33. Dacă un sistem are doar două procese, are sens să folosești o barieră pentru a le
sincroniza? De ce da sau de ce nu?
34. Se pot oare sincroniza două fire din același proces folosind un semafor de nucleu dacă
firele sunt realizate în spațiul nucleului? Ce se întâmplă dacă sunt implementate în spațiul
utilizatorului? Presupunem că niciun fir în orice alte procese nu au acces la semafor. Discutați
răspunsurile dvs.
35. Sincronizarea cu ajutorul monitoarelor utilizează variabile de tip condiție și două operații
speciale, wait și signal. O formă mai generală de sincronizare ar fi să aem o singură primitivă,
waituntil, care ar avea ca parametru un predicat boolean arbitrar. Astfel, s-ar putea spune, de
exemplu,
waituntil x < 0 or y + z < n
Primitiva signal nu ar mai fi necesară. Această schemă este în mod clar mai generală decât
cea a lui Hoare sau Brinch Hansen, dar nu este folosită. De ce nu? (Sugestie: gândiți-vă la
implementare.)
36. Un restaurant fast-food are patru tipuri de angajați: (1) recepționeri de comenzi, care iau
comenzile clienților; (2) bucătari, care pregătesc mâncarea; (3) specialiști în ambalare, care
împachetează mâncarea în pungi; și (4) casieri, care dau pungile clienților și iau banii. Fiecare
angajat poate fi privit ca un proces secvențial de comunicare. Ce formă de comunicare între
procese folosesc? Ralaționați acest model cu procesele din UNIX.
37. Presupunem că avem un sistem de mesageie care utilizează căsuțe poștale. Când trimiteți
la o căsuță plină sau încercați să preluați din una goală, un proces nu se blochează. În schimb,
un cod de eroare este generat. Procesul răspunde la codul de eroare printr-o nouă încercare,
repetat, până când reușește. Poate oare aceasta schema genera conditii de cursa?
38. Calculatoarele CDC 6600 pot gestiona până la 10 procese de I/E simultan folosind o formă
interesantă de planificare round-robin, numită partajarea procesorului. Comutarea procesului
are loc după fiecare instrucțiune, deci instrucțiunea 1 provine de la procesul 1, instrucțiunea 2 –
de la procesul 2, etc. Comutarea era realizată de hardware special, iar timpul de comutare
poate fi neglijat. Dacă un proces în absența concurenței avea nevoie de T sec pentru a fi
terminat, de cât timp era nevoie dacă procesorul partajat era fost folosit de n procese?
39. Fie fragmentul de cod C:
void main () {
fork();
fork();
exit();
}
Câte procese descendent vor fi create la executarea acestui program?
40. Planificatorii round-robin mențin în mod normal o listă cu toate procesele rulabile, fiecare
proces existând exact o dată în listă. Ce s-ar întâmpla dacă un proces ar apărea de două ori în
listă? Ați putea vreun motiv pentru care să se permită acest lucru?
41. Ar fi posbil oare ca în rezultatul analizei codului să se determine categoria procesului –
CPU-bound sau I/O-bound? Cum poate fi stabilit acest lucru în timpul executării programului?
42. Explicați modul în care valoarea cuantei timpului de procesor și timpul necesar pentru
comutarea contextului se afectează reciproc, într-un algoritm de planificare round-robin.
43. Măsurătorile efectuate într-un sistem concret au arătat că execuția până la blocare pentru
I/E a unui proces mediu în sens statistic durează T. Comutarea procesului durează S, cheltuit
în van (cheltuieli de regie). Pentru planificarea round-robin cu cuanta Q, elaborați o formulă
pentru calcularea eficienței utilizării procesorului central pentru următoarele cazuri:
(a) Q = ∞
(b) Q > T
(c) S < Q < T
(d) Q = S
(e) Q ≈ 0
44. În firul de așteptare al proceselor ready sunt 5 procese. Timpul estimat de executare pentru
fiecare proces este respectiv 9, 6, 3, 5 și X. În ce ordine ar trebui să fie executate procesele
pentru a minimiza timpul mediu de răspuns? (Răspunsul dvs. va depinde de X.)
45. La un centru de calcul cu tratare pe loturi sosesc practic simultan cinci lucrări, notate de la
A la E. Timpul estimat de execuție este 10, 6, 2, 4 și 8 minute. Prioritățile lor (determinate
extern) sunt 3, 5, 2, 1, 5 fiind cea mai mare prioritate. Pentru fiecare dintre următorii algoritmi de
planificare, determinați timpul mediu de aflare în sistem pentru fiecare dintre procese. Ignorați
timpul de comutare a procesorului.
(a) Round-robin.
(b) Planificarea prioritară.
(c) Prim venit, prim servit (executați în ordinea 10, 6, 2, 4, 8).
(d) Cel mai scurt servit primul.
Pentru (a), presupunem că sistemul este multiprogramat și că fiecare lucrare primește cota sa
corectă de procesor. Pentru (b) până la (d), presupunem că la un moment dat este executată o
singură lucrare, până când se termină. Toate lucrările sunt CPU-bound.
46. Un proces care rulează pe CTSS are nevoie de 30 de cuante de timp pentru a fi finalizat.
De câte ori trebuie executat swapping-ul, inclusiv prima dată (înainte de lansare)?
47. Fie un sistem de timp real cu două apeluri vocale cu periodicitatea 5 msec fiecare și timp de
procesare per apel de 1 msec și un flux video cu periodicitatea 33 ms și timp de procesare de
11 msec. Calculați dacă este planificabil acest sistem.
48. Pentru problema de mai sus întrebare: este posibil de adăugat un alt flux video, păstrând
planificabilitatea?
49. Algoritmul de îmbătrânire cu a = 1/2 este utilizat pentru a prezice timpii de rulare. Cele patru
runde anterioare, de la cele mai vechi până la cele mai recente, sunt 40, 20, 40 și 15 msec.
Care este predicția pentru perioada următoare?
50. Un sistem flexibil de timp real are patru evenimente periodice cu perioade de 50, 100, 200
și 250 msec fiecare. Să presupunem că cele patru evenimente necesită 35, 20, 10 și X msec
timp de procesor, respectiv. Care este cea mai mare valoare a lui X pentru care sistemul este
planificabil?
51. În problema Cina filosofilor este folosit următorul protocol: un filosof cu un număr par ridică
întotdeauna furculița stângă înainte de a ridica furculița dreaptă; un filosof cu un număr impar
ridică întotdeauna furculița dreaptă înainte de a ridica furculița stângă. Poate acest protocol să
garanteze funcționarea fără blocaj?
52. Într-un sistem de timp real sunt tratate două apeluri vocale cu periodicitatea 6 msec și timp
de procesor 1 msec la fiecare alocare, plus un videoclip cu 25 de cadre/sec, fiecare cadru
necesitând 20 msec de timp CPU. Calculați dacă este planificabil acest sistem?
53. Propuneți un mijloc de realizare a separării politicii de mecanismul pentru planificarea firelor
realizate la nivelul nucleului.
54. Din care cauză în procedura take_forks a soluției problemei Cina filosofilor (fig. 2-47),
variabila de stare este setată în HUNGRY?
55. Fie procedura put_forks din fig. 2-47. Presupunem că variabilei state[i] i-a fost atribuită
valoarea THINKING după două apeluri a lui test, nu înaintea lor. Cum va afecta soluția această
modificare?
56. Problema cititorilor și scriitorilor poate fi formulată în mai multe moduri în dependență de
categoria și momentul lansării procesului. Descrieți cu exactitate trei variații diferite ale
problemei, fiecare favorizând (sau nu favorizând) o anumită categorie de procese. Pentru
fiecare variantă, specificați ce se întâmplă când un cititor sau un scriitor trece în starea gata să
acceseze baza de date și ce se întâmplă când un proces finalizează utilizarea bazei de date.
57. Scrieți un script shell care produce un fișier conținutul căruia sunt numere secvențiale
obținute citind ultimul număr din fișier la care se adaugă valoarea 1, iar rezultatul este introdus
în fișier. Rulați o instanță a scriptului în fundal și alta în prim plan, fiecare accesând același
fișier. Cât durează până se manifestă o condiție de cursă? Care este regiunea critică?
Modificați scriptul pentru a preveni cursa.
Sugestie: utilizați ln file file.lock pentru a bloca fișierul de date.
58. Presupunem că aveți un sistem de operare care oferă semafore. Implementați un sistem de
mesagerie. Scrieți procedurile de expediere și de recepție a mesajelor.
59. Rezolvați problema Cina filosofilor folosind monitoare în loc de semafoare.
60. Să presupunem că o universitate dorește să demonstreze corectitudunea politicii
universitare, aplicând doctrina „separat dar egal - inerent inegal” a Curții Supreme a SUA în
ceea ce privește genul sau rasa, punând capăt practicii de segregare. Cu toate acestea, ca o
concesie pentru tradiție, decretează că atunci când o femeie este într-o baie, poate intra și o
altă femeie, dar nu un bărbat, și invers. Un semn cu un marcaj glisant pe ușa fiecărei camere
de baie indică în care dintre cele trei stări posibile se află în prezent camera:
a. liberă
b. femei prezente
c. bărbați prezenți
Scrieți într-un limbaj de programare următoarele proceduri: pentru o femeie care vrea să intre -
woman_wants_to_enter, pentru un bărbat care vrea să intre man_wants_to_enter, pentru o
femeie care pleacă – woman_leaves, pentru un bărbat care pleacă man_leaves. Puteți utiliza
orice contoare și tehnici de sincronizare.
61. Modificați programul din fig. 2-23 pentru a putea gestiona mai mult de două procese.
62. Propuneți o soluție a problemei producător-consumator care utilizează fire și partajează un
tampon comun. Însă, nu folosiți semafoare sau alte primitive de sincronizare pentru a proteja
structurile de date partajate. Doar permiteți fiecărui fir să le acceseze când dorește. Folosiți
sleep și wakeup pentru a respecta condițiile tampon_plin și tampon_gol. Stabiliți timpul necesar
pentru apariția unei condiții de cursă fatală. De exemplu, face ca producătorul să imprime un
număr din când în când. Nu imprimați mai mult de un număr în fiecare minut, deoarece
operațiile de I/E ar putea afecta condițiile de cursă.
63. Conform algoritmului round-robin, un proces poate fi reintrodus în coadă de așteptare a
proceselor ready de mai multe ori cu o prioritate tot mai mare. Rularea mai multor instanțe ale
unui program care lucrează fiecare la o parte diferită a unui grup de date poate avea același
efect. Scrieți un program care testează o listă de numere pentru a stabili care numere sunt
primare. Propuneți o metodă care să permită mai multor instanțe ale programului să fie rulate
simultan, astfel încât două instanțe ale programului să nu funcționeze la același număr. Este
oare posibilă sporirea vitezei de triere a listei rulând simultan mai multe copii ale programului?
Rețineți că rezultatele dvs. vor depinde dacă computerul mai execută și alte sarcini. Pe un
computer personal care rulează doar instanțe ale acestui program, s-ar putea să nu se observe
o îmbunătățire substanțială, dar pe un sistem în care există și alte procese, va fi posibil în acest
mod să fie obținută o parte mai mare a timpului de procesor.
64. Obiectivul acestui exercițiu este implementarea unei soluții multithreaded pentru a afla dacă
un număr dat este un număr perfect. N este un număr perfect dacă suma tuturor divizorilor săi
mai mici ca N, este N. Exemple: 6 și 28. Intrarea este un număr întreg, N. La ieșire va fi true
dacă numărul este un număr perfect și fals în caz contrar. Programul principal va citi numerele
N și P din linia de comandă și va va genera un set de fire P. Se vor lua numere de la 1 la N
care vor fi împărțite între aceste fire, astfel încât două fire să nu lucreze cu același număr.
Pentru fiecare număr din acest set, firul va determina dacă numărul său este divizorul lui N.
Dacă este, firul adaugă numărul-divizor într-un tampon partajat care stochează divizorii lui N.
Procesul părinte așteaptă finalizarea tuturor firelor. Utilizați primitiva de sincronizare
corespunzătoare. Procesul părinte va determina apoi dacă numărul de intrare este perfect,
adică dacă N este o sumă a tuturor divizorilor săi și apoi va raporta în consecință. (Notă: Puteți
face calcularea mai rapidă restricționând numerele căutate de la 1 la rădăcina pătrată a lui N.)
65. Elaborați un program care va calcula frecvența cuvintelor dintr-un fișier text. Fișierul text
este partiționat în N segmente. Fiecare segment este procesat de un fir separat care produce
numărul de frecvențe intermediare pentru segmentul său. Procesul principal așteaptă până
când toate firele se termină; apoi calculează frecvența pentru fiecare cuvânt.
3. BLOCAJE RECIPROCE (IMPAS)
Sistemele informatice au multe resurse care pot fi utilizate doar de un singur proces la un
moment de timp dat. Ca exemplu pot fi imprimantele, unitățile de bandă magnetică pentru
copiile de rezervă ale companiei sau elementele din tabelele interne de sistem. Dacă două
procese încearcă să imprime simultan informații la o imprimantă, rezultatul este complet aiurea.
Dacă două procese folosesc simultan aceeași intrare în tabelul sistemului de fișiere, acest
sistem va fi inevitabil deteriorat. Prin urmare, toate sistemele de operare sunt capabile să
acorde temporar unui proces drepturi exclusive de acces la resurse concrete.
În multe aplicații, un proces are nevoie de acces exclusiv simultan nu doar la o resursă, ci la
mai multe. Să presupunem, de exemplu, că fiecare din două procese, A și B, dorește să
salveză un document scanat pe un disc blu-ray. Procesul A solicită și obține permisiunea de a
utiliza scanerul. Procesul B este programat diferit: mai întâi solicită permisiunea de a folosi
dispozitivil de scriere al discului blu-ray și primește permisiunea respectivă. A solicită apoi
permisiunea de a folosi discul blu-ray, dar solicitarea este respinsă până când dispozitivul nu va
fi eliberat de procesul B. Din păcate, în loc să elibereze unitatea, B solicită permisiunea de a
folosi scanerul. Și în acest moment, ambele procese sunt blocate pentru totdeauna. Această
situație se numește blocare reciprocă sau impas (deadlock).
Blocări reciproce se pot întâmpla și între mașini. De exemplu, multe birouri au o rețea locală la
care sunt conectate multe computere. Frecvent, dispozitive precum scanere, discuri Blu-ray sau
DVD, imprimante și unități de bandă magnetică sunt în rețea ca resurse partajate disponibile
oricui de pe orice mașină. Dacă pe aceste dispozitive informația poate fi salvată de la distanță
(de exemplu, de la computerul de domiciliu al utilizatorului), atunci poate să apară un impas
similar cu cel discutat. În circumstanțe mai complicate, trei, patru sau mai multe dispozitive și
utilizatori pot fi implicați într-un impas.
Blocările reciproce pot să apară în multe alte circumstanțe. De exemplu, în sistemele de
gestionare a bazelor de date, un program poate avea nevoie să blocheze mai multe înregistrări
pe care le folosește pentru a evita apariția unor condiții de cursă. Dacă procesul A blochează
înregistrarea R1, iar procesul B blochează înregistrarea R2, după care fiecare proces încearcă
să blocheze înregistrarea celuilalt proces, se produce același impas. Astfel, blocajele pot
apărea atât în cazul resurselor tehnice, cât și resurselor logice.
În acest capitol vom discuta despre tipurile de blocaje, condițiile de apariție a acestora și unele
modalități de prevenire sau de evitare a blocajelor. În timp ce acest material tratează blocajele
în contextul sistemelor de operare, blocajele apar și în sistemele de gestionare a bazelor de
date și în multe alte zone din domeniul calculatoarelor, astfel încât informațiile prezentate aici
pot fi utile pentru o varietate largă de sisteme multitasking. Există multe lucrări științifice pe
tema impasurilor. Bibliografii la acest subiect au fost publicate de două ori în Operating
Systems Review și merită analizate în calitate de surse de referință (Newton, 1979; Zobel,
1983). Deși aceste bibliografii sunt relativ “bătrâne”, cea mai mare parte a lucrărilor la blocaje a
fost finalizată înainte de 1980, astfel încât acestea sunt încă relevante.

3.1 Resurse
Majoritatea blocajelor provin de la resursele pentru care unor procese li s-au acordat drepturi
de acces exclusiv. Acestea includ dispozitive, înregistrări de date, fișiere etc. Pentru a discuta
referitor la blocări într-un cadru cât mai general, vom numi obiectele la care se acordă acces
resurse. Resurse pot fi dispozitivele tehnice (cum ar fi o unitate de disc Blu-ray) sau unele
informații (cum ar fi o înregistrare a bazei de date). De obicei, un computer poate avea multe
resurse care pot fi puse la dispoziția unui proces. Unele resurse pot fi disponibile în mai multe
copii identice, cum ar fi trei unități de disc Blu-ray. Când sunt disponibile mai multe copii ale
unei resurse, una dintre copii poate fi utilizată pentru a satisface orice solicitare a resursei. Pe
scurt, o resursă este ceva ce trebuie achiziționat, utilizat și eliberat după un anumit interval de
timp, deoarece un singur proces poate utiliza resursa la un moment de timp.

3.1.1 Resurse preemptibile si non-preemptibile


Resursele sunt de două tipuri: preemptibile și non-preemptibile. O resursă preemptibilă este
resursa care poate fi retrasă de la procesul care o utilizează fără efecte nedorite. Memoria este
un exemplu de resursă preemptibilă. Exemplu: considerăm un sistem cu 64 MB de memorie, o
imprimantă și două procese de 64 MB, fiecare proces dorind să tipărească ceva.
Procesul A cere și primește acces la imprimantă. Înainte de a termina prelucrarea, i se termină
cuanta de timp și este scos din memorie și pus pe disc (swapped out). Procesul B rulează
acum și încearcă fără succes să acceseze imprimanta. Avem o situație de interblocare
potențială deoarece A are imprimanta, iar B are memoria și nici unul nu poate continua fără
resursa deținută de celălalt.
Soluția: memoria poate fi luată de la procesul B prin punerea acestuia pe disc (swapped out) și
prin aducerea procesului A în locul său (swapped in). Acum procesul A poate rula, tipări și apoi
elibera imprimanta. Nu apare astfel nici o interblocare
Resursele non-preemptibile reprezintă resursele ce nu pot fi luate de la deținătorii lor fără ca
execuția acestora să eșueze. Exemplu: unitățile de inscripționare CD-uri. Dacă un proces a
început inscripționarea unui CD, luându-i brusc recorder-ul și dându-l altui proces va avea ca
rezultat defectarea CD-ului.
Dacă o resursă este de preemptibilă depinde de context. Pe un computer standard, memoria
este preemptibilă deoarece paginile pot fi întotdeauna descărcate pe disc pentru a asigura
volumul solicitat de memorie operativă. În același timp, pe un smartphone care nu acceptă
swappingul sau paginarea, blocajele nu pot fi evitate printr-un simplu swapping din cauza
capacității insuficiente a memoriei și programelor cu “pofte mari” (hog) de memorie.
In general, blocajele apar din cauza resurselor non-preemptibile. Blocajele implicate de resurse
preemptibile pot fi rezolvate de obicei prin realocarea resurselor de la un proces la altul. Din
această cauză ne vom concentra atenția pe resursele non-preemptibile.
La nivelul cel mai general, utilizarea resurselor se produce în conformitate cu următoarea
secvență de evenimente:
1. Cerere resursă
2. Folosire resursă
3. Eliberare resursă
Dacă resursa nu este disponibilă atunci când este solicitată, procesul solicitant este obligat să
aștepte. În unele sisteme de operare, procesul este blocat automat atunci când o solicitare de
resursă eșuează și este relansat atunci când resursa devine disponibilă. În alte SO, solicitarea
eșuează cu un cod de eroare și decizia cum se va proceda - să se aștepte o anumită perioadă
de timp sau să se încerce din nou imediat, este pusă în sarcina procesului apelant.
Un proces pentru care cererea de acces la resursă a eșuat, va sta într-o buclă de cerere a
resursei. Teoretic, acest proces nu este blocat, dar practic este ca și blocat deoarece nu poate
face lucruri utile. În viitoarele discuții vom presupune că atunci când unui proces îi este refuzată
o resursă, acesta este trimis “la culcare”.
Natura exactă a solicitării unei resurse depinde în mare măsură de sistem. În unele sisteme,
există un apel de sistem request pentru a permite proceselor să ceară în mod explicit resursa.
În altele, singurele resurse cunoscute de sistemul de operare sunt fișierele speciale. Fiecare
fișier special poate fi deschis la un moment de timp dat de către un singur proces. Acestea sunt
deschise prin apelul open obișnuit. Dacă fișierul este deja utilizat, apelantul este blocat până
când deținătorul actual va închide fișierul.

3.1.2 Accesul la resursă


Pentru unele tipuri de resurse, cum ar fi înregistrările într-o bază de date, gestionarea
resurselor depinde mai mult de procesele utilizatorilor, decât de sistem. Un mod de a introduce
acest tip de control al resurselor este să asociați cu fiecare resursă un semafor. Acest semafor
este inițializat în 1. În egală măsută pot fi utilizate mutexele. Cele trei etape enumerate mai sus
sunt implementate printr-o operație down de semafor pentru a achiziționa resursa, a utiliza-o și,
în final, o operație up pentru a o elibera. Acești pași sunt arătați în fig. 6-1 (a).
typedef int semaphore; typedef int semaphore;
semaphore resource_1; semaphoreresource_1;
semaphore resource_2;

void process_A(void) { void process A(void) {


down(&resource_1); down(&resource_1);
use resource_1(); down(&resource_2);
up(&resource_1); use_both_resources();
} up(&resource_2);
up(&resource_1);
}
(a) (b)
Figura 6-1. Utilizarea unui semafor pentru protejarea resurselor.
(a) O resursă. (b) Două resurse.
Uneori procesele au nevoie de două sau mai multe resurse. Acestea pot fi achiziționate
secvențial, așa cum este prezentat în fig. 6-1 (b). Dacă sunt necesare mai mult de două
resurse, acestea sunt achiziționate una după alta.
Până acum, bine. Atâta timp cât este implicat un singur proces, totul funcționează bine.
Desigur, cu un singur proces, nu este necesară achiziția formală de resurse, deoarece nu
există concurență pentru acestea.
Acum să luăm în considerare o situație cu două procese, A și B și două resurse. În listingul din
fig. 6-2 sunt prezentate două scenarii. În fig. 6-2 (a), ambele procese cer resursele în aceeași
ordine. În Fig. 6-2 (b) resursele sunt solicitate într-o ordine diferită. Această diferență poate
părea minoră, dar nu este.
typedef int semaphore;
semaphore resource_1; semaphore resource_1;
semaphore resource_2; semaphore resource_2;

void process_A(void) { void process_A(void) {


down(&resource_1); down(&resource_1);
down(&resource_2); down(&resource_2);
use_both_resources( ); use_both_resources( );
up(&resource_2); up(&resource_2);
up(&resource_1); up(&resource_1);
} }

void process_B(void) { void process_B(void){


down(&resource_1); down(&resource_2);
down(&resource_2); down(&resource_1);
use_both_resources( ); use_both_resources( );
up(&resource_2); up(&resource_1);
up(&resource_1); up(&resource_2);
} }
(a) (b)

Figura 6-2. (a) Cod fără blocaje. (b) Codul cu un impas potențial.
În Fig. 6-2 (a), unul dintre procese solicită prima resursă înainte celuilalt. Apoi, același proces
va obține cu succes a doua resursă și își va face munca. Dacă procesul al doilea încearcă să
achiziționeze resursa 1 înainte de eliberarea acesteea, el va fi pur și simplu blocat până resursa
devine disponibilă.
În Fig. 6-2 (b), situația este diferită. S-ar putea întâmpla ca unul dintre procese să obțină
ambele resurse și să blocheze eficient celălalt proces până când este își va termina lucrul. Cu
toate acestea, s-ar putea întâmpla și ca procesul A să obțină resursa 1, iar și procesul B -
resursa 2. Fiecare va fi blocat atunci când încearcă să achiziționeze cealaltă rresursă. Niciunul
dintre cele două procese nu-și va putea continua lucrul. Această situație este un impas.
Aici vedem cum ceea ce pare a fi o diferență minoră în stilul de programare - care resursă să
fie achiziționată mai întâi - programul poate merge, dar și poate eșua într-un mod greu de
detectat. Deoarece blocajele pot apărea atât de ușor, există o mulțime de cercetări pentru a
lupta cu ele. În acest capitol vom analiza în detaliu blocajele și mijloacele de luptă cu ele.

3.2 Introducere în blocaje


Formal un blocaj reciproc poate fi definit după cum urmează:
Un set de procese este blocat dacă fiecare proces din set așteaptă un eveniment pe care doar
un alt proces din set îl poate provoca.
Deoarece toate procesele sunt în starea de așteptare, niciunul dintre ele nu va putea provoca
vreodată vreun eveniment care ar “trezi” pe oricare dintre membrii setului și așteptarea durează
la infinit pentru toate procesele. În acest model se presupune că procesele au un singur fir de
execuție și nu există întreruperi, capabile să deblocheze un proces blocat. Condiția lipsei
întreruperilor este una necesară pentru a nu permite unui proces blocat din alte cauze să
reînceapă execuția în rezultatul unui semnal de alarmă iar apoi să provoace evenimente care
deblochează alte procese din set.
În cele mai multe cazuri, evenimentul pe care îl așteaptă fiecare proces este eliberarea unor
resurse deținute în prezent de un alt membru al setului. Cu alte cuvinte, fiecare membru al
setului de procese blocate așteaptă o resursă care este deținută de un proces blocat din set.
Niciunul dintre procesele setului nu poate rula, niciunul dintre ele nu poate elibera resurse și
nici unul nu poate reîncepe execuția. Numărul de procese, numărul și tipul resurselor deținute
și solicitate nu au importanță. Acest rezultat este valabil pentru orice fel de resurse, inclusiv
tehnice și logice. Acest tip de impas este denumit blocaj al resurselor. Este probabil cel mai
comun tip, dar pe departe nu este unicul tip. Vom studia mai întâi în mod detaliat blocajele
resurselor, iar la sfârșitul acestui capitol vom reveni succint la alte tipuri de blocaje.

3.2.1 Conditii pentru blocaje reciproce


Coffman și colab. (1971) au stabilit că un blocaj al resurselor se produce atunci când
următoarele patru condiții au loc simultan:
1. Excluderea reciprocă: fiecare resursă este atribuită exact unui proces sau este
disponibilă.
2. Reținere și așteptare (hold-and-wait). Procesele care dețin în prezent resurse care au
fost acordate anterior pot solicita noi resurse.
1. Non-preemtibilitate. Resursele acordate anterior nu pot fi retrase forțat de la un proces.
Acestea trebuie să fie eliberate în mod explicit de procesul care le ține.
3. Starea circulară de așteptare. Trebuie să existe o listă circulară a două sau mai multe
procese, fiecare așteptând o resursă deținută de următorul membru al lanțului.
Toate aceste patru condiții trebuie să fie prezente pentru ca un blocaj al resurselor să se
producă. Dacă o condiție este lipsă, un blocaj al resurselor este imposibil.
Trebuie de remarcat că fiecare condiție depinde de politica sistemului, dacă este sau nu este
respectată. O anumită resursă poate fi alocată mai multor procese simultan? Poate un proces
să dețină o resursă și să ceară alta? Poate fi retrasă o resursă? Pot exista așteptări circulare?
Vom vedea mai târziu cum blocajele pot fi excluse înlăturând unele din aceste condiții.

3.2.2 Modelarea blocajelor


Holt (1972) a arătat cum pot fi modelate aceste patru condiții cu ajutorul grafurilor orientate.
Grafurile au două tipuri de noduri: procese, notate prin cerculețe și resurse, notate prin pătrate.
Un arc care pornește de la un nod de resursă (pătrat) și se oprește la un nod de proces (cerc)
semnifică că această resursă a fost anterior solicitată, alocată și este în prezent deținută de
acest proces. În fig. 6-3(a) resursa R este alocată procesului A.

Fig. 6-3. Graful alocării resurselor. (a) Deținerea unei resurse.


(b) Cererea unei resurse. (c) Blocare reciprocă.
Un arc care iese din proces și intră în resursă semnifică că procesul este în prezent blocat în
așteptarea eliberării respectivei resurse. În fig. 6-3(b), procesul B așteaptă eliberarea resursei
S. Graful din fig. 6-3(c) reprezintă un impas: procesul C este în așteptarea resursei T, care în
prezent este deținută de procesul D. D nu este pe cale să elibereze resursa T, deoarece
așteaptă resursa U, deținută de C. Ambele procese vor aștepta pentru totdeauna. Un ciclu în
grafic semnifică existența unui blocaj care implică procesele și resursele din ciclu (în sistem
există câte o singură resursă de fiecare tip). În acest exemplu, ciclul este C - T - D −U - C.
Să vedem acum cum pot fi utilizate grafurile resurselor. Imaginează-ți că avem trei procese, A,
B și C, și trei resurse, R, S și T. Secvențele de acțiuni pentru cererea și eliberarea resurselor,
acțiuni executate de aceste trei procese, sunt prezentate în fig. 6-4 (a) - (c). Sistemul de
operare poate să ruleze orice proces deblocat în orice moment, adică ar putea decide să ruleze
A până terminare, apoi să ruleze B până la capăt, iar în final să ruleze C.

Fig. 6-4. Exemplu de producere și de evitare a blocajului


Această ordine de rulare nu va duce la blocaje (deoarece nu există concurență pentru resurse),
dar paralelismul este lipsă. Pe lângă solicitarea și eliberarea resurselor, procesele mai execută
și anumite calcule și operații de I/E. Când procesele sunt rulate secvențial, nu este posibil ca în
timp ce un proces așteaptă finalizarea unei operații de I/E, procesorul central să fie alocat altui
proces. Din această cauză executarea strict secvențială a proceselor poate să nu fie o soluție
optimă. Pe de altă parte, dacă niciunul dintre procese nu realizează vre-o operație de I/E,
algoritmul cererea cea mai scurtă-prima este mai bun decât round-robin, deci în anumite
circumstanțe rularea secvențială a tuturor proceselor poate fi cea mai bună soluție.
Să presupunem acum că procesele sunt în egală măsură I/O-bound și CPU-bound, astfel încât
round-robin este cel mai rezonabil algoritm de planificare. Cererile de resurse pot avea loc în
ordinea din fig. 6-4 (d). Dacă aceste șase solicitări sunt efectuate anume în această ordine, lor
le vor corespunde șase grafuri de resurse rezultante prezentate în fig. 6-4(e)-(j). După
generarea cererii 4, procesul A este blocat în așteptarea lui S (fig. 6-4(h)). În următoarele două
etape B și C de asemenea se vor bloca, ceea ce conduce în final la o ciclare și la impasul din
fig. 6-4(j).
Cum a fost menționat deja, sistemul de operare nu este obligat să ruleze procesele într-o
ordine anume. Dacă satisfacerea unei solicitări concrete poate duce la impas, sistemul de
operare poate suspenda pur și simplu acest proces fără a satisface cererea lui (simplu, nu
planifică procesul) până când riscul blocajului va dispărea. În fig. 6-4, dacă sistemul de operare
ar ști despre viitorul impas, el ar putea suspenda procesul B în loc să-i aloce resursa S. Rulând
doar procesele A și C, vom obține secvența acțiunilor de solicitare și eliberare a resurselor din
fig. 6-4(k) în locul celei din fig. 6-4(d). Această secvență este prezentată prin graful de resurse
din fig. 6-4 (l) - (q), și nu conduce la impas.
După finalizarea etapei (q), procesului B îi poate fi alocată resursa S, deoarece procesul A este
terminat, iar procesul C are tot ce-i trevuie. Chiar dacă B se va bloca la resursa T, un impas nu
se va produce. Procesul B va aștepta doar până se termină procesul C.
Mai târziu în acest capitol vom studia detaliat un algoritm de luare a deciziilor referitor la alocări
care nu conduc la impas. Pentru moment, este necesar să se înțeleagă că grafurile resurselor
sunt un instrument care ne permite să vedem dacă o anume secvență de solicitare/eliberare
conduce la impas. Cererile și eliberările resurselor sunt executate pas cu pas, și după fiecare
pas verificăm graful pentru a vedea dacă conține cicluri. Dacă da, avem un impas; dacă nu, nu
există un impas. Deși aici am discutat doar despre un graf al resurselor pentru cazul când
există câte o singură resursă de fiecare tip, grafurile de resurse pot fi utilizate la gestionarea
mai multor resurse de același tip (Holt, 1972).
În general, pentru a contracara blocajele sunt utilizate patru strategii.
1. Ignorarea problemei. Poate dacă noi o ignorăm, ea ne va ignora pe noi.
2. Detectarea și recuperarea. Permitem blocajelor să se manifeste, le detectăm și
executăm acțiunile necesare.
3. Evitarea dinamică prin alocarea atentă a resurselor.
4. Prevenirea prin suprimarea structurală a uneia din cele patru condiții enunțate anterior.
În următoarele patru secțiuni, vom examina pe rând fiecare dintre aceste metode.

3.3 Algoritmul struțului


Cea mai simplă abordare este algoritmul struțului: ascundem capul în nisip și ne prefacem că
nu există nici o problemă. Oamenii reacționează la această strategie în diferite moduri.
Matematicienii consideră că este inacceptabil și spun că blocajele trebuie prevenite cu orice
preț. Inginerii se întreabă cât de des se produce problema, cât de des sistemul se prăbușește
din alte motive și cât de grav este un impas. În cazul în care blocajele se produc în medie odată
la cinci ani, iar prăbușirea sistemului din cauza defecțiunilor hardware sau a erorilor sistemului
de operare - odată pe săptămână, majoritatea inginerilor nu vor fi dispuși să plătească
eliminarea blocajelor cu diminuarea esențială a productivității sau a comodității utilizării.
Pentru a amplifica contrastul între aceste două poziții, vom analiza un sistem de operare care
blochează procesul apelant atunci când apelul de sistem open la un dispozitiv fizic, cum ar fi un
driver Blu-ray sau o imprimantă, nu poate fi efectuat deoarece dispozitivul este ocupat. De
obicei, anume driverului dispozitivului decide ce acțiuni și în care circumstanțe să întreprindă.
Blocarea sau returnarea unui cod de eroare sunt două posibilități evidente. Dacă un proces
reușește să deschidă cu succes unitatea Blu-ray și un alt proces “are fericirea” să “deschidă” cu
succes imprimanta, iar apoi fiecare proces încearcă să obțină cealaltă resursă, avem un impas.
Puține sisteme moderne pot să detecteze această situație.

3.4 Detectarea blocajelor și recuperarea


A doua tehnică este detectarea și recuperarea. Când se folosește această tehnică, sistemul nu
încearcă să împiedice apariția blocajelor. Blocajele sunt lăsate să apară, iar sistemul încearcă
să detecteze momentul când se întâmplă acest lucru și apoi ia unele măsuri pentru recuperare,
la necesitate. În această secțiune vom analiza unele dintre modurile în care pot fi detectate
blocajele și modalitățile de recuperare a capacității de funcționare.

3.4.1 O singură resursă de fiecare tip


Să începem cu cel mai simplu caz: există o singură resursă de fiecare tip. Un astfel de sistem
poate avea un scaner, o unitate de inscripționare Blu-ray, un plotter și o unitate de bandă, dar
nu mai mult de una din fiecare clasă de resurse. Cu alte cuvinte, excludem sistemele cu două
imprimante pentru moment. Le vom trata mai târziu, folosind o metodă diferită.
Pentru un astfel de sistem, putem construi un graf de resurse de felul celui din fig. 6-3. Dacă
acest graf conține unul sau mai multe cicluri, există un impas. Orice proces care este parte a
ciclului este blocat. Dacă nu există cicluri, sistemul nu este blocat.
În calitate de exemplu de sistem mai complex în comparație cu cele analizate anterior fie un
sistem cu șapte procese, de la A la G și șase resurse, de la R la W. Fiecare resursă se află în
starea curentă de ocupare, iar solicitările sosite pentru fiecare resursă sunt următoarele:
1. Procesul A deține resursa R și o vrea pe S.
2. Procesul B nu deține nici o resursă, dar o vrea pe T.
3. Procesul C nu deține nici o resursă, dar o vrea pe S.
4. Procesul D deține U și dorește pe S și T.
5. Procesul E deține pe T și o vrea pe V.
6. Procesul F deține pe W și o dorește pe S.
7. Procesul G deține pe V și o dorește pe U.
Întrebarea sună astfel: „Este acest sistem blocat și, dacă da, care sunt procesele implicate?”
Pentru a răspunde la această întrebare, construim graful resurselor din fig. 6-5 (a). Acest graf
conține un ciclu, care poate fi identificat prin inspecție vizuală. Ciclul este prezentat în fig. 6-5
(b). Din acest ciclu, putem vedea că procesele D, E și G sunt blocate. Procesele A, C și F nu
sunt blocate, deoarece S poate fi alocată oricăruia dintre procese, care o va utiliza iar la
finalizare o returnează. După care celelalte două procese o pot utiliza pe rând cu returnare.
(Rețineți că pentru a face acest exemplu mai interesant, am permis proceselor, și anume
procesului D, să solicite simultan două resurse.)
Fig. 6-5. (a) Graful resurselor. (b) Un cicllu extras din (a).
Deși este relativ simplu de detectat procesele blocate prin inspecția vizuală a unui graf orientat,
în sistemele reale avem nevoie de algoritmi formali pentru detectarea blocajelor. Există o
mulțime de algoritmi pentru detectarea ciclurilor în grafurile orientate. Mai jos vom prezenta un
algoritm simplu care inspectează graful și se oprește fie atunci când detetctează un ciclu, sau
atunci când stabilește că cicluri nu există. Este utilizată o structură de date dinamică, L - lista
nodurilor, precum și lista arcelor. Arcele parcurse în timpul executării algoritmului sunt marcate
pentru a preveni inspecțiile repetate. Algoritmul funcționează conform următorilor pași:
1. Pentru fiecare nod N, sunt efectuați următorii cinci pași cu N în calitate de nod inițial.
2. Este inițializată lista L iar marcajele tuturor arcelor sunt șterse.
3. Nodul inițial este introdus la sfârșitul listei L și se verifică dacă nu cumva acest nod apare
acum în L de două ori. Dacă da, graful conține un ciclu (reflectat în lista L) și algoritmul se
termină.
4. Pentru nodul dat, se verifică dacă există arce de ieșire nemarcate. Dacă da, trecem la
pasul 5; dacă nu, la pasul 6.
5. Este ales la întâmplare și marcat un arc de ieșire nemarcat. Nodul în care intră acest arc
devine nod inițial și întoarcere la pasul 3.
6. Dacă acest nod este nodul inițial, graful nu conține cicluri și algoritmul se încheie. În caz
contrar, am ajuns la un punct mort. Nodul este eliminat și se revine la nodul anterior.
Acest nod devine nod inițial și revenim la pasul 3.
Acest algoritm ia fiecare vârf ca rădăcină a ceea ce speră să fie un arbore și efectuează o
căutare în adâncime. Dacă în timpul căutării algoritmul se întoarce la un vârf întâlnit anterior,
înseamnă că a fost găsit un ciclu. Dacă epuizează toate arcele din orice vîrf, acesta se întoarce
la vârful anterior. Dacă se întoarce la vârful rădăcină și nu poate merge mai departe, subgraful
accesibil din nodul inițial nu conține cicluri. Dacă această proprietate se păstrează pentru toate
vârfurile grafului – graful întreg nu conține cicluri, iar sistemul nu se află în stare de impas.
Pentru a vedea cum funcționează algoritmul în practică, vom utiliza graful din fig. 6-5 (a).
Ordinea procesării nodurilor este arbitrară, deci să le inspectăm doar de la stânga la dreapta,
de sus în jos, rulând mai întâi algoritmul începând de la R, apoi succesiv A, B, C, S, D, T, E, F,
si asa mai departe. Dacă va fi depistat un ciclu, algoritmul se oprește.
Începem de la R și inițializăm L în lista goală. Apoi adăugăm R la listă și trecem la singura
posibilitate, A, și o adăugăm la L, obținem L = [R, A]. De la A la S, avem lista L = [R, A, S]. S nu
are arce de ieșire, deci este un punct mort - ne întoarcem spre A. Deoarece A nu are arce de
ieșire nemarcate, ne întoarcem la R, finalizând inspecția asupra lui R.
Repornim algoritmul începând de la A, resetând L în lista goală. Această căutare se oprește
rapid. Repornim cu B, continuăm să urmăm arcurile ieșite până ajungem la D, moment în care
L = [B, T, E, V, G, U, D]. Acum trebuie să facem o alegere (la întâmplare). Dacă alegem S
ajungem la un punct mort și ne întoarcem înapoi la D. A doua oară alegem T, actualizăm L care
devine [B, T, E, V, G, U, D, T], moment în care descoperim existența ciclului și oprim algoritmul.
Acest algoritm este departe de a fi optim. Pentru unul mai bun, consultați Even (1979). Totuși,
exemplul prezentat demonstrează existența unui algoritm pentru detectarea impasului.

3.4.2 Mai multe resurse din fiecare tip


Când există mai multe exemplare ale unor resurse, pentru a detecta blocajele reciproce este
necesară o abordare diferită. Vom prezenta un algoritm bazat pe utilizarea matricelor pentru
detectarea impasului dintre n procese, de la P1 până la Pn. Fie m numărul de tipuri de resurse,
cu resursele E1 cantitatea de instanțe de tipul 1, E2 – de tipul 2, și, în general, Ei – numărul de
instanțe ale resursei de tipul i (1 ≤ i ≤ m). E este vectorul resurselor existente, coordonatele lui
sunt definite de numărul total de instanțe ale fiecărei resurse existente. De exemplu, dacă tipul
1 este unități de bandă magnetică, atunci E1 = 2 înseamnă că sistemul are două unități de
bandă magnetică.
În orice moment, unele dintre resurse pot fi alocate și, deci nu sunt disponibile. Fie A vectorul
resurselor disponibile, Ai reflectând numărul de instanțe ale resursei i disponibile la moment
(adică, nealocate). Dacă ambele unități de bandă magnetică sunt alocate, A1 va fi 0.
Acum avem nevoie de două tablouri: C - matricea de alocare curentă și R, matricea cererilor.
Linia i a matricei C indică câte instanțe din fiecare tip de resurse sunt deținute la moment de
procesul Pi. Astfel, Cij este numărul de instanțe ale resursei j care sunt deținute de procesul i. În
mod similar, Rij este numărul de instanțe ale resursei j solicitate de procesul Pi. Aceste patru
structuri de date sunt prezentate în Fig. 6-6.

Un invariant important este valabil pentru aceste patru structuri de date. În special, fiecare
resursă este fie alocată, fie este disponibilă. Această observație înseamnă că, cu alte cuvinte,
dacă adăugăm toate instanțele resursei j care au fost alocate și la aceasta adăugăm toate
instanțele disponibile, rezultatul este numărul de instanțe ale clasei de resurse care există.
Algoritmul de detectare a impasului se bazează pe compararea vectorilor. Să definim relația A
≤ B pe doi vectori A și B pentru a însemna că fiecare element din A este mai mic sau egal cu
elementul corespunzător lui B. Matematic, A ≤ B ține dacă și numai dacă Ai ≤ Bi pentru 1 ≤ i ≤
m.
Se spune că fiecare proces este inițial marcat. Pe măsură ce algoritmul progresează, procesele
vor fi marcate, ceea ce indică faptul că sunt capabile să se finalizeze și astfel nu sunt blocate.
Când algoritmul se încheie, se știe că toate procesele nemarcate sunt blocate. Acest algoritm
presupune un scenariu cel mai rău: toate procesele păstrează toate resursele dobândite până
la ieșirea lor. Algoritmul de detectare a impasului poate fi acum prezentat după cum urmează.
1. Căutați un proces nemarcat, Pi, pentru care rândul R de R este mai mic
decât sau egal cu A.
2. Dacă se găsește un astfel de proces, adăugați șirul lui C la A, marcați procesul și mergeți
înapoi la pasul 1.
3. Dacă nu există un astfel de proces, algoritmul se încheie.
Când algoritmul se termină, toate procesele marcate, dacă există, sunt blocate.
Ceea ce algoritmul face în pasul 1 este căutarea unui proces care poate fi rulat până la
finalizare. Un astfel de proces este caracterizat ca având cerințe de resurse care pot fi
satisfăcute de resursele disponibile în prezent. Procesul selectat este apoi rulat până când se
termină, moment în care returnează resursele pe care le deține în grupul de resurse
disponibile. Apoi este marcat ca finalizat. Dacă toate procesele pot fi executate până la final,
niciunul dintre ele nu este blocat. Dacă unele dintre ele nu pot termina niciodată, acestea sunt
blocate. Deși algoritmul este nedeterminist (deoarece poate rula procesele în orice ordine
fezabilă), rezultatul este întotdeauna același.
Ca un exemplu de funcționare a algoritmului de detectare a impasului, a se vedea Fig. 6-7. Aici
avem trei procese și patru clase de resurse, pe care le-am etichetat în mod arbitrar unități de
bandă, plotere, scanere și unități Blu-ray. Procesul 1 are un scaner.
Procesul 2 are două unități de bandă și o unitate Blu-ray. Procesul 3 are un plotter și două
scanere. Fiecare proces are nevoie de resurse suplimentare, după cum arată matricea R.

S-ar putea să vă placă și