Sunteți pe pagina 1din 82

1.

Noţiuni şi termeni din domeniul resurselor tehnice


Un calculator constă dintr-un ansamblu de componente funcţionale fizice şi logice, care cooperează pentru
a satisface cerinţele utilizatorilor privind introducerea, stocarea, prelucrarea, căutarea şi transmiterea
informaţiilor. Aceste componente funcţionale sunt structurate pe niveluri, care interacţionează prin interfeţe
bine definite. Prin noţiunea de sistem de operare înţelegem modulele program ale unui SC, care
administrează resursele tehnice. Modulele în cauză soluționează situațiile de conflict, optimizează
productivitatea sistemului, sporesc eficiența utilizării lui.
Valorile concrete ale atributelor sistemelor de operare şi combinaţii ale acestora determină diverse tipuri de
SO. Conform acestor atribute pot fi evidenţiate următoarele tipuri de sisteme de operare:
 secvenţiale,
 cu multiprogramare,
 cu prelucrare multiplă,
 în timp real, etc.

1. Noţiuni şi termeni din domeniul SO


Un sistem de operare este un ansamblu de programe de control şi de serviciu care ghidează un calculator în
execuţia sarcinilor sale, asistă programele de aplicaţie şi interacţionează cu utilizatorul prin intermediul
anumitor funcţiuni. Natura funcţiilor şi modul în care acestea sunt realizate determină atributele care
caracterizează un sistem de operare: timpul de răspuns- exprimă durata intervalului delimitat de lansarea
unei cereri de serviciu şi achitarea acesteia de către sistem, simultaneitatea utilizării- măsoară gradul în care
un sistem poate să execute în acelaşi timp mai multe lucrări., eficienţa- măsoară proprietatea unui sistem de
a folosi în mod optim resursele de care dispune, partajarea resurselor şi protecţia informaţiei în calculator-
caracterizează nivelul la care utilizatorii au posibilitatea să utilizeze în comun informaţia prezentă în sistem
şi nivelul la care pot să comunice între ei, în deplină siguranţă, generalitatea, flexibilitatea, extensibilitatea,
executia
tatea şi disponibilitatea, transparenţa şi vizibilitatea. Un sistem de operare este obligat:
 să păstreze informaţia despre starea fiecărei resurse,
 să ia decizia cărui proces să i se aloce resursa, în ce cantitate, când, cum şi unde,
 să aloce resursa şi, la momentul oportun, să o retragă.

1. Tipuri de sisteme de operare, obiective şi funcţii


Valorile concrete ale atributelor sistemelor de operare şi combinaţii ale acestora determină diverse tipuri de
SO. Conform acestor atribute pot fi evidenţiate următoarele tipuri de sisteme de operare:
Un sistem secvenţial (tratare pe loturi, engl. batch processing, fr. traitement par lots) execută la un moment
dat un singur program, care trebuie terminat înainte de a lua începe execuţia unui alt program.
Sistemele cu multiprogramare acceptă la un moment dat mai multe programe în memoria centrală, acestea
aflându-se în diferite stadii de execuţie.
Un sistem de calcul cu prelucrare multiplă dispune de mai multe procesoare, care pot să execute simultan
unul sau mai multe programe.
Sistemele de timp real funcţionează, de obicei, în cadrul unor sisteme de comandă şi este necesar ca
valorile anumitor atribute să se încadreze în limite destul de restrictive, dictate de dinamica proceselor
comandate.

2. Exemple de sisteme de operare


Windows,Unix, MS DOS.

3. Cazul calculatoarelor personale


Cea mai simplă configuraţie a unui calculator personal (PC) include o unitate centrală, o memorie
principală, un display, o tastatură şi un mouse. Această configuraţie, de regulă, este completată de o
memorie secundară şi o imprimantă.
Utilizatorul unui astfel de sistem va cere minimum următoarele două tipuri de servicii:
identificarea şi crearea unor fişiere sau mulţimi structurate de informaţii;
stocarea acestor fişiere în memoria secundară; transferarea informaţiilor între fişiere şi dispozitivele de
intrare/ieşire;
execuţia unor programe existente sau introduse sub formă de fişiere în PC;
introducerea datelor necesare pentru execuţia programului (de la tastatură, dintr-un fişier sau de la alte
surse periferice); listarea rezultatelor la display, imprimantă sau copierea lor într-un fişier.

4. Comanda unor procese industriale


Procesul de producere este comandat de un calculator care îndeplineşte următoarele funcţii:
 Reglare. Pentru o derulare bună a procesului de fabricaţie parametrii de funcţionare (temperatura,
presiunea, concentraţia, etc.) trebuie să se afle într-o plajă de valori predefinite. Pentru aceasta va fi
acţionat debitul de intrare a materiilor prime A sau B. Parametrii de funcţionare sunt măsuraţi cu
ajutorul unor captoare. Calculatorul preia aceste măsurări şi, în dependenţă de algoritmul de
comandă, acţionează robinetele de intrare.
 Înregistrare. Rezultatele măsurărilor sunt periodic înregistrate; valorile lor sunt afişate pe un tablou
de bord şi recopiate într-un fişier ("jurnal de bord") în scopul unor prelucrări ulterioare (date
statistice).
 Securitate. În cazul în care unul dintre parametrii măsuraţi depăşeşte o valoare critică predefinită
reactorul trebuie oprit imediat.

1. Sisteme tranzacţionale
Caracteristicile principale:
 sistemul gestionează un set de informaţii sau baze de date, care pot atinge volume importante de
informaţie;
 asupra acestor informaţii pot fi executate un anumit număr de operaţii predefinite sau tranzacţii,
adesea interactive;
 sistemul este dotat cu un mare număr de puncte de acces şi un mare număr de tranzacţii se pot
derula simultan. Caracteristicile obligatorii ale unui astfel de sistem tranzacţional sunt
disponibilitatea şi fiabilitatea; pentru unele sisteme poate fi importantă şi toleranţa la defecţiuni. O
caracteristică importantă ale sistemelor tranzacţionale este multitudinea activităţilor paralele, iar în
multe cazuri şi repartizarea geografică a componentelor.

1. Sisteme în timp partajat


Destinaţia principală a unor astfel de sisteme este furnizarea serviciilor necesare unei mulţimi de utilizatori,
fiecare dintre ei beneficiind de servicii:
 echivalente serviciilor unui calculator individual;
 legate de existenţa unei comunităţi de utilizatori: partajarea informaţiilor, comunicaţii între
utilizatori.
Problemele care apar datorită conceptului de partajare a timpului sunt o combinaţie a problemelor existente
în cazul unui calculator individual cu cele din sistemele tranzacţionale. Caracteristicile obligatorii ale unui
astfel de sistem îmbină, în egală măsură, calităţile unui sistem de operare al unui calculator individual şi al
unui sistem tranzacţional, cum ar fi: disponibilitatea, fiabilitatea, securitatea, exploatarea optimă a
caracteristicilor resurselor fizice, calitatea interfeţei şi serviciilor utilizatorului, facilitatea adaptării şi
extensibilităţii.

1. SO și procesele
Noţiunea de proces este asociată conceptului de lucrare şi poate fi definită ca o suită temporală de execuţii
de instrucţiuni, considerată ca fiind o entitate de bază în descrierea sau analiza funcţionării unui sistem.
Evoluţia în timp a unui proces presupune un consum de resurse, dictat de natura şi complexitatea
instrucţiunilor de execuţie. În particular, rezultă că ori de câte ori se execută procedurile de sistem,
resursele, pe care le utilizează acesta, intră în administrarea procesului, care a cerut serviciul. Resursele
alocate unui proces variază în timp.

2. Mașina extinsă și maișina ierarhică


Setul de instrucţiuni realizat hardware împreună cu instrucţiunile suplimentare ale sistemului de operare
formează sistemul de comenzi al maşinii extinse.
Nucleul sistemului de operare va fi executat pe maşina “goală”, iar programele utilizatorului – pe maşina
extinsă.
3. Alte puncte de vedere asupra SO: abordarea funcţională, interfaţa cu utilizatorul
Sistemele de operare pot fi abordate din diferite puncte de vedere, cum ar fi SO şi procesele, SO şi maşina
extinsă sau SO şi maşina ierarhică. Există şi alte puncte de vedere asupra sistemelor de operare pe care un
specialist ar trebui să le cunoască.
Abordare funcţională-Pentru un utilizator obişnuit, convins că un calculator este doar un instrument care îl
ajută în soluţionarea unor probleme din domeniul său de activitate, noţiunile, cum ar fi administrarea
memoriei cu paginaţie sau driverele dispozitivelor, nu semnifică prea multe. Destinaţia principală a unui
sistem de operare pentru această categorie de utilizatori este punerea la dispoziţie a unui set de programe
care l-ar ajuta în formularea şi soluţionare problemelor concrete ce ţin de domeniul său de activitate.
Abordare din punctul de vedere al interfeţei cu utilizatorul
Interfaţa sistemului de operare cu utilizatorul prezintă un interes aparte. Progresul în acest domeniu este
spectaculos, dacă vom lua în consideraţie că în primele sisteme de operare utilizatorul era obligat să indice
în mod explicit şi manual (în regim textual) fiecare pas, oricât de nesemnificativ ar fi părut. Formularea
paşilor cu ajutorul unui limbaj specializat, cum ar fi Job Control Language (JCL), nu a schimbat substanţial
situaţia.

4. Evoluția SO: de la "poartă deschisă " la tratarea pe loturi


Primele sisteme erau caracterizate prin prelucrarea secvenţială a taskurilor. Timpul de execuţie a
programelor era relativ mare, instrumentele de depanare – primitive, fiecare programator îşi încărca în mod
individual programul (pachetul de cartele perforate), apăsa butoane, controla conţinutul locaţiunilor de
memorie, etc. (1950 – 1956). Au fost propuse programe de monitorizare (monitoare), care treceau de la o
lucrare la alta în mod automat, utilizatorul fiind responsabil de organizarea corectă a programelor în cadrul
unui pachet – primele încercări de prelucrare pe loturi (1956 – 1959).
După 1965 au apărut primele sisteme cu partajare a timpului (time sharing), au fost propuse sisteme
sofisticate de administrare a informaţiei Memoria virtuală şi maşinile virtuale sunt nişte principii care
nici până astăzi nu au fost exploatate până la capăt. Progresele ultimilor ani în domeniul resurselor tehnice
au permis implementarea acestor principii nu numai în cadrul sistemelor de calcul mari, ci şi pentru
calculatoarele personale.
Primele calculatoare nu dispuneau de sisteme de operare. Fiecărui utilizator i se rezerva pentru un timp
determinat calculatorul cu toate resursele acestuia. Interacţiunea era directă, programul şi datele fiind
introduse în mod manual sub formă de zerouri şi unităţi. Utilitele care au apărut aveau destinaţia de a asista
elaborarea programelor (asambloare, compilatoare, etc.) sau de a facilitata operaţiile de intrare-ieşire.Acest
mod de exploatare, numit "poartă deschisă" , era de o eficacitate minimă. Din această cauză la sfârşitul
anilor '50 au apărut primele "monitoare de înlănţuire" - programe care permiteau executarea secvenţială a
unui set de lucrări, pregătite anticipat, trecerea de la o lucrare la alta fiind automatizată.

5. Evoluția SO: multiprogramarea şi partajarea timpului


Utilizarea principiului multiprogramării sau partajarea memoriei între mai mulţi utilizatori a permis o
utilizare şi mai bună a procesorului central. Exploatarea unui calculator conform principiului timpului
partajat oferă utilizatorilor posibilităţi analogice unui calculator individual, permiţând beneficiul unor
servicii comune la un preţ redus.

6. Organizarea intrărilor - ieşirilor în memorii tampon


Pentru excluderea influenţei perifericelor asupra vitezei de lucru a sistemului de calcul s-a propus să se
păstreze în memorie în anumite zone tampon datele de intrare şi rezultatele mai multor lucrări.Deşi
utilizarea memoriilor tampon prezintă o serie de avantaje, totuşi două momente negative pot fi menţionate:
 atunci când lucrarea în curs de execuţie are nevoie de nişte date unitatea centrală rămâne inactivă pe
toată perioada citirii acestora;
 o lucrare de scurtă durată, sosită în timpul execuţiei unei lucrări "lungi", trebuie să aştepte
terminarea acesteia din urmă.

1. Multiprogramarea
Multiprogramarea este un termen utilizat în cazul unui sistem în care pot exista simultan câteva procese
în stare de execuţie. Un proces se consideră în stare de execuţie, dacă calculele au început, dar la momentul
considerat nu au fost terminate sau întrerupte. Multiprogramarea permite menţinerea unităţii centrale
în stare activă pentru perioada încărcării programelor sau operaţiilor de intrare-ieşire. Acest mod de
funcţionare este adaptat tratării pe loturi pe un calculator, care nu dispune de un mecanism de
reamplasare dinamică.

2. Sisteme cu timp partajat


Destinaţia principală a unor astfel de sisteme este furnizarea serviciilor necesare unei mulţimi de utilizatori,
fiecare dintre ei beneficiind de servicii:
 echivalente serviciilor unui calculator individual;
 legate de existenţa unei comunităţi de utilizatori: partajarea informaţiilor, comunicaţii între
utilizatori.
Problemele care apar datorită conceptului de partajare a timpului sunt o combinaţie a problemelor existente
în cazul unui calculator individual cu cele din sistemele tranzacţionale şi pot fi clasificate după cum
urmează:
 definirea maşinii virtuale oferite fiecărui utilizator;
 partajarea şi alocarea resurselor fizice comune: procesoare, memorii, dispozitive de comunicaţie;
 gestionarea informaţiilor partajate şi a comunicaţiilor.

1. Windows, Unix şi alte sisteme


Paralel cu evoluţia tehnică şi funcţională a sistemelor de operare a avut loc şi o importantă evoluţie
conceptuală, care a permis o mai bună înţelegere a funcţionării sistemelor de operare şi a condus la
elaborarea unor metode proprii de concepere.
Debutul unei cercetări ştiinţifice a sistemelor de operare poate fi considerat anul 1964, care a succedat o
etapă importantă de dezvoltare tehnică: primele sisteme cu partajare a timpului (Thor, CTSS). Sistemul de
operare UNIX, primul sistem mobil care asigură un mediu fiabil de dezvoltare şi utilizare a softului de
aplicaţie, este fundamentul practic de elaborare a sistemelor fizico-logice deschise.

2. SО UNIX şi standardele sistemelor deschise


Sistemul de operare UNIX, primul sistem mobil care asigură un mediu fiabil de dezvoltare şi utilizare a
softului de aplicaţie.
Implementarea largă a sistemului de operare UNIX a permis trecerea de la declararea sistemelor deschise la
dezvoltarea practică a acestui concept.
Variantele SO UNIX, propuse de compania SCO şi destinate exclusiv platformelor Intel, sunt bazate pe
modulele iniţiale ale System V 3.2, fiind total compatibile cu toate standardele de bază
Unul dintre primele standarde de-facto a fost cel publicat de USL pentru versiunea SO UNIX System V
Release 4 - System V Interface Definition (SVID).
Mai menţionăm standardul de-facto SPARC Complience Definition, propus de organizaţia SPARC
International,
Pentru lumea UNIX este foarte important şi standardul limbajului de programare C, adoptat mai întâi de
ANSI şi apoi de ISO. În acest standard sunt specificate, în afara limbajului C, bibliotecile necesare într-o
realizare standard. Deoarece chiar de la apariţie limbajul C şi sistemele de programare respective erau
strâns legate de UNIX, componentele bibliotecilor standard corespundeau exact mediului standard al SO
UNIX.

3. OSF-1 şi alte variante UNIX


Open Software Foundation (OSF) a fost prima companie comercială, care a încercat elaborarea SO UNIX
în baza micronucleului Mach. A fost creat sistemul de operare OSF-1, care nu era în sens de licenţiere
„curat”, deoarece folosea o parte a modulelor iniţiale din SVR 4.0.
Nu putem să nu amintim aici şi de realizarea originală a SO UNIX pentru platformele Intel, propusă de
Torvald Linus – LINUX
Prin standard al unei interfeţe al SO subînţelegem un set de proprietăţi, mai mult sau mai puţin formale,
sintactice sau semantice ale componentelor sistemului de operare.

4. Standarde UNIX
Unul dintre primele standarde de-facto a fost cel publicat de USL pentru versiunea SO UNIX System V
Release 4 - System V Interface Definition (SVID).
Mai menţionăm standardul de-facto SPARC Complience Definition, propus de organizaţia SPARC
International,
Pentru lumea UNIX este foarte important şi standardul limbajului de programare C, adoptat mai întâi de
ANSI şi apoi de ISO. În acest standard sunt specificate, în afara limbajului C, bibliotecile necesare într-o
realizare standard. Deoarece chiar de la apariţie limbajul C şi sistemele de programare respective erau
strâns legate de UNIX, componentele bibliotecilor standard corespundeau exact mediului standard al SO
UNIX.

5. Sisteme de operare cu micronucleu


Micronucleul este partea minimă principală a unui sistem de operare, folosită pentru asigurarea
modularităţii şi transportabilităţii.
Noţiunea de micronucleu a fost introdusă de compania Next prin sistemul de operare cu micronucleul
Mach.
Următorul SO cu micronucleu a fost MS Windows NT, în care momentul principal declarat era, în afara
modularităţii, transportabilitatea. Acest sistem de operare poate fi utilizat în sistemele mono- şi
miltiprocesor, bazate pe procesoarele Intel, Mips, şi Alpha
Au aderat la tehnologia „micronucleară” şi companiile Novell/USL, Open Software Foundation (OSF),
IBM, Apple şi altele. Unul din concurenţii principali ai lui NT în domeniul SO cu micronucleu sunt Mach
3.0, creat în Universitatea Carnegy-Mellon, şi Chorus 3.0 al companiei Chorus Systems.

6. Modul secvențial de execuție a unui program. Noţiuni fundamentale


Un program secvenţial = o mulţime de proceduri, care se pot apela reciproc. Fiecărei proceduri îi este
asociat un segment distinct de procedură. Datele sunt reprezentate prin segmente, pot fi proprii unei
proceduri sau partajate între mai multe proceduri.
Numim activitate fenomenul care rezultă din execuţia neîntreruptă a unei proceduri unice. !!Execuţia unui
program secvenţial constă dintr-un lanţ de activităţi!!!!
Numim context al unei activităţi mulţimea informaţiilor accesibile procesorului în cursul acestei activităţi.
Contextul activităţii este compus din contextul procesorului (registrele programabile şi interne) şi contextul
memoriei (segmentul procedurii şi segmentul datelor). Trecerea de la o activitate la alta este realizată de
instrucţiuni speciale: apelarea şi returul din procedură, care realizează comutarea contextului.

7. Modul secvențial de execuție a unui program. Apelarea și returul


Operaţiile executate la apelarea şi returul procedurii sunt următoarele:
Apelare
1. alocarea unei zone în stiva de execuţie pentru mediul procedurii apelate (dimensiunea acestei zone,
cu excepţia spaţiului de lucru, este cunoscută anticipat)
temp:=baza
baza:=top
top:=top+dimensiunea mediului
2. salvarea informaţiilor de retur
baza_veche:=temp
memorizarea adresei de retur
3. ordonarea parametrilor
4. executarea unei ramificaţii la procedura apelată.
Retur
1. salvarea rezultatului într-un amplasament stabilit
2. restabilirea informaţiilor de retur şi eliberarea mediului
temp:=adresa de retur
top:=baza
baza:=baza_veche
3. returul
ramificare *temp ramificare indirectă

1. Activităţi asincrone și protecția reciprocă între activități


Pentru cazuri mai generale sunt necesare mecanisme suplimentare, cum ar fi conceptele de asincronism sau
de protecţie reciprocă între activităţi.
Prin asincronism înţelegem efectul care îl pot avea asupra derulării unei activităţi anumite evenimente
exterioare.
Numim protecţie reciprocă între activităţi o modificare mai profundă a contextului, atunci când se
trece de la o activitate la alta, în comparaţie cu ceea ce are loc în cazul unei simple apelări de
procedură.
Un caz tipic de asincronism este executarea intrărilor-ieşirilor simultan cu execuţia unui program. Trebuie
să fie asigurată posibilitatea informării programului despre terminarea unui transfer de informaţii.

2. Mecanisme de comutare a contextului


Comutarea contextului unui procesor permite executarea într-o manieră indivizibilă (atomară) a
următoarelor două operaţii:
trecerea cuvântului de stare într-un amplasament specificat al memoriei,
încărcarea în cuvântul de stare a conţinutului unui alt amplasament specificat al memoriei. Comutarea
contextului poate fi necesară din mai multe cauze distincte. Presupunem că fiecărei cauze i-a fost asociat un
număr de ordine. Pot fi întâlnite două scheme de comutare a contextului.
1. Salvare în amplasamente fixe.
2. Salvare într-o stivă.

1. Întreruperi
O întrerupere este comutarea contextului procesorului declanşată de o cauză externă derulării instrucţiunii
curente. Fizic, întreruperea înseamnă trimiterea unui semnal procesorului, acest semnal provocând
schimbarea stării unuia dintre indicatorii, consultaţi în cursul executării fiecărei instrucţiuni. Semnalul
poate proveni de la alt procesor, de la un organ de I/E, de la un dispozitiv extern, şi în genere, de la orice
proces fizic, extern procesorului întrerupt. O întrerupere permite să cerem procesorului să suspende
executarea programului curent, din primul punct întreruptibil, şi să treacă la execuţia unui program
predefinit. Acesta din urmă este numit program de tratare a întreruperii (interrupt handler, eng., traitant
de l'interruption, fr.). Programul de tratare a întreruperii este executat într-un context diferit de cel al
programului întrerupt, diferenţa fiind legată de modul de tratare, protecţie, informaţiile accesibile, etc.

2. Devieri şi apelarea supervizorului


O deviere semnalizează o anomalie în derularea unei instrucţiuni, care prohibitează executarea
instrucţiunii. Originile pot fi diverse:
date incorecte, instrucţiune neexecutabilă . Devierile pot fi clasificate, ca şi întreruperile, conform cauzelor
care le generează. O deviere poate fi suprimată, dar nici intr-un caz retardată. Un apel al
supervizorului (supervisor call, prescurtat SVC, eng., appel au superviseur, fr.) este o instrucţiune
chemată să provoace o comutare a contextului procesorului. Acest efect este analogic apelării unei
proceduri, însă modificarea contextului este mai profundă,
Destinaţia unui apel al supervizorului este de a permite apelarea unei proceduri a sistemului de operare,
pretinzând la drepturi mai mari

3. Exemple de sisteme de întreruperi


Sistemul de întreruperi are 5 nivele (în ordinea de descreştere a priorităţilor): eroare hardware, deviere,
apelare supervizor, extern şi intrare-ieşire. Fiecărui nivel îi corespunde în memoria operativă un cuplu de
amplasamente rezervate cuvintelor de stare vechi şi nou. Fiecare nivel conţine mai multe cauze de
întrerupere.

4. Utilizarea devierilor şi apelării supervizorului


Simularea instrucţiunilor lipsă. Unele instrucţiuni din setul de bază de instrucţiuni ale procesorului pot fi
opţionale şi, deci, pot lipsi în unele configuraţii ale calculatorului. Tentativa executării unei instrucţiuni
opţionale lipsă generează o drivere de tipul instrucţiune inexistentă. Mecanismul devierilor poate fi utilizat
pentru a realiza prin program instrucţiunea inexistentă în setul de bază. De exemplu, pentru o configuraţie
în care operaţiile aritmetice în virgulă mobilă nu sunt disponibile ele pot fi simulate prin apelarea
procedurilor corespunzătoare. Aceste proceduri trebuie executate în mod slave pentru a permite tratarea
explicită a erorilor: ele sunt apelate prin încărcarea cuvântului de stare respectiv. O adresă de retur trebuie
să fie pregătită de către programul de tratare a devierii în top-ul stivei pentru a asigura returul la
instrucţiunea care urmează instrucţiunii de operaţie aritmetică simulată.
Măsurarea capacităţii memoriei. Un sistem de operare presupune a fi utilizat pentru diverse configuraţii
ale calculatorului. Capacitatea memoriei, numărul şi natura dispozitivelor periferice, etc., pot fi diferite.
Sistemul de operare trebuie să se adapteze configuraţiei concrete, ca şi condiţiilor particulare de utilizare
(număr de utilizatori, priorităţi, etc.). Crearea unei asemenea versiuni specifice se numeşte generarea
sistemului de operare. Pentru a reduce frecvenţa generărilor S.O. unii parametri de configurare pot fi
determinaţi în mod automat la iniţializare.
Gestionarea devierilor de către utilizatorul sistemului. Reacţia standard la o deviere care are loc în
timpul execuţiei unui program utilizator este apelarea unei proceduri de sistem, care provoacă emiterea
unui mesaj de eroare, oprirea programului curent şi trecerea la programul următor.
Este de dorit ca fiecare utilizator să aibă posibilitatea să asocieze fiecărei cauze distincte de deviere i o
procedură proprie, tratarea căreia ar înlocui reacţia standard. Pentru a evita o buclă infinită, o deviere din
aceeaşi cauză i în interiorul acestei proceduri trebuie să apeleze procedura standard.
Asocierea unei proceduri proc de reluare a cauzei i de deviere se face cu ajutorul unui apel al
supervizorului (SVC asociere_deviere) cu parametrii i şi proc.
Pentru trecerea la programul următor se va apela o procedură a sistemului de operare, apel realizat prin
încărcarea unui cuvânt de stare, numit schimbare.

5. Exemple de utilizare a întreruperilor


Principala utilizare a întreruperilor este măsurarea timpului şi administrarea operaţiilor de intrare-ieşire.
Prezentăm mai jos câteva exemple de utilizare a ceasului calculatorului.
procedura iniţializare;
intr_ceas_nou:=<activ,master,mascat,adr intr_ceas>;
svc_nou :=<activ,master,mascat,adr tratare_svc >;
dezarmare(intr_ceas);
procedura tratare_svc;
save(zonă);
case cod of

apel_lim_timp_ex: -- parametrii p, q, tratare_eroare
ceas:=q;
cspretur:=svc_vechi; -- salvare pentru retur
csplucrare:= <activ,slave,demascat,adr p>;
csperoare:= <activ,slave,demascat,adr tratare_eroare>;
armare(intr_ceas);
restabileşte(zonă);
încarcă_csp(csplucrare);

retur: -- datorat terminării procedurii p
dezarmare(intr_ceas);
încarcă_csp(cspretur);

endcase
procedura intr_ceas; -- expirarea timpului limită
dezarmare(intr_ceas);
încarcă_csp(csperoare);

6. Programarea operațiilor de I/O. Organizarea generală


Prin noţiunea de intrare-ieşire (prescurtat I/E; eng. input/output, I/O; fr. entrée-sortie, E/S) numim orice
transfer de informaţii din sau spre nucleul calculatorului. Operaţiile de I/E semnifică:
 transferurile de informaţii dintre diferite nivele ierarhice ale memoriei,
 transferurile de informaţii din sau spre mediul exterior (organe periferice locale sau la distanţă, captoare
sau dispozitive de acţionare, alte calculatoare, etc.).

Organizarea generală-Periferice, controlere, canale


Un organ de intrare-ieşire este un dispozitiv capabil să transfere informaţii între procesorul sau memoria
calculatorului şi un suport extern de informaţie. Acest transfer este comandat de către procesorul central. În
cel mai simplu caz, o instrucţiune specială a procesorului permite transferarea informaţiei între suportul
extern şi un registru al procesorului, care va fi deci ocupat pe toată perioadă transferului informaţiei. Odată
cu evoluţia calculatoarelor, în scopul unei mai bune utilizări a procesorului s-a ajuns la necesitatea acordării
unei autonomii organelor de intrare-ieşire încredinţându-le funcţii tot mai complicate de înlănţuire şi
comandă, procesorului central lăsându-i-se doar iniţiativa de lansare şi de control a operaţiilor. Din
considerente economice mai apoi s-a trecut la separarea dispozitivelor de comandă a perifericelor de
perifericele propriu-zise, pentru ca dispozitivele de comandă să poată fi partajate între mai multe periferice.
Câteva scheme de organizare a perifericelor sunt prezentate în fig.2.5. Schema (c) este proprie unui
calculator de putere mare, (a) şi (b) corespund configuraţiilor calculatoarelor de putere mai mică. Precizăm
funcţiile organelor reprezentate în această figură.
1) Un canal (sau unitate de schimb) este un procesor specializat în operaţiile de intrare-ieşire. El poate fi
lansat doar de un procesor central, nu posedă întreruperi, dar poate întrerupe un procesor central. Setul
de instrucţiuni ale canalului îi permite să acţioneze controlerele şi perifericele, care-i sunt conectate.
Mini- şi microcalculatoarele pot poseda organe, numite Unităţi de acces direct la memorie (ADM), care
sunt nişte canale simplificate.
2) Un contróler este un dispozitiv de comandă adaptat la un tip concret de echipament periferic.
Autonomia sa este limitată de operaţii foarte elementare. Destinaţia principală a unui controler este de a
permite conectarea a mai multor periferice de acelaşi tip la un singur controler. Un singur dispozitiv
periferic poate transmite informaţii prin intermediul controlerului la un moment de timp dat. În acelaşi
timp, este posibilă executarea simultană a unor operaţii pe alte periferice, operaţii care nu implică
transferul de informaţii (rebobinarea unei bande magnetice, deplasarea braţului unui disc, etc.).
Partajarea funcţiilor între periferic şi controler depinde de tipul perifericului. De obicei, funcţiile logice
(înlănţuirea şi sincronizarea operaţiilor, emiterea unor semnale în caz de accident sau terminarea
normală a execuţiei) sunt încredinţate controlerului, iar funcţiile fizice (transferul propriu-zis) -
perifericului. În programarea intrărilor-ieşirilor nu este necesar, de obicei, să se facă o deosebire între
un controler şi un periferic.
3) Un periferic este un organ capabil să transfere informaţii din sau spre un suport extern. Controlerul este
legat de periferic printr-o interfaţă, care conţine un set de funcţii (intrare, ieşire, semnalizări de
comandă şi de accident) şi o linie de comunicaţie, care serveşte la transferul informaţiilor.
Un canal poate comanda un singur dispozitiv periferic cu debit ridicat (disc, de ex.) sau poate fi multiplexat
pentru mai multe periferice cu debitul mai mic. Încercarea mai multor procesoare (unităţi centrale sau
canale) de a accesa simultan memoria operativă poate genera conflicte. Conflictele sunt reglate de către
dispozitivul hardware de accesare, care impune o ordine de acces conform unor priorităţi prestabilite.
Canalele au prioritate faţă de unităţile centrale, deoarece ele trebuie să reacţioneze cât se poate de rapid la
evenimentele externe.

