``Micro'' sau ``macro''?

Mihai Budiu -- mihaib+@cs.cmu.edu http://www.cs.cmu.edu/~mihaib/

12 iunie 1997

Subiect:
Nucleele monolitice par să fie preferate în sistemele de operare.
Cunoștințe necesare:
Cunoștințe de bază despre sisteme de operare.
Cuvinte cheie:
monolit, micronucleu, apel de sistem, proces, domeniu protejat.


Contents



De la apariția ideii de micro-nucleu aceasta a suscitat un enorm entuziasm printre cercetători și industriași. Tehnica promitea să rezolve elegant o mulțime de probleme din proiectarea sistemelor de operare, și să permită scrierea de sisteme distribuite cu mare ușurință. În mod paradoxal însă, la această dată toate sistemele de operare de uz general au mai curînd o arhitectură monolitică. Chiar și despre Windows NT, un cal pe care multă lume serioasă pariază ca învingător în cursa sistemelor de operare, se rîde adesea: ``a plecat ca un micro-nucleu, dar s-a umflat pînă a ajuns mai mare ca un macro-nucleu''.

Acest articol își propune să explice care este motivația acestei spectaculoase rezistențe a tehnologiei tradiționale. Pentru cei nerăbdători concluzia se poate rezuma într-un rînd: costul serviciilor este prea mare (mult prea mare) într-un sistem de operare micro-nucleu.

Am spus mai sus ``sisteme de operare de uz general''. Toate considerațiile arhitecturale pe care le prezint sunt valabile pentru majoritatea sistemelor de operare existente la zi. Considerațiile despre eficiență, care sunt cruciale în supraviețuirea comercială a unui sistem, sunt însă semnificative numai pentru sistemele de operare pentru calculatoare obișnuite. Prin contrast, sistemele de operare specializate (de exemplu sistemele de timp real pentru controlul proceselor, sau pentru mașini electronice de jocuri) sunt într-adevăr micro-nuclee, și își fac foarte bine treaba lor. Cheia este însă aceasta: treaba lor este într-adevăr foarte specializată; o mașină SEGA de jocuri electronice nu are nici disc, nici rețea, nici periferice prea multe, așa că sistemul de operare este special scris. Atenția noastră se apleacă mai ales asupra sistemelor tip Unix/Windows (3.1/NT/95)/VMS, care sunt concepute să permită rularea unei varietăți nelimitate de aplicații și partajarea resurselor între programe care nu știu unul despre celălalt, adesea în medii ``deschise'' (rețele).

O să procedez pe parcursul acestui articol indirect: voi atinge tot felul de probleme care aparent nu au mare legătură cu subiectul central, după care, în final, într-o secțiune sumară voi arăta cum consecințele faptelor pe care le-am tot înșiruit, și pe care le socotesc nu lipsite de interes în sine, se adună întru concluzia indicată mai sus, care promite dominația sistemelor monolitice.

Serviciile oferite de nucleu: apeluri de sistem

În această secțiune voi face o scurtă recapitulare a modului de funcționare al nucleului, pentru a înțelege de unde izvorăsc toate problemele. Pentru că am vorbit aiurea despre aceste lucruri mai pe larg, aici voi fi oarecum succint. Cititorul interesat poate găsi o descriere a funcționării unui nucleu de sistem de operare în articolul meu publicat în serial în PC Report în septembrie/octombrie 19961.

Nucleul unui sistem de operare se poate asemui cu o bibliotecă de funcții care sunt puse la dispoziția proceselor utilizatorilor. Practic întreg accesul la perifericele conectate este mediat de nucleu, din motive de reutilizare a codului, eficiență și, mai ales, securitate2.

Pentru utilizatorul normal acest lucru se manifestă prin prezența unei colecții de funcții gata făcute, cu care el poate manipula perifericele (terminal, disc, fișiere, rețea, etc.). Un exemplu tipic este funcția write() din Unix, prin care se pot trimite date spre un periferic. Funcțiile puse la dispoziție de către nucleu se numesc apeluri de sistem (system calls).

O altă funcție importantă a nucleului, vizibilă utilizatorului prin apeluri de sistem pentru crearea, distrugerea și manipularea proceselor, este cea de management al proceselor. Un proces este un program care se execută. Nucleul permite mai multor programe independente să fie ``încărcate'' în memorie, puse în execuție, oprite și terminate. O funcție care este mai rar sub controlul utilizatorului este cea de ``planificare'' (scheduling) a proceselor: oprirea proceselor care s-au executat prea mult și pornirea celor care tînjesc după puțină activitate.

Nucleul implementează de asemenea noțiunea de spațiu de adrese, folosind sistemul de memorie virtuală. În arhitecturile ``clasice'' fiecare proces are impresia că posedă în întregime memoria calculatorului. Acest truc este realizat folosind translatarea adreselor (address mapping): pentru fiecare proces nucleul menține o listă a zonelor de memorie care-i sunt vizibile, iar orice referință la memorie a unui proces este re-calculată și tradusă într-o referință într-una din zonele care i-au fost alocate. Astfel, adresa 5 (``adresă virtuală'') va indica o locație diferită de memorie în RAM (``adresă fizică'') pentru fiecare proces.

Vom vedea un pic mai jos că sistemul de memorie virtuală permite cîteodată vizibilitate a unei zone de memorie mai multor procese, pentru ușurarea comunicării între ele.

Nucleul însuși este protejat folosind memoria virtuală. Pentru a putea apela serviciile nucleului, el trebuie să fie cumva vizibil proceselor. Dar zona de memorie în care se află nucleul devine accesibilă numai atunci cînd procesele invocă serviciile nucleului, fiind invizibilă sau inaccesibilă în mod normal.

Structura unui apel de sistem

Să vedem cum poate un proces ordinar beneficia de serviciile nucleului, păstrînd totuși nucleul inaccesibil. (Nucleul posedă o grămadă de structuri de date, despre toate procesele, așa încît citirea lor ar putea reprezenta o periculoasă scurgere de informații. Cu atît mai mult scrierea în zona de memorie fizică în care se află nucleul trebuie să fie prohibită în mod normal).

Atîta vreme cît un proces se execută el folosește Unitatea Centrală într-un mod neprivilegiat (user mode). Procesul ``vede'' din memoria fizică numai porțiunea care i-a fost alocată de nucleu, cam ca în figura 1.

Figura 1: Traducerea adreselor în modul utilizator.
       memorie                      memorie
       virtuala                     fizica
       proces curent
       ________________________     ____________
       |           |           \    | memorie  |
       |  proces 1 |            \   | proces k |
       |           |             \__|__________|
       |___________|____________    | memorie  |
       |inaccesibil|            \   | fizica   |
       |           |             \  | proces 1 |
       -------------              \_|__________|
                                    |          |
                                    |__________|
                                    |          |
                                    |  nucleu  |
                                    ------------

