Somipp Raspunsuri [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

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 calculatorcaracterizează 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:=; svc_nou :=; 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:= ; csperoare:= ; 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 zonatampon, 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 2tnlibere 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:= 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;

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;

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 : 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;

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)

...

[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ărilorieşirilor la acest periferic. Acest monitor are următoarea formă generală (pentru un sistem mono-utilizator): perif: monitor; var ..., sfr_schimb_i,...: condiţie;

... procedura schimb_i(); begin ; if starea ≠ preg then

endif; lansare_transfer_i(parametri); sfr_schimb_i.aşteptare;

-- întrerupere demascată

if starea ≠ ok then -- în timpul aşteptării

endif;

end; ... begin

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 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: procedura intrare(l:linie); procedura intrare(l:linie); tm1[coadă] := l; coadă := coadă+1 mod N1 procedura ieşire(var l:linie);

tm2[coadă] := l; coadă := coadă+1 mod N2 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);

impr.scriere(l);

tampon_disc.scriere(l);

endciclu

endciclu

tampon_disc.citire(l); tampon2.depozitare(l);

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;

procedura suspendare(durata:integer); var proc: descriptor; begin ora_exactă:=ora_de_bază+t_aşt-contor; proc.ora_deblocării:=ora_exactă+durata proc.i := p;

-- ora absolută a deblocării

-- 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ă := ; 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 else

codul procesului fiu

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ă

else

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, carel 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

#include 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 coutRecursionCount = 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 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 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: -- comună tuturor proceselor : ciclu

-- intrare în secţiunea critică



-- ieşire din secţiunea critică

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;

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)

2)

br (R0=0)

test0

21)

3)

stz

31)

c

test1: load R1 br (R1=0) stz

c test1

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 ... scriere(a);

procesul q ... 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 : R:=Mp[m] Mp[m]:=1

Excluderea mutuală prin aşteptare activă poate fi programată cu ajutorul următoarelor secvenţe: iniţializare

: stz

prolog : tas

R, m

br(R≠0) epilog : stz

m

-- Mp[m]:=0

$-1

-- test iterat

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.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=.  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); stare[p]:=blocat else

M.disp:=liber 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): prolog;

ieşire(M): prolog;

p:=;

p:=;

cerere_disp(M, p);

eliberare_disp(M);

alocare_procesor;

intrare(p, f_eligibil); alocare_procesor;

c.aşteptare:

c.semnalizare:

prolog;

prolog;

p:=;

p:=;

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:

ciclu test

if nonM.c.într_sosită then c.aşteptare;

-- evitarea pierderii unei întreruperi

go to test endif;

endciclu

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:=; intrare(p, f_eroare); ; 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 , 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ă:=; 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:=; 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: -- 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

; 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:

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//fd, iar referinţa la catalogul rădăcină este păstrată ca /proc//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.