1. Metode de comandă a perifericelor


Programul, care controlează funcţionarea elementară a unui dispozitiv periferic se numeşte driver.
Driverul gestionează în mod direct interfaţa controlerului perifericului, tratează întreruperile generate de
acesta, detectează şi tratează cazurile de eroare. El este, de obicei, invizibil unui utilizator obişnuit al
sistemului, care apelează funcţii de intrare-ieşire prin intermediul unor servicii de înalt nivel, realizate prin
apelări ale supervizorului.
2. Intrări-ieşiri buferizate în memorie
Diferenţa considerabilă dintre viteza de lucru a unităţii centrale şi cea a organelor periferice impune
"buferizarea" intrărilor-ieşirilor, adică introducerea unei zone de memorie-tampon între periferic şi
programul utilizatorului. Scopul este de a reduce timpul de inactivitate a unităţii centrale, dezlegând
funcţionarea acesteia de periferice. Programul utilizatorului va transfera informaţiile din sau spre zona-
tampon, iar această zonă-tampon va servi, în mod paralel, drept sursă sau destinaţie la schimbul de date cu
perifericul. Dezlegarea unităţii centrale de periferic este cu atât mai bine realizată cu cât este mai mare
capacitatea zonei-tampon. Adesea, pentru a nu supraîncărca memoria principală, zona-tampon este situată
pe discul fix.

3. Încărcarea iniţială
Pentru a începe lucrul maşina "goală" trebuie să aibă încărcat în memoria operativă programul sistemului
de operare şi să execute un salt la prima instrucţiune a acestui program. Aceasta presupune prezenţa în
memorie a unui program de citire, care el însuşi trebuie încărcat în prealabil. La primele calculatoare, un
program de câteva instrucţiuni, introdus manual în memorie cu ajutorul unor comutatoare ale pupitrului de
comandă la o adresă fixă, permitea încărcarea unui program de citire mai elevat capabil să citească
versiunea completă a sistemului de operare. Acest proces era numit încărcare (bootstrapping, eng.).
În calculatoarele contemporane, programul de iniţializare se află permanent într-o memorie constantă
(BIOS). Execuţia acestui program este declanşată odată cu punerea sub tensiune a calculatorului
(autoîncărcare).

4. Gestionarea activităţilor paralele. Administrarea buferizată a intrărilor-ieşirilor


Reluăm exemplul imprimării cu gestionarea unor zone tampon pe disc, descris în capitolul 2. Analiza
acestui mod de funcţionare (fig.3.1) pune în evidenţă patru activităţi, care pot avea loc simultan:
1) primitiva scriere_linie SL (unitatea centrală)
2) scriere pe disc SD (canal 1)
3) citire de pe disc CD (canal 2)
4) imprimare fizică IF (canal 3)

Aceste patru activităţi sunt în mare măsură autonome, or ele sunt executate pe procesoare distincte, cu
programe diferite. Ele nu sunt totuşi independente, deoarece accesează obiecte comune: două tampoane în
 În realitate, citirea şi scrierea pe disc sunt executate pe acelaşi canal, ceea ce poate impune unele restricţii privind simultaneitatea executării lor. Vom ignora
aici aceste restricţii, justificând mai apoi acest mod de procedare.
memorie, tm1 şi tm2 şi un tampon pe disc, td. Pot fi evidenţiate două tipuri de condiţii, care trebuie
respectate:
1) Condiţii, care stabilesc posibilitatea existenţei unor activităţi
O înregistrare nu poate fi preluată dintr-un tampon înainte de a nu fi depozitată aici. Tampoanele au
capacităţi limitate; dacă un tampon este ocupat cu înregistrări, care nu au fost încă preluate, este imposibilă
depozitarea fără a pierde informaţii. Acţiunile de depozitare şi preluare sunt, deci, supuse unor condiţii de
posibilitate de existenţă, enumerate mai jos.
Activitate Acţiune Condiţie
SL scriere în tm1 tm1 nu este plin
SD citire din tm1 tm1 nu este vid
SD scriere în td td nu este plin
CD citire din td td nu este vid
CD scriere în tm2 tm2 nu este plin
IF citire din tm2 tm2 nu este vid
Execuţia activităţilor modifică veridicitatea acestor condiţii: astfel, imprimarea unei linii pune în TRUE
condiţia “tm2 nu este plin”.
O activitate, care nu poate executa o acţiune, deoarece condiţia asociată are valoare FALSE, trebuie să
aştepte, adică execuţia unei acţiuni este retardată până când valoarea logică a condiţiei devine TRUE. În
capitolul 2 a fost discutată realizarea acestui mecanism de aşteptare şi continuare cu ajutorul întreruperilor.
2) Condiţii de validitate a informaţiilor partajate
Dacă vom examina procesul de accesare a tampoanelor, vom descoperi o altă formă de interacţiune,
cauzată de posibilitatea de accesare simultană de către două activităţi a unui amplsament de memorie.
Astfel, dacă SD citeşte conţinutul unei înregistrări din tm1 pe care SL este în curs de a o modifica,
rezultatul acestei citiri riscă să fie incoerent, dacă nu au fost luate măsuri speciale de precauţie. Problema
poate fi rezolvată impunând una din activităţi, aflate în conflict, să “aştepte” până când cealaltă va termina
accesarea.
Concluziile acestei prime analize:
Lucrarea “imprimare tamponată” este realizată prin patru activităţi simultane, în mare măsură autonome,
care cooperează pentru atingerea scopului final.
Executarea corectă a lucrării impune restricţii logice în vederea derulării acestor activităţi. Aceste restricţii
pot conduce la întârzierea execuţiei unei activităţi, care este obligată să aştepte producerea unui eveniment,
provocat de o altă activitate.

1. Gestionarea activităţilor paralele. Comanda unui proces industrial


Considerăm două reactoare identice, R1 şi R2, care funcţionează în paralel. Vom examina două soluţii
posibile:
1) utilizarea a două calculatoare (câte unul pentru fiecare reactor),
2) folosirea unui singur calculator pentru comanda ambelor reactoare.
Prima variantă nu prezintă nimic nou în raport cu descrierea din capitolul 1. Soluţia a doua cere stabilirea
condiţiilor suplimentare pentru realizarea sa. Implementarea variantei doi impune verificarea posibilităţii
apariţiei unor condiţii suplimentare. Fie P1, P2, D1, D2 segmentele procedurilor şi datelor pentru comanda
celor două reactoare R1 şi R2, memoria principală având capacitatea necesară pentru păstrarea acestor
segmente. Programele P1 şi P2 sunt executate pe rând de procesor. Raţionând ca şi în capitolul 1,
concluzionăm că relaţia 2t<T trebuie să fie respectată. Dacă acesta va fi cazul, funcţionarea reactoarelor
pentru un observator extern pare identică pentru ambele soluţii. Trebuie, totuşi să subliniem, că soluţia doi
impune restricţii mai severe în ceea ce priveşte performanţele calculatorului (capacitatea memoriei şi viteza
de procesare)
Vom examina mai jos modalităţile de implementare a soluţiei doi.
Partajarea procesorului
Între două execuţii succesive ale lui P1, procesorul este utilizat de P2, ca rezultat starea sa internă
(conţinutul registrelor, cuvântul de stare, contorul ordinal) va fi modificată. Pentru a permite reluarea lui
P1, informaţiile sale trebuiesc salvate la terminarea fiecărei secvenţe de execuţie şi restabilite la începerea
execuţiei următoare. Aceleaşi afirmaţii sunt valabile şi în cazul executării lui P2.
Partajarea programului
Programele P1 şi P2, fiind identice, putem păstra doar o singură copie, să-i zicem P, pentru economisirea
memoriei. Să examinăm condiţiile necesare pentru partajarea programului P:
 nu este permisă modificarea codului programului de execuţia sa,
 atunci când P este executat pentru reactorul Ri (i=1 sau 2), el va accesa segmentul de date Di; această
adresare selectivă a datelor va fi realizată de un mecanism care nu modifică textul programului.
Un program, existent în exemplar unic, dar care poate fi utilizat pentru executarea mai multor activităţi
independente, eventual simultane, se numeşte program reentrant (reenterabil). Acest mod de utilizare
implică:
 invarianţa textului programului în cursul executării sale,
 desemnarea uniformă, de către program, a datelor proprii fiecărei activităţi.
Printre altele, dacă activităţile partajează un procesor unic, informaţiile despre starea acestuia (în particular,
cele care servesc la desemnarea datelor) trebuiesc salvate la fiecare comutare.
Să rezumăm rezultatele celui de-al doilea exemplu.
 Au fost evidenţiate două activităţi logic independente: comanda reactoarelor R1 şi R2. Aceste două
activităţi pot fi puse pe două maşini distincte fără să existe vre-o legătură între ele.
 Considerente de economie pot impune ca aceste activităţi să partajeze resurse fizice şi resurse
program comune. Buna funcţionare a acestei partajări restricţionează execuţia activităţilor
(utilizarea alternativă a procesorului) şi modul de utilizare a obiectelor partajate (salvarea
contextului de executare, reentranţa programelor).
Ca şi concluzie pentru ambele exemple putem evidenţia două momente:
 existenţa unor activităţi evolutive, care pot derula simultan,
 existenţa unor relaţii între aceste activităţi: cooperare pentru executarea unor sarcini comune,
concurenţă în utilizarea resurselor partajate. Aceste relaţii sunt determinate de specificaţiile de “bună
funcţionare”. Ele se pot traduce în restricţii de execuţie, care conduc la întârzieri temporare a
progresării unei activităţi.

1. Noţiune de proces secvenţial. Proces unic. Context


Noţiunea de proces este mai întâi introdusă într-un mod concret pentru cazul unui calculator care conţine
un procesor unic şi o memorie adresabilă. Vom trece mai apoi la definiţia abstractă a unui proces
independent de suportul său fizic, ceea ce ne va permite introducerea problemei sincronizării.
Noţiunea de proces pune la dispoziţie un model pentru reprezentarea unei activităţi, care rezultă din
executarea unui program pe calculator. Starea calculatorului este definită de starea procesorului (conţinutul
registrelor) şi starea memoriei (conţinutul amplasamentelor). Această stare este modificată de către
procesor prin executarea instrucţiunilor programului. Fiecare executare a unei instrucţiuni constituie o
acţiune: o acţiune are ca efect trecerea în timp finit a calculatorului dintr-o stare iniţială într-o stare finală.
Acest efect este descris în specificaţiile instrucţiunii.
Executarea unei instrucţiuni va fi considerată indivizibilă sau atomară, adică este interzisă observarea sau
definirea stării maşinii în timpul acestei execuţii. Timpul utilizat pentru descrierea evoluţiei stării este un
parametru t, care poate lua o serie de valori discrete crescătoare, corespunzătoare începutului şi sfârşitului
instrucţiunilor. Aceste instanţe sunt numite puncte de observare, iar stările corespunzătoare ale maşinii
sunt numite puncte observabile. Prin abuz de limbaj, acest ultim termen desemnează în egală măsură şi
punctele de observare. Acestea permit să se asocieze o dată (valoarea curentă a lui t) unei stări a maşinii; o
atare stare datată defineşte un eveniment. Evenimentele permit reperarea modificărilor stării maşinii.
Începutul şi sfârşitul unei acţiuni a sunt evenimente datele cărora sunt notate început(a) şi sfârşit(a); vom
avea, evident, început(a)<sfârşit(a).
Executarea unui program se traduce, deci, într-o suită de acţiuni a1, a2,..., ai,..., cu început(ai)<sfârşit(ai).
O astfel de suită este numită proces secvenţial (sau simplu proces).
Un proces poate, deci, fi descris cu ajutorul succesiunii evenimentelor început(a1), sfârşit(a1), început(a2),
sfârşit(a2),... Această suită de stări datate ale maşinii se numeşte traiectorie temporală (sau istorie) a
procesului. Putem de asemenea defini un proces ca o suită de activităţi în sensul lui 2.1.
Mulţimea informaţiilor pe care acţiunile unui proces le pot consulta sau modifica se numeşte contextul
procesului. Reluând modelul de execuţie secvenţială introdus în 2.1, contextul unui proces rezultant din
executarea unui program conţine:
1) Contextul procesorului (cuvântul de stare şi registrele),
2) Un context în memorie, sau spaţiul de lucru (segmente procedură, date, stivă de execuţie),
3) O mulţime de atribute ataşate procesului şi care specifică diferite proprietăţi:
a) Nume. Numele unui proces, care serveşte pentru identificarea lui, este de regulă, un număr intern
atribuit la crearea procesului şi care permite să se ajungă la reprezentarea contextului său (v.4.2).
b) Prioritate. Prioritatea proceselor permite ordonarea lor pentru alocarea procesorului (v.4.2, 4.3). Dacă
toate procesele au aceeaşi prioritate, alocarea se face conform ordinii “primul sosit, primul servit”.
c) Drepturi. Drepturile unui proces specifică operaţiile care îi sunt permise, în scopul asigurării protecţiei
informaţiei (v. 5.1.4).

1. Relaţii între procese. Mulţimi de procese. Paralelism


-traiectoria temporală a unei mulţimi de procese este şirul evenimentelor formate de începuturile şi
sfârşiturile acţiunilor rezultante din executarea programelor acestor procese.

 Într-un mod mai riguros ar trebui să adăugăm aici “punctele întreruptible” care pot fi prezente în unele instrucţiuni de lungă durată; o executare a unei
asemenea instrucţiuni poate fi considerată o suită de mai multe acţiuni.
 Pentru comoditatea expunerii considerăm, că sfârşitul unei acţiuni şi începutul acţiunii următoare sunt două evenimente distincte, datele cărora sunt diferite,
deşi stările corespunzătoare sunt identice.
-contextele unor procese diferite pot avea părţi comune. Două procese, contextele cărora sunt disjuncte, se
numesc independente; ele nu pot avea interacţiuni reciproce. Partea contextului, care aparţine unui singur
proces, se numeşte context privat al procesului dat.
Să considerăm două programe distincte P şi Q, fiecare având în memorie un segment cod şi un segment de
date. Numim p şi q procesele rezultante din executarea respectivă a acestor două programe. Executarea
setului (p, q) poate să se producă în diferite moduri, caracterizate de forma particulară a traiectoriei sale
temporale. Aceste traiectorii sunt reprezentate în figura 3.2.
Schemele de mai sus pot fi comentate astfel:
 schema 1: este executat mai întâi tot procesul p, apoi procesul q la fel în întregime,
 schema 2: sunt executate şiruri de instrucţiuni ale procesului p în mod alternativ cu şiruri de instrucţiuni
ale procesului q, şi tot aşa până la terminarea ambelor procese,
 schema 3: executarea proceselor p şi q este simultană; în acest caz sunt necesare două procesoare.
Pentru compararea acestor scheme de execuţie introducem noţiunea nivel de observare. Putem considera o
suită de acţiuni ale unui proces ca o acţiune unică, adică să observăm derularea unui proces considerând o
unitate de execuţie

mai puţin fină decât instrucţiunea. De exemplu, dacă vom redefini noţiunea de acţiune elementară ca
execuţie a unei proceduri, traiectoria procesului va conţine doar stările definite de fiecare apel şi retur de
procedură. Nivelul de observare cel mai fin (cel al instrucţiunilor) este numit nivel de bază.
Să ne situăm mai întâi la nivelul de observare la care, prin convenţie, executarea completă a fiecărei dintre
programele P şi Q reprezintă o acţiune unică. Definiţiile care urmează sunt pentru acest nivel.
a) schema de tip 1 este a unei execuţii secvenţiale a lui p şi q. Ea este caracterizată de relaţiile:
sfârşit(q) < început(p) sau sfârşit(p) < început(q)
b) schemele de tip 2 sau 3 sunt scheme de execuţie paralelă. Ele sunt caracterizate de
sfârşit(p) > început(q) sau sfârşit(q) > început(p).
Revenim la nivelul de bază. Putem face o distincţie între schemele 2 şi 3. Într-adevăr, în schema 2, din
considerente de existenţă a unui singur procesor, la un moment de timp dat doar o singură acţiune poate fi
executată, contrar schemei 3. Se va spune că în schema 3 are loc un paralelism real, iar în schema 2 – un
pseudo-paralelism. Paralelismul real necesită două procesoare distincte. Două observaţii importante sunt
necesare:
1) Diferenţa acestor scheme de execuţie este legată de alegerea nivelului de observare. Astfel, la nivelul de
bază, diferenţa dintre schemele 1 şi 2 dispare: ambele sunt secvenţiale.
2) Alegerea nivelului de bază depinde de fineţea fenomenelor, care dorim să le considerăm elementare.
Dacă trebuie să studiem executarea instrucţiunilor în “pipe-line” pe un procesor microprogramat, în
calitate de nivel de bază va fi ales
1. Concurenţa proceselor. Resurse virtuale
Situaţia descrisă de schemele 1 şi 2 nu rezultă dintr-o legătură logică între p şi q, ci doar din existenţa unui
singur procesor. Ea poate fi caracterizată astfel: fie o mulţime de procese contextele cărora au un obiect
comun, care poate fi utilizat la un moment de timp dat de un singur proces. Se va spune în acest caz, că
obiectul constituie o resursă critică pentru procesele date sau că procesele sunt în excludere mutuală
(excludere reciprocă sau concurenţă) pentru utilizarea unei resurse. În situaţia descrisă, procesorul este o
resursă critică pentru procesele p şi q.
Observăm că excluderea mutuală a unei resurse conduce la “serializarea” execuţiei proceselor concurente,
în cazul unor acţiuni, care cer această resursă (în exemplul dat, toate acţiunile). Schemele 1 şi 2 diferă doar
prin nivelul de fineţe la care este executată serializarea.
Funcţionarea corectă a unei mulţimi de procese, care participă la îndeplinirea unei lucrări comune, implică
relaţii logice de cooperare (v.3.1.1). Este comod să se separe această cooperare de concurenţa pentru
resursele fizice cu scopul de a simplifica înţelegerea şi aplicarea celor două tipuri de relaţii. Pentru aceasta
este folosită noţiunea de resurse virtuale: fiecărei resurse fizice critice i se asociază tot atâtea copii
imaginare (sau virtuale) ale acestei resurse câte procese concurente solicită utilizarea ei. Suntem nevoiţi să
tratăm două probleme distincte:
1) respectarea relaţiilor de cooperare între procesele, care, fiecare posedă (conceptual) resursele fizice
solicitate şi pentru care paralelismul în execuţie nu este restricţionat de competiţia pentru resurse,
2) reglarea problemei de concurenţă pentru resursele fizice printr-o serializare convenabilă a execuţiei
proceselor în cauză. Se va spune în acest caz, că realizăm o alocare a resurselor fizice.
Introducerea resurselor virtuale are o consecinţă foarte importantă pe care o vom ilustra-o prin exemplul
proceselor p şi q, definite în 3.2.2.1. Să ataşăm fiecărui proces un procesor virtual. Conceptual, totul va
avea loc ca şi cum procesele s-ar derula paralel, conform unei scheme, numite logice sau virtuale, analogice
schemei 3 din fig.3.2. Cu toate acestea, trebuie de menţionat, că această schemă logică reprezintă doar o
notaţie compactă pentru mulţimea schemelor reale posibile şi că ele sunt obligatoriu de forma 1 sau 2 din
considerentele unicităţii procesorului. Pentru o schemă reală şi una virtuală a unui proces dat este păstrată
doar ordinea de succesiune a evenimentelor (începutul şi sfârşitul acţiunii) şi nu sunt păstrate valorile
absolute ale intervalelor de timp, care le separă. În absenţa altor informaţii, nu putem spune nimic apriori
despre ordinea evenimentelor, asociate unor procese distincte. Timpul folosit la reperarea evenimentelor în
schema logică este numit timp logic; relaţiile sale cu timpul real sunt prezentate în fig.3.3.
a1 a2
+------+-----------------+------+ procesul p
b1 b2 (timp logic)
+--+-------+---------+ procesul q
a1 a2
+------+---+ +----------+ +----+------+ p (timp real, execuţia 1)
+--+--+ +-----+---------+ q
b1 b2
a1 a2
+---+ +---+-----------------+------+ p (timp real, execuţia 2)
+--+----+ +---+---------+ q
b1 b2
În toate cazurile a1 precede a2, b1 precede b2.
Fig.3.3. Timpul logic şi timpul real
Vom considera, că evoluţia proceselor are loc în timp logic, adică vor fi interzise orice ipoteze despre viteza
relativă a proceselor în cauză: aceasta este consecinţa simplificării considerabile, introduse de noţiunea de
resurse virtuale, care permite ignorarea mecanismelor de alocare. O singură excepţie, totuşi va fi făcută
pentru studierea sincronizării temporale, în care timpul fizic intervine în mod precis ca măsură a duratei şi
nu numai ca mod de reperare relativă.