Să presupunem că procesul vrea să cheme un apel de sistem (write(), ca să fim concreți). (Un scenariu asemănător este valabil pentru cazul survenirii unei întreruperi sau executării unei operații ilegale.) Pentru acest scop procesul cheamă o funcție de bibliotecă oferită de fabricanții sistemului, care împachetează argumentele în niște regiștri, iar într-un registru convenit (de pildă AX) codul apelului de sistem (write() să zicem că are codul 3), după care execută o instrucțiune specială a microprocesorului.

Această instrucțiune are un efect dramatic: cauzează trecerea procesorului în mod privilegiat (kernel-mode), după care sare la o rutină specială. Această rutină în primul rînd transformă modul în care se face translatarea adreselor, ``aducînd'' nucleul în spațiul de adrese al procesului curent. (Această ``aducere'' se poate face automat prin faptul că zonele de memorie ale nucleului pot fi accesibile numai în modul privilegiat; depinde de caracteristicile unității de management a memoriei și procesorului prin ce detalii anume se obține vizibilitatea.) Cert este că subit imaginea arată ca în figura 2.

Figura 2: Traducerea adreselor în modul nucleu.
       memorie                       memorie
       virtual'a                     fizic'a
       proces curent
       ________________________     ------------
       |           |           \    | memorie  |
       |  proces 1 |            \   | proces k |
       |           |             \__|__________|
       |___________|____________    | memorie  |
       |  nucleu   |   \        \   | fizica   |
       |___________|    \        \  | proces 1 |
                    \    \        \_|__________|
                     \    \         |          |
                      \    \________|__________|
                       \            |  nucleu  |
                        \___________|__________|

Deodată toate structurile de date și codul nucleului au devenit vizibile. Apoi rutina specială (care tocmai se execută) se uită în regiștri conveniți pentru a depista apelul făcut (în exemplul nostru găsește în AX un 3). Apoi în funcție de acesta cheamă una sau alta din procedurile de tratare din codul nucleului (de-multiplexează apelul, de obicei folosind o tabelă care pentru fiecare apel conține o adresă în nucleu: call syscall[AX]).

Mai departe, procedura de tratare a apelului de sistem, care este specifică pentru apelul nostru (write) caută în locurile convenite argumentele (de obicei tot în regiștri), verifică validitatea lor și începe executarea apelului.

Un apel de sistem de genul lui write() roagă nucleul să transfere date spre un periferic. În cazul lui write datele sunt indicate prin adresa virtuală a unui buffer și mărimea lui: write(periferic, buffer, marime). În mod normal nucleul trebuie să copieze conținutul întregului buffer în interiorul nucleului pentru prelucrare. De ce? Pentru că acest proces va fi suspendat acum, în așteptarea terminării executării apelului de sistem (în general interacțiunea cu perifericele este foarte lentă și cauzează suspendarea proceselor). Ori dacă acest proces este suspendat, un altul va fi pornit. Dar acest lucru va schimba modul în care este translatat spațiul de adrese virtuale, deci adresa virtuală a buffer-ului indicat nu va mai avea aceeași semnificație pentru nucleu!

Făcînd tot felul de trucuri uneori nucleul reușește să evite copierea datelor3. Pentru anumite operații însă copierea datelor în interiorul nucleului nu poate fi evitată: de exemplu cînd datele trebuie să plece în rețea ele trebuie împachetate și sparte în bucăți mai mici, sau atunci cînd merg spre disc trebuie re-aliniate și mutate în cache. De asemenea, cînd datele se duc spre un alt proces (de pildă printr-un pipe în Unix) ele trebuie din nou copiate în interiorul nucleului, pentru a lăsa procesul care face write să continue să folosească buffer-ul fără a modifica datele deja trimise (după scrierea într-o ``țeavă'' (pipe) procesul care face scrierea de obicei își continuă execuția, dar datele sunt păstrate pînă cînd un proces de la celălalt capăt al ``țevii'' le citește. Păstrarea se face în nucleu).

Comutarea proceselor

Să ne uităm acum și la operațiile care însoțesc comutarea execuției de la un proces la altul, pentru că acest cost este foarte important în micro-nuclee.

Comutarea proceselor implică salvarea stării procesului curent și încărcarea stării procesului care urmează pentru execuție. Pentru că cea mai mare parte din stare este conținută în tabele aflate în memorie, schimbarea se poate face relativ simplu încărcînd valoarea unui pointer spre noua căsuță din tabelă care se va folosi (pentru a comuta de la procesul 3 la procesul 5 nucleul va pune în pointerul spre căsuța din tabel cu datele procesului curent valoarea 5 în locul lui 3).

În general însă trebuie luate în calcul mai multe operații. Anume trebuie făcute următoarele operațiuni, nici una foarte complicată:

Faza 1: salvarea stării:
1.
Salvarea regiștrilor curenți;
2.
Salvarea stării coprocesorului matematic;
3.
Salvarea regiștrilor de depanare, dacă procesul curent era depanat;
4.
Salvarea regiștrilor și stării unității de management a memoriei; salvarea tabelei de translatare a adreselor a procesului curent (tabela de translatare indică modul în care se interpretează adresele virtuale pentru procesul curent);
5.
Modificarea contoarelor și ceasurilor de execuție pentru a reflecta timpul consumat de procesul care se oprește;

Faza 2: comutarea:

6.
Rularea algoritmului de planificare (scheduling), care parcurge cozile de procese gata de execuție în ordinea priorităților, alegînd pe cel mai urgent;
7.
Golirea cache-urilor de translatare a adreselor -- Translation Lookaside Buffer4 TLB este un cache care reține felul în care se traduc adresele virtuale cele mai des folosite pentru procesul curent; din moment ce semnificația adresei virtuale 5 pentru noul proces va fi alta, vechea ei asociere trebuie ștearsă și din TLB.

Faza 3: încărcarea noului proces:

8.
Încărcarea tuturor regiștrilor salvați (de pașii 1-3), cu valorile lor pentru noul proces;
9.
Încărcarea tabelei de translatare a adreselor a noului proces și a regiștrilor unității de management a memoriei. În acest fel noul spațiu de adrese virtuale devine vizibil și cel vechi invizibil;
10.
Pentru că în timp ce noul proces ``dormea'' s-au putut întîmpla evenimente interesante pentru el (de exemplu, în Unix i-a fost trimis un semnal), acum este momentul de a lua acțiuni speciale (în cazul semnalelor Unix se construiesc cadre pe stivă pentru procedurile de tratare a semnalelor, sau procesul este omorît);
11.
Schimbarea pointerilor spre a puncta spre noul proces.

După cum vedeți sunt totuși o sumedenie de operații de făcut. Nuclee foarte sofisticate pot avea operațiile de comutare a proceselor chiar mai complicate decît cele descrise aici.

Să observăm că în comutarea proceselor mai există cel puțin un cost ascuns, implicat de operația de schimbarea localității de adresare: pentru că începem rularea unui nou proces, care va folosi un spațiu de adrese complet diferit, cache-ul microprocesorului va genera foarte multe rateuri pentru început, fiind încărcat cu date din spațiul vechiului proces. De asemenea, TLB a fost golit (în pasul 7 mai sus), deci pentru a-l umple din nou cu traducerea adreselor în noul proces, va trebui să fie consultată tabela de traducere a adreselor pentru noul proces, operație costisitoare, deoarece implică accese suplimentare la memorie.

Un alt posibil cost va fi plătit pînă noul proces își aduce de pe memoria secundară (disc) paginile de memorie din setul de lucru (working set); datorită faptului că paginile de memorie îndelung ne-folosite sunt de obicei scoase afară pe disc, s-ar putea ca procesul care tocmai pornește să trebuiască să și le ia de acolo. Aducerea unei pagini este o operație extrem de costisitoare, care implică pe lîngă accesul la disc și oprirea procesului care cere pagina pînă la venirea acesteia, ceea ce înseamnă încă o comutare de procese!

Plasarea serviciilor

Există în mod logic trei locuri unde poate fi implementat un serviciu:

  1. În spațiul procesului care îl folosește, ca o bibliotecă de funcții;
  2. În interiorul nucleului, accesat printr-un apel de sistem;
  3. În gestiunea unui proces separat, numit ``server''.

(Un al patrulea loc, mai puțin uzual, va fi de asemenea discutat.)

Figura 3 arată cele trei variante. Le vom analiza pe fiecare pe scurt. Pentru un serviciu dat, foarte adesea proiectantul sistemului are la dispoziție toate cele 3 posibilității.

Figura 3: Plasamentul serviciilor.
  ------------           ------------          -----------------------
  |  proces  |           |          |          |  proces  |  proces  |
  |          |           |          |          | (client) | (server) |
  |          |           |          |          |          |          |
  |          |           |  proces  |          |          |          |
  ----|-------           |          |          |          |          |
  |          |           |          |          |          |          |
  |biblioteca|           |          |          |          |          |
  ============           ====|=======          =====|===========|=====
  |          |           |          |          |     \__________/    |
  | nucleu   |           | nucleu   |          | nucleu              |
  ------------           ------------          -----------------------
   varianta 1             varianta 2                  varianta 3

Modul dominant în care sunt plasate serviciile (adică locul majorității serviciilor) dă și clasificarea unui sistem în taxonomia sistemelor de operare. Practic fiecare sistem de operare va avea servicii în toate cele trei părți, astfel încît diferența este mai curînd una de grad decît de natură. Astfel, sistemele care aleg varianta 2 pentru majoritatea serviciilor se numesc monolitice, pentru că tind să aibă un nucleu foarte mare, cu o grămadă de cod.

Sistemele care optează pentru varianta 3 se numesc prin contrast ``micro-nuclee'', pentru că nucleul avînd puține servicii devine foarte mic.

În fine, sisteme în care majoritatea serviciilor sunt plasate în spațiul proceselor însele, în funcții de bibliotecă, sunt relativ puțin răspîndite. Vom vedea însă niște candidați puțin mai jos.

Biblioteci

Cu servicii plasate în biblioteci este obișnuit orice programator care a folosit un limbaj de genul C sau Pascal. O bibliotecă este o colecție de funcții gata scrise, la care programele utilizatorilor se pot ``lega'', și pe care le pot folosi. Legarea (linking) la funcțiile din biblioteci se poate face fie atunci cînd programul este creat (la sfîrșitul compilării), și atunci se numește ``legare statică'' (static linking), fie abia după ce programul a fost pornit în execuție, fiind atunci numită legare dinamică (dynamic linking). Cert este că se face doar odată, așa încît costul legării se ``amortizează'' cînd funcțiile din bibliotecă sunt folosite intens.

``Costul'' unui astfel de serviciu este extrem de scăzut; cel mai scăzut posibil probabil, pentru că implementarea unui apel de funcție în termenii microprocesorului este foarte ieftină. Trebuie însă să observăm că natura codului din bibliotecile partajate care se încarcă dinamic5 îl face cîteodată mai ineficient decît codul obișnuit, cu factori cuprinși între 1% 'si 30%.

Să vedem niște exemple concrete faimoase de servicii plasate în biblioteci.

Cazul MS-DOS

Într-un anumit sens sistemul de operare MS-DOS este o mare bibliotecă de funcții pe care procesele le pot chema; este adevărat că chemarea funcțiilor se face nu plasînd pe stivă argumente, ci în regiștri, și apelînd apoi o ``întrerupere software'' (de exemplu, dacă îmi aduc aminte bine, toate funcțiile pentru operațiuni grafice sunt chemate punînd în registrul AX codul funcției și executînd INT 10h). Această întrerupere software este un rudimentar apel de sistem, care de fapt este un apel indirect de funcție din ``nucleu''.

Datorită faptului că MS-DOS nu oferă memorie virtuală, apelul de sistem este mult mai simplu decît cel descris mai sus în prima secțiune a articolului, și este practic la fel de eficient ca un apel de procedură.

Viteza MS-DOS este explicația popularității sale enorme, pe care s-a clădit averea Microsoft și a fabricanților de jocuri. Din păcate (sau din fericire), viteza de execuție a apelurilor de sistem nu este singurul criteriu de merit; faptul că nu oferă multitasking (mai multe procese simultan, ceea ce implică și memorie virtuală pentru izolarea lor) și că lucrul cu perifericele este foarte ineficient, au dus la moartea acestui sistem de operare.

Cazul C

Limbajele de programare de nivel înalt posedă adesea funcții de bibliotecă pentru a fi independente de arhitectura calculatorului. În Pascal astfel de funcții sunt write și new. O să ne aruncăm privirea asupra unei instanțe în limbajul C.

Limbajul ANSI C pune la dispoziția utilizatorilor o serie de funcții de bibliotecă pentru manipularea de ``stream''-uri (nu îmi vine în minte nici o traducere rezonabilă). Un ``stream'' este un cîrnat de octeți; tipul stream este în C notat cu FILE. Operațiile pe stream-uri au numele prefixate cu litera f: fopen(), fclose(), fprintf(), fscanf(), fflush(), fputc(), fgetc(), fseek(), fputs(), etc. C transformă toate perifericele în stream-uri; astfel se pot folosi aceleași funcții pentru terminale, fișiere, și alte minuni, depinzînd de sistemul de operare.