1. Excluderea mutuală.
Introducerea resurselor virtuale ne permite să tratăm problema excluderii mutuale la accesarea resurselor
fizice corespunzătoare în cadrul mecanismelor de alocare. Această problemă poate fi pusă într-un alt mod
cum o demonstrează exemplul următor.
Exemplu 3.3. Două procese p şi q trebuie fiecare să actualizeze valoarea unei variabile comune n (de
exemplu, n este starea unui cont bancar pentru care p şi q efectuează o depozitare). Acţiunile respective se
vor scrie în programele lui p şi q:
procesul p: Ap : n=n+np procesul q: Aq : n=n+nq
Să realizăm decompoziţia acestor acţiuni în instrucţii, notând prin Rp şi Rq registrele locale ale proceselor
p şi q respectiv:
procesul p procesul q
1. load Rp, n 1’. load Rq, n
2. add Rp, np 2’. add Rq, nq
3. sto Rp, n 3’. sto Rq, n
Dacă aceste acţiuni sunt executate în ordinea 1, 1’, 2, 2’, 3, 3’, efectul lor global este de executat n:=n+nq şi
nu n:=n+np+nq: una din actualizări a fost pierdută şi valoarea finală a variabilei n este incorectă. Pentru
evitarea acestei incoerenţe acţiunile Ap şi Aq trebuie să fie executate în excludere reciprocă; se zice de
asemenea că ele constituie pentru p şi q secţiuni critice. Este necesar să se respecte condiţia
sfârşit(Ap) < început(Aq) sau sfârşit(Aq) < început(Ap).
Această condiţie de serializare, care are efectul de a face Ap şi Aq să devină atomare, este identică condiţiei
deja întâlnite în 3.2.2.2 la accesarea unei resurse fizice critice. ◄
Excluderea mutuală constă în extinderea pentru secvenţa de acţiuni a proprietăţii de indivizibilitate a
acţiunilor nivelului de bază (acţiuni atomare). Posibilitatea executării unor acţiuni atomare se află la baza
mecanismelor care realizează sincronizarea.

1. Sincronizarea proceselor-42,43

2. Exprimarea şi implementarea restricţiilor de precedare


Introducem două exemple, care ne vor ajuta la formularea restricţiilor logice impuse de cooperare.
Exemplul 3.4.Procesul p transmite informaţii procesului q scriind într-un segment a, consultat de q (se
presupune că această transmitere are loc o singură dată). Este necesar să se verifice condiţia:
sfârşit(scriere(a)) < început(citire(a))
Această relaţie exprimă restricţia, că citirea lui a de către q nu poate începe înainte de terminarea scrierii lui
a de către p. ◄
Exemplul 3.5.Rendez-vous. Fie N procese p1,..., pN. Definim în programul fiecărui proces un punct, numit
rendez-vous, pe care procesul nu-l poate trece înainte ca alte procese să ajungă la punctul lor propriu de
rendez-vous.
Dacă programul procesului pi are forma
<debut_i>;
<rendez-vous>
<continuare_i>;
atunci restricţiile de sincronizare se vor exprima după cum urmează:
 i, j  [1..N]: sfârşit(debut_i) < început(continuare_j) ◄
Restricţiile de sincronizare pot fi exprimate prin următoarele două forme echivalente:
1. Se va impune o ordine de succesiune în timp logic pentru unele puncte ale traiectoriei temporale
ale procesului,
2. Se va impune unor procese o condiţie de autorizare a depăşirii acestor puncte ale traiectoriei lor
temporale.
Punctele privilegiate astfel se vor numi puncte de sincronizare.
Expresia (2) arată, că restricţiile de sincronizare pot fi satisfăcute impunând un proces “să aştepte” să
execute o acţiune până când o oarecare condiţie va fi satisfăcută. Această noţiune de aşteptare nu poate fi
exprimată cu ajutorul instrumentarului introdus până acum; pentru aceasta vom introduce o nouă stare
pentru un proces, stare în care procesul se zice în aşteptare sau blocat, prin opoziţie stării activ,
considerate până acum în mod implicit. Un proces, care intră în starea de aşteptare, plecând de la un punct
observabil t, opreşte progresarea începând cu acest punct şi stopează executarea acţiunilor. În momentul în
care procesul revine în starea activ, el reia execuţia sa şi contextul său privat restabileşte starea pe care
procesul o avea în punctul t (partea neprivată a contextului poate fi modificată de execuţia altor procese).
Se numeşte blocare tranziţia activaşteptare şi deblocare tranziţia inversă. Modalităţile de realizare a
blocării şi deblocării sunt obiectul subparagrafului 3.3.3.
Vom utiliza noţiunea de aşteptare pentru specificarea sincronizării proceselor. Această specificare se va
produce în două etape:
1) definirea punctelor de sincronizare pentru fiecare proces,
2) asocierea unei condiţii de depăşire fiecărui punct de sincronizare, condiţie exprimată prin intermediul
variabilelor de stare a sistemului.
Vom ilustra acest mod de specificare pentru cele două exemple precedente. Notăm un punct de sincronizare
prin ps, iar condiţia de depăşire asociată prin aut(ps). Dacă această condiţie are valoare TRUE, procesul în
cauză este autorizat să execute instrucţiunea etichetată cu ps.
Exemplul 3.6: var f : boolean init false
procesul p procesul q
scriere(a); <debut_q>;
f:=true;
<continuare_p> ps : citire(a)

aut(ps) : f:=true
A fost introdusă variabila de stare f pentru a exprima condiţia “procesul p a terminat acţiunea scriere(a)”.◄
Exemplul 3.7: var n : integer init 0;
procesul pi
<debut_i>;
n:=n+1
ps: <continuare_i>
aut(psi): n=N (i=1,...,N);
Variabila de stare n este în acest caz numărul procesului sosit în punctul de rendez-vous.◄

1. Probleme de realizare a sincronizării


Vom încerca să implementăm sincronizarea specificată de condiţiile de depăşire. Pentru aceasta trebuie să
definim un mecanism de aşteptare şi să introducem noţiunea de evenimente memorizate. Un eveniment
memorizat (e) este o variabilă, care poate lua două valori: sosit şi non_sosit, valoarea iniţială este non-
sosit. Asupra evenimentului memorizat sunt posibile două operaţii, care sunt acţiuni indivizibile:
e:=<valoare>-- atribuirea imediată a unei valori
aşteptare(e).
Operaţia aşteptare(e), executată de un proces p, are următoarea specificaţie:
if e = non_sosit then
starea(p) := blocat -- p este trecut în “aşteptarea lui e”
endif
Când e ia valoarea sosit, toate procesele care aşteptau e trec în starea activ.
Vom încerca acum să realizăm cu ajutorul acestui mecanism sincronizarea pentru cele două exemple.
Exemplul 3.8. var e : eveniment memorizat;
procesul p procesul q
scriere(a); <debut_q>;
e:=sosit; aşteptare(e);
<continuare_p> citire(a) ◄
Analiza acestui sistem (care nu conţine alte variabile de stare, decât evenimentul memorizat e) poate fi
făcută enumerând traiectoriile temporale posibile. Această analiză arată, că sincronizarea este corectă
atunci când operaţiile asupra evenimentului memorizat sunt indivizibile.
Exemplul 3.9. var e : eveniment memorizat;
n : integer init 0;
procesul pi
<debuti>;
(A) n:=n+1;
(B) if n<N then
aşteptare(e)
else
e:=sosit
endif
<continuarei> ◄
O analiză mai atentă, analogică celei din exemplul din 3.2.2.3, ne arată că acest program este incorect.
Notând un registru local al procesului i prin Ri, acţiunile (A) şi (B) pot fi descompuse după cum urmează:
(1) load Ri, n
(2) ai Ri, 1 -- adunare imediată
(3) ci Ri, N --comparare imediată
(4) br () etichetă-- salt dacă Ri  N
<aşteptare (e)>
...
etichetă : ...
Presupunem, că toate proceselor sunt în aşteptarea punctelor lor de rendez-vous, cu excepţia a două, notate
prin pj şi pk. Dacă pj şi pk vor fi executate pe traiectoria temporală (1j, 1k, 2j,...), atunci fiecare va atribui lui
n valoarea finală N-1 şi se va bloca, drept rezultat toate procesele se vor bloca pentru un timp indefinit.
Reieşind din analiza de mai sus putem concluziona, că implementarea condiţiilor de sincronizare nu poate
fi corect realizată numai cu ajutorul operaţiilor de aşteptare. Consultarea şi modificarea variabilelor de
stare, care intervin în aceste condiţii, trebuie să fie executate în regim de excludere reciprocă. Observaţia
dată ne impune să introducem un mecanism de sincronizare, care în mod automat ar asigura acest regim de
funcţionare.

1. Monitorul – mecanism de sincronizare. Definiţii. Exemple de utilizare

Un monitor este constituit dintr-o mulţime de variabile de stare şi o mulţime de proceduri, care utilizează
aceste variabile. Unele dintre aceste proceduri, numite externe, sunt accesibile utilizatorilor monitorului;
numele acestor proceduri sunt numite puncte de intrare ale monitorului. Procesele, care utilizează
monitorul pentru a se sincroniza, nu au acces direct la variabilele de stare. Monitorul poate fi utilizat doar
prin apelarea procedurilor sale externe; acestea permit blocarea sau deblocarea proceselor conform
specificaţiilor problemei. Condiţiile de blocare sau deblocare sunt exprimate ca funcţie ale variabilelor de
stare, iar mecanismul de execuţie a monitorului garantează manipularea acestor variabile în regim de
excludere mutuală. În fine, un monitor conţine un fragment de cod de iniţializare, executat o singură dată la
crearea monitorului.
Blocarea şi deblocarea proceselor se exprimă, în procedurile monitorului, prin intermediul unor condiţii. O
condiţie este declarată ca şi o variabilă, dar nu are “valoare”: o condiţie c poate fi utilizată doar prin
intermediul a trei operaţii sau primitive, efectul cărora este descris mai jos (prin p am notat procesul, care le
execută)
c.aşteptare : blochează procesul p şi îl plasează în “aşteptarea lui c”
c.vid : funcţie cu valori booleene (true, dacă nu există nici un proces în aşteptarea lui c, altfel false)
c.semnalizare: if non c.vid then <deblocarea proceselor care sunt în aşteptarea lui c> endif
Procesele, care sunt în aşteptarea unei condiţii c, sunt grupate într-un fir de aşteptare asociat lui c. Putem
spune, că o condiţie furnizează proceselor un instrument de desemnare a unui astfel fir de aşteptare.
Un proces deblocat de c.semnalizare îşi reia execuţia de la instrucţiunea, care urmează imediat primitivei
c.aşteptare, care-l blocase. Necesitatea asigurării excluderii reciproce pentru variabilele de stare impune o
restricţie suplimentară mecanismelor de deblocare: atunci când un proces p deblochează un proces q printr-
o operaţie de semnalizare, p şi q nu pot fi menţinute simultan active. Se va impune blocarea lui p până în
momentul în care q, la rândul său, se va bloca sau va părăsi monitorul. Pentru evitarea unei blocări
indefinite a lui p este necesar ca operaţia de transfer a comenzii de la p la q să fie atomară (indivizibilă) şi
se va garanta, că un proces care a fost temporar blocat este deblocat de operaţia semnalizare înainte ca un
nou proces să poată executa o procedură a monitorului.
3.3.3.2. Exemple de utilizare
Exemplul 3.10. sinc: monitor;
var fact: boolean;
term: condiţie;
procedura terminare_scriere;
begin
fact:=true;
term.semnalizare
end
procedura debut_citire;
if non fact then
term.aşteptare
endif
begin -- iniţializare
fact := false
end
end sinc
Monitorul este utilizat după cum urmează:
procesul p procesul q
scriere(a); <debut_q>
sinc.terminare_scriere; sinc.debut_citire;
<continuare_p> citire(a);
Exemplul 3.11. rendez-vous : monitor;
var n : integer;
toţi_sosiţi : condiţie;
procedura sosire;
begin
n := n+1;
if n < N then
toţi_sosiţi.aşteptare --nu au sosit toate procesele
endif
toţi_sosiţi.semnalizare -- a sosit şi ultimul
end
begin -- iniţializare
n:=0
end
end rendez_vous
Programul procesului pi va fi de forma:
procesul pi
<debut i>
rendez_vous.sosire;
<continuare i>
Este foarte important să înţelegem funcţionarea sincronizării: procesul, care soseşte ultimul la rendez_vous,
execută toţi_sosiţi.semnalizare şi deblochează unul dintre procesele, care sunt în starea aşteptare. Acesta, la
rândul său, execută toţi_sosiţi.semnalizare, deblocând următorul proces şi aşa mai departe.

2. Implementarea sincronizării. Probleme-tip

Experienţa demonstrează, că problemele de sincronizare logică întâlnite în practică pot fi reduse, în marea
lor majoritate, la combinaţia unui număr mic de situaţii elementare, schemele de soluţionare ale cărora sunt
cunoscute. Secţiunile 3.4.2 – 3.4.5 sunt consacrate studierii acestor probleme-tip, utilizând instrumentarul
de bază, pus la dispoziţie de monitoare. Problemele-tip sunt următoarele:
 accesarea de către o mulţime de procese a unei resurse partajate comune,
 comunicarea între procese,
 gestionarea perifericelor şi intrărilor-ieşirilor tamponate,
 sincronizare temporală.

1. Administrarea unei resurse partajate

Considerăm o resursă (fizică sau logică) partajată de o mulţime de procese. Utilizarea acestei resurse
trebuie să respecte nişte reguli de utilizare, destinaţia cărora constă în garantarea unor proprietăţi
specificate sau restricţii de integritate. Aceste restricţii sunt specificate pentru fiecare resursă. O
modalitate de garantare a respectării regulilor de utilizare a unei resurse constă în adoptarea următoarei
scheme:
 modul de folosire a resursei presupune utilizarea obligatorie a procedurilor de acces asociate resursei;
orice tentativă de utilizare, care nu respectă acest mod este detectată automat,
 procedurile de accesare sunt grupate într-un monitor, sau mai multe, programul căruia impune
respectarea restricţiilor de integritate.
Cel mai simplu caz este acela al unei resurse pentru care singura restricţie de integritate este de a fi utilizată
în excludere reciprocă. Simpla grupare a procedurilor sale de acces într-un monitor unic garantează
respectarea acestor restricţii.

1. Alocarea resurselor banalizate

Considerăm o resursă pentru care există un număr fix de N exemplare. Un proces poate accesa la cerere n
unităţi din cele N, le poate utiliza şi apoi elibera. O unitate, folosită de un proces, se numeşte alocată
procesului, care o utilizează, pentru toată perioada de la accesare până la eliberare. Toate unităţile sunt
echivalente din punctul de vedere al proceselor utilizatoare, vom mai zice că resursa este banalizată.
Zonele-tampon din memoria principală sau pe disc, unităţile de bandă magnetică, etc. sunt exemple de
resurse banalizate.
Vom admite următoarele reguli de utilizare:
 o unitate poate fi alocată la un moment de timp dat doar unui singur proces,
 o unitate poate fi alocată unui proces, care cere alocarea, doar dacă ea este liberă (nu este alocată nici
unui proces),
 o operaţie de eliberare este aplicată totdeauna ultimelor resurse, achiziţionate de procesul care execută
eliberarea,
 o cerere de alocare este blocantă în caz de eşec (număr insuficient de unităţi libere).
Definim două proceduri cerere şi eliberare ale unui monitor. Utilizarea resursei are loc conform schemei
de mai jos.
ps:resurse.cerere(n); -- cerere pentru n unităţi
-- aşteptare în caz de eşec
<utilizarea unităţilor primite>
resurse.eliberare(n) -- eliberarea resurselor
Condiţia de sincronizare se va scrie pentru orice proces:
aut(ps) : n  nlibere
Prima formă a unui monitor resurse se va obţine utilizând direct condiţia de sincronizare:
resurse: monitor;
var nlibere : integer;
disp : condiţie;
procedura cerere(n);
begin
while n>nlibere do
disp.aşteptare;
endwhile;
nlibere:=nlibere-n
end;
procedura eliberare(n);
begin
nlibere:=nlibere+n;
disp.semnalizare -- deblocare în lanţ
end;
begin -- iniţializare
nlibere:=N
end
end resurse
Nu a fost făcută nici o ipoteză despre ordinea proceselor în firul de aşteptare al condiţiei disp. Drept
consecinţă, la fiecare eliberare vor fi deblocate toate procesele. Este o soluţie greoaie şi poate fi foarte
costisitoare, dacă există multe procese. Este preferabil să se programeze în mod explicit administrarea
firului de aşteptare a cererilor, ceea ce obligă să avem câte o condiţie distinctă pentru fiecare proces.
Pentru elaborarea programelor vom introduce un tip discret proces, variabilele căruia permit desemnarea
proceselor.
resurse : monitor
type element : struct
lungime : integer
proc : proces
end;
var nlibere : integer;
disp :array[proces] of condiţie;
<declaraţia firului de aşteptare f şi procedurilor sale de accesare: introducere, extragere,
primul>
procedura cerere(n);
begin
var e: element;
if n>nlibere then
e.lungime:=n;
e.proc:=p; -- p este procesul apelant
introducere(e,f); -- în ordinea de creştere a lungimii
disp[p].aşteptare
endif;
nlibere:=nlibere-n
end;
procedura eliberare(n);
var e: element;
begin
nlibere:=nlibere+n;
while primul(f).lungime  nlibere do
extragere(e,f); -- elementul extras = primul (f)
disp[e.proc].semnalizare -- e.proc = procesul deblocat
endwhile;
end;
begin -- iniţializare
nlibere:=N;
f:=<vid>
end
end resurse
Această soluţie este mult mai compactă şi mai generală, deoarece permite o mai bună separare a expresiei
sincronizării (ceea ce rezultă din structura monitorului) de politica de alocare (care este definită de
procedurile de gestionare a firului de aşteptare introducere şi extragere).

1. Modelul cititorului şi redactorului

Să considerăm un fişier manipulat de procese din două clase diferite: cititori, care consultă fişierul fără a
modifica conţinutul lui şi scriitori, care pot modifica acest conţinut. Fie pentru un moment arbitrar de timp
ncit şi nscr numărul de cititori şi de scriitori, respectiv, care folosesc o procedură de acces la fişier. Cererea
de asigurare a coerenţei fişierului ne impune să respectăm următoarele restricţii:
(nscr=0) şi (ncit0) -- fişier în citire
sau (nscr =1) şi (ncit=0) -- fişier în scriere
Fie fich un monitor care asigură respectarea acestor restricţii. Vom impune următoarea formă a acceselor la
fişier:
proces cititor proces scriitor
fich.debut_citire; fich.debut_scriere;
<acces citire> <acces scriere>
fich.terminare_citire; fich.terminare_scriere;
Procedurile debut_citire, terminare_citire, debut_scriere, terminare_scriere trebuie să asigure
respectarea restricţiilor de mai sus. Vom implementa aceste restricţii autorizând depăşirile; pentru aceasta
este necesar să fie precizate priorităţile între cititori şi scriitori.
Cu titlu de exemplu, presupunem că cititorii au prioritate în faţa redactorilor (o scriere nu va fi autorizată,
dacă există cititori în aşteptare). Definim următoarele variabile de stare:
scr = o scriere este în curs (valoare booleană)
nc = numărul de cititori în aşteptare sau în curs de citire
În acest caz, condiţiile de depăşire se vor exprima după cum urmează:
aut(citire) : scr=false (nu există scrieri curente)
aut(scriere): scr=false şi nc=0 (nici scrieri nici citiri în curs, nici cititori în aşteptarea)
Monitorul, care urmează traduce direct aceste condiţii.
fich : monitor;
var scr : boolean;
nc : integer;
c_scr, c_cit : condiţie;
procedura debut_citire;
begin
nc:=nc+1;
if scr then
c_cit.aşteptare;
endif
end
procedura terminare_citire;
begin
nc:=nc-1;
if nc=0 then -- ultimul cititor a terminat
c_scr.semnalizare
endif
end
procedura debut_scriere;
begin
if scr or nc>0 then -- scriere sau citire în curs
c_scr.aşteptare
endif;
scr:=true
end
procedura terminare_scriere;
begin
scr:=false;
if nc>0 then -- prioritate cititorilor care aşteaptă
c_cit.semnalizare
else
c_scr.semnalizare
endif
end
begin -- iniţializare
scr:=false;
nc:=0
end
end fich
Pot fi definite şi programate şi alte reguli de prioritate.

2. Comunicarea între procese

Procesele pot comunica prin accesarea unei mulţimi de variabile comune. Acest mod de comunicare este
slab structurat şi ineficace, deoarece el trebuie să asigure excluderea reciprocă a variabilelor. Este utilizat
doar în cazuri speciale, cum ar fi un nucleu de sincronizare, unde excluderea mutuală globală este redusă la
secvenţe scurte şi bine protejate. Pentru cazuri generale sunt utilizate alte moduri de comunicare. Vom
prezenta mai întâi o schemă de bază pentru comunicarea prin mesaje - modelul producătorului şi
consumatorului, realizat cu ajutorul monitoarelor (3.4.3.1). O altă posibilitate, descrisă în 3.4.3.2, constă în
a considera operaţiile de comunicare ca un fel de mecanisme primitive de sincronizare. În 3.4.3.3
prezentăm o aplicaţie frecventă de comunicare – modelul client-server.

3. Modelul producătorului şi consumatorului

O schemă uzuală de comunicare este cea în care un proces (producătorul) trimite mesaje unui alt proces
(consumatorul), utilizând un tampon în memoria comună. Mesajele sunt de lungime fixă şi capacitatea
tamponului este de N mesaje.
Specificaţiile comunicaţiei sunt următoarele:
 un mesaj dat poate fi preluat doar o singură dată după ce a fost depozitat în tampon,
 un mesaj nu trebuie să poată fi pierdut; dacă tamponul conţine N mesaje nepreluate, nu pot fi depozitate
aici mesaje suplimentare,
 o operaţie “imposibilă” (depozitare într-un tampon plin sau preluare dintr-un tampon vid) blochează
procesul, care încearcă să o execute.
Condiţiile de depăşire pot fi exprimate după cum urmează, notând prin n numărul de mesaje din tampon,
care nu au fost încă preluate:
aut(depozitare) : n < N -- tamponul nu este plin
aut(preluare) : n > 0 -- tamponul nu este vid
Respectarea acestor restricţii este asigurată de un monitor tampon, utilizat după cum urmează:
proces producător proces consumator
... ...
produce(mesaj_emis); tampon.preluare(mesaj_recepţionat);
tampon.depozitare(mesaj_emis); consumator(mesaj_recepţionat);
Monitorul tampon poate fi elaborat, transcriind în mod direct autorizările de depăşire:
tampon : monitor;
var n : 0..N;
non_plin, non_vid : condiţie;
<declaraţii ale procedurilor depozitare şi preluare>
procedura depozitare(m:mesaj);
begin
if n=N then
non_plin.aşteptare
endif;
n:=n+1;
introducere(m);
non_vid.semnalizare
end
procedura preluare(var m:mesaj);
begin
if n=0 then
non_vid.aşteptare
endif;
preluare(m);
n:=n-1;
non_plin.semnalizare
end;
begin -- iniţializare
n:=0;
end
end tampon
Procedurile introducere(m) şi preluare(m) definesc politica de gestionare a tamponului şi reprezentarea
internă a mesajelor. Un caz frecvent este acela în care mesajele sunt reprezentate de elementele succesive
ale unui tablou, administrat în mod circular. În acest caz mesajele sunt preluate în ordinea depozitării.
Procedurile de gestionare a tamponului se vor scrie astfel:
type mesaj : <descrierea formatului mesajelor>
ptr : 0..N-1;
var fa : array[ptr] of mesaj;
top, coadă: ptr;
procedura intrare(m:mesaj);
begin
fa[coadă]:=m;
coadă:=coadă+1 mod N
end;
procedura ieşire(var m:mesaj);
begin
m:= fa[top];
top:=top+1 mod N
end;
<iniţializarea se va completa cu top:=0; coadă:=0;>
Această schemă poate fi extinsă pentru mai mulţi producători şi consumatori. Drept efect, procedurile
monitorului asigură accesarea exclusivă la mesaje între producători şi consumători. Totuşi, în cazul a mai
mulţi consumatori schema nu permite direcţionarea unui mesaj către un consumator anumit: putem doar
garanta, că un mesaj va fi preluat de un consumator (şi numai de unul singur) fără a specifica concret de
care.

1. Primitive de comunicare
Schimbul de mesaje între procese, în afară de funcţia de transmitere a informaţiei, poate fi utilizat şi pentru
ordonarea evenimentelor în cadrul unor procese distincte, deoarece emiterea unui mesaj precede
întotdeauna recepţia sa. Operaţiile de schimb de mesaje pot fi definite ca nişte mecanisme primitive şi să le
utilizăm pentru sincronizarea proceselor.
Primitivele de bază în comunicarea prin mesaje sunt:
emitere(mesaj,destinaţie)
recepţie(mesaj,origine)
Specificările acestor primitive trebuie să precizeze:
 natura şi forma mesajelor,
 modul de adresare a proceselor emiţătoare şi destinatare,
 modul de sincronizare a acestor procese,
 tratarea erorilor.
1) Natura mesajelor
În conformitate cu nivelul de exprimare la care este definit mecanismul de comunicare, mesajele pot fi
specificate de un tip, analogic obiectelor unui limbaj sau prin lungimea fizică a lor. Lungimea poate fi
constantă sau variabilă. Mai frecvent sunt utilizate mesajele de lungime constantă, care pot fi create mai
simplu, mesajele de lungime variabilă vor fi transmise prin referinţă, pasând adresa fizică sau
identificatorul informaţiei transmise.
2) Modul de adresare
Procesele, care fac schimb de mesaje, se pot desemna reciproc prin numele lor (desemnare directă) sau pot
utiliza numele unui obiect intermediar ori cutie poştală (desemnare indirectă). Aceste nume sunt folosite ca
parametri origine şi destinaţie. Schema a doua facilitează modificarea dinamică a interconexiunilor
proceselor sau chiar componentele unei mulţimi de procese, care comunică.
În cazul desemnării directe parametrul origine a primitivei recepţie poate fi interpretat în două moduri:
 fie ca dată: receptorul specifică explicit că aşteaptă un mesaj de la un destinatar special (recepţie
selectivă),
 fie ca rezultat: receptorul primeşte un mesaj care i-a fost adresat împreună cu identitatea emiţătorului.