După ce un stream este creat (cu fopen()) sau moștenit, (ca stdio), pot fi trimise date spre el cu fputc(), fputs() sau fprintf(). fputc(caracter, stream) trimite un caracter spre stream-ul indicat, oricărui periferic i-ar fi acesta asociat. ``Trimiterea'' se realizează de obicei prin invocarea unui apel de sistem pentru transmis date; la sistemul de operare Unix folosind apelul write().

puts(sir, stream) trimite un șir de mai multe caractere. Funcția fprintf(), (a cărei binecunoscută variantă printf() este o abreviere) face două operații: formatează și transmite datele (de exemplu fprintf(stdio, "%d", x); transformă întîi valoarea lui x într-un șir de caractere, după care trimite acest șir spre stream).

Pe lîngă acest gen de servicii de conversie, biblioteca de operații cu stream-uri mai face un serviciu de ``buffer''-ing: strînge caracterele trimise spre stream-uri laolaltă și le trimite în grămezi. De ce? Am văzut că un apel de sistem este o operație relativ costisitoare. Din cauza asta, în loc să invoce nucleul pentru fiecare caracter, mai multe caractere sunt strînse laolaltă și pasate cu un singur apel de sistem. Asta implică imediat o creștere de eficiență.

Ca să verific această aserțiune am compilat următoarele două programe:

/* streams */                          /* system call */
int main(void)                         int main(void)
{                                      {
  unsigned long i;                       unsigned long i;

  for (i=0l; i < 1000000; i++)           for (i=0l; i < 1000000; i++)
    putchar('0');                          write(1, "0", 1);
  return 0;                              return 0;
}                                      }

Varianta din stînga scrie de un milion de ori caracterul 0 folosind stream-uri. Stream-ul strînge (în implementarea de bibliotecă pe care o am eu) cîte 1024 de caractere pe care le trimite6; asta înseamnă că face 1 000 000/1024 = 977 de apeluri de sistem write().

Varianta din dreapta pur și simplu scrie un milion de caractere făcînd un milion de apeluri de sistem, unul pentru fiecare caracter. Iată timpii cronometrați7 pe o mașină Linux 486/33Mhz:

streams apel de sistem raport
4.66 sec 87.08 sec [sic!] 1:18

Pot, firește, estima aproximativ durata unui apel de sistem. Astfel am durata a 1 000 000 - 977 apeluri de sistem write() spre un periferic nul, de aproximativ 82.5 secunde (scăzînd dispare diferența dintre timpii de legare). Asta înseamnă un timp de 82 microsecunde pentru un apel de sistem write. Prin contrast, apelul unei funcții durează sub 1 microsecundă pe aceeași mașină, dar este mai greu de estimat precis.

Cazul NT

Sistemul Windows NT de la Microsoft este aparent un sistem micro-nucleu, dar vom mai avea un cuvînt de spus asupra acestei categorisiri. Oricum, proiectanții lui Windows NT au sesizat și ei importanța plasării serviciilor frecvente în biblioteci legate direct la codul proceselor, și au încercat din răsputeri să folosească această tehnică pentru eficiență. Iată o ilustrație:

La NT 3.5 gestiunea ecranului era făcută de un proces separat, un ``server''; la NT 4.0 gestiunea ecranului a fost mutată în interiorul nucleului. Important este acum pentru noi că există o entitate exterioară proceselor utilizatorilor care gestionează în întregime ecranul. Cînd ai de desenat un punct, o linie, un caracter sau un ``bitmap'', trebuie să discuți cu această entitate. Transmiterea unui mesaj spre un alt proces (la 3.5) sau un apel de sistem (la 4.0) costă, iar cînd vrei să trasezi zeci de mii de elemente (aplicațiile Windows sunt în mod normal risipitoare în grafică) costul se înmulțește cu acest factor. Din cauza asta, proiectanții lui NT au aplicat tehnica mutării serviciilor grafice în bibliotecile utilizatorului.

Pe lîngă faptul că au aplicat tehnica ``buffer''-ului, de a strînge cît mai multe operații la un loc înainte de a le trimite serverului de ecran, au ajuns la adevărate ``exagerări'': de pildă culoarea curentă este menținută atît de serverul ecranului cît și de bibliotecă; atunci cînd utilizatorul cheamă o funcție pentru a afla valoarea culorii, biblioteca răspunde direct, fără a interoga serverul, economisind o comunicație.

Atunci cînd utilizatorul schimbă culoarea curentă, biblioteca nu transmite modificarea spre server decît în momentul în care utilizatorul face și o desenare; în acest fel se mai economisește un mesaj, fără ca vreun efect vizual să arate schimbarea.

Cazul exokernel

PC Report din Septembrie 1996 a găzduit un articol despre un nou prototip de arhitectură a sistemului de operare, ``exokernel'' -ul. Exokernel-ul este o idee împinsă la extrem, încarnată într-un prototip de sistem de operare numit ExOS, dezvoltat la MIT. Ideea este de a elimina aproape complet nucleul (mai mult chiar decît în cazul sistemelor micro-nucleu), mutînd absolut toate funcțiile acestuia, (mai puțin o oarecare funcție de arbitrare a accesului la resurse de nivel foarte jos, cum ar fi pagini de memorie) în biblioteci gigantice legate de procesele utilizatorilor. În acest caz toate operațiile se pot executa la întreaga viteză a microprocesorului, evitînd costisitoarele apeluri de sistem. (De aici și numele: kernel (nucleu) ``exterior'': la purtător.)

Ideea este foarte tentantă, sîanumite aplicații dezvoltate special pentru exokernel au într-adevăr viteze uluitoare. Anumite probleme rămîn însă foarte greu de rezolvat în contextul exokernel-ului. Una dintre ele, extrem de spinoasă, este tratată sumar în secțiunea consacrată semanticii operațiilor, aflată puțin mai jos.

Nucleu

Al doilea loc unde poate fi plasat un serviciu este în nucleu. Sistemele de operare monolitice pun în nucleu mai toate serviciile care au o utilizare frecventă.

Sisteme monolitice tipice sunt (și veți recunoaște toate sistemele dominante pe piață): Windows 3.1, Windows 95, Unix, VMS, JavaOS8. Windows NT este considerat în continuare un sistem micro-nucleu, deși conține în nucleu servicii care la Unix (un monolit tipic) sunt înafara nucleului, cum ar fi sistemul de ferestre sau serverul de fișiere. După cum vedeți, granițele sunt difuze între categorii...

Sistemele monolitice sunt comercial cele mai răspîndite.

Pentru ilustrație, să vedem care sunt categoriile de servicii oferite de un sistem Unix tipic:

*
Operații cu procese (creare, distrugere, etc.);
*
Depanarea și măsurarea (profiling) proceselor;
*
Planificarea și execuția proceselor;
*
Accounting și tarifare după consumul resurselor;
 
Operații cu fișiere;
*
Comunicație inter-proces: țevi (pipes), semnale, memorie partajată, semafoare, mesaje;
 