În cazul desemnării indirecte asocierea proceselor cutiilor poştale poate fi statică sau dinamică. În ultimul
caz, sunt utilizate două primitive conectare şi deconectare pentru ataşarea procesului la o cutie poştală (în
calitate de receptor) şi de abrogare a acestei ataşări, respectiv. În unele sisteme un receptor sau mai multe
pot fi ataşate unei cutii poştale date; cutiile poştale supuse unor asemenea restricţii sunt adesea numite
porţi. Este posibilă şi situaţia inversă când un proces poate fi asociat la mai multe porţi distincte. Dacă
asocierea între procesul receptor şi poartă este statică, un nume de poartă specifică fără ambiguitate un
proces receptor ca şi în metoda desemnării directe.
1) Moduri de sincronizare
Pentru primitivele de comunicare pot fi specificate mai multe moduri de sincronizare. În caz general,
operaţia de recepţie blochează, în absenţa mesajului, receptorul. Unele sisteme pun la dispoziţie primitive
care dau posibilitatea să se determine dacă o cutie poştală este vidă, ceea ce permite evitarea blocării.
Pentru emitere sunt utilizate două moduri de sincronizare:
 Schema producător-consumator, în care cutia poştală este realizată printr-o zonă tampon. Emiterea nu
este blocantă, cu excepţia cazului în care tamponul este plin. Folosirea tampoanelor de lungime
variabilă cu alocarea dinamică a locaţiunilor reduce probabilitatea blocării emiţătorului.
 Schema rendez-vous, în care emiţătorul este blocat până la preluarea mesajului de către receptor.
Această schemă poate fi considerată caz limită a precedentei cu lungimea nulă a tamponului.
În fine, atunci când un proces este asociat în recepţie la mai multe porţi, poate fi definit un mod de
sincronizare, zis aşteptare multiplă, în care sosirea unui mesaj la o oarecare din aceste porţi deblochează
receptorul.
1) Tratarea erorilor
Scopul tratării erorilor este de a evita blocările infinite ale proceselor, care se pot produce în diverse
circumstanţe:
 Emiterea unui mesaj cu o destinaţie (proces sau poartă) inexistentă. În acest caz primitiva nu este
blocantă; eroarea este tratată prin emiterea unui cod de eroare sau printr-o deviere.
 Distrugerea unui proces de la care alte procese aşteaptă un mesaj sau un răspuns: procesele în aşteptare
sunt blocate şi recepţionează un cod de eroare.
Ultima situaţie nu este totdeauna detectabilă. O tehnică uzuală constă în stabilirea unui interval de timp
maxim de aşteptare a unui mesaj şi deblocarea procesului care aşteaptă la expirarea acestui interval (v.
3.4.5).
Vom ilustra prin câteva exemple reprezentative utilizarea acestor mecanisme de comunicare.
Exemplul 3.12. Sistemul de operare Thoth [8]. În acest sistem comunicarea foloseşte desemnarea
directă şi sincronizarea prin rendez-vous. Mesajele sunt de lungime constantă. Sunt utilizate patru
primitive:
id:=send(message, id_dest)
emite procesului id_dest un mesaj; blochează emiţătorul până la primirea unui răspuns, transmis în
message. Această primitivă indică identitatea procesului care a transmis răspunsul (sau nil, dacă
destinatarul nu există).
id:=receive(message, id_orig)
recepţionează un mesaj; procesul origine poate să nu fie specificat. Valoarea transmisă este identitatea
emiţătorului.
reply(message, id_orig, id_dest)
trimite un răspuns destinatarului specificat (care trebuie să-l aştepte); nu este blocantă; fără consecinţe,
dacă răspunsul nu era aşteptat.
forward(message, id_orig, id_dest)
această operaţie non blocantă este utilizată de un proces după recepţionarea unui mesaj trimis de către
id_orig, pentru ca să impună mesajul să ajungă la id_dest, care are acum obligaţia de a răspunde lui
id_orig. ◄
Exemplul 3.13. Sistemul de operare Unix [9]. În sistemul Unix comunicarea între procese utilizează
tampoane, numite pipes (tuburi), administrate conform schemei producător-consumator. Mesajele
transmise sunt caractere. Un pipe (tub) leagă un emiţător şi un receptor, conexiunea fiind stabilită dinamic.

Exemplul 3.14. Rendez-vous în limbajul de programare Ada [10, 11]. Limbajul Ada permite definirea
proceselor. Forma sintactică a comunicărilor între procese este apelarea procedurii, însă transmiterea
parametrilor şi a rezultatelor are loc conform principiului de transmitere a mesajelor cu rendez-vous.
Recepţionarea poate fi condiţionată (un apel este acceptat doar dacă o condiţie specificată este satisfăcută)
şi există posibilitatea de aşteptare multiplă. ◄

1. Aplicaţii : relaţia client-server

O aplicaţie curentă a comunicărilor între procese este relaţia client-server. Un proces server are în şarjă
îndeplinirea unor servicii (executarea unui program predefinit) proceselor client. Pentru aceasta poate fi
utilizată următoarea schemă:
procesul server procesul client
ciclu poartă_serviciu.emitere(cerere)
poartă_serviciu.recepţionare(cerere)
<executare serviciu> ...
[poartă_client.emitere(rezultat)] ...
endciclu [poartă_client.recepţionarere(rezultat)]
Secvenţele din parantezele pătrate sunt facultative.
Procesul server este asociat unei porţi, unde clienţii îşi depun cererile, trimiţând cereri; el este blocat atâta
timp cât nu există cereri de servicii în aşteptare.
Serviciul cerut poate conţine trimiterea la client a rezultatului. În acest caz clientul trebuie să trimită
serverului în cererea sa numărul unei porţi la care el se va bloca în aşteptarea rezultatului.
Fără a modifica schema de mai sus putem introduce mai multe procese server echivalente, oricare dintre ele
fiind în stare să satisfacă o cerere a unui serviciu. Aceste servere în recepţie vor fi asociate la una şi aceeaşi
cutie poştală.
Modelul din 3.4.2.1 (alocarea resurselor banalizate) şi modelul client-server de mai sus sunt reprezentative
pentru două scheme de obţinere a unui serviciu cu ajutorul proceselor într-un sistem de operare: apelarea de
procedură într-un monitor sau activarea uni proces server ciclic prin emiterea mesajelor. Alegerea între
aceste două scheme este dictată de considerente de eficacitate (schema serverului este preferată, atunci
când există paralelism real între client şi server) sau de uniformitate a structurii.

2. Administrarea intrărilor-ieşirilor

Vom arăta cum poate fi integrată administrarea intrărilor-ieşirilor în mecanismul monitoarelor. Prezentăm
mai întâi gestionarea unui periferic izolat, apoi, ca exemplu de aplicaţie, principiul unei gestionări
tamponate a intrărilor-ieşirilor.

3. Administrarea unui periferic

Fiecărui periferic îi este asociat un monitor procedurile externe ale căruia permit executarea intrărilor-
ieşirilor la acest periferic. Acest monitor are următoarea formă generală (pentru un sistem mono-utilizator):
perif: monitor;
var ..., sfr_schimb_i,...: condiţie;
<declaraţiile variabilelor de stare ale perifericului>
...
procedura schimb_i(<parametri>);
begin
<mascarea întreruperilor>;
if starea ≠ preg then
<tratare eroare(perifericul nu este gata)>
endif;
lansare_transfer_i(parametri);
sfr_schimb_i.aşteptare; -- întrerupere demascată
if starea ≠ ok then -- în timpul aşteptării
<tratare eroare(incident de transfer)>
endif;
<demascare întreruperi>
end;
...
begin
<iniţializare>
end
end perif
Procedura lansare_transfer_i pregăteşte programul pentru schimbul cerut (construirea programului
canalului sau al ADM - Acces Direct la Memorie, ţinând cont de parametrii schimbului) şi lansează
execuţia sa (instrucţiunea SIO – Start Input Output). Procesele apelante aşteaptă sfârşitul transferului
datorită condiţiei sfr_schimb_i. Sosirea unei întreruperi, care marchează sfârşitul schimbului de tip i
provoacă în mod automat executarea următoarei secvenţe:
if sfr_schimb_i.vid then <tratarea eroare întrerupere care nu este aşteptată>
else sfr_schimb_i.semnalizare
endif
Pentru un proces care execută o intrare-ieşire apelând o procedură de schimb a acestui monitor, totul se
petrece ca şi cum schimbul este sincron: la returul din procedură, informaţia a fost efectiv transferată (sau o
eroare a fost detectată şi semnalizată). Mecanismul de blocare evită aşteptarea activă şi procesorul poate fi
utilizat în timpul transferului de un alt proces.

4. Buferizarea imprimării

Putem elabora acum programul administrării tamponate a imprimării, descrise în 3.1. Avem nevoie de trei
tampoane tm1 şi tm2 de capacitate N1 şi N2 în memoria centrală şi unul pe disc, td, de lungime Ndisc.
Pentru simplitate presupunem, că transferurile se fac cu blocuri constante egale cu o linie. Fiecare tampon
este comandat de un monitor cu aceeaşi structură care are rolul de a asigura excluderea mutuală şi
sincronizarea condiţiilor tampon plin şi tampon vid.
Aceste tampoane, numite tampon1, tampon2 şi tampon_disc au aceeaşi structură cu tamponul descris în
3.4.3, înlocuind N cu N1, N2 şi Ndisc, respectiv. Definind tm1, tm2 şi td ca tablouri de elemente de
lungimea unei linii şi pointerii top şi coadă locali fiecărui monitor, procedurile de depozitare şi preluare pot
fi:
<pentru tamponul 1> <pentru tamponul 2>
procedura intrare(l:linie); procedura intrare(l:linie);
tm1[coadă] := l; tm2[coadă] := l;
coadă := coadă+1 mod N1 coadă := coadă+1 mod N2
procedura ieşire(var l:linie); procedura ieşire(var l:linie);
l := tm1[top]; l := tm2[top];
top := top+1 mod N1 top := top+1 mod N2
În monitorul tampon_disc operaţiile de depozitare şi preluare sunt intrări-ieşiri, care utilizează monitorul de
gestionare a discului:
procedura intrare(l:linie);
disc.scriere(l,td[coadă]);
coadă := coadă+1 mod Ndisc
procedura ieşire(var l:linie);
disc.scriere(l,td[top]);
top := top+1 mod Ndisc
Programul de intrare-ieşire este realizat prin cooperarea a patru procese programul cărora este prezentat
schematic mai jos (trei procese ale sistemului de operare şi procesul utilizator). Pentru a simplifica
expunerea au fost omise secvenţele de tratare a erorilor şi am admis, că sistemul funcţionează în regim
permanent fără limitarea numărului de linii la imprimare. Programele folosesc trei monitoare de gestionare
a perifericelor (tampon1, tampon2 şi tampon_disc) şi două monitoare de gestionare a perifericelor (impr şi
disc), construite în baza modelului perif, prezentat în 3.4.4.1
proces imprimare linie proces scriere_disc proces citire_disc
ciclu ciclu ciclu
tampon2.preluare(l); tampon1.preluare(l); tampon_disc.citire(l);
impr.scriere(l); tampon_disc.scriere(l); tampon2.depozitare(l);
endciclu endciclu endciclu
Imprimarea unei linii este cerută de procedura:
procedura scriere_linie(l:linie);
tampon1.depozitare(l)
Putem constata, că programele de mai sus sunt mult mai simple decât cele care folosesc direct întreruperile.
Structura modulară, introdusă de monitoare permite separarea totală a gestionării tampoanelor de cea a
perifericelor. Schimbarea capacităţii unui tampon modifică doar monitorul care comandă acest tampon;
înlocuirea unui periferic cu un altul implică rescrierea doar a monitorului, care comandă acest periferic.

5. Sincronizarea temporală

Sincronizarea temporală face ca timpul să intervină nu numai ca mijloc de ordonare a evenimentelor, dar şi
ca măsură de durată absolută. Acest mod de sincronizare este utilizat în aplicaţiile de timp real, care conţin
interacţiuni cu organe externe (comanda proceselor industriale, de exemplu). Sincronizarea temporală
solicită folosirea unui ceas, realizat prin intermediul unui oscilator cu quartz, care emite impulsuri la
intervale regulate. Aceste impulsuri pot fi utilizate pentru a declanşa o întrerupere la fiecare impuls sau
pentru a decrementa în mod automat conţinutul unui registru contor, o întrerupere este declanşată atunci
când conţinutul acestui registru atinge valoare 0.
Vom utiliza a doua metodă de funcţionare. Unitatea de timp folosită este perioada ceasului.
Pentru rezolvarea problemelor principale de sincronizare temporală poate fi folosită primitiva
suspendare(t), care are ca efect blocarea procesului apelant pentru o durată (fizică) egală cu t unităţi de
timp. Prezentăm principiul de realizare a acestei primitive cu ajutorul unui ceas.
Pentru rezolvarea acestei probleme vom fi nevoiţi să întreţinem:
 valoarea absolută a timpului (ora absolută), care măsoară la orice moment timpul trecut de la o
instanţă iniţială,
 un registru, adică o listă a proceselor care aşteaptă deblocarea, ordonat conform timpului absolut de
deblocare.
Toate procesele, care apelează primitiva suspendare(t) sunt inserate în registru în poziţia, care corespunde
orei sale absolute de deblocare.
Numim ora_de_bază ora absolută a ultimei înnoiri a ceasului, adică a ultimei iniţializări a contorului; fie
t_aşt ultima valoare încărcată. Ora absolută exactă este dată la fiecare moment de timp de relaţia
ora_exactă = ora_de_bază + t_aşt - contor
(t_aşt - contor este timpul care s-a scurs după ultima înnoire a contorului). De la o întrerupere de ceas (la
trecerea contorului prin 0), după ultima înnoire s-a scurs un timp egal cu t_aşt; ora_de_bază poate, deci, fi
iniţializată conform relaţiei
ora_de_bază := ora_de_bază + t_aşt
Variabila ora_de_bază, odată iniţializată, va fi corect întreţinută cu condiţia ca registrul să nu fie nicicând
vid; în caz general această condiţie va fi asigurată introducând un proces, numit paznic:
procesul paznic
ciclu
suspendare(tmax)
endciclu
în care tmax este un interval foarte mare de timp, paznicul rămânând pentru toată perioada de lucru în
coada registrului.
Mecanismele descrise sunt realizate într-un monitor numit ceas, care are două intrări: procedura
suspendare (apelată prin ceas.suspendare(t)) şi procedura tratare_întrerupere, pentru tratarea întreruperii
de ceas (trecerea contorului prin zero). Registrul este realizat cu ajutorul unui fir de aşteptare, care conţine
descriptorii proceselor. Un descriptor este format din numele procesului şi timpul absolut de deblocare;
firul de aşteptare este ordonat în ordinea creşterii timpului deblocării.
Presupunem, că întreruperea de ceas activează un proces, unica activitate a căruia constă în apelarea
procedurii ceas.tratare_întrerupere.
ceas: monitor;
type descriptor: struct
i : proces;
ora_debl : integer
end;
var contor, t_aşt;

 Problema ar fi uşor de rezolvat, dacă fiecare proces ar dispune de un ceas propriu: ar fi suficient să se introducă în contor valoarea t şi să se aştepte
întreruperea la trecerea prin zero. Realizarea primitivei suspendare cu ajutorul unui ceas unic comun presupune ataşarea fiecărui proces a unui ceas virtual.
ora_de_bază, ora_exactă: integer
deblocare : array[proces] of condiţie;
<declaraţiile firului de aşteptare şi procedurilor sale de acces>
procedura suspendare(durata:integer);
var proc: descriptor;
begin
ora_exactă:=ora_de_bază+t_aşt-contor;
proc.ora_deblocării:=ora_exactă+durata -- ora absolută a deblocării
proc.i := p; -- p este procesul apelant
intrare(proc,fir) -- ordonare de ora_deblocării
if proc=primul(fir) then
t_aşt:=contor:=durata;
ora_de_bază:=ora_exactă;
endif;
deblocare[p].aşteptare
end;
procedura tratare_întrerupere; -- contor în zero
var proc: descriptor;
delta_t:integer;
begin
ieşire(proc,fir); -- primul din firul de aşteptare
ora_de_bază := ora_exactă+t_aşt;
if vid(fir) then
delta_t := tmax
else
delta_t := primul(fir).ora_deblocării – ora_de_bază;
endif;
t_aşt := contor := delta_t; -- deblocarea viitoare
deblocare[proc.i].semnalizare
end;
begin
ora_de_bază := <ora_iniţială>;
contor := t_aşt := tmax;
intrare(paznic,fir)
end
end ceas
Metoda de mai sus (întreţinerea unui registru şi a orei absolute) este utilizată pentru realizarea programelor
de simulare discretă. Un atare program realizează un model în care diferite procese, executate în regim
pseudo-paralel, reprezintă activităţi care se derulează în paralelism real. Timpul utilizat este un timp
simulat, adică o variabilă, care reprezintă trecerea timpului fizic, respectând proporţionalitatea intervalelor
timpului simulat cu intervalele corespunzătoare ale timpului fizic.

1. Gestionarea dinamică a proceselor

Doar în sistemele cele mai simple procesele sunt în număr constant şi create odată pentru totdeauna la
iniţializarea sistemului. În sistemele concurente, mai ales în cele interactive, procesele sunt comandate
dinamic. Astfel, în Multics, un proces nou este creat odată cu admiterea unui nou utilizator; în Unix, la
executarea fiecărei comenzi. Primitivele de creare şi distrugere a proceselor pot fi puse în şarja sistemului
de operare sau la dispoziţia utilizatorilor. Crearea unui proces presupune alocarea resurselor şi iniţializarea
contextului, elementele căruia au fost specificate în 3.2.1. Distrugerea unui proces eliberează toate resursele
care i-au fost alocate.
Primele primitive, propuse pentru gestionarea dinamică a proceselor, au fost fork şi join. Istoric şi
cronologic, aceste operaţii au fost introduse pentru organizarea executării paralele a programelor pe un
sistem multiprocesoral, noţiunea de proces nefiind încă clară. Vom descrie câteva variante ale acestor
primitive.
Fie P o procedură. Instrucţiunea
id := fork(p),
executată de un proces p (părinte), crează un proces nou q (fiul), care va fi executat paralel cu p. Primitiva
fork prezintă ca rezultat identificatorul lui q (sau nil, dacă crearea este imposibilă). Contextul iniţial al lui q
este o copie a lui p, mai puţin contorul ordinal, care este fixat la prima instrucţiune a lui p. Procesul fiu se
termină cu o primitivă, numită exit sau quit, care provoacă dispariţia sa. După ce fork crează un proces fiu
q, primitiva join q permite procesului părinte să fixeze un punct de rendez-vous cu acest fiu. Executarea lui
join q blochează procesul părinte până când q nu va executa exit. Primitivele fork şi join au avantajele şi
dezavantajele instrucţiunii go to din programarea secvenţială.
Exemplul 3.15. În sistemul de operare Unix crearea unui proces poate fi realizată de către interpretorul limbajului de comandă (shell) sau cu ajutorul
instrucţiunii fork() de un program. Ultima situaţie este prezentată schematic în fig.3.4.
Efectul instrucţiunii fork():
 duplicarea procesului părinte;
 returnarea valorii pid (numărului procesului fiu) în procesul părinte;
 returnarea valorii 0 în procesul fiu:
procesul părinte procesul fiu

if (fork() == 0) if (fork() == 0)
codul procesului fiu codul procesului fiu
else else
codul procesului părinte codul procesului părinte

returnarea pid al procesului fiu ( 0) returnare 0


Altfel spus, în Unix primitiva fork (fără parametri) creează un proces al cărui spaţiu de lucru este o copie a
spaţiului de lucru a creatorului, inclusiv şi contorul ordinal. Diferenţa poate fi determinată consultând
valoarea returnată de primitivă (0 pentru fiu; identificatorul fiului sau nil pentru părinte). O primitivă wait
permite părintelui să aştepte terminarea execuţiei unuia dintre programele fiu (fără a putea alege care
anume, dacă există mai multe). Un proces termină execuţia sa cu primitiva exit. Primitiva exec(p) permite
unui proces să schimbe contextul, apelând o procedură specificată de p.
La lansarea Unix-ului sunt create două procese: procesul numit Swaper, care administrează memoria, cu
pid=0 şi procesul Init cu pid=1, care creează toate celelalte procese.
Ilustrăm folosirea primitivelor fork şi exec:
...
id := fork();
if id = 0 then -- eu sunt fiul
exec(p) -- programul fiului
else -- eu sunt părintele
if id = -1 then -- nil : creare imposibilă
<tratare eroare>
else
<programul părintelui>
endif
endif
Primitiva wait este utilizată după cum urmează:
id := wait(cod) -- blocare până la terminarea programului unuia dintre fii
... -- id = numărul programului fiu terminat, cod = cauza terminării ◄
1. Sincronizarea în Windows. Procese şi fire

Platforma pe 32 de biţi pune la dispoziţia programatorului instrumente evoluate pentru multiprogramare,


atât la nivelul unei mulţimi de lucrări, cât şi a unei lucrări singulare. Poate să apară întrebarea CÂND să fie
utilizată multiprogramarea în cadrul unei singure aplicaţii. Răspunsul este foarte simplu: atunci când dorim
ca mai multe fragmente de cod să fie executate simultan (pseudosimultan, dacă există mai multe fragmente
decât procesoare). De exemplu, dacă dorim ca unele activităţi să fie îndeplinite în regim de fond sau
programul să continue să reacţioneze la unele evenimente exterioare în timpul îndeplinirii unor calcule
foarte „costisitoare”. Pot fi aduse şi alte exemple. Numim proces în Windows o instanţă (un exemplar) a
programului, încărcat în memoria operativă. Această instanţă poate crea fire (thread) - secvenţe de
instrucţiuni, care urmează a fi executate. Este important să se înţeleagă că în Windows anume firele sunt
executate (nu procesele!), fiecărui proces fiindu-i asociat minimum un fir, numit firul principal al aplicaţiei.
Deoarece în realitate există mult mai multe fire decât procesoare fizice, firele vor fi executate secvenţial,
timpul de procesor repartizându-se între fire. Dar viteza mare de execuţie şi frecvenţa mare de comutare a
firelor lasă impresia unei execuţii paralele a acestora.
Fără comentarii suplimentare subliniem, că stările elementare ale unui fir sunt aceleaşi ca şi în cazul
proceselor: ales (exe), eligibil (ready) şi blocat (wait). Starea blocat este asociată aşteptării unui anume
eveniment. Când evenimentul se produce firul este trecut în mod automat în starea eligibil. De exemplu,
dacă un fir execută anumite calcule, iar un alt fir este obligat să aştepte rezultatele pentru a le salva pe disc,
al doilea fir ar putea utiliza un ciclu de tipul "while(!isCalcFinished ) continue;". Este însă simplu să ne
convingem, că în timpul execuţiei acestui ciclu procesorul este ocupat 100% - este cazul aşteptării active.
Astfel de cicluri trebuie evitate prin utilizarea mecanismelor de sincronizare.
În cadrul sistemului de operare Windows există două tipuri de fire – fire interactive, care execută un ciclu
propriu de prelucrare a mesajelor (de exemplu, firul principal al unei aplicaţii) şi fire de lucru, care sunt
funcţii simple. În ultimul caz execuţia firului se încheie atunci când calculele, generate de funcţia
respectivă, iau sfârşit.
Merită atenţie şi modalitatea organizării ordinii de execuţie a firelor. Algoritmul FIFO este departe de a fi
cel mai optimal. În Windows toate firele sunt ordonate conform priorităţilor. Prioritatea unui fir este un
număr întreg de la 0 la 31 şi este determinată de prioritatea procesului, care a generat firul şi prioritatea
relativă a firului. În acest mod se ajunge la o flexibilitate maximă, fiecărui fir punându-i-se la dispoziţie –
în caz ideal – exact atâta timp de procesor, cât are nevoie.
Prioritatea firului poate fi modificată dinamic. Firele interactive, care au prioritatea Normal, sunt executate
în mod deosebit de către sistem, prioritatea acestor fire fiind majorată, atunci când procesul, care le-a
generat, se află în planul central (foreground). În rezultat, aplicaţia curentă reacţionează mai repede la
cererile utilizatorului.

2. Necesitatea sincronizării în Windows

Când un proces este creat în mod automat este creat firul principal al acestuia. Acest fir poate crea în timpul
execuţiei alte fire, care la fel pot crea fire noi şi aşa mai departe. Timpul de procesor fiind repartizat între
fire, fiecare fir „lucrează” în mod independent.
Toate firele unui proces împart resursele comune, de exemplu, spaţiul de adrese al memoriei operative sau
fişierele deschise. Aceste resurse aparţin întregului proces, deci şi fiecărui fir. Fiecare fir poate să utilizeze
aceste resurse fără nici un fel de restricţii. În realitate, din cauza multitaskingului controlat (preemptive
multitasking - la orice moment de timp sistemul poate întrerupe execuţia unui fir şi transmite controlul unui
alt fir), se poate întâmpla ca un fir să nu fi terminat încă lucrul cu o resursă comună oarecare, iar sistemul
să treacă la un alt fir, care utilizează aceeaşi resursă. Rezultatele pot fi imprevizibile. Asemenea conflicte se
pot produce şi în cazul unor fire, care aparţin chiar unor procese diferite. Problema poate să apară
întotdeauna când două sau mai multe fire folosesc o resursă comună. Este necesar un mecanism de
coordonare a lucrului firelor cu resurse comune. În Windows acest mecanism se numeşte sincronizarea
firelor (thread synchronization).