Protocoale de comunicație în rețea (TCP/IP);
*
Gestiunea memoriei virtuale;
*
Alocarea și eliberarea memoriei;
 
Timere și alarme;
*
Mecanisme de protecție și securitate;
 
Legarea dinamică;
 
Managementul perifericelor.

Am marcat cu asterisc serviciile care în orice implementare a unui sistem de operare, fie ea monolit sau micro-nucleu, trebuie să fie oferite de nucleu. (Cum se descurcă exokernel-ul fără ele, mie personal nu îmi este foarte clar.)

Sistemele monolitice sunt destul de greu de scris și relativ inflexibile: o schimbare a serviciilor oferite se poate face în mod tradițional doar oprind sistemul și recompilînd o imagine a nucleului9. Există însă o cantitate considerabilă de experiență în folosirea și manipularea acestor sisteme.

Cea mai dezirabilă trăsătură a acestor sisteme (comparate cu bibliotecile) este separația netă între spațiul de adrese al nucleului și cel al proceselor utilizator. Aceasta permite nucleului să aibă un control foarte strîns10asupra operațiilor care pot fi efectuate de procese, și îi permite să forțeze cu ușurință respectarea politicilor de folosire a resurselor.

Server(e)

În fine, putem lua o resursă partajată și o putem depune în brațele unui proces, care să aibă grijă de ea; procesul care ne ``servește'' cu această resursă se va numi ``server''. Nucleul trebuie să pună la dispoziția proceselor o metodă eficace prin care să comunice între ele; de îndată ce au această metodă la dispoziție, procesele care au nevoie de resursa deținută de server devin ``clienții'' lui, trimițîndu-i un mesaj cu cererea lor. Serverul le răspunde clienților cu datele cerute.

Marele avantaj al acestei formule este că teoretic se poate aplica și în cazul în care clientul și serverul sunt pe mașini diferite și comunică printr-o rețea. Într-adevăr, majoritatea covîrșitoare a aplicațiilor din rețea folosesc această arhitectură.

De aici apar însă și problemele, după cum vom vedea într-o secțiune ulterioară consacrată în mod special sistemelor micro-nucleu, care tind să exploateze tehnologia client-server.

Pentru a ilustra diferența de performanță, pe același calculator pe care am făcut măsurătorile anterioare, un nucleu experimental extrem de simplu (PicOS -- implementat de autor), fără memorie virtuală, cu procese integral rezidente în RAM, permite schimbarea a circa 5000 de mesaje/sec între două procese pe aceeași mașină. Asta înseamnă deja 200 de microsecunde pentru un mesaj, adică 400 pentru un apel complet cerere/răspuns. Comparați cu performanța unui apel de sistem și cu a unui apel de procedură.

Vom petrece restul rîndurilor din această secțiune chiorîndu-ne la cîteva exemple reale de folosire a serverelor pentru gestiunea resurselor.

Servere în Unix

În Unix cele mai faimoase servere care rulează ca procese sunt:

Windows NT

Windows NT are o arhitectură în care serverele ocupă un loc central; probabil din această cauză este asimilat cu un micro-nucleu, deși cele două noțiuni sunt oarecum independente, după cum se poate vedea din exemplele citate. Ideea de bază a lui NT a fost ``furată'' de arhitectul lui șef, David Cutler (la rîndul lui ``furat'' de la Digital de către Microsoft) de la sistemul de operare Mach, un prototip de cercetare micro-nucleu construit la Universitatea Carnegie Mellon și acum dezvoltat la Universitatea din Utah.

Ideea, extrem de elegantă, este de a construi un mediu foarte bogat în proprietăți pe care să se poată apoi construi servere independente care să emuleze sisteme de operare distincte. Astfel, pe o mașină NT pot rula simultan aplicații Windows 95, MS-DOS, OS/2 și Unix, fiecare avînd impresia că folosește sistemul de operare respectiv, dar discutînd de fapt cu un server, ca în figura 4.

Figura 4: Arhitectura lui Windows NT.
            | server  | proces   |  server   | server  | proces |
            | Windows | Windows  | securitate| Unix    | Unix   |
            |         |          |           |         |        |
            |         |          |           |         |        |
            ===|=|==========|====================|=|========|====
            |   \ \_________/      |grafica|____/   \_______/   |
  Nucleu    |    \_________________| Win95 |                    |
   (4.0)    |                      |_______|                    |
            |                                                   |

În mod normal un server lucrează astfel: un thread11 așteaptă blocat să vină cereri de operații de la clienți. Cînd vine o cerere, acest thread crează un nou thread, căruia îi pasează mesajul cu operația cerută, după care se duce din nou la culcare, așteptînd noi mesaje. Thread-ul fiu decodifică mesajul, execută operația, răspunde și moare.

În acest fel server-ul poate executa simultan mai multe cereri. Pe de altă parte, dacă nu există cereri, server-ul nu consumă aproape nici un fel de resurse, pentru că are un singur thread, care doarme.

Resurse fără servere (serverless); Memoria distribuită partajată

Să notăm în treacăt că toate cele trei soluții citate dau fiecare resursă pe seama cuiva: o bibliotecă, un nucleu, un proces. Există și o soluție ``democratică'', în care nimeni nu posedă un obiect; acest stil de proiectare se numește ``fără servere'' (serverless). Din păcate algoritmii folosiți pentru acest fel de probleme sunt în general puțin robuști și extrem de complicați, și trădează adesea tocmai cauza pentru care erau creați: evitarea unei ``gîtuituri'' (bottleneck) în accesul la resursă, reprezentată de serverul care o gestionează.

Să notăm totuși o aplicație a acestui gen de algoritmi în sistemele de calcul paralele și distribuite, mai ales a celor care implementează ceea ce se numește memorie distribuită partajată (Distributed Shared Memory, DSM). Ideea centrală este de a avea pentru toate calculatoarele dintr-o rețea un singur spațiu de adrese uriaș, în care toate scriu și citesc, dar care nu este memorat fizic în vreun loc fixat, ci ale cărui ``locații'' se ``plimbă'' după necesități între mașinile care le folosesc.

Există și sisteme de fișiere implementate după această schemă, dar progresele comerciale sunt (încă) slăbuțe. Nici noi nu o să le consacrăm deci prea mare importanță în acest articol, care iar a început să ia proporții mai mari decît cele anticipate inițial de autor.

Semantica operațiilor

``Unde să plasăm un serviciu, în care din cele trei posturi?'' Aparent răspunsul la această întrebare depinde doar de considerații de eficiență și estetică, precum și de extravaganța design-erului. Dar lucrurile nu stau chiar așa!

Vom vedea că anumite însușiri ale unui serviciu depind esențial de plasarea sa, și că fiecare din cele trei scheme are proprietăți speciale, care nu pot fi simulate nicicum în întregime de celelalte. (Vom ignora asemenea considerații elementare cum ar fi că nu putem plasa servicii de creare a proceselor într-o bibliotecă, pentru că atunci cine crează procesul în care se află biblioteca?)