3. Structura mecanismului de sincronizare în Windows

Mecanismul de sincronizare este un set de obiecte ale sistemului de operare Windows, create şi gestionate
program, comune pentru toate firele sistemului (unele pentru firele unui singur proces) şi utilizate pentru
coordonarea accesului la resurse. În calitate de resurse pot fi toate obiectele, care pot fi accesate de două şi
mai multe fire – un fişier pe disc, un port, un articol al unei baze de date, o variabilă globală a unui
program, accesibilă firelor unui singur procesor, un obiect al dispozitivului interfeţei grafice (Graphic
Device Interface), etc.
De obicei, sunt utilizate mecanismele (obiectele) de sincronizare, introduse mai sus: excluderea mutuală
(mutex), secţia critică (critical section), eveniment memorizat (event) şi semaforul (semaphore), fiecare
realizând metoda proprie de sincronizare. În calitate de obiecte sincronizate pot fi chiar procesele sau firele
(când un fir aşteaptă terminarea execuţiei unui proces sau a unui alt fir), fişierele, dispozitivele de
comunicaţie, etc.
Sensul mecanismelor de sincronizare constă în faptul, că fiecare poate să fie în starea set. Pentru fiecare
mecanism de sincronizare această stare poate să aibă sens propriu. Firele pot să testeze starea curentă a
mecanismului de sincronizare şi/sau să aştepte modificarea acestei stări, coordonându-şi în acest fel
acţiunile proprii. Este foarte important să se sublinieze, că atunci când un fir lucrează cu mecanismele de
sincronizare (le creează, le modifică starea) sistemul nu întrerupe execuţia firului, până nu va fi terminată
această acţiune, adică toate operaţiile finite din mecanismele de sincronizare sunt atomare (nu pot fi
întrerupte).
Menţionăm de asemenea, că nu există nici o legătură reală între mecanismele de sincronizare şi resurse.
Mecanismele de sincronizare nu pot interzice accesul nedorit la o resursă, ele doar indică firului momentul
când acesta poate accesa resursa, sau când acesta trebuie să aştepte (de exemplu, un semafor la o intersecţie
doar indică când este permisă trecerea. Cineva poate trece „pe roşu”, dvectortar consecinţele pot fi grave).

4. Administrarea obiectelor de sincronizare în Windows

Crearea unui obiect de sincronizare se produce prin apelarea unei funcţii speciale din WinAPI de tipul
Create… (de exemplu, CreateMutex). Acest apel returnează descriptorul obiectului (handle), care poate
fi folosit de toate firele procesului dat. Un obiect de sincronizare poate fi accesat şi dintr-un alt proces, dacă
acest proces a moştenit descriptorul obiectului dat, sau folosind funcţia de deschidere a unui obiect
(Open…). Obiectului, dacă el nu este destinat doar pentru uz intern (în interiorul unui singur proces), în
mod obligator i se acordă un nume unic. Nu poate fi creat un eveniment memorizat şi un semafor cu acelaşi
nume.
Folosind descriptorul poate fi determinată starea curentă a obiectului cu ajutorul funcţiilor de aşteptare. De
exemplu, funcţia WaitForSingleObject(x, y) cu doi parametri (primul este descriptorul obiectului, iar al
doilea – timpul de aşteptare în ms) returnează WAIT_OBJECT_0, dacă obiectul se află în starea set (adică
nu aparţine nici unui fir şi poate fi utilizat pentru sincronizare), WAIT_TIMEOUT – dacă a expirat timpul
de aşteptare şi WAIT_ABANDONED, dacă obiectul de sincronizare nu a fost eliberat înainte ca firul, care-
l comanda, să se fi terminat. Dacă timpul de aşteptare este egal cu 0, atunci funcţia returnează rezultatul
imediat, în caz contrar, aşteaptă intervalul de timp indicat. În cazul în care starea obiectului de sincronizare
va deveni set până la expirarea acestui timp, funcţia returnează WAIT_OBJECT_0, altfel -
WAIT_TIMEOUT.
Dacă în parametrul timp este indicată constanta simbolică INFINITE, funcţia va aştepta până când starea
obiectului va deveni set, fără vre-o restricţie.
Starea mai multor obiecte poate fi aflată cu ajutorul funcţiei WaitForMultipleObjects. Pentru încheierea
lucrului cu un obiect de sincronizare şi eliberarea descriptorului se apelează funcţia CloseHandle. Este
important de ştiut, că apelarea unei funcţii de aşteptarea blochează firul curent, adică atâta timp cât un fir se
află în starea de aşteptare el nu are acces la procesor.

5. Excluderea mutual

Cum a fost menţionat deja, mecanismele de excludere mutuală (mutex-ele, de la MUTual EXclusion)
permit coordonarea accesului la o resursă partajată. Starea set a obiectului corespunde momentului de timp
în care obiectul nu aparţine nici unui fir şi poate fi „utilizat”, iar starea reset – momentului când un fir
oarecare controlează deja mutex-ul. Accesarea va fi permisă doar după eliberare.
Pentru a lega mutex-ul de firul curent trebuie apelată una din funcţiile de aşteptare. Firul, căruia îi aparţine
mutex-ul, îl poate „ocupa” de mai multe ori, fără autoblocare, însă mai apoi acesta va trebui eliberat tot de
atâtea ori cu ajutorul funcţiei ReleaseMutex

6. Evenimentele

Obiectele-evenimente sunt utilizate pentru a informa firele, care sunt în aşteptare, despre producerea unui
eveniment. În Windows există două tipuri de evenimente – cu resetare manuală şi automată. Resetarea
manuală se execută cu funcţia ResetEvent. Aceste evenimente sunt folosite pentru informarea mai multor
fire, iar evenimentele cu resetare automată sunt utilizate pentru informarea unui anumit fir, celelalte
rămânând în aşteptare.
Funcţia CreateEvent crează un obiect-eveniment, funcţia SetEvent setează evenimentul în starea set, iar
funcţia ResetEvent resetează evenimentul. Funcţia PulseEvent setează evenimentul, iar după
semnalizarea firelor, care erau în aşteptare (toate în cazul resetării manuale şi doar unul la resetarea
automată), resetează obiectul. Dacă nu există fire în aşteptare, PulseEvent doar resetează obiectul, fără
semnalizare.

7. Semafoarele

Un obiect-semafor este în ultimă instanţă un mutex cu contor. Acest obiect permite să fie „ocupat” de un
număr anume de fire, după care „ocuparea” va fi posibilă numai dacă unul din fire va „elibera” semaforul.
Semafoarele sunt utilizate pentru a limita numărul de fire, care lucrează simultan cu resursa. La iniţializare
se specifică numărul maxim de fire, la fiecare „ocupare” valoarea contorului semaforului scade.
8. Secţiunile critice

Obiectul-secţiune critică permite programatorului să evidenţieze un fragment de cod în care firul obţine
acces la o resursă partajată, preîntâmpinând utilizarea resursei de mai mulţi utilizatori. Pentru a utiliza
resursa firul va intra mai întâi în secţiunea critică (apelarea funcţiei EnterCriticalSection). Dacă intrarea a
avut loc, nici un alt fir nu va avea acces la aceeaşi secţiune critică, execuţia acestuia fiind suspendată.
Reluarea se va produce în momentul în care primul fir părăseşte secţiunea critică (funcţia
LeaveCriticalSection). Diferenţa de mutex constă în faptul că secţiunea critică este utilizată numai pentru
firele unui singur proces.
Cu ajutorul funcţiei TryEnterCriticalSection se poate stabili, dacă secţiunea critică este liberă. Utilizând
această funcţie, un proces, fiind în aşteptarea resursei, poate să nu se blocheze, îndeplinind operaţii utile.

9. Protejarea accesării variabilelor

Există o serie de funcţii, care permit lucrul cu variabilele globale ale tuturor firelor, fără a ne preocupa de
sincronizare, deoarece aceste funcţii singure rezolvă problema sincronizării. Aceste funcţii sunt
InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd şi
InterlockedCompareExchange. De exemplu, funcţia InterlockedIncrement incrementează valoarea unei
variabile pe 32 biţi cu o unitate.
10. Sincronizarea în MFC

Biblioteca MFC conţine clase speciale pentru sincronizarea firelor (CMutex, CEvent, CCriticalSection şi
CSemaphore). Aceste clase corespund obiectelor de sincronizare WinAPI şi sunt derivate de la clasa
CSyncObject. Pentru utilizarea acestor clase trebuie consultaţi constructorii şi metodele lor – Lock şi
Unlock. În principiu, aceste clase sunt doar un fel de ambalaj pentru obiectele de sincronizare.
O altă modalitate de utilizare a acestor clase constă în crearea aşa numitelor clase thread-safe. O clasă
thread-safe reprezintă o anumită resursă în program. Tot lucrul cu resursa este realizat numai prin
intermediul acestei clase, care conţine toate metodele necesare. Clasa este proiectată în aşa mod, ca
metodele ei să rezolve problema sincronizării, adică în cadrul aplicaţiei să apară ca o simplă clasă. Obiectul
de sincronizare MFC este inclus în această clasă în calitate de membru privat şi toate funcţiile clasei, care
realizează accesarea resursei, îşi coordonează lucrul cu acest membru.
Utilizând funcţiile Lock şi Unlock clasele de sincronizare MFC pot fi utilizate direct, iar în mod indirect –
prin funcţiile CSingleLock şi CmultiLock.

11. Exemplu de sincronizare în Windows

Prezentăm un exemplu simplu de lucru cu obiectul de sincronizare mutex [22]. Pentru simplitate a fost
utilizată o aplicaţie Win32 de consolă, deşi nu este obligator.

#include <windows.h>
#include <iostream.h>

void main()
{
DWORD res;

// creăm obiectul excludere mutuală


HANDLE mutex = CreateMutex(NULL, FALSE, "NUME_APLICATIE-MUTEX01");
// dacă obiectul există deja, CreateMutex va returna descriptorul obiectul existent,
// iar GetLastError va returna ERROR_ALREADY_EXISTS

// timp de 20 s încercăm să ocupăm obiectul


cout<<"Încerc să ocup obiectul...\n"; cout.flush();
res = WaitForSingleObject(mutex,20000);
if (res == WAIT_OBJECT_0) // dacă ocupare s-a terminat cu succes
{
// aşteptăm 10 s
cout<<"L-am prins! Aşteptare 10 secunde...\n"; cout.flush();
Sleep(10000);

// eliberăm obiectul
cout<<"Acum eliberăm obiectul\n"; cout.flush();
ReleaseMutex(mutex);
}

// închidem descriptorul
CloseHandle(mutex);
}
Pentru a controla modul de funcţionare a mecanismului de excludere mutuală se vor lansa două instanţe ale
acestei aplicaţii. Prima instanţă va ocupa obiectul imediat şi-l va elibera doar peste 10 secunde. Numai
după aceasta instanţa a doua va reuşi să ocupe obiectul. În acest exemplu obiectul de sincronizare este
folosit pentru sincronizarea proceselor, din care cauză în mod obligatoriu trebuie să aibă nume.

12. Utilizarea secţiunilor critice în Windows


În acest caz secţiunile critice sunt utilizate pentru a permite la un moment de timp dat accesul la unele date
importante unui singur fir al aplicaţiei, celelalte fire fiind blocate. De exemplu, fie variabila m_pObject
şi câteva fire, care apelează metodele obiectului referit de m_pObject. Presupunem că această variabilă
din timp în timp îşi poate schimba valoarea, valoarea 0 nu este interzisă. Fie următorul fragment de cod:

// Firul #1
void Proc1()
{
if (m_pObject)
m_pObject->SomeMethod();
}

// Firul #2
void Proc2(IObject *pNewObject)
{
if (m_pObject)
delete m_pObject;
m_pObject = pNewObject;
}

În acest exemplu există pericolul potenţial de apelare m_pObject->SomeMethod() după ce obiectul a


fost distrus cu ajutorul delete m_pObject, deoarece în sistemele de operare cu multitasking controlat
execuţia oricărui fir poate fi întreruptă în cel mai neconvenabil (pentru firul dat) moment şi să înceapă
execuţia unui alt fir. Pentru exemplul nostru momentul nedorit este atunci când firul #1 a testat deja
m_pObject, dar nu a reuşit să apeleze SomeMethod(). Execuţia firului #1 a fost întreruptă şi a început
execuţia firului #2. Iar firul #2 reuşise deja să apeleze destructorul obiectului. Ce se va întâmpla atunci
când firului #1 i se va acorda din nou timp de procesor şi va fi apelat SomeMethod() al unui obiect deja
inexistent? Greu de presupus.
Aici ne vin în ajutor secţiunile critice. Să modificăm exemplul de mai sus.

// Firul #1
void Proc1()
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
m_pObject->SomeMethod();
::LeaveCriticalSection(&m_lockObject);
}
// Firul #2
void Proc2(IObject *pNewObject)
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
delete m_pObject;
m_pObject = pNewObject;
::LeaveCriticalSection(&m_lockObject);
}

Fragmentul de cod inclus între ::EnterCriticalSection() şi ::LeaveCriticalSection() cu una şi


aceeaşi secţiune critică în calitate de parametru nu va fi executat nici o dată în mod paralel. Aceasta
înseamnă, că dacă firul #1 a reuşit să „acapareze” secţiunea critică m_lockObject, încercarea firului #2
să intre în aceeaşi secţiune critică va conduce la blocarea acestuia până în momentul când firul #1 va
elibera m_lockObject prin apelul ::LeaveCriticalSection(). Invers, dacă firul #2 a accesat secţiunea
critică înaintea firului #1, acesta din urmă va fi nevoit “să aştepte”, înainte de a începe lucrul cu
m_pObject.
Menţionăm, că secţiunile critice nu sunt obiecte ale nucleului sistemului de operare. Practic, tot lucrul cu
secţiunile critice are loc in procesul care le-a creat. Din aceasta rezultă, că secţiunile critice pot fi utilizate
numai pentru sincronizare în cadrul unui proces. Să cercetăm mai aprofundat o secţiune critică.

13. Structura RTL_CRITICAL_SECTION

Este definită după cum urmează:

typedef struct _RTL_CRITICAL_SECTION


{
PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // Folosit de sistemul de operare
LONG LockCount; // Contorul de utilizări
LONG RecursionCount; // Contorul accesării repetate din firul utilizatorului
HANDLE OwningThread; // ID firului utilizatorului (unic)
HANDLE LockSemaphore; // Obiectul nucleului folosit pentru aşteptare
ULONG_PTR SpinCount; // Numărul de cicluri goale înaintea apelării nucleului
}
RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
Câmpul LockCount este incrementat cu o unitate la fiecare apelare ::EnterCriticalSection() şi
decrementat cu unu la fiecare apel ::LeaveCriticalSection(). Acesta este primul (adesea şi unicul)
control pentru testarea secţiunii critice. Dacă după incrementare în acest câmp avem 0, aceasta înseamnă că
până la acest moment nu au avut loc apeluri impare ::EnterCriticalSection() din alte fire. În acest caz
datele “păzite” de această secţiune critică pot fi utilizate în regim monopol. Adică, dacă secţiunea critică
este folosită intensiv de un singur fir, atunci ::EnterCriticalSection() se transformă practic în +
+LockCount, iar ::LeaveCriticalSection() în--LockCount. Aceasta înseamnă, că folosirea a mai multor
mii de secţiuni critice într-un singur proces nu va conduce la creşterea substanţială a utilizării nici a
resurselor de sistem, nici a timpului de procesor. Ca şi concluzie: nu se va face economie pe contul
secţiunilor critice. Mult totuna nu vom putea economisi.
În câmpul RecursionCount este păstrat numărul de apeluri repetate ::EnterCriticalSection() din unul
şi acelaşi fir. Dacă se va apela ::EnterCriticalSection() din unul şi acelaşi fir de mai multe ori, toate
apelurile vor avea succes. Adică următorul cod nu se va opri pentru totdeauna în cel de-al doilea apel
::EnterCriticalSection(), ci va merge până la capăt.
// Firul #1
void Proc1()
{
::EnterCriticalSection(&m_lock);
// ...
Proc2()
// ...
::LeaveCriticalSection(&m_lock);
}

// Încă Firul #1
void Proc2()
{
::EnterCriticalSection(&m_lock);
// ...
::LeaveCriticalSection(&m_lock);
}
Într-adevăr, secţiunile critice sunt destinate să protejeze datele la accesarea din câteva fire. Utilizarea
multiplă a uneia şi aceeaşi secţiuni critice de un singur fir nu va genera eroare, ceea ce este normal. Trebuie
doar să avem grijă ca numărul de apeluri ::EnterCriticalSection() şi ::LeaveCriticalSection() să
coincidă şi totul va fi în regulă.
Câmpul OwningThread conţine 0 în cazul secţiunilor critice libere sau identificatorul unic al firului-
posesor. Acest câmp este testat, dacă la apelul ::EnterCriticalSection() câmpul LockCount, după
incrementarea cu o unitate, a devenit mai mare ca 0. Dacă OwningThread coiincide cu identificatorul unic
al firului curent, atunci valoarea lui RecursionCount creşte cu o unitate şi ::EnterCriticalSection() este
returnat imediat. În caz contrar ::EnterCriticalSection() va aştepta până firul, care posedă secţiunea critică,
va apela ::LeaveCriticalSection() de un număr necesar de ori. Câmpul LockSemaphore este folosit, dacă
este necesar să se aştepte până secţiunea critică este eliberată. Dacă LockCount este mai mare ca 0 şi
OwningThread nu coiincide cu identificatorul unic al firului curent, atunci firul blocat crează un obiect al
nucleului – eveniment – şi apelează ::WaitForSingleObject(LockSemaphore). Firul posesor, după
decrementarea câmpului RecursionCount, testează acest câmp şi dacă valoarea lui este 0, iar LockCount
este mai mare ca 0, constată că există minimum un fir în aşteptarea momentului când LockSemaphore se
va afla în starea “sosit”. Pentru aceasta firul-posesor apelează ::SetEvent() şi un fir oarecare (doar unul)
dintre cele blocate este deblocat şi are acces la datele critice.
Windows NT/2000 generează o excepţie, dacă încercarea de a crea un eveniment a eşuat. Aceasta este just
atât pentru funcţiile ::Enter/LeaveCriticalSection(), cât şi pentru
::InitializeCriticalSectionAndSpinCount() cu bitul cel mai semnificativ al parametrului SpinCount setat.
În cazul sistemului de operare WindowsXP creatorii nucleului au procedat un pic altfel. Aici, funcţiile
::Enter/LeaveCriticalSection() în loc să genereze o excepţie, dacă nu pot crea un eveniment propriu, vor
folosi un obiect global, unic pentru toţi, creat în avans. În aşa mod, în caz de deficienţă catastrofală a
resurselor de sistem, programul sub comanda lui WindowsXP “şchiopătează” un timp oarecare mai departe.
Într-adevăr, este foarte complicat să scrii programe, care ar fi în stare să continue lucrul după ce
::EnterCriticalSection() a generat o excepţie. De obicei, chiar dacă programatorul a prezis o astfel de
situaţie, nu se ajunge mai departe de generarea unui mesaj de eroare şi stoparea forţată a execuţiei
programului. Drept rezultat, WindowsXP ignorează bitul cel mai semnificativ al câmpului LockCount.
În sfârşit, câmpul LockCount. Acest câmp este folosit doar în sistemele multiprocesorale. În sistemele
monoprocesorale, dacă secţiunea critică este ocupată de un fir oarecare, putem doar comuta comanda la
această secţiune şi trece în aşteptarea evenimentului nostru. În sistemele multiprocesorale există o
alternativă: să executăm de câteva ori un ciclu vid, testând de fiecare dată dacă secţiunea critică nu a fost
eliberată (aşteptare activă). Dacă după un număr de SpinCount ori secţiunea critică nu a fost eliberată,
trecem în starea blocat. Această modalitate este mult mai eficientă decât comutarea la planificatorul
nucleului şi înapoi. În WindowsNT/2000 bitul cel mai semnificativ al acestui câmp indică dacă obiectul
nucleului, variabila handle a căruia se află în câmpul LockSemaphore, trebuie creat anticipat. Dacă
pentru aceasta nu dispunem de resurse de sistem suficiente, sistemul generează o excepţie şi programul îşi
poate „micşora” posibilităţile funcţionale sau chiar termina execuţia.

14. Funcţii API pentru secţiunile critice

Descriem mai jos câteva funcţii din API pentru lucrul cu secţiunile critice.
Mai întâi funcţiile BOOL InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection) şi
BOOL InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD
dwSpinCount). Completează câmpurile structurii lpCriticalSection adresate. După apelare secţiunea
critică este gata de lucru. Iată pseudocodul funcţiei RtlInitializeCriticalSection din ntdll.dll

VOID RtlInitializeCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{
RtlInitializeCriticalSectionAndSpinCount(pcs, 0)
}

VOID RtlInitializeCriticalSectionAndSpinCount(LPRTL_CRITICAL_SECTION pcs,


DWORD dwSpinCount)
{
pcs->DebugInfo = NULL;
pcs->LockCount = -1;
pcs->RecursionCount = 0;
pcs->OwningThread = 0;
pcs->LockSemaphore = NULL;
pcs->SpinCount = dwSpinCount;
if (0x80000000 & dwSpinCount)
_CriticalSectionGetEvent(pcs);
}

Funcţia DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection,


DWORD dwSpinCount) setează valoarea câmpului SpinCount şi returnează valoarea precedentă a
acestuia. Amintim, că bitul cel mai semnificativ este responsabil de “legarea” evenimentului, folosit pentru
aşteptarea accesului la secţiunea critică dată. Pseudocodul funcţiei RtlSetCriticalSectionSpinCount din
ntdll.dll este listat mai jos.

DWORD RtlSetCriticalSectionSpinCount(LPRTL_CRITICAL_SECTION pcs, DWORD dwSpinCount)


{
DWORD dwRet = pcs->SpinCount;
pcs->SpinCount = dwSpinCount;
return dwRet;
}

VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection) eliberează resursele,


ocupate de secţiunea critică. Are următorul pseudocod:
VOID RtlDeleteCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
pcs->DebugInfo = NULL;
pcs->LockCount = -1;
pcs->RecursionCount = 0;
pcs->OwningThread = 0;
if (pcs->LockSemaphore)
{
::CloseHandle(pcs->LockSemaphore);
pcs->LockSemaphore = NULL;
}
}

VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection), BOOL


TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection) permit intrarea în secţiunea
critică. Dacă secţiunea critică este ocupată de un alt fir, atunci ::EnterCriticalSection() va aştepta până
aceasta va fi eliberată, iar ::TryEnterCriticalSection() va returna valoarea FALSE. Listingurile din
ntdll.dll sunt:

VOID RtlEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{
if (::InterlockedIncrement(&pcs->LockCount))
{
if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
pcs->RecursionCount++;
return;
}

RtlpWaitForCriticalSection(pcs);
}

pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}

BOOL RtlTryEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{
if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
{
pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
::InterlockedIncrement(&pcs->LockCount);
pcs->RecursionCount++;
}
else
return FALSE;

return TRUE;
}

VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection) eliberează secţiunea critică.


Pseudocodul este următorul:
VOID RtlLeaveCriticalSectionDbg(LPRTL_CRITICAL_SECTION pcs)
{
if (--pcs->RecursionCount)
::InterlockedDecrement(&pcs->LockCount);
else if (::InterlockedDecrement(&pcs->LockCount) >= 0)
RtlpUnWaitCriticalSection(pcs);
}

15. Clase de secţiuni critice

Pentru o utilizare corectă a secţiunilor critice prezentăm mai jos codul claselor secţiunilor critice:
class CLock
{
friend class CScopeLock;
CRITICAL_SECTION m_CS;
public:
void Init() { ::InitializeCriticalSection(&m_CS); }
void Term() { ::DeleteCriticalSection(&m_CS); }
void Lock() { ::EnterCriticalSection(&m_CS); }
BOOL TryLock() { return ::TryEnterCriticalSection(&m_CS); }
void Unlock() { ::LeaveCriticalSection(&m_CS); }
};

class CAutoLock : public CLock


{
public:
CAutoLock() { Init(); }
~CAutoLock() { Term(); }
};

class CScopeLock
{
LPCRITICAL_SECTION m_pCS;
public:
CScopeLock(LPCRITICAL_SECTION pCS) : m_pCS(pCS) { Lock(); }
CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
~CScopeLock() { Unlock(); }
void Lock() { ::EnterCriticalSection(m_pCS); }
void Unlock() { ::LeaveCriticalSection(m_pCS); }
};

Clasele CLock şi CAutoLock sunt utilizate, de obicei, pentru sincronizarea accesării variabilelor clasei, iar
CScopeLock este destinat, în special, pentru a fi utilizată în proceduri. Compilatorul singur va avea grijă
să apeleze ::LeaveCriticalSection() prin intermediul destructorului. Urmează un exemplu de folosire a
CScopeLock.

CAutoLock m_lockObject;
CObject *m_pObject;
void Proc1()
{
CScopeLock lock(m_ lockObject); // apelarea lock.Lock();
if (!m_pObject)
return; // apelarea lock.Unlock();
m_pObject->SomeMethod();
// apelarea lock.Unlock();
}

16. Depanarea secţiunilor critice