Lista de diferențe care urmează nu este în nici un caz exhaustivă, ci doar vrea să ilustreze prin exemple problema.

Diferențe semantice bibliotecă-nucleu

Deosebirea dintre bibliotecă și nucleu este cu siguranță familiară oricărui programator în C care frustrat a încercat să-și depaneze programele punînd printf()-uri pe ici-colo, dar care nu dădeau nici un efect!

Explicația este simplă: printf() scrie, după cum am văzut, într-un buffer, care este golit numai în anumite circumstanțe. Dacă o eroare survine înainte ca apelul de sistem write() să fie executat și procesul moare, conținutul din buffer este definitiv pierdut! (Soluția este, firește, să forțăm golirea buffer-ului folosind funcția fflush().)

Așa ceva nu se va întîmpla dacă folosim direct write(), pentru că odată apelul de sistem executat, datele au fost copiate de nucleu și vor ajunge pînă la urmă la perifericul-destinație.

Diferența esențială între cele două cazuri este de durată de viață a informației: dacă informația este într-o bibliotecă, locală unui proces, atunci ea nu poate supraviețui morții procesului12.

Nucleul în schimb supraviețuiește tuturor proceselor (teoretic), deci poate menține în siguranță informațiile globale pentru întregul sistem.

Un alt exemplu faimos este implementarea protocoalelor de rețea în biblioteci, care pune infernale dificultăți (de altfel acesta este unul dintre motivele atacurilor la exokernel, care, vă amintiți, este o bibliotecă mare): de exemplu standardul TCP/IP impune ca după închiderea unei conexiuni de rețea, unul din capete să rețină identificatorul conexiunii pentru o vreme nefolosit (``30 de secunde'' scrie la carte), în așa fel încît să nu fie însușit de o altă conexiune (în acel caz, pachete întîrziate ale conexiunii precedente care mai rătăcesc pe rețea ar putea fi incorect primite pe conexiunea nouă). Puteți vedea în Unix lista tuturor conexiunilor cu comanda netstat. Cele care sunt în starea TIME_WAIT sunt conexiuni de acest gen: terminate, dar memorate. Pe un server de web trebuie să fie o mulțime de astfel de conexiuni la un moment dat13.

Ei bine, pe un proces care are protocoalele de comunicație implementate în biblioteci îl vor trece toate sudorile să mențină identificatorul conexiunii după moartea procesului.

Diferențe semantice nucleu-server

O deosebire de același gen de semantică (semnificație) de același gen a serviciilor subzistă între serviciile oferite de nucleu și cele oferite de servere aflate la distanță: în mod normal pe o mașină ori merge nucleul și procesele, ori, dacă nucleul nu merge, nu merge nimic. Cu alte cuvinte, dacă procesele pot cere servicii de la nucleu sunt sigure că gestionarul lor este sănătos. Acest lucru nu mai este adevărat în cazul proceselor care cer servicii de la distanță: cînd scuipi niște informație în rețea și nu primești răspuns este greu de zis dacă informația a ajuns și răspunsul nu, sau informația s-a pierdut, sau a ajuns și serverul a murit înainte sau după ce a primit-o. De aici și complexitatea enormă a protocoalelor de rețea și a algoritmilor distribuiți.

O altă diferența greu de mascat între soluția unei probleme cu nucleu și soluția cu servere este în încredere. Un nucleu știe că toate procesele care rulează pe mașina lui au fost create de el și sunt ``legitime''. Pe de altă parte cînd un server primește o cerere de la distanță (sau chiar pe aceeași mașină), el nu are la dispoziție mijloacele nucleului de verificare. Un mecanism complet diferit trebuie inventat pentru a asigura serverul de identitatea clienților, un lucru nenecesar pentru un serviciu plasat în nucleu.

Promisiunile micro-nucleelor

Iată care sunt unele din avantajele (reale sau fictive) ale arhitecturii micro-nucleu; unele din aceste merite sunt atributabile arhitecturii client-server, altele arhitecturii de micro-nucleu, dar am văzut că a doua o implică pe prima.

Modularitate
Într-un nucleu monolitic diferitele părți comunică foarte adesea folosind variabile globale, ceea ce ridică probleme dificile privitoare la corectitudinea codului. Mai ales pe multiprocesoare, unde mai multe programe pot acționa simultan asupra aceleiași structuri de date, se pot ivi tot felul de comportamente ciudate. Prin contrast, în soluția cu servere, un server gestionează o cantitate redusă de resurse, iar interacțiunea cu alte programe se face prin interfețe foarte bine precizate (mesaje). E clar, micro-nucleele încurajează modularitatea.

Scalabilitate
Vrei un serviciu mai puternic: mai adaugi niște servere sau niște clienți, înlocuiești serverele cu altele mai performante. O mașină este prea încărcată: muți din servere pe alta. Toate acestea sunt aspecte ale creșterii incrementale a unui sistem, sau creștere în raport cu resursele și necesitățile disponibile.

Distribuire facilă
Dacă primitiva ta de bază este trimite_mesaj(), atunci ești încurajat să dezvolți soluții pentru programe în care nu contează unde se află server-ul. Mediile de calcul distribuit (DCE: Distributed Computing Environment al lui OSF: Open Software Foundation) sunt un exemplu de standard de scriere a unei aplicații independent de numărul de calculatoare pe care se execută.

Adaptabilitate (customizability)
Nucleul nu poate fi schimbat decît în mică măsură, cu mare grijă, și arar fără a opri sistemul. Pe de altă parte, serverele sunt simple procese, care pot fi create și omorîte dinamic, fără a avea nevoie de privilegii administrative extraordinare. Cu alte cuvinte fiecare utilizator al unui micro-nucleu ar putea să-și construiască mediul care îi convine; cine nu folosește fișiere nu pornește nici un server de fișiere, și are mai multe resurse pentru alte ocupații.

Dimensiune redusă
Vîrful de lance al creatorilor de micro-nuclee este: plătești numai pentru ce folosești. Nu ai nevoie de ceva: nu primești. Un nucleu monolit conține toate serviciile, fie că le vrei, fie că nu.

Comunicație puternică inter-proces
Aceasta este o condiție necesară pentru viabilitatea unui micro-nucleu. Comunicația trebuie să fie eficientă, pentru că fiecare serviciu implică cel puțin un schimb de două mesaje (cerere/răspuns), și flexibilă pentru a permite transmiterea unei variate game de mesaje (de la un număr, la un fișier de megaocteți cu imagini).

RPC

Orice discuție serioasă despre arhitectura client-server trebuie să atingă măcar în trecere subiectul apelului procedurilor la distanță (remote procedure calls). Subiectul este fascinant și merită o tratare mult mai amplă.