Depanarea secţiunilor critice este o ocupaţie foarte interesantă, dar şi dificilă. Poţi căuta ore şi chiar zile în
şir cauza apariţiei unei probleme. Erorile, legate de secţiunile critice sunt de două tipuri: de realizare şi de
arhitectură. Erorile de realizare pot fi depistate relativ uşor şi, de regulă, sunt generate de utilizarea
incorectă (lipsa perechii) a apelurilor ::EnterCriticalSection() şi ::LeaveCriticalSection(). Urmează un
fragment de cod în care este omis apelul ::EnterCriticalSection().
// În procedură se presupune, că m_lockObject.Lock(); a fost deja apelat
void Pool()
{
for (int i = 0; i < m_vectSinks.size(); i++)
{
m_lockObject.Unlock();
m_vectSinks[i]->DoSomething();
m_lockObject.Lock();
}
}
Apelul ::LeaveCriticalSection() fără ::EnterCriticalSection() va conduce la faptul că chiar
primul apel ::EnterCriticalSection() va stopa execuţia firului pentru totdeauna.
În fragmentul de cod de mai jos lipseşte apelul ::LeaveCriticalSection():
void Proc()
{
m_lockObject.Lock();
if (!m_pObject)
return;
// ...
m_lockObject.Unlock();
}
În acest exemplu are sens să fie utilizată o clasă de tipul CSopeLock. Se mai poate întâmpla ca
::EnterCriticalSection() să fie apelată fără iniţializarea secţiunii critice cu ajutorul
::InitializeCriticalSection() (de exemplu, în proiectele scrise cu ajutorul lui ATL). În versiunea debug
totul poate lucra foarte bine, iar în versiunea release „moare”. Aceasta are loc din cauza aşa-zisului CRT
(_ATL_MIN_CRT) „minimal”, care nu apelează constructorii obiectelor statice (Q166480,
Q165076). În versiunea ATL 7.0 această problemă a fost rezolvată. Pot să apară probleme, dacă atunci
când este folosită o clasă de tipul CScopeLock a fost omis identificatorul variabilei, de exemplu,
CScopeLock (m_lock). Compilatorul apelează constructorul CScopeLock şi imediat distruge acest
obiect fără nume (în conformitate cu standardul!). Adică, imediat după apelarea metodei Lock() are loc
apelarea metodei Unlock() şi sincronizarea nu se produce.
Dintre erorile de arhitectură cea mai frecventă este îmbrăţişarea fatală (deadlock, v.5.1.3.3), când două
fire încearcă să acceseze două şi mai multe secţiuni critice. Prezentăm un exemplu pentru două fire.
void Proc1()
// Firul #1
{
::EnterCriticalSection(&m_lock1);
// ...
::EnterCriticalSection(&m_lock2);
// ...
::LeaveCriticalSection(&m_lock2);
// ...
::LeaveCriticalSection(&m_lock1);
}

// Firul #2
void Proc2()
{
::EnterCriticalSection(&m_lock2);
// ...
::EnterCriticalSection(&m_lock1);
// ...
::LeaveCriticalSection(&m_lock1);
// ...
::LeaveCriticalSection(&m_lock2);
}
Pot să apară probleme şi în cazul copierii unor secţiuni critice. Este greu de presupus că codul de mai jos a
fost scris de un programator sănătos:

CRITICAL_SECTION sec1;
CRITICAL_SECTION sec2;
// ...
sec1 = sec2;
Din atribuirea de mai sus este dificil să obţii foloase. Dar fragmentul următor poate fi adesea întâlnit:
struct SData
{
CLock m_lock;
DWORD m_dwSmth;
} m_data;

void Proc1(SData& data)


{
m_data = data;
}
şi totul ar fi OK, dacă structura SData ar avea on constructor de copiere, de exemplu:

SData(const SData data)


{
CScopeLock lock(data.m_lock);
m_dwSmth = data.m_dwSmth;
}
Presupunem că programatorul a considerat, că este suficient să se îndeplinească o simplă copiere a
câmpurilor şi, în rezultat, variabila m_lock a fost copiată, iar anume în acest moment ea fusese accesată
dintr-un alt fir şi valoarea câmpului LockCount este ≥ 0. După apelul ::LeaveCriticalSection() din acel fir
valoarea câmpului LockCount pentru variabila iniţială m_lock a fost decrementată cu o unitate, pentru
variabila copiată rămânând fără schimbare. Ca rezultat, orice apel ::EnterCriticalSection() nu se va
întoarce niciodată în acest fir. Va rămâne pentru totdeauna în aşteptare. Pot exista situaţii mult mai
complicate.
Fie un obiect care apelează metodele unui alt obiect, obiectele aflându-se în fire diferite. Apelurile se fac în
mod sincron, adică obiectul #1 transmite execuţia firului obiectului #2, apelează metoda şi se va comuta
înapoi la firul său. Execuţia firului #1 va fi suspendată pentru toată perioada de execuţie a firului obiectului
#2. Presupunem acum, că obiectul #2 apelează o metodă a obiectului #1 din firul său. Controlul va fi întors
obiectului #1, dar din firul obiectului #2. Dacă obiectul #1 apelase metoda obiectului #2, intrând într-o
secţiune critică oarecare, atunci la apelarea metodei obiectului #1 acesta se va bloca pe sine însuşi la
intrarea repetată în aceeaşi secţiune critică. Fragmentul de cod care urmează vine să exemplifice această
situaţie.
// Firul #1
void IObject1::Proc1()
{
// Intrăm în secţiunea critică a obiectului #1
m_lockObject.Lock();
// Apelăm metoda obiectului #2, are loc comutarea la firul obiectului #2
m_pObject2->SomeMethod();
// Aici nimerim numai după întoarcerea din m_pObject2->SomeMethod()
m_lockObject.Unlock();
}

// Firul #2
void IObject2::SomeMethod()
{
// Apelăm metoda obiectului #1 din firul obiectului #2
m_pObject1->Proc2();
}

// Firul #2
void IObject1::Proc2()
{
// Încercăm să intrăm în secţiunea critică a obiectului #1
m_lockObject.Lock();
// Aici nu vom ajunge niciodată
m_lockObject.Unlock();
}
Dacă în acest exemplu nu ar fi avut loc comutarea firelor, toate apelurile ar fi avut loc în firul obiectului #1
şi nu am fi avut probleme. Exemple de acest gen stau la baza tehnologiei compartimentului COM
(apartments). Nu sunt recomandate apelurile obiectelor, dacă au avut loc intrări în secţiunile critice.
Primul exemplu din acest subparagraf va fi rescris astfel:
// Firul #1
void Proc1()
{
m_lockObject.Lock();
CComPtr<IObject> pObject(m_pObject); // apelarea pObject->AddRef();
m_lockObject.Unlock();
if (pObject)
pObject->SomeMethod();
}

// Firul #2
void Proc2(IObject *pNewObject)
{
m_lockObject.Lock();
m_pObject = pNewobject;
m_lockObject.Unlock();
}
Accesul la obiect a rămas ca şi mai înainte sincronizat, dar apelul SomeMethod() are loc în afara secţiunii
critice. Situaţia a fost aproape rezolvată. Mai există o problemă mică. Să cercetăm mai atent Proc2():

void Proc2(IObject *pNewObject)


{
m_lockObject.Lock();
if (m_pObject.p)
m_pObject.p->Release();
m_pObject.p = pNewobject;
if (m_pObject.p)
m_pObject.p->AddRef();
m_lockObject.Unlock();
}

Este evident, că apelurile m_pObject.p->AddRef() şi m_pObject.p->Release() au loc în interiorul


secţiunii critice. Şi dacă apelarea metodei AddRef() nu generează, de obicei probleme, apelarea metodei
Release() poate fi ultimul apel al Release() şi obiectul se va autodistruge. În metoda FinalRelease() a
obiectului #2 poate fi orice, de exemplu, eliberarea unor obiecte, care se află în alte compartimente. Dar
aceasta din nou va conduce la comutarea firelor şi poate genera autoblocarea obiectului #1 ca şi în
exemplul de mai sus. Pentru a preîntâmpina aceasta vom folosi aceeaşi tehnică ca şi în Proc1().
// Firul #2
void Proc2(IObject *pNewObject)
{
CComPtr<IObject> pPrevObject;
m_lockObject.Lock();
pPrevObject.Attach(m_pObject.Detach());
m_pObject = pNewobject;
m_lockObject.Unlock();
}
Potenţial, acum ultimul apel IObject2::Release() va fi executat după părăsirea secţiunii critice. Iar
atribuirea unei valori noi este sincronizată ca şi mai înainte cu apelul IObject2::SomeMethod() din firul
#1.
Concluzii:
 secţiunile critice sunt executate relativ repede şi nu cer multe resurse de sistem;
 pentru sincronizarea accesării a mai multor variabile independente este mai bine să fie utilizate
câteva secţiuni critice (nu una pentru toate variabilele);
 codul unei secţiuni critice va fi redus la minimum;
 nu este recomandat să fie apelate metode ale unor obiecte “străine” dintr-o secţiune critică.

1. Administrarea proceselor. Realizarea excluderii mutuale. Specificarea problemei


Mecanismele care realizează excluderea mutuală pentru un set de programe sunt bazate pe un principiu
comun: utilizarea mecanismului de excludere mutuală existent deja la un nivel inferior. Drept rezultat, sunt
utilizate variabile comune ale proceselor concurente, iar coerenţa acestor variabile trebuie ea însăşi să fie
garantată. La nivelul de bază (cel al resurselor fizice) există două mecanisme elementare: excluderea
mutuală la accesarea unui amplasament de memorie şi masca întreruperilor. Aceste două mecanisme sunt,
în principiu, suficiente pentru toate necesităţile. Dar, din considerente de eficacitate, la nivelul resurselor
fizice sau microprogramelor există dispozitive mai sofisticate, cum ar fi instrucţiunea Test and Set sau
semafoarele.

Vom preciza mai întâi problema excluderii mutuale. Fie {p1, p2,...,pn} o mulţime de procese pe care le vom
considera ciclice; programul fiecărui proces conţine o secţiune critică. Excluderea mutuală este asigurată
prin două fragmente de program (prolog şi epilog), care încadrează secţiunea critică a fiecărui proces.
Presupunem, că fiecare proces, care intră în secţiunea critică o părăseşte într-un interval de timp finit.
Soluţia trebuie să posede următoarele proprietăţi:
a) excludere mutuală: la fiecare moment de timp cel mult un proces execută secţiunea critică,
b) absenţa blocajelor intempestive (care nu sunt la timpul lor): dacă în secţiunea critică nu se află vreun
proces, nici un proces nu trebuie să fie blocat de mecanismul excluderii mutuale,
c) toleranţă la defecte: soluţia trebuie să rămână validă şi în cazul unor defecte în unul sau în mai multe
procese, care se află în afara secţiunii critice,
d) absenţa privaţiunilor: un proces, care a cerut intrarea într-o secţiune critică nu trebuie să aştepte un timp
infinit (presupunând, că toate procesele au aceeaşi prioritate),
e) simetrie: prologul şi epilogul trebuie să fie identice pentru toate procesele şi independente de numărul
lor.
Ţinând cont de aceste specificaţii vom construi o soluţie de forma:
<iniţializare> -- comună tuturor proceselor
<programul procesului pi>:
ciclu
<prolog> -- intrare în secţiunea critică
<secţiunea critică>
<epilog> -- ieşire din secţiunea critică
<restul programului>
endciclu
Trebuie să elaborăm fragmentele iniţializare, prolog şi epilog.

1. Excluderea mutuală prin aşteptare active

Înainte de a descrie implementarea excluderii mutuale prin operaţii elementare de blocare şi deblocare a
proceselor prezentăm un mecanism, care permite simularea efectului acestor operaţii, menţinând procesele
în stare activă. Un proces în aşteptare activă simulează blocarea efectuând o testare repetată a condiţiei de
depăşire, care poate fi actualizată de alte procese.

2. Algoritmul lui Dekker

Pentru început considerăm cazul a două procese p0 şi p1. O primă abordare constă în reprezentarea condiţiei
de aşteptare (care este complementară condiţiei de depăşire) printr-o variabilă booleană c; vom avea atunci:
c = “un proces este în secţiunea critică”.
Putem propune următorul program:
iniţializare: c := false;
prolog : test: if c then
go to test
else
c := true
endif;
<secţiunea critică>
epilog : c := false;
La o analiză mai atentă observăm (v.3.2.2.3), că dacă nu vom face nişte ipoteze suplimentare, acest
program nu rezolvă problema pusă. Iată secvenţa de intrare în secţiunea critică, descompusă în instrucţiuni:
procesul p0 procesul p1
1) test0: load R0 c 11) test1: load R1 c
2) br (R0=0) test0 21) br (R1=0) test1
3) stz c 31) stz c
(am reprezentat true prin 0, false prin 1, br este operaţia de salt condiţionat iar stz pune în 0 un
amplasament de memorie). Dacă ordinea de execuţie este 1, 11, 2, 21, 3, 31 vedem că excluderea mutuală
este greşită. Problema provine de la faptul că procesul p1 poate consulta c între momentul când p0 a
consultat c (găsindu-l false) şi momentul în care p0 l-a pus pe c în true. Altfel spus, este necesar ca
secvenţele de acţiuni (1, 2, 3) şi (11, 21, 31) să fie executate în mod atomar. Anume acest principiu stă la
baza instrucţiunii Test and Set.
O soluţie (algoritmul lui Dekker) poate totuşi fi construită fără a folosi alte mecanisme de excludere
mutuală, în afară de indivizibilitatea accesării în citire sau actualizarea unui amplasament de memorie.
Prezentăm algoritmul pentru două procese, deşi el poate fi extins pentru un număr arbitrar de procese.
Programul foloseşte trei variabile comune celor două procese:
var c : array [0..1] of boolean;
tur : 0..1;
iniţializare: c[0]:=c[1]:=false;
tur:=0;
prolog : -- pentru procesul i; se va pune j=1-i (celălalt proces)
c[i]:=true;
tur:=j;
test: if c[j] and tur=j then
go to test
endif;
...
epilog : -- pentru procesul i
c[i]:=false;
Această soluţie, demonstrarea validităţii căreia este propusă ca exerciţiu, prezintă un interes pur teoretic. În
practică sunt utilizate instrucţiuni speciale care asigură indivizibilitatea secvenţelor de testare şi modificare.
Aşteptarea activă poate fi în egală măsură utilizată ca mecanism elementar de sincronizare în cazul unor
alte probleme, diferite de excluderea mutuală.
Exemplul 4.1.Reluăm problema exemplului din 3.3.1 (comunicarea a două procese printr-un segment
comun). Reprezentăm printr-o variabilă booleană c condiţia de aşteptare:
c=”procesul p a terminat scrierea în segmentul a”
Valoarea iniţială c=false. Programele se vor scrie astfel:
procesul p procesul q
... ...
scriere(a); test: if −c then

c:=true go to test
endif;
citire(a)
Putem verifica, că soluţia este corectă: oricare ar fi ordinea de executare a acestor două procese, deblocarea
lui q este, în cel mai rău caz, retardată cu un ciclu de aşteptare activă. Printre altele, această proprietate
rămâne adevărată dacă vom presupune, că mai multe procese q1, q2,,..., qn aşteaptă terminarea operaţiei de
scriere. ◄
Proprietatea, care asigură validitatea schemei de mai sus, constă în faptul că modificarea variabilei c este
efectuată de către un singur proces. Notăm, că această schemă a fost deja întâlnită în capitolul 2 la
administrarea intrărilor-ieşirilor la CDC 6600: un cuplu de procese comunică prin intermediul unei perechi
de indicatori, fiecare dintre care este actualizat de un proces şi consultat de altul. Pentru aceasta este
necesar să se poată executa în excludere mutuală instrucţiunile de testare şi modificare.
1. Aşteptarea activă în sisteme multiprocesorale: Test & Set

Pentru tratarea cu ajutorul aşteptării active a cazului în care mai multe procese actualizează şi consultă
variabile comune, unele maşini au o instrucţiune, care realizează într-o manieră indivizibilă consultarea şi
actualizarea unui amplasament de memorie. Această instrucţiune, adesea numită Test And Set (tas), este
utilizată în sistemele multiprocesorale (în sistemele monoprocesor mascarea întreruperilor este suficientă
pentru asigurarea excluderii mutuale).
Fie m adresa amplasamentului de memorie considerat, sau lacătul, iar R un registru al procesorului. Prin
convenţie, dacă lacătul este în 0, secţiunea critică este liberă, iar dacă este 1 – ea este ocupată. Efectul lui
Test And Set este descris mai jos (Mp[m] desemnează amplasamentul de memorie cu adresa m):
tas R, m : <blocare acces la Mp[m]>
R:=Mp[m]
Mp[m]:=1
<eliberare acces la Mp[m]>
Excluderea mutuală prin aşteptare activă poate fi programată cu ajutorul următoarelor secvenţe:
iniţializare : stz m -- Mp[m]:=0
prolog : tas R, m
br(R≠0) $-1 -- test iterat
epilog : stz m

2. Semaforul – instrument elementar pentru excluderea mutuală. Definiţii

Un semafor s este constituit prin asocierea unui contor cu valori întregi, notat s.c., şi a unui fir de aşteptare,
notat s.f. La crearea semaforului contorului i se atribuie o valoare iniţială s0 (s0≥0), şi firul de aşteptare s.f.
este vid. Un semafor serveşte la blocarea proceselor aşteptând să se producă o condiţie pentru deblocarea
lor; procesele blocate sunt plasate în s.f. Mai multe procese pot fi sincronizate prin semafoare, care aparţin
părţii comune a contextului lor. Un procesor poate fi manipulat doar cu ajutorul a două operaţii P(s) şi V(s),
numite primitive. Valoarea contorului şi starea firului de aşteptare sunt inaccesibile, chiar şi pentru citire.
Fie p un proces care execută P(s) sau V(s), iar q un proces care se află în firul de aşteptare s.f. Algoritmul
primitivelor este următorul:
P(s): V(s):
s.c.:=s.c.-1; s.c.:=s.c.+1;
if s.c.<0 then if s.c.≤0 then
stare(p):=blocat; extragere(q,s.f.);
introducere(p,s.f.) stare(q):=activ
endif endif
Aceste operaţii sunt executate în excludere mutuală. Modalitatea de realizare efectivă a semafoarelor şi a
primitivelor P şi V este descrisă în 4.3. Operaţiile introducere şi extragere permit inserarea unui proces într-
un fir de aşteptare sau, respectiv, extragerea. Nu facem aici nici un fel de ipoteze despre politica de
gestionare a firului de aşteptare: algoritmii de sincronizare trebuie să fie independenţi. Politica aleasă în
cazul alocării unui procesor este discutată în 4.3, la fel şi detaliile legate de operaţiile de blocare şi
deblocare.
Exemplul 4.2.Funcţionarea unui semafor poate fi comparată cu lucrul unui magazin accesul în care este
admis doar dacă la intrare există coşuri libere (sau cărucioare) în care cumpărătorii îşi duc marfa (intrarea
fără coş este interzisă). La deschiderea magazinului un anumit număr de coşuri libere sunt puse la
dispoziţia cumpărătorilor la intrare. Un cumpărător ia un coş liber, intră în magazin şi îşi alege marfa, iar
dacă la intrare nu există un coş liber, cumpărătorul este obligat să aştepte într-un fir de aşteptare (operaţia
P). La ieşire cumpărătorul pune coşul de unde l-a luat (operaţia V). ◄
Doar executarea primitivei P poate bloca un proces, acesta va putea fi deblocat doar de un alt proces, care a
executat primitiva V pe acelaşi semafor. Executarea operaţiei V nu este blocantă.

3. Semaforul – Proprietăţi

Proprietăţile principale ale sincronizării cu ajutorul semafoarelor pot fi deduse din câteva relaţii invariante:
relaţii verificate iniţial şi care rămân neschimbate după executarea primitivelor P şi V un număr arbitrar de
ori.
1) Fie, pentru un semafor s:
np(s) – numărul total de execuţii a operaţiei P(s),
nv(s) – numărul total de execuţii a operaţiei V(s).
Are loc relaţia:
s.c. = s0 – np(s) + nv(s) (1)
deoarece valoarea iniţială a lui s.c. este s0, fiecare operaţie P(s) scade din această valoare o unitate, iar V(s)
adaugă 1.
Aceste operaţii, fiind executate în excludere mutuală, nici o modificare nu este pierdută.
2) Fie nbloc(s) numărul proceselor blocate în s.f. Are loc relaţia:
nbloc(s) = if s.c. ≥ 0 then 0 else –s.c. endif (2)
care poate fi de asemenea scrisă
nbloc(s) = max(0, –s.c.) (21)
Relaţia (2) iniţial este adevărată. Tabelul de mai jos, care indică efectul operaţiilor P(s) şi V(s) asupra
valorii variabilei nbloc, arată că aceste operaţii lasă relaţia (2) invariantă.
s.c.<0 s.c.=0 s.c.>0
Efectul operaţiei P(s) +1 +1 --
Efectul operaţiei V(s) -1 -- --
3) Relaţia (2) poate fi scrisă sub o altă formă, care ne va fi utilă mai departe. Fie nf(s) numărul de “treceri”
de către procese a primitivei P(s), adică suma numărului de executări a lui P(s) fără blocare şi a
numărului de deblocări realizate de către V(s). Vom avea în acest caz:
nbloc(s) = np(s) – nf(s).
Introducând această valoare în (21) obţinem:
- nf(s) = max(-np(s), -s.c.-np(s)), sau
nf(s) = min(np(s), s.c.+np(s)).
În fine, utilizând valoarea lui s.c. din (1), avem:
nf(s) = min(np(s), s.c.+nv(s)). (3)

1. Realizarea excluderii mutuale cu ajutorul semafoarelor

Prezentăm o schemă, care rezolvă problema excluderii mutuale pentru n procese. În cazul în care nu se fac
ipoteze speciale despre gestionarea firului de aşteptare, nu se garantează lipsa privaţiunilor (blocărilor
indefinite).
iniţializare : semafor mutex init 1
prolog : P(mutex),
epilog : V(mutex).
Să ne convingem, că soluţia prezentată este în conformitate cu specificaţiile din 4.1.1. Trebuie să verificăm
proprietăţile a, b şi c au loc.
Fie nc numărul de procese, care se află în secţiunea critică la un moment concret de timp. Avem:
nc = nf(mutex) – nv(mutex) (4)
Proprietăţile în cauză pot fi verificate aplicând semaforului mutex relaţia (3) din 4.1.3.2:
nf(mutex) = min(np(mutex), 1+nv(mutex)) (5)
a) Excluderea mutuală
Din (5) avem:
nf(mutex) ≤ 1+nv(mutex)
şi, utilizând (4), obţinem nc ≤ 1: excluderea mutuală este asigurată.
b) Absenţa blocajelor
Presupunem, că nici un proces nu se află în secţiunea critică. Vom avea în acest caz nc = 0, sau
nf(mutex) = nv(mutex) sau încă
nf(mutex) ≤ 1+nv(mutex)
Vom avea conform relaţiei (5):
nf(mutex) = np(mutex) sau
nbloc(mutex) = 0
Deci, proprietatea b este prezentă, ca şi proprietatea c, deoarece nu s-a făcut nici o ipoteză despre starea
proceselor, dacă ele nu se află în secţiunea lor critică.
1). Secţiuni critice incorporate. Blocaje
Vom considera două procese p şi q pentru care programul conţine două secţiuni critice distincte,
corespunzătoare utilizării a două resurse critice distincte.
procesul p procesul q
... ...
(1) P(mutex1) (11) P(mutex2)
... ...
(2) P(mutex2) (21) P(mutex1)
... ...
V(mutex2) V(mutex1)
... ...
V(mutex1) V(mutex2)
Dacă traiectoria temporală de execuţie a proceselor p şi q începe cu (1, 11, 2, 21), se va ajunge la o situaţie
în care ambele procese sunt blocate pentru un timp infinit, deoarece fiecare dintre procese poate fi deblocat
doar de către celălalt. Această situaţie este numită blocare sau îmbrăţişare fatală (eng. deadlock, fr. étreinte
fatale).
2). Aşteptare infinită în secţiunea critică sau impas
Validitatea soluţiei propuse se bazează pe presupunerea, că toate procesele părăsesc secţiunea critică în
timp finit. Numai ce am stabilit, că această ipoteză poate fi infirmată dacă secţiunile critice se intersectează.
Pot fi şi alte cauze, care conduc la o aşteptare infinită. Astfel, blocarea, incorectitudini sau ciclări infinite
într-un proces, care se află în secţiunea critică, pot paraliza toate procesele concurente cu procesul dat. În
cazul unor secţiuni critice globale (care prezintă interes pentru toţi utilizatorii), realizate pentru un sistem
de operare, pot fi propuse următoarele soluţii:
 oricărui proces, care execută o secţiune critică globală, i se atribuie, pe toată durata acestei execuţii, un
statut special, care îi conferă anumite drepturi particulare: prioritate înaltă, protecţie contra distrugerii,
etc.
 un orologiu de gardă este armat la intrarea unui proces în secţiunea critică; dacă procesul nu părăseşte
secţiunea critică după un interval de timp predefinit, sistemul de operare forţează ieşirea procesului şi
eliberează astfel secţiunea critică. Această soluţie nu este cea mai bună, or datele globale, manipulate în
secţiunea critică, pot să devină incoerente. Este necesar să se ofere posibilitatea restabilirii acestor date
la o stare anterioară, considerată validă, ceea ce implică salvări periodice.
3). Privaţiune
Algoritmul excluderii mutuale garantează intrarea exact a unui proces în secţiunea critică, dacă mai multe
procese încearcă acest lucru, când secţiunea critică este liberă. Se poate întâmpla ca un proces particular să
fie reţinut pentru un interval de timp nedefinit: acest fenomen se numeşte privaţiune, pe care o vom
reîntâlni atunci când vom studia alocarea resurselor. Întradevăr, procesele candidate la intrare, aşteaptă în
firul de aşteptare mutex.f şi ordinea lor de deblocare (prin V(mutex)) depinde de politica de gestionare a
acestui fir, despre care nu s-a făcut nici o ipoteză.
Pentru cazul cel mai frecvent, când firele de aşteptare a semafoarelor sunt gestionate conform ordinii “prim
sosit – prim servit” fără prioritate, riscul de privaţiune este eliminat
Întradevăr, dacă presupunem că execuţia unei secţiuni critice durează totdeauna un interval finit de timp,
orice proces, după o perioadă finită de timp, va deveni cel mai vechi în firul său de aşteptare.