Observați că în cazul folosirii serverelor nu numai că am mutat un serviciu într-un proces separat, dar am schimbat și natura modului în care serviciul este invocat: înainte era printr-un apel de funcție (sau de sistem), dar acum este prin trimiterea unui mesaj. E aceeași diferență de perspectivă ca între o procedură și un fișier; pentru un programator paradigma mesajului este mai incomodă.

Din cauza asta a fost inventată împachetarea mesajelor în proceduri. Practic programatorul cheamă o procedură (dintr-o bibliotecă), care procedură construiește mesajul, îl trimite, așteaptă răspunsul și se întoarce în mod uzual. În acest fel programatorul operează din nou cu conceptul familiar de procedură.

O astfel de procedură chemată la distanță se numește în engleză ``remote procedure call'', prescurtat RPC. Un pachet RPC face multe lucruri: dintr-o specificare de nivel înalt a procedurii oferite de server14 el construiește automat funcțiile pentru client și server. Funcțiile acestea, care manipulează mesajul se numesc în engleză stub. Misiunea lor este de a împacheta argumentele procedurii într-un mesaj (operație numită marshalling), de a trimite mesajul și a despacheta valorile la recepția unui mesaj. Funcționarea este prezentată schematic în figura 5. Procedurile stub sunt generate de compilatoare speciale din simpla descriere a interfeței lor (tipul argumentelor și al rezultatelor).

Figura 5: Operațiile într-un RPC.
------------           ------------        1. clientul cheama stub-ul
|  client  |           |  server  |        2. 'impachetarea (marshalling)
|          |           |          |        3. stub-ul client trimite mesaj
| 11^  |1  |           |   _6_    |        4. despachetarea (demarshalling)
|===|==v===|           |=5|===|7==|        5. apelul procedurii 'in server
|   |  2\__|____3_>>>__|__/4  |   |        6. execu'tia procedurii 'in server
|  10\_____|___________|_____/ 8  |        7. procedura serverului se 'intoarce
|          |    9 <<<  |          |        8. stub 'impacheteaz'a rezultatele
| stub     |           |     stub |        9. rezultatele transmise clientului
------------           ------------       10. stub-ul client despacheteaz'a
                                          11. stub-ul client se 'intoarce

O altă problemă rezolvată de un pachet RPC este de a localiza serverele care oferă servicii. Cînd un client pornește el știe doar că undeva ar trebui să se afle un server care exportă serviciile care îl interesează. Operația de descoperire a server-ului se numește tot ``legare'', dar în engleză folosește termenul binding, care este un sinonim parțial pentru ``linking'' (corespunzînd acestei faze din compilarea tradițională).

O calitate a unui pachet RPC bun este că permite invocarea de proceduri pe mașini diferite arhitectural de cea pe care se află clientul. Pentru asta procedurile stub de împachetare trebuie să folosească un standard comun de reprezentare a datelor, astfel încît calculatoare care folosesc reprezentări diferite (ex. big/little endian) diferite să se înțeleagă totuși între ele.

Există o mulțime de probleme cu RPC, care scad oarecum din meritele metodei; în principal o serie de diferențe între semantica unui apel de procedură obișnuită și al unei proceduri distante sunt aproape de nedepășit. De exemplu cum trimiți un pointer la distanță și la ce-i folosește server-ului, care are alt spațiu de adrese?

Problema micro-nucleelor

Din cît am divagat, probabil că problema sare-n ochi: într-un micro-nucleu costul unui serviciu este prea ridicat. Asta pentru că transmiterea unui mesaj implică:

  1. Apeluri de sistem pentru trimitere și recepție de mesaje, atît de partea clientului cît și a serverului;
  2. Cel puțin o comutare de procese;
  3. Pentru RPC împachetarea și despachetarea argumentelor;
  4. Copierea argumentelor din spațiul de adrese al clientului, în nucleu, eventual prin rețea, și apoi în spațiul serverului;
  5. Crearea unui thread în server pentru a trata cererea;
  6. Dacă mesajul trece prin rețea este posibil să fie copiat de mai multe ori: de pildă din nucleu pe placa de rețea și invers la recepție;
  7. Copierea răspunsului pe traseul invers server --> client.

O altă observație interesantă este că în general în micro-nuclee tendința este de a fragmenta un serviciu oferit de un nucleu monolit în mai multe servicii oferite de servere diferite. De exemplu, apelul open(fisier) din Unix de obicei devine un apel pentru a traduce numele de fișier într-un identificator unic, executat de serverul de directoare (sau, mai rău, traducerea fiecărei părți din ``cărare'' (path) independent, printr-un apel separat al unui alt server!), și apoi ``deschiderea'' fișierului la serverul de fișiere propriu-zis (figura 6 ilustrează acest fapt). În acest caz, ceea ce inițial era un singur apel de sistem poate deveni o suită de zeci de mesaje schimbate cu mai multe servere!

Figura 6: Deschiderea fișierului /usr/bin/bash într-un sistem distribuit.
              1         __________                          Mesaje schimbate
        /---------->>--|          | server-ul          1. unde e serverul usr?
        | /--------<<--|__________| directorului /     2. la adresa xx
        | |   2     
------------       3    __________
|  client  |____/--->>-|          | server-ul (xx)     3. unde e `bin'?
|          |--------<<-|__________| directorului /usr  4. la adresa yy
|          |____   4              
|          |--\ \     5 __________ 
------------   \ \-->>-|          | server-ul (yy)     5. unde e `bash'?
        | |     \---<<-|__________| directorului /bin  6. la zz, cod ww
        | |           6            
        | |             __________
        | \__7__/--->>-|          | server-ul (zz)     7. scoate `ww'!
        \-----------<<-|__________| de fisiere         8. uite-l! (date) 
             8         

În loc de concluzie: arhitectura NT

Vom încheia acest articol urmărind cîteva din trucurile făcute de proiectanții sistemului Windows NT în lupta cu microsecundele. Trebuie spus din capul locului că pentru a păstra compatibilitatea cu atîtea sisteme existente (Windows 95, OS/2, etc.) ei nu aveau de ales între prea multe variante arhitecturale, și că soluția cu serverele din figura 4 este cea mai flexibilă.

Local procedure call (LPC)

Un pachet RPC face o grămadă de operații de care nu este nevoie în cazul în care clientul și serverul sunt pe aceeași mașină. De exemplu conversia datelor într-un format standard și înapoi este curată sinucidere, din moment ce procesorul este același. Pe de altă parte o cantitate impresionantă de mesaje în NT tind să fie între programe locale; de exemplu tot ce ține de afișare pe ecran se plimbă între unul din serverele de emulare și serverul Windows 95, care singur are în mînă gestiunea ecranului. Din cauza asta între NT 3.54 și NT 4.0 serverul de ecran a coborît în nucleu, cum apare și în figura 4.

Mai apar și alte mesaje locale, de exemplu între un program Unix și serverul de emulare Unix.

Proiectanții NT au optimizat în mod special acest gen de comunicare, numind-o ``apel de procedură locală'' (Local Procedure Call, LPC). Cînd apelul unei funcții dintr-un server este pe aceeași mașină, majoritatea operațiilor costisitoare sunt pur și simplu evitate. Din păcate nici atîta nu este suficient pentru a atinge performanța necesară. Iată alte două trucuri care sunt folosite cu succes în cazul NT, pentru servere locale.

Memoria partajată

Dacă se elimină operațiile care transformă datele (marshalling), atunci cel mai mare cost nu este dat nici de apelul de sistem, nici de comutarea proceselor, ci de copierea argumentelor mari. Foarte adesea serverele de fișiere sunt implementate folosind RPC; operații tipice vor transfera cantități mari de date din/spre fișier. Copierea datelor din client în nucleu și apoi în server pentru un write() (sau invers la citirea unui fișier) este o risipă majoră (overhead) fără nici un beneficiu real.

Proiectanții lui Windows NT au folosit un mod special de transmitere a datelor între un client și un server, care folosește mecanismul de memorie virtuală oferit de nucleu. Astfel, clientul alocă o zonă de memorie partajată (pagini din RAM care sunt vizibile în mai multe spații virtuale) și trimite server-ului doar o descriere a zonei partajate. De pildă pentru un write() clientul alocă zona (printr-un apel special de sistem), scrie datele în zonă, face un LPC write() la server, dînd descrierea zonei partajate. Serverul poate accesa acum direct memoria comună cu clientul. Nici un octet nu a fost mutat în bufferele din nucleu și înapoi! Singura operație este modificarea tabelelor de translatare a adreselor pentru a face zona vizibilă și în server. Zona partajată este făcută vizibilă în spațiul server-ului în timp ce server-ul execută apelul, după care devine din nou invizibilă pentru server.

Aceeași tehnologie, a memoriei partajate, este folosită și în anumite implementări ale serverelor de ferestre X Windows, în care clienții locali pot da direct zone de memorie, și nu conținutul lor.

Prealocarea resurselor

Amintiți-va, din secțiunea dedicată serviciilor oferite de servere, cum funcționează un server în NT: face ``pui'' (thread-uri) pentru fiecare nouă cerere, care mor după execuția cererii. Această creare de thread-uri pentru fiecare serviciu este de asemenea o sursă importantă de ineficiență. De aceea Windows NT mai încalcă încă odată regulile bunelor maniere și face o optimizare specială. Proiectanții spun că această optimizare este atît de urîtă încît nici nu este accesibilă utilizatorului obișnuit; ea este folosită numai între serverele de emulare și modulul grafic.

Pe scurt între un client foarte activ și un server se stabilește o cale extrem de rapidă de comunicare la cererea clientului. Astfel server-ul alocă un thread special pentru acel client, care va executa numai cererile acestui client. Mai există o zonă partajată alocată permanent pentru uzul celor doi, și o pereche de ``evenimente'' (event pair). Perechea de evenimente este de fapt un întrerupător prin care clientul și thread-ul alocat din server își semnalează reciproc (mai exact îi semnalează planificatorului din nucleu) cînd au nevoie de serviciile celuilalt.

Acest mecanism simplifică multe operații: thread-ul nu mai este creat/distrus, tabela de pagini pentru memoria partajată nu mai este modificată (pentru că zona partajată va fi folosită pentru totdeauna de două thread-uri fixate), planificatorul (scheduler-ul) nu mai are de făcut nici o decizie de alegere, pentru că va rula server-ul în cuanta de timp neconsumată a client-ului.

Concluzii

Plasarea serviciilor în mîinile unor procese face gestiunea mult mai simplă, dar este un factor mare de ineficiență datorită granițelor numeroase de trecut între sisteme autonome (procese și nucleu). Diferența este semnificativă și forțează programatorii de sistem la tot felul de soluții extravagante optimizate pentru cazuri particulare.

Windows NT, care este considerat sistemul de operare al viitoarei decade, a constatat pe propria piele dezavantajele arhitecturilor micro-nucleu, căpătînd caracteristici accentuate de monolit, și folosind tehnici extreme pentru a recupera eficiența.

Una peste alta, în ziua de astăzi în sisteme de operare viața nu este prea simplă pentru programatori!



Footnotes

... 19961
Pentru cei care nu au cumpărat revista, articolele la care fac referință sunt disponibile în postscript din pagina mea de web.
... securitate2
Se înregistrează firește și tendințe de a permite accesul proceselor direct la periferice, cum ar fi de exemplu în tehnologia Unet, în care procesele pot scrie direct pe placa de rețea; deocamdată sistemele comerciale însă nu au mers atît de departe.
... datelor3
De pildă nucleul poate reține adresa fizică a buffer-ului, și poate marca în tabelele interne acea zonă ca fiind ``tabu'' (pentru a nu fi modificată de algoritmii de paginare, care refolosesc memoria fizică) pînă conținutul ei a fost prelucrat.
... Buffer4
vedeți și articolul meu despre cache-uri din PC Report din martie 1997 pentru o discuție a TLB.
... dinamic5
Este vorba de faptul că acest cod este ``independent de poziție'' (position independent code; PIC).
... trimite6
Am verificat acest lucru folosind comanda strace.
... ti7
Timpii au fost cronometrați cu comanda time program >/dev/null, cu ieșirea trimisă la perifericul null, care face operațiile instantaneu, deci nu intervine în măsurătoare.
... JavaOS8
Informațiile mele în legătură cu JavaOS nu sunt foarte ample, dar cred că poate fi categorisit ca monolitic.
... nucleului9
Sistemele moderne Unix pot încărca dinamic unele porțiuni de nucleu.
... ns10
Cel puțin teoretic; faptul că se raportează mereu noi bug-uri în securitatea sistemelor Unix nu zguduie de loc încrederea adepților în această teză.
... thread11
Dacă noțiunea nu vă este familiară, puteți asimila un thread cu un proces; o descriere amplă a conceptului și o implementare a unui pachet de thread-uri am publicat în PC Report din ianuarie 1997. De altfel în Unix-ul tradițional, ne-existînd thread-uri, se folosesc chiar procese pentru servere.
... procesului12
Firește, o bibliotecă partajată poate servi de repozitoriu de informație
... dat13
Dacă nu aveți la îndemînă un server de web, creați o conexiune cu telnet localhost, vedeți ambele capete cu netstat, închideți conexiunea cu exit și apoi vedeți informația rămasă din nou cu netstat.
... server14
Într-un limbaj de descriere a interfeței, ``interface definition language'', prescurtat IDL.