1. Funcţionarea şi structura unui nucleu de sincronizare. Stările unui proces. Fire de aşteptare

Noţiunea de proces şi operaţiile asociate nu fac, de obicei, parte din setul de bază al calculatorului. Ele vor
fi implementate cu ajutorul unor programe şi/sau microprograme, care formează nucleul de administrare a
proceselor. În cadrul descrierii unui sistem de operare cu ajutorul maşinilor abstracte ierarhice (v. cap.9),
nucleul constituie nivelul cel mai inferior, realizat direct pe maşina fizică. Maşina abstractă, realizată astfel
poate fi numită o maşină a proceselor, care posedă, în afara setului de instrucţiuni de bază, primitivele care
permit crearea, distrugerea şi sincronizarea proceselor. Ca şi orice maşină abstractă, maşina realizată în
acest mod ascunde unele proprietăţi ale maşinii fizice. Astfel:
 noţiunea de proces, care este echivalentă cu cea de proces virtual, ascunde utilizatorilor nucleului
mecanismul de alocare a procesoarelor fizice. La un nivel superior nivelului nucleului chiar şi numărul
procesoarelor nu intervine decât doar asupra performanţelor sistemului şi nici într-un fel asupra
structurii sale logice,
 primitivele de sincronizare, realizate de nucleu, ascund mecanismele fizice de comutare a contextului,
de exemplu, cele oferite de întreruperi.
Structura unui nucleu de sincronizare depinde, printre altele, de specificaţiile maşinii fizice (gestiunea
întreruperilor, structura cuvântului de stare, sistem mono- sau multiprocesoral, etc.) şi de specificaţiile
maşinii abstracte care trebuie realizate, îndeosebi de mecanismul de sincronizare ales. Este, totuşi, posibil
de evidenţiat câteva caracteristici comune ale acestei structuri, pe care le vom prezenta înainte de a descrie
o realizare concretă.
Am considerat până acuma că un proces se poate afla în două stări: activ sau blocat. Luarea în consideraţie
a alocării fizice a unui procesor ne impune să descompunem starea activă în două stări noi. Un proces activ
se numeşte ales, dacă el este în curs de execuţie pe un procesor fizic; el se numeşte eligibil dacă nu poate fi
executat din cauza lipsei unui procesor disponibil. Această evidenţiere este legată doar de disponibilitatea
procesorului şi nu are vre-un suport logic. Figura 4.1 descrie stările unui proces şi tranziţiile lui.
Tranziţiile 3 şi 4 (blocare şi deblocare) sunt tranziţiile “interne”, datorate sincronizării proceselor.
Tranziţiile “tehnologice” 1 şi 2 sunt datorate alocării procesoarelor fizice proceselor. În particular, tranziţia
2 se produce atunci când algoritmul de alocare ia procesorul de la un proces, care încă mai are nevoie de el.
Această operaţie este numită retragere (eng. preemption, fr. réquisition) a procesorului.
Administrarea proceselor face apel la fire de aşteptare. Astfel, fiecărei cauze distincte de blocare (semafor,
condiţie într-un monitor, etc.) i se asociază un fir de aşteptare pentru a stabili o ordine a proceselor blocate.
Mai mult, procesele eligibile sunt menţinute într-un fir special de aşteptare, gestionarea căruia permite
implementarea unei politici de alocare a procesoarelor fizice. Dacă presupunem, că viitorul proces ales este
totdeauna primul din firul proceselor eligibile, algoritmul de alocare poate fi definit
 cu ajutorul algoritmului de inserare în firul proceselor eligibile,
 cu ajutorul algoritmului care determină retragerea procesoarelor fizice.
Mulţimea programelor, care realizează aceşti algoritmi se numeşte planificator (eng. scheduler, fr.
ordonnanceur). Programul, care realizează alegerea propriu-zisă se numeşte dispecer (eng. dispatcher, fr.
distributeur). Schema generală a firelor de aşteptare ale proceselor este prezentată în fig.4.2. Deplasarea
proceselor între aceste fire corespunde schimbării stării.

1. Administrarea contextelor şi schemele primitivelor


1) Conţinutul contextului
Operaţia de alocare a procesorului fizic impune păstrarea pentru fiecare proces a unei copii a contextului.
Această copie a contextului descrie starea procesorului pentru procesul considerat. În acest scop fiecărui
proces i se asociază o mulţime de informaţii rezidente în memorie şi numită vector de stare, bloc de
control al procesului sau blocul contextului, care conţine:
 informaţiile despre starea procesorului, necesare la realocarea lui (conţinutul cuvântului de stare,
registrelor),
 valorile atributelor procesului (prioritate, drept de acces),
 pointeri la spaţiul de lucru al procesului (segmentele procedură şi date, stiva de execuţie),
 informaţii de gestiune (starea, legăturile de înlănţuire).
O descriere mai detaliată a blocului contextului unui proces este dată în 4.3.
1) Organizarea nucleului
Execuţia programelor nucleului este declanşată în două moduri (fig.4.3):
 prin apelarea unei primitive de administrare a proceselor (creare, distrugere, sincronizare, etc.); aceste
primitive sunt realizate sub formă de apel al supervizorului,
 printr-o întrerupere: programele de tratare a întreruperilor fac parte din nucleu, deoarece întreruperile
sunt traduse în operaţii de sincronizare şi sunt invizibile la nivelurile superioare.
În ambele cazuri, mecanismul de intrare în nucleu conţine salvarea automată a cuvântului de stare şi,
eventual, a registrelor procesorului, care execută apelul supervizorului sau care tratează întreruperea. În
dependenţă de organizarea calculatorului, aceste informaţii sunt salvate într-un amplasament fix (legat de
nivelul întreruperii sau de apelul supervizorului) sau într-o stivă (îndeosebi în cazul sistemelor
multiprocesorale). Părţile contextului, salvate în acest fel, sunt cele ale proceselor alese pentru procesorul
în cauză, în momentul întreruperii sau apelării supervizorului.
Programele primitivelor şi cele de tratare a întreruperilor manipulează blocurile contextului şi firele
proceselor. Pentru asigurarea coerenţei informaţiilor aceste programe trebuie executate în excludere
mutuală. Executarea unui program al nucleului se termină în toate cazurile prin realocarea procesorului sau
procesoarelor, adică prin apelarea dispecerului. Deoarece este posibil ca firul de aşteptare a proceselor alese
să fi fost modificat de execuţia primitivelor, noul proces ales poate să difere de ultimul proces ales pentru
procesorul întrerupt.

Programul unei primitive a nucleului are următoarea schemă generală:


prolog; -- salvarea contextului şi intrarea în secţiunea critică
control; -- verificarea drepturilor şi a parametrilor
<corpul programului> -- manipulează firele proceselor
alocare_procesor; -- programul dispecer şi ieşirea din secţiunea critică
Secvenţa prolog, comună tuturor operaţiilor, salvează contextul procesului, care execută operaţia, şi asigură
intrarea în secţiunea critică. Secvenţa control verifică drepturile procesului apelant de a executa primitiva şi
validitatea parametrilor transmişi. Detaliile depind de primitivă. Secvenţa alocare_procesor este programul
dispecerului: ea realizează realocarea procesorului şi ieşirea din secţiunea critică.
Diferenţa între sistemele mono- şi multiprocesorale se manifestă în mod esenţial la realizarea excluderii
mutuale şi alocării procesorului (secvenţele prolog şi alocare_procesor).
Tratarea întreruperilor de către nucleu trebuie să fie coordonată cu mecanismele de sincronizare alese. De
obicei sunt considerate două scheme de bază:
1) Asocierea unui proces, care tratează fiecare întrerupere. Doar acest proces se va afla în aşteptarea unei
întreruperi anume, existând pentru aceasta o instrucţiune specială.
2) Asocierea unei operaţii de deblocare (semnalizare asociată unei condiţii, V la un semafor, etc.) la o
întrerupere.
Problemele de mascare şi de prioritate a întreruperilor sunt, de obicei, tratate asociind priorităţi proceselor.

1. Realizarea unui nucleu de sincronizare. Organizarea general intrebarile de mai jos

2. Realizarea unui nucleu de sincronizare. Interfeţele

a) Gestionarea proceselor
Procesele pot fi create şi distruse în mod dinamic, şi sunt organizate ierarhic conform relaţiei de legătură
(v.3.5). Un proces este creat cu ajutorul primitivei:
creare(p, context iniţial, atribute)
Atributele unui proces conţin prioritatea (exprimată printr-un întreg) şi drepturile de a executa anumite
operaţii. Contextul iniţial specifică starea iniţială a cuvântului de stare şi a registrelor procesorului, şi a
spaţiului de lucru asociat procesului (stiva, date proprii). Procesul este creat în starea eligibil. Numărul său
p (numele) este returnat ca rezultat (valoarea nil, dacă crearea este imposibilă). O funcţie
determină_număr_propriu permite unui proces să afle numărul său propriu.
Primitivele care urmează pot fi aplicate unui proces existent şi pot fi executate doar de procesul-părinte.
Procedura distrugere poate fi utilizată de către un proces pentru a se autodistruge. Deci, distrugere(p) va
distruge toate procesele desemnate de procesul p şi toţi descendenţii acestora.
Procedura suspendare(p) întrerupe execuţia unui proces p, plasându-l într-un fir de aşteptare special.
Execuţia lui p poate fi reluată doar cu ajutorul primitivei reluare(p). Primitivele suspendare şi reluare sunt
introduse din considerente de securitate, în special pentru a permite monitorizarea unui proces de către
procesul-părinte.
Utilizarea primitivelor creare, distrugere, suspendare şi reluare este condiţionată de un drept, care
figurează în atributul drepturi al procesului.
b) Sincronizarea
Procesele sunt sincronizate cu ajutorul monitoarelor. Gestiunea întreruperilor este integrată în mecanismul
monitoarelor: o întrerupere este asociată unei condiţii. Specificarea primitivelor, care diferă un pic de cea
din 3.3, este precizată în 4.3.2. Monitoarele sunt declarate în programele proceselor; un monitor este creat
la compilarea programului, unde el este declarat, şi este mai apoi utilizat conform regulilor, definite de
limbajul de programare utilizat (care nu este aici specificat).

1. Realizarea unui nucleu de sincronizare. Structuri şi algoritmi

Din momentul creării sale unui proces i se asociază un număr fix (process handler), care serveşte la
desemnarea lui şi permite accesarea blocului său de context. Blocul de context conţine următoarele
câmpuri:
Csp : zona de salvare a cuvântului de stare a procesorului,
Reg : zona de salvare a registrelor generale ale procesorului,
Stare : valoarea stării procesului (eligibil, blocat, ...),
Prio : prioritatea procesului,
Drepturi : drepturile procesului,
Fire, etc. : legături de înlănţuire în ierarhia proceselor,
Suc, etc. : legături de înlănţuire în firele de aşteptare (FA).
Administrarea proceselor utilizează fire de aşteptare, ordonate în ordinea de descreştere a priorităţilor şi
comandate de un set de proceduri de accesare, specificate mai jos (p specifică un proces, f – un fir de
aşteptare):
introducere(p, f) Introduce p în f, în elementul lui f care corespunde priorităţii procesului şi în ordinea
sosirii pentru procesele cu aceeaşi prioritate.
primul(f) Întoarce numărul (numele) procesului din vârful lui f (nil dacă f este vid); nu
modifică f.
ieşire(p, f) Extrage din firul f primul proces, numărul acestuia fiind pus în p (nil dacă f este vid).
extragere(p, f) Extrage din firul f procesul cu numărul p specificat, oricare ar fi elementul în
care acesta se află; pune în p valoare nil, dacă procesul nu există în firul f.
vid(f) Funcţie booleană cu valoarea adevărat, dacă firul f este vid, fals în caz contrar.
Figura 4.4 prezintă schematic organizarea unui fir de aşteptare a proceselor.

2. Realizarea unui nucleu de sincronizare. Realizarea monitoarelor.

Monitorul, descris mai jos, este inspirat de realizarea folosită în limbajul Mesa [12], care a fost validată de
o vastă experienţă de utilizare. În raport cu noţiunea clasică de monitor (3.3.3), această realizare prezintă
următoarele diferenţe:
1) Semantica primitivei semnalizare. Specificarea iniţială a primitivei c.semnalizare precizează că unul
din procesele care sunt în aşteptarea condiţiei c (dacă există) este imediat deblocat, ceea ce implică
trecerea temporară în starea blocat a procesului care execută semnalizare. În specificarea prezentă
procesul deblocat este simplu trecut în starea eligibil şi trebuie să intre în monitor; el se află, deci, în
competiţie cu alte procese, care aşteaptă să intre în monitor, şi nu este garantat că va fi imediat ales.
Condiţia, care provocase deblocarea, poate fi modificată înainte ca procesul deblocat să-şi reia execuţia
în monitor. Pentru această nouă interpretare trebuie să fie modificată forma punerii în aşteptare. O
construcţie de forma
if continuare_non_posibilă then
c.aşteptare
endif
devine în acest caz
while continuare_non_posibilă do
c.aşteptare
endwhile
Deşi această construcţie introduce un risc de privaţiune, ea prezintă o serie de avantaje de ordin practic. Ea
evită o comutare a contextului, cu preţul unei evaluări suplimentare a condiţiei. Dar principalul este că ea
permite definirea simplă a unor posibilităţi suplimentare utile (deblocare multiplă, întârzieri de control sau
de gardă), precizate mai jos. În fine, trebuie să notăm, că verificarea validităţii monitorului este
simplificată, deoarece condiţia de depăşire (continuare_posibilă) este consultată în timpul deblocării:
procesul care execută semnalizare poate să se mulţumească cu garantarea unei condiţii mai slabe decât
condiţia de depăşire.
2) Deblocare multiplă. Problema deblocării multiple poate fi rezolvată uşor introducând o primitivă nouă
c.difuzare_semnal efectul căreia se exprimă astfel:
while c.non_vid do
c.semnalizare
endwhile
Fiind dat, că toate procesele deblocate vor testa din nou condiţia şi cer din nou acces la monitor, această
primitivă va avea evident efectul aşteptat.
3) Întârziere de gardă. Din probleme de securitate, în special pentru tratarea blocajelor, poate fi util să se
asocieze o întârziere de gardă condiţiei unui monitor. Această întârziere este egală cu durata maximă de
blocare a unui proces într-un fir de aşteptare asociat condiţiei date. La expirarea întârzierii de gardă va
fi efectuată o tratare specificată. Această tratare poate consta în simpla deblocare a procesului (care va
testa din nou condiţia de depăşire) sau transferul său într-un fir de aşteptare special (v.4.3.2.3).
Fie M.c.întârziere întârzierea de gardă asociată unei condiţii c în monitorul M. Se presupune disponibil un
ceas habs, care pune la dispoziţie timpul absolut. Trebuie să adăugăm în programul primitivei c.semnalizare
instrucţiunea următoare:
hdeblocare[p]:=habs+M.c.întârziere
unde hdeblocare[p] este un câmp nou al blocului de context al procesului p. Un proces, numit “gardian”,
deblocat la intervale regulate de timp parcurge mulţimea contextelor şi efectuează tratamentul specificat
proceselor pentru care hdeblocare[p]>habs.

1. Realizarea unui nucleu de sincronizare. Algoritmi de bază

Programul monitorului trebuie să asigure două funcţii:


 excluderea mutuală pentru procedurile monitorului,
 blocarea şi deblocarea asociate primitivelor aşteptare şi semnalizare.
Fiecărui monitor M îi sunt asociate următoarele structuri de date:
 un dispozitiv de excludere mutuală M.disp (lacăt), care poate lua două valori liber şi ocupat, şi un fir de
aşteptare M.fir asociat acestui dispozitiv. Iniţial M.disp=liber, M.fir=<vid>.
 fiecărei condiţii c de M îi este asociat un fir M.c.fir, un contor de gardă M.c.întârziere şi, pentru
condiţiile asociate unei întreruperi, un indicator boolean M.c.într_sosită.
Firul proceselor eligibile este determinat de f_eligibil.
Pentru un monitor M vom cerceta programul a patru secvenţe intrare, ieşire, c.aşteptare şi c.semnalizare
(secvenţele intrare şi ieşire sunt inserate de compilator şi încadrează execuţia procedurilor externe ale
monitorului). Să definim mai întâi procedurile de gestiune a dispozitivului:
cerere_disp(M, p): eliberare_disp(M):
if M.disp=ocupat then if vid(M.fir) then
intrare(p, M.fir); M.disp:=liber
stare[p]:=blocat else
else ieşire(q, M.fir);
M.disp := ocupat; intrare(q, f_eligibil);
intrare(p, f_eligibil); stare[q]:=eligibil
stare[p]:=eligibil endif
endif
Cele patru secvenţe se vor scrie utilizând următoarele proceduri:
intrare(M): ieşire(M):
prolog; prolog;
p:=<proces apelant>; p:=<proces apelant>;
cerere_disp(M, p); eliberare_disp(M);
alocare_procesor; intrare(p, f_eligibil);
alocare_procesor;
c.aşteptare: c.semnalizare:
prolog; prolog;
p:=<proces apelant>; p:=<proces apelant>;
intrare(p, M.c.fir); if non_vid(M.c.fir) then
stare[p]:=blocat; ieşire(q, M.c.fir);
eliberare_disp(M); cerere_disp(M, p);
alocare_procesor; cerere_disp(M, q);
eliberare_disp(M)
else
intrare(p, f_eligibil)
endif
alocare_procesor;
Să ne amintim, că secvenţa prolog asigură salvarea contextului şi intrarea în secţiunea critică, iar secvenţa
alocare_procesor asigură alocarea procesorului şi părăsirea secţiunii critice (v.4.3.4).
Notăm, că în primitiva semnalizare, procesul apelant p şi procesul deblocat q sunt introduse (cu ajutorul
primitivei cerere_disp) în firul de aşteptare pentru a intra în monitor. Procesul activat prin intermediul
primitivei este primul proces din acest fir. Nu am încercat să reducem numărul transferurilor între fire
pentru a realiza o implementare optimală.

1. Realizarea unui nucleu de sincronizare. Tratarea întreruperilor


Pentru asigurarea uniformităţii mecanismelor de sincronizare fiecărei întreruperi i se asociază:
 o condiţie într-un monitor,
 un proces ciclic care realizează tratarea întreruperilor, în stare de repaus acest proces este în aşteptarea
condiţiei.
O condiţie poate fi asociată unui singur nivel de întrerupere. Sosirea unei întreruperi provoacă executarea
funcţiei semnalizare pentru condiţia asociată. Prioritatea relativă a întreruperilor este tradusă în prioritatea
proceselor, care tratează întreruperile.
Mecanismul descris mai sus nu este absolut perfect. De exemplu, excluderea procedurilor monitorului nu
poate fi aplicată întreruperilor. Se poate întâmpla ca o întrerupere să fie cerută atunci când procesul, care
tratează întreruperile, este încă activ, din care cauză întreruperea va fi pierdută. Evitarea acestui fenomen se
va face cu ajutorul unui indicator boolean, care memorizează sosirea unei întreruperi. Vom avea:
<proces de prelucrare a întreruperii>
ciclu
test if nonM.c.într_sosită then
c.aşteptare; -- evitarea pierderii unei întreruperi
go to test
endif;
<tratarea întreruperii>
endciclu
<sosirea unei întreruperi asociate lui M.c>
M.c.într_sosită := true;
c.semnalizare;

1. Realizarea unui nucleu de sincronizare. Tratarea erorilor

Principiul de tratare a erorilor constă în blocarea procesului care a provocat eroarea şi expedierea unui
mesaj procesului părinte, care va putea lua măsurile necesare (corectarea erorii şi relansarea sau distrugerea
procesului, care a generat eroare). Pentru aceasta este folosit un fir special f_eroare (în conformitate cu
organizarea sistemului, poate fi prevăzut un fir unic sau un fir pentru fiecare utilizator, pentru fiecare
subsistem, etc.).
Presupunem că o eroare care are loc în cursul execuţiei unui proces provoacă o deviere, tratarea căreia se
va scrie astfel:
prolog;
p:=<proces apelant>;
intrare(p, f_eroare);
<tratare specifică>;
stare[p]:=suspendat;

alocare_procesor;
Am definit o stare nouă (“suspendat”), care se aplică unui proces activitatea căruia a fost întreruptă de un
eveniment, considerat anormal (eroare de execuţie sau acţiunea primitivei suspendare, v.4.3.3).
Nu detaliem aici <tratare specifică>, care trebuie să fie specificat de către procesul părinte la momentul
creării procesului descendent. Acest program conţine, evident, codul de diagnosticare (identitatea
procesului generator de eroare, natura erorii), care trebuie transmis procesului părinte într-un mod special,
conform gradului de urgenţă (actualizarea unui indicator, deblocare, etc.).

2. Operaţii asupra proceselor. Crearea şi distrugerea proceselor. Suspendarea şi reluarea


Problema principală, condiţionată de gestiunea dinamică a proceselor, este alocarea contextelor şi numelor
proceselor. Pentru aceasta sunt utilizate două metode principale:
 pentru blocurile contextelor sunt rezervate un număr fix de amplasamente; amplasamentele neutilizate
sunt determinate de o valoare specială (nil) a câmpului stare al lor; fiecare bloc este desemnat printr-un
număr, care este numărul utilizat pentru desemnarea procesului asociat;
 amplasamentele rezervate blocurilor de context sunt alocate dinamic în memorie; numerele sunt alocate
proceselor de asemenea în mod dinamic şi un tabel de corespondenţă, asociază numărului fiecărui
proces adresa în memorie a blocului său de context.
În ambele cazuri vom presupune disponibilă o procedură alocare_context(p), care realizează alocarea
contextului (blocul de context şi spaţiul de lucru) şi întoarce ca rezultat un număr p al procesului (nil, dacă
crearea este imposibilă, de exemplu, din cauza lipsei de spaţiu suficient în memorie). Metodele de alocare a
spaţiului de lucru nu sunt precizate aici. Numărul procesului creat este întors drept rezultat al primitivei:
creare(p, context iniţial):
prolog;
control; -- verificarea drepturilor
alocare_context(p);
if p  nil then
iniţializare_context(i);
intrare(p, f_eligibil)
endif;
intrare(proces apelant, f_eligibil);
alocare_procesor; -- este întors p drept rezultat
Contextul iniţial este specificat de către procesul creator: el trebuie să definească valoarea iniţială a
registrelor şi a cuvântului de stare a procesului creat, starea iniţială a spaţiului de lucru, atributele, cum ar fi
prioritatea şi drepturile. Unele câmpuri ale cuvântului de stare sunt predefinite şi nu pot fi modificate
(modul, mascarea întreruperilor, etc.). Pentru elementele legate de protecţie (drepturile de acces), procesul
creat nu poate avea drepturi superioare drepturilor procesului creator; în caz contrar atributele de protecţie
sunt declarate defecte.
Distrugerea unui proces trebuie să implice eliberarea resurselor, care îi fuseseră alocate. Printre aceste
resurse, doar numele şi contextul sunt gestionate direct de nucleu; celelalte resurse, cum ar fi fişierele, sunt
preluate de mecanisme specifice.
Distrugerea unui proces, care se află în secţiunea critică poate conduce la o blocare. Secţiunile critice ale
monitoarelor sunt gestionate direct de nucleu. Este posibil să se asocieze unui proces numărul
dispozitivului de blocare, care se află în posesia procesului dat (el poate fi angajat în mai multe apeluri
incorporate), şi să diferenţiem distrugerea procesului până când valoarea acestui număr nu va fi 0. O altă
soluţie constă în examinarea periodică a fiecărui dispozitiv de blocare şi să eliberăm dispozitivul de
blocare, dacă procesul care îl posedă a fost distrus.
Principiul primitivei distrugere este dat în schema de mai jos:
distrugere (p):
prolog;
control; -- verificarea drepturilor
eliberare_context(p);
intrare(proces apelant, f_eligibil);
alocare_procesor;
Procedura eliberare_context trebuie să asigure eliberarea resurselor ocupate de procesul distrus şi de
descendenţii acestuia:
eliberare_context(p):
listă:=<lista firelor procesului p>;
restituire_bloc_context(p);
restituire_memorie(p);
for q listă do
eliberare_context(q)
endfor;

Primitiva suspendare
Primitiva suspendare permite procesului-părinte să controleze activitatea unui proces descendent,
întrerupând în mod forţat execuţia acestuia. O utilizare curentă este suspendarea unui proces, angajat într-o
buclă infinită. Procesul întrerupt în acest mod este transferat într-un fir de aşteptare special, care poate fi
firul f_eroare, utilizat pentru devieri.
Efectul primitivei suspendare poate fi ca şi al unei devieri şi programul de tratare poate fi analogic.
Suspendarea unui proces pune o problemă analogică celei de distrugere, dacă procesul se află în secţiunea
critică într-un monitor.
suspendare(p):
prolog;
control;
< tratare secţiune critică>;
f:=<fir care conţine p>;
extragere(p, f);
intrare(p, f_eroare);
stare[p]:=suspendat;
intrare(proces apelant, f_eligibil);
alocare_procesor;
Primitiva reluare permite unui proces să deblocheze un fir suspendat, după modificarea eventuală a
contextului său.
reluare(p):
prolog;
control;
extragere(p, f_eroare);
stare[p]:=eligibil;
intrare(proces apelant, f_eligibil);
intrare(p, f_eligibil);
alocare_procesor;

1. Excluderea mutuală şi alocarea procesorului intrebarile de mai jos

2. Realizarea pentru cazul monoprocesor

În acest caz excluderea mutuală este realizată prin mascarea întreruperilor. Pentru aceasta trebuie pregătită
masca întreruperii în cuvântul de stare, care ar specifica programele asociate primitivelor de tratare a
întreruperilor. Dacă notăm prin proces_ales o variabilă globală, care conţine numărul procesului ales, iar
prin salv_csp locaţiunea în care a fost salvat cuvântul de stare a procesorului la apelarea supervizorului sau
la întrerupere, prologul va fi de forma:
prolog:
<mascarea întreruperilor> -- masca în cuvântul de stare
csp[proces_ales] := salv_csp;
salv_registre(Reg[proc_al]);
Programul dispecerului, care de asemenea realizează ieşirea din secţiunea critică, are grijă să aloce
procesorul primului proces din firul de procese eligibile. Pentru simplificarea manipulării acestui fir este
binevenit să fie introdus aici un proces special cu prioritate joasă, care rămâne tot timpul în coada firului şi
nu poate fi blocat. Acest proces, care poate fi ales doar atunci când el este unicul eligibil, execută o
activitate de fond, care nu este urgentă sau o simplă buclă de aşteptare. El garantează, deci, că firul
proceselor eligibile nu este niciodată vid.
Programul dispecerului este de forma:
alocare_procesor:
ieşire(proces_ales, f_eligibil);
încărcare_registre(Reg[proc_al]);
încărcare_csp(csp[proces_ales]);
Figura 4.5 ilustrează principiul de funcţionare a nucleului, exemplificând efectul global al unei realocări a
procesorului după blocarea procesului ales.

3. Realizarea pentru cazul unui sistem multiprocesoral

Descrierea, care urmează este inspirată din [13]. Vom specifica mai întâi organizarea fizică a sistemului.
Avem n procesoare identice, care accesează o memorie comună. Fiecare procesor, desemnat printr-un
număr (de la 0 la n-1), îşi cunoaşte numărul propriu. Regimului de funcţionare multiprocesor îi sunt
specifice două instrucţiuni:
Test and Set(R, m) : asigură excluderea mutuală (4.1.1.2)
Întrerupere(k) : provoacă o întrerupere a procesorului k.
O întrerupere sau un apel de supervizor provoacă introducerea cuvântului de stare şi a registrelor
procesorului în cauză într-o stivă în memorie, proprie acestui procesor.
Orice primitivă a nucleului poate fi executată de oricare din procesoare. Vom împărţi întreruperile în
întreruperi, destinate unui procesor specific (ceasul procesorului, instrucţia Întrerupere(k)) şi întreruperi
banalizate (intrări-ieşiri, de exemplu), care sunt tratate de orice procesor. În acest caz pentru a fi retras se va
alege procesorul, care execută procesul cu cea mai mică prioritate. Numărul acestui proces se află într-un
amplasament rezervat de memorie kmin şi vom considera, că un dispozitiv fizic îndreaptă întreruperile
banalizate spre procesorul cu numărul kmin. Vom utiliza acelaşi principiu de alegere şi în cazul unei alocări a
procesoarelor.
Pentru ca firul proceselor eligibile să nu fie vid vom adăuga aici n procese de prioritate joasă. Numărul
procesului ales pentru a fi executat de procesorul k vom nota proc_al[k].
Excluderea mutuală a primitivelor nucleului utilizează un dispozitiv de blocare, consultat şi actualizat cu
ajutorul instrucţiunii Test And Set.
prolog: -- procesorul k
<mascare întreruperi>;
Csp[proc_al[k]]:=top(stivă_csp);
Reg[proc_al[k]]:=top(stivă_reg);
test: Test And Set(R, dispozitiv de blocare);
if R ≠ 0 then
go to test -- aşteptare activă
endif
Dispecerul va asigura ca cele n procese alese să fie la orice moment de timp cu prioritatea cea mai înaltă
printre procesele eligibile. Deoarece executarea primitivei curente poate modifica firul proceselor eligibile,
prioritatea primului proces eligibil este comparată cu prioritatea procesului ales pentru procesorul kmin.
Dacă aceasta este superioară procesorul dat este retras cu ajutorul instrucţiunii Întrerupere.
alocare_procesor: -- pentru procesorul cu numărul k
ieşire(proces_ales[k], f_eligibil);
pr1:=Prio[primul(f_eligibil)];
pr2:=Prio[proc_al[kmin]];
if Prio[proc_al[k]] < pr2 then -- actualizarea lui kmin
kmin:=k
endif;
disp_de_blocare:=0; -- terminarea secţiunii critice
if pr1 > pr2 then
Întrerupere(kmin) -- retragere
endif;
încărcare_registre(Reg[proc_al[k]];
încărcare_csp(Csp[proc_al[k]]; -- demascarea întreruperilor
Tratarea întreruperii de retragere (instrucţiunea Întrerupere) a procesorului se reduce la realocarea
procesorului întrerupt:
<tratarea întreruperii de retragere a procesorului k>
prolog;
intrare(proc_al[k], f_eligibil);
alocare_procesor;

4. Procese şi fire în Linux. Intrebarea de mai jos

5. Crearea şi distrugerea proceselor. Demoni în Linux. Obţinerea informaţiilor despre procese

În Linux procesele „se înmulţesc” prin clonare: apelul de sistem, care crează un proces nou, se numeşte
clone, iar procesul fiu este o copie aproape exactă a procesului părinte, doar că mai departe va executa
codul său, iar procesul părinte – ceea ce este scris după apelarea lui clone. Diferenţele pot deveni foarte
mari şi dacă dorim să evităm diferenţierea, apelarea lui clone permite să definim următorii indicatori
(flags), care vor specifica momentele comune ale fiului şi părintelui:
 Spaţiul de adrese (Clone_VM);
 Informaţiile despre sistemul de fişiere (Clone_FS);
 Tabelul fişierelor deschise (Clone_FILES);
 Tabelul programelor de tratare a semnalelor (Clone_SIGHAND);
 Părintele (Clone_PARENT) – în acest caz, evident, va fi creat un proces – frate.
Firele sunt realizate în biblioteca standard de susţinere a programelor cu mai multe fire ca şi procesele,
generate cu indicatorul Clone_VM, şi, din punctul de vedere al nucleului sistemului, nu se deosebesc de
alte procese. Însă în unele biblioteci de alternativă pot exista diferenţe.
Mai există fire, numite „handicapate”, generate de funcţia kernel_thread pentru necesităţi interne ale
sistemului. Acestea nu au parametri pentru linia de comandă, de obicei nu au fişiere deschise, etc. Deoarece
aceste fire (procese) de asemenea figurează în lista lucrărilor, adesea în literatură poate fi întâlnită noţiunea
de proces propriu-zis, creat din „spaţiul utilizatorului” (userspace), şi noţiunea de lucrare, prin aceasta
înţelegându-se toate procesele, inclusiv procesele interne ale nucleului.
Procesele sunt create prin utilizarea funcţiilor din familia exec ale bibliotecii Linux standard: execl,
execlp, execle, execv, execve, execvp. Deşi formatul de apelare este diferit, în ultimă instanţă execută
acelaşi lucru: înlocuiesc codul din procesul curent cu codul, care se află în fişierul indicat. Fişierul poate fi
un fişier binar executabil Linux, un script al interpretorului limbajului de comandă, un fişier binar de un alt
format (de exemplu, o clasă java, un fişier executabil DOS). În ultimul caz modalitatea de prelucrare va fi
determinată de modulul de adaptare a nucleului binfmt_misc. Din această cauză, operaţia de lansare a unui
program, care în DOS şi Windows formează un tot întreg, în Linux (şi în Unix, în general) este împărţită în
două: mai întâi are loc lansarea propriu-zisă, iar apoi se va determina care program va fi executat. Are oare
aceasta sens şi care sunt cheltuielile suplimentare? Doar crearea copiei unui proces presupune copierea
unui volum semnificativ de informaţii!
În mod sigur putem afirma că are sens această abordare. Foarte frecvent un program trebuie să
îndeplinească unele acţiuni înainte de începerea propriu-zisă a execuţiei lui. De exemplu, lansăm două
programe, care-şi transmit reciproc date prin intermediul unui canal fără nume. Astfel de canale sunt create
prin apelarea de sistem pipe, care returnează o pereche de descriptori de fişiere de care pot fi legate, de
exemplu, fluxul standard de intrare (stdin) al unui program şi de ieşire (stdout) al celuilalt program.
Aceasta este posibil deoarece mai întâi sunt create procesele, apoi vor fi executate manipulările necesare cu
descriptorii fişierelor şi doar după toate acestea este apelată funcţia exec.
Aceleaşi rezultate pot fi obţinute în Windows NT într-un singur pas, dar într-un mod mult mai complicat.
Cât despre cheltuielile suplimentare ele sunt în majoritatea cazurilor minime, deoarece dacă este creată
copia unui proces datele proprii ale acestuia nu sunt fizic copiate. Şi aceasta din cauza că este utilizat
procedeul copy-on-write: paginile datelor ambelor procese sunt marcate într-un anumit mod şi doar atunci
când un proces va încerca să modifice conţinutul uneia din paginile sale aceasta va fi duplicată.
Primul proces este lansat în sistem la iniţializarea nucleului. Fragmentul de cod de mai jos este partea de
încheiere a procedurii de iniţializare a nucleului sistemului de operare Linux:
if (execute_command)
execve(execute_command,argv_init, envp_init);
execve("/sbin/init",argv_init,envp_init);
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
execve("/bin/sh",argv_init,envp_init);
panic("No init found. Try passing init= option to kernel.");}
Este făcută încercarea de comutare a procesului la fişierul, indicat în linia de comandă a nucleului, apoi la
fişierele
/sbin/init, /etc/init, /bin/init şi, în sfârşit, la fişierele din /bin/sh.
Distrugerea proceselor
La terminarea execuţieia unui proces (normal, forţat sau accidental), el este distrus eliberând toate
resursele, care fusese alocate anterior. Dacă procesul părinte se termină înaintea procesului descendent,
ultimul devine “orfan” (orphaned process). Toţi “orfanii” sunt “înfiaţi” în mod automat de programul init,
executat de procesul cu numărul 1, care duce evidenţa terminării execuţiei lor.
Dacă a fost terminată deja execuţia procesului descendent, iar procesul părinte nu este gata să recepţioneze
de la sistem semnalul despre acest eveniment, descendentul nu dispare total, ci este transformat în Zombie;
în câmpul Stat aceste procese sunt notate cu litera Z. Procesele Zombi nu cer timp de procesor, dar în
tabelul proceselor este păstrată linia lor şi structurile respective ale nucleului nu sunt eliberate. După
terminarea execuţiei procesului părinte, procesul Zombi orfan devine pentru o perioadă scurtă de timp
descendentul lui init, ca mai apoi să “moară” definitiv.
Un process poate să “cadă în hibernare”, fără a putea fi scos din această stare: în câmpul Stat acest
eveniment se va nota prin litera D. Procesele aflate în hibernare nu reacţionează la cererile de sistem şi pot
fi distruse doar prin reîncărcarea sistemului.
“Demoni” în Linux
Demon (daemon) în Linux este numit procesul predestinat să lucreze în regim de fond fără terminal şi
care execută anumite operaţii pentru alte procese (nu obligator pe calculatorul Dumneavoastră). De obicei,
demonii îşi îndeplinesc în linişte lucrul şi ne amintim de ei doar în cazul unor situaţii ieşite din comun:
spaţiu insuficient – demonul singur informând utilizatorul despre aceasta, sau refuz să lucreze şi sunteţi
întrebat de şef când se vor termina problemele cu imprimantă .
Pentru multe calculatoare demonii, care servesc procesele altor calculatoare, sunt rar utilizaţi din care cauză
nu trebuiesc păstraţi constant în memorie cu cheltuieli neraţionale ale resurselor sistemului. Pentru
coordonarea lucrului acestora a fost creat un superdemon – inetd (Internet daemon).
În fişierul de configurare inetd (/etc/inetd.conf) este indicat care demon accesează un serviciu anume de
Internet. De obicei, cu ajutorul lui inetd sunt apelate programele pop3d, imap4d, ftpd, telnetd (exerciţiu -
determinaţi serviciul pus la dispoziţie), etc. Aceste programe nu sunt în mod constant active, în rezultat, ele
nu pot fi considerate demoni în adevăratul sens al cuvântului, dar, deoarece ele sunt create de un demon
adevărat, sunt numite demoni.

Pentru obţinerea informaţiilor despre procese, vizualizate de programele ps şi top, Linux-ul utilizează
un sistem special de fişiere, numit procfs. În majoritatea distributivelor el este iniţializat la lansarea
sistemului de operare cu titlul de catalog /proc. Datele despre procesul cu numărul 1 (de obicei /sbin/init)
se află în subcatalogul /proc/1, despre procesul cu numărul 182 - în /proc/182, etc. Toate fişierele,
deschise de un proces, sunt reprezentate sub forma unor referinţe simbolice în catalogul /proc/<pid>/fd, iar
referinţa la catalogul rădăcină este păstrată ca /proc/<pid>/root.
Sistemului de gestiune a fişierelor procfs îi sunt asociate şi alte funcţii. De exemplu, cu ajutorul comenzii
echo 100000>/proc/sys/fs/file-max un superuser poate indica, că se permite deschiderea unui număr de
până la 100000 de fişiere, iar comanda echo 0>/proc/sys/kernel/cap-bound va retrage proceselor din
sistem toate drepturile suplimentare, adică va priva sistemul de noţiunea superuser.
Informaţii utile pune la dispoziţie programul lsof. Acesta returnează lista tuturor fişierelor, utilizate la
momentul curent de către procese, inclusiv cataloagele folosite de către unele procese în calitate de catalog
curent sau catalog rădăcină, bibliotecile dinamice, încărcate în memorie, etc.

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

  • Examen Somipp
    Examen Somipp
    Document65 pagini
    Examen Somipp
    Ana Toma
    100% (1)
  • SOMIPPRaspunsuri
    SOMIPPRaspunsuri
    Document82 pagini
    SOMIPPRaspunsuri
    Urmanschi Mihail
    Încă nu există evaluări
  • Lab7 AMOO
    Lab7 AMOO
    Document11 pagini
    Lab7 AMOO
    Сергей Борта
    Încă nu există evaluări
  • Somipp Linux 4 UTM
    Somipp Linux 4 UTM
    Document4 pagini
    Somipp Linux 4 UTM
    Cristi Poselețchi
    Încă nu există evaluări
  • PS TS
    PS TS
    Document14 pagini
    PS TS
    Victor Turculet
    Încă nu există evaluări
  • Lab6 AMOO - Diagrame de Stari Si Activitati
    Lab6 AMOO - Diagrame de Stari Si Activitati
    Document10 pagini
    Lab6 AMOO - Diagrame de Stari Si Activitati
    Dan
    Încă nu există evaluări
  • Lab 3 AMOO
    Lab 3 AMOO
    Document9 pagini
    Lab 3 AMOO
    Gheorghe Felicia
    Încă nu există evaluări
  • Lab 1 Tmps
    Lab 1 Tmps
    Document5 pagini
    Lab 1 Tmps
    Victor Turculet
    Încă nu există evaluări
  • Laboratorul 1 TIDPP
    Laboratorul 1 TIDPP
    Document11 pagini
    Laboratorul 1 TIDPP
    Ion Popescu
    0% (1)
  • Examen TIDPP
    Examen TIDPP
    Document3 pagini
    Examen TIDPP
    Rosca Doinita
    Încă nu există evaluări
  • Lab. 3
    Lab. 3
    Document5 pagini
    Lab. 3
    Cristina Florea
    Încă nu există evaluări
  • Examen PW
    Examen PW
    Document71 pagini
    Examen PW
    DorinRotaru
    Încă nu există evaluări
  • Amo 5
    Amo 5
    Document8 pagini
    Amo 5
    Fil Gorea
    Încă nu există evaluări
  • Proiect de Curs AMOO Druta
    Proiect de Curs AMOO Druta
    Document48 pagini
    Proiect de Curs AMOO Druta
    nn nnn
    Încă nu există evaluări
  • Lab4 AMOO
    Lab4 AMOO
    Document6 pagini
    Lab4 AMOO
    Mihai Ciubotaru
    100% (1)
  • SOMIPP
     SOMIPP
    Document6 pagini
    SOMIPP
    Damean Alexandra
    Încă nu există evaluări
  • Amoo 2
    Amoo 2
    Document8 pagini
    Amoo 2
    AlionaCrigan
    Încă nu există evaluări
  • A1
    A1
    Document11 pagini
    A1
    Amarfii Sergiu
    Încă nu există evaluări
  • Lab 7
    Lab 7
    Document5 pagini
    Lab 7
    danielploaia
    Încă nu există evaluări
  • SOMIPP Lab6
    SOMIPP Lab6
    Document4 pagini
    SOMIPP Lab6
    Dan
    Încă nu există evaluări
  • Somipp Lab4
    Somipp Lab4
    Document3 pagini
    Somipp Lab4
    Raducan Alina
    Încă nu există evaluări
  • Lab 1 Somipp
    Lab 1 Somipp
    Document5 pagini
    Lab 1 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • SOMIPP7
    SOMIPP7
    Document3 pagini
    SOMIPP7
    Damean Alexandra
    Încă nu există evaluări
  • SOMIPP Lab5
    SOMIPP Lab5
    Document4 pagini
    SOMIPP Lab5
    X3 KTO
    Încă nu există evaluări
  • Examen IOC Chirtoacă Maxim
    Examen IOC Chirtoacă Maxim
    Document3 pagini
    Examen IOC Chirtoacă Maxim
    Maxim Chirtoacă
    Încă nu există evaluări
  • Git Laborator 1
    Git Laborator 1
    Document1 pagină
    Git Laborator 1
    Daniil
    Încă nu există evaluări
  • Lab.6 FC
    Lab.6 FC
    Document3 pagini
    Lab.6 FC
    Cristina Florea
    Încă nu există evaluări
  • Laborator 1 Prelucrarea Semnalelor
    Laborator 1 Prelucrarea Semnalelor
    Document19 pagini
    Laborator 1 Prelucrarea Semnalelor
    Ion Cornea
    Încă nu există evaluări
  • Pam Examen
    Pam Examen
    Document7 pagini
    Pam Examen
    Daniil
    Încă nu există evaluări
  • Laborator NR.2
    Laborator NR.2
    Document9 pagini
    Laborator NR.2
    Daniil
    Încă nu există evaluări
  • Lab 7 Somipp
    Lab 7 Somipp
    Document5 pagini
    Lab 7 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • Somipp SOMIPP5
    Somipp SOMIPP5
    Document6 pagini
    Somipp SOMIPP5
    Damean Alexandra
    Încă nu există evaluări
  • AMOO Lab 3
    AMOO Lab 3
    Document7 pagini
    AMOO Lab 3
    Anya Mr
    Încă nu există evaluări
  • Lab 2
    Lab 2
    Document4 pagini
    Lab 2
    Valeria Ţînţaru
    Încă nu există evaluări
  • Pam PDF
    Pam PDF
    Document4 pagini
    Pam PDF
    Andreas Petrakis Houloutsas
    Încă nu există evaluări
  • PAm Examen-1
    PAm Examen-1
    Document12 pagini
    PAm Examen-1
    Eric Semeniuc
    Încă nu există evaluări
  • PAM Lab 1
    PAM Lab 1
    Document6 pagini
    PAM Lab 1
    Vladislav Crivenco
    Încă nu există evaluări
  • Lab3 TSA
    Lab3 TSA
    Document7 pagini
    Lab3 TSA
    Renat Rusu
    Încă nu există evaluări
  • Lucrare de Curs LFPC
    Lucrare de Curs LFPC
    Document17 pagini
    Lucrare de Curs LFPC
    mikeylino
    Încă nu există evaluări
  • Pad Examen
    Pad Examen
    Document7 pagini
    Pad Examen
    Varvara Ciorba
    Încă nu există evaluări
  • Laborator-1 TMPS Borta
    Laborator-1 TMPS Borta
    Document10 pagini
    Laborator-1 TMPS Borta
    Сергей Борта
    Încă nu există evaluări
  • Lab 1 BD
    Lab 1 BD
    Document7 pagini
    Lab 1 BD
    Augusta Bucataru
    Încă nu există evaluări
  • SOMIPP Lab 5
    SOMIPP Lab 5
    Document5 pagini
    SOMIPP Lab 5
    Augusta Bucataru
    Încă nu există evaluări
  • AMOO Lab 4
    AMOO Lab 4
    Document6 pagini
    AMOO Lab 4
    Anya Mr
    Încă nu există evaluări
  • Pad LP 01
    Pad LP 01
    Document7 pagini
    Pad LP 01
    Andrei Guritanu
    Încă nu există evaluări
  • Ac 3
    Ac 3
    Document10 pagini
    Ac 3
    Maria Sevciuc
    Încă nu există evaluări
  • Pam Exam
    Pam Exam
    Document2 pagini
    Pam Exam
    Culea Constantin
    Încă nu există evaluări
  • SOMIPP5
    SOMIPP5
    Document7 pagini
    SOMIPP5
    saptesate31
    Încă nu există evaluări
  • Examen PPe
    Examen PPe
    Document11 pagini
    Examen PPe
    crismaruion
    100% (2)
  • Lab 2 Somipp
    Lab 2 Somipp
    Document5 pagini
    Lab 2 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • LL3 (Amoo)
    LL3 (Amoo)
    Document6 pagini
    LL3 (Amoo)
    Eric Semeniuc
    Încă nu există evaluări
  • Lab 3
    Lab 3
    Document4 pagini
    Lab 3
    Rosca Doinita
    Încă nu există evaluări
  • Lab 2
    Lab 2
    Document6 pagini
    Lab 2
    Maria Sevciuc
    Încă nu există evaluări
  • AI-191 Medinschi Ion SO4
    AI-191 Medinschi Ion SO4
    Document5 pagini
    AI-191 Medinschi Ion SO4
    Carolin
    Încă nu există evaluări
  • SOMIPP Labs
    SOMIPP Labs
    Document107 pagini
    SOMIPP Labs
    Ionel Boaghe
    Încă nu există evaluări
  • LAB 2 RC Zolotoi Veaceslav IA-202
    LAB 2 RC Zolotoi Veaceslav IA-202
    Document11 pagini
    LAB 2 RC Zolotoi Veaceslav IA-202
    slaVA
    Încă nu există evaluări
  • Raspunsuri La SOMIIP1 Ates
    Raspunsuri La SOMIIP1 Ates
    Document35 pagini
    Raspunsuri La SOMIIP1 Ates
    Ina Cornos
    Încă nu există evaluări
  • Referat Clasificarea Sistemelor de Operare
    Referat Clasificarea Sistemelor de Operare
    Document6 pagini
    Referat Clasificarea Sistemelor de Operare
    Radu
    Încă nu există evaluări
  • Prezentare Somipp
    Prezentare Somipp
    Document274 pagini
    Prezentare Somipp
    Alexandru Fiodor
    Încă nu există evaluări
  • Prezentare Somipp
    Prezentare Somipp
    Document274 pagini
    Prezentare Somipp
    Alexandru Fiodor
    Încă nu există evaluări
  • Lab 4 Ts
    Lab 4 Ts
    Document7 pagini
    Lab 4 Ts
    Victor Turculet
    Încă nu există evaluări
  • TS Lab3
    TS Lab3
    Document12 pagini
    TS Lab3
    Augusta Bucataru
    Încă nu există evaluări
  • LAb 1 IDweb
    LAb 1 IDweb
    Document4 pagini
    LAb 1 IDweb
    Augusta Bucataru
    Încă nu există evaluări
  • Intrebari de Logica ..
    Intrebari de Logica ..
    Document5 pagini
    Intrebari de Logica ..
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 1
    Lab 1
    Document7 pagini
    Lab 1
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 3 Pam
    Lab 3 Pam
    Document4 pagini
    Lab 3 Pam
    Augusta Bucataru
    Încă nu există evaluări
  • SSM PWP 3, Comp
    SSM PWP 3, Comp
    Document244 pagini
    SSM PWP 3, Comp
    Augusta Bucataru
    Încă nu există evaluări
  • Proiect Baze de Date
    Proiect Baze de Date
    Document12 pagini
    Proiect Baze de Date
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 1 Pam
    Lab 1 Pam
    Document6 pagini
    Lab 1 Pam
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 1 BD
    Lab 1 BD
    Document7 pagini
    Lab 1 BD
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 2 Pam
    Lab 2 Pam
    Document5 pagini
    Lab 2 Pam
    Augusta Bucataru
    Încă nu există evaluări
  • SOMIPP Lab 5
    SOMIPP Lab 5
    Document5 pagini
    SOMIPP Lab 5
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 2 BD
    Lab 2 BD
    Document19 pagini
    Lab 2 BD
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 1 Somipp
    Lab 1 Somipp
    Document5 pagini
    Lab 1 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 6 Somipp
    Lab 6 Somipp
    Document5 pagini
    Lab 6 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 3 Somipp
    Lab 3 Somipp
    Document14 pagini
    Lab 3 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 7 Somipp
    Lab 7 Somipp
    Document5 pagini
    Lab 7 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 4 SAV
    Lab 4 SAV
    Document7 pagini
    Lab 4 SAV
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 2 Somipp
    Lab 2 Somipp
    Document5 pagini
    Lab 2 Somipp
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 1 SAV
    Lab 1 SAV
    Document6 pagini
    Lab 1 SAV
    Augusta Bucataru
    Încă nu există evaluări
  • Lab 1 SOMIPP
    Lab 1 SOMIPP
    Document6 pagini
    Lab 1 SOMIPP
    Augusta Bucataru
    Încă nu există evaluări
  • 1-25 Întrebari Sav
    1-25 Întrebari Sav
    Document14 pagini
    1-25 Întrebari Sav
    Augusta Bucataru
    Încă nu există evaluări
  • Lucrare de Lab nr.5 PC
    Lucrare de Lab nr.5 PC
    Document18 pagini
    Lucrare de Lab nr.5 PC
    Augusta Bucataru
    Încă nu există evaluări