Nucleul

Mihai Budiu -- budiu@cs.cornell.edu


Contents

Cuvinte cheie:
nucleu, întrerupere, controler, time-sharing, securitate, proces
Nivelul tehnic:
programator cu 1-2 ani de experiență
Cunoștințe necesare:
un limbaj de asamblare

Cea mai importantă parte a unui sistem de operare, cea care se regăsește în oricare implementare, de la MS-DOS la VMS, este nucleul. Acest articol încearcă să explice pe scurt ce, cum, cînd și de ce face un nucleu.

Ca să vă convingeți de importanța nucleului încercați să faceți următorul experiment cu PC-ul dumneavoastră (presupunînd că folosiți MS-DOS): ștergeți msdos.sys de pe hard-disc (unul din fișierele care conține nucleul sistemului) și apoi încercați să folosiți calculatorul fără dischete de boot. Dacă ați reușit, nu mai citiți articolul ăsta. Mai bine scrieți unul.

Vom folosi pe alocuri pentru ilustrații exemple din MS-DOS, Windows și UNIX, însă conceptele discutate încearcă să fie independente de aceste sisteme de operare.

Anumiți termeni sunt atît de consacrați în engleză încît traducerea lor ar suna caraghios, și nu ar face articolul mai lizibil. Vom amesteca tacit astfel de barbarisme frecvent în text.

Ce se dă și ce se cere

Un sistem de operare este în general compus dintr-o sumă de componente relativ independente, dar care toate concură spre un singur scop: a face viața utilizatorului mai ușoară. A explica ce este un nucleu nu este foarte simplu atîta vreme cît nu este clar:

Din cauza asta trebuie sa aruncăm întîi o privire asupra a ceea ce știe să facă un calculator fără a avea un nucleu, și asupra a ceea ce nucleul adaugă.

Mașina

Să luăm un calculator tipic. Ce poate el să facă fără nici un fel de programe? În primul rînd microprocesorul -- sau mai precis Unitatea Centrală (UC), care nu este întotdeauna un microprocesor -- știe să interpreteze programe. Cum face asta? Are un pointer, numit Program Counter (PC), spre o adresă a memoriei, de unde citește cîte un număr. Acesta este interpretat drept codul unei instrucțiuni, care este executată. PC-ul este modificat și apoi UC o ia de la capăt cu cititul numărului.

Ce fel de instrucțiuni ale UC există? De transfer de date între memorie și regiștri, pentru felurite operații aritmetice/logice, instrucțiuni pentru controlul execuției programului (if, goto, etc). Coprocesorul matematic și cu instrucțiunile lui îl includem tot în UC; o aproximație bună pentru nevoile noastre.

Calculatoarele moderne sunt echipate și cu o unitate de management al memoriei, UMM. Cîteodată ea este parte din microprocesor, ca la Intel, cîteodată un circuit separat, ca la Motorola. Rolul ei este următorul: de fiecare data cînd cineva (de exemplu UC) intenționează sa acceseze o locație de memorie indicînd o adresă, această adresă este verificată și transformată după un set de reguli (care este sub controlul UC). Rezultatul transformării adresei este o altă adresă de memorie sau o eroare (excepție) dacă ceva nu e în regulă.

Pe lînga UMM mai există o parte activă într-un calculator, de data asta mult mai greu de descris, pentru ca are o natura extrem de eterogenă. În general apelativul aplicat este ``dispozitive periferice''. Cu riscul de a fi prea generali și de a nu respecta adevărul întotdeauna, putem să le descriem astfel: perifericele sunt dispozitive care permit comunicarea cu ``lumea exterioară''. La nivelul cel mai de jos întruparea lor este ``controlerul'', care este un fel de microprocesor foarte specializat. Controlerele primesc mici programe de executat de la UC, în limbajele lor specifice (fiecare controler are comenzile lui), și apoi le execută. Execuția se soldează cu un transfer de date dinspre/spre memorie. Unele controlere pot transfera singure date, altele doar pun datele la dispoziția UC.

Instrucțiunile înțelese de controlere sunt de nivel extrem de scăzut. De exemplu, pentru a scrie un șir de octeți pe disc, trebuie trimis controlerului de disc un program care specifică locul precis pe disc (partiție, cilindru, cap, sector, adresă), codul operației (scriere), adresa în memorie unde se află octeții, numărul lor. După terminare trebuie aflat dacă totul a mers cum trebuie: poziția pe disc la care s-a ajuns (dacă suntem în alt loc decît ne așteptam e bucluc), dacă datele s-au scris, dacă nu de ce nu (eroare tranzitorie, disc protejat, time-out); dacă nu a mers bine trebuie încercat (dacă are sens) de mai multe ori, sau trebuie resetat controlerul, sau trebuie trimis capul hard-disc-ului la începutul discului (recalibrare), ca sa fim siguri unde se află -- poate nu s-a mișcat cum trebuia -- sau cine mai știe ce se poate întîmpla.

Metoda universal folosită actualmente, prin care controlerul atrage atenția UC că s-a petrecut ceva interesant (de exemplu s-a terminat transferul, sau s-a produs o eroare, sau s-a întîmplat ceva ``în lumea de-afară'' -- de exemplu s-a apăsat o tastă) este întreruperea. Grosier descris, o întrerupere se soldează pentru UC cu încărcarea PC-ului cu o valoare anumită, care depinde de natura întreruperii. Asta înseamnă de fapt un salt la o anume adresă, salt care se petrece fără a fi consecința executării programului curent, ci a ceva care s-a întîmplat ``înafară''. Ca sa rămînem în cadrul exemplului de mai sus, după ce a programat controlerul de hard-disc să scrie ce are de scris, UC face altceva. Cînd controlerul a terminat operația (cu bine sau nu) generează o întrerupere pentru a atrage atenția asupra sa. UC știe că trebuie să se ocupe de controler.

Întreruperi pot fi generate nu numai de periferice, ci și de UMM, sau chiar de UC însăși: de exemplu cînd cineva încearcă să împartă la 0 se poate produce o întrerupere generată de chiar UC. UC poate inhiba generarea de întreruperi (prin executarea unor instrucțiuni speciale) dacă dorește, dar de obicei o face pentru perioade foarte scurte.

Uneori avem de a face cu un controler special numai pentru întreruperi, care are grijă ca o întrerupere ignorată la un moment dat să fie livrată mai tîrziu, cînd UC permite. Asta pentru că întreruperile semnifică evenimente importante, care nu pot fi trecute cu vederea.

Ei bine, cam atît poate face un calculator! După cum vedeți nu este prea simplu de folosit un calculator gol-goluț: lucrurile pe care știe să le facă sunt mult prea simple, și este poate nevoie de mii de instrucțiuni numai pentru a scrie un număr pe ecran. Aici intră nucleul în joc.

Nucleul ca bibliotecă de funcții

Una din primele utilizări la care se gîndește cineva cînd este întrebat despre nucleu, este drept colecție de funcții care sunt necesare marii majorități a programelor. Cele mai evidente sunt funcțiile pentru intrare-ieșire; utilizatorul în loc să dea zeci de comenzi controlerului, să aștepte întreruperea, să verifice rezultatele, eventual să încerce din nou, să trateze erorile, cheamă o funcție din nucleu (printr-un apel de sistem - un fel de apel de procedură mai simandicos), procedură care de exemplu ar putea avea numele mnemonic write_file, cu niște argumente simple: un nume de fișier și un array.

De altfel în cazul sistemului de operare MS-DOS nucleul -- grosso modo -- la cam atîta se rezumă: la a oferi niște funcții foarte utile. La MS-DOS nucleul este format din mai multe componente: una se cheamă BIOS, (Basic Input-Output System) și este o suită de funcții înscrise în memorii nevolatile. De exemplu pe plăcile video VGA se află cîte un BIOS care oferă funcții pentru a face un punct, o linie, a trece în modul text/grafic, a schimba rezoluția, a șterge ecranul, etc. Acestea sunt, logic vorbind, părți ale nucleului. MS-DOS mai oferă (ca mai toate sistemele de operare) funcții pentru accesul discului. Astfel, în loc de a avea de a face cu un obiect foarte complicat, care are capete, cilindri, șamd., avem o structură de directoare, care permite fișierelor să aibă nume, și să crească fără să se calce pe picioare între ele. Funcții de genul ``adaugă un octet la sfîrșitul fișierului cutare'' sunt tot părți din nucleu. La MS-DOS, alte părți ale nucleului (afară de BIOS) se citesc de pe disc la ``încălțarea'' sistemului (``boot''-are), din fișierele msdos.sys și io.sys.

Nucleul ca interpretor

Deja am început să ne facem o impresie (favorabilă) despre natura nucleului. Cu ce poate fi el asemuit cel mai tare? Din punct de vedere al utilizatorului, nucleul este un interpretor, pentru că oferă niște noi instrucțiuni spre uzul tuturor (apelurile de sistem sunt aceste instrucțiuni). Utilizatorul folosește aceste ``instrucțiuni'' ca și cum ele i-ar fi oferite de către mașina însăși; de fapt ele sunt interpretate: sunt obținute prin executarea funcțiilor din nucleu, care sunt scrise în termenii instrucțiunilor de bază ale mașinii.

Aproximația aceasta, a nucleului cu un interpretor, poate fi dusă la limită, cu niște consecințe foarte interesante. În 1952 a fost creat limbajul BASIC (nu disprețuiți, vă rog, venerabilul, chiar dacă ușor senil), tocmai pentru acest scop: pentru a ține loc de nucleu al sistemului de operare, oferind numai instrucțiuni de nivel mai ridicat decît cele ale UC, UMM, etc.

O altă tentativă interesantă, care se bucură de ceva mai mult succes, este de a folosi un limbaj mult mai puternic pe post de nucleu, și anume LISP. LISP-ul este tot un limbaj interpretat (în esență), mult mai expresiv decît BASIC-ul, care poate folosi pe post de sistem de operare (ați auzit de ``mașinile LISP''?).

În toate aceste cazuri nucleul transformă de fapt calculatorul într-o unealtă mult mai ușor de folosit: într-o mașină virtuală.

Procesele - time-sharing

Ei bine, este grozav să poți scrie și citi date de la periferice printr-un simplu apel de sistem. Dar cine face citirea? ``Programele'', o să spuneți. Da, dar care programe? Suntem obișnuiți ca într-o sesiune de utilizare a calculatorului să jonglăm cu o sumedenie de programe diferite, fără să ne dăm prea bine seama. Editoare de texte, compilatoare, programe de gestiune, de poștă electronică, etc.

Una din foarte importantele funcții ale nucleului unui sistem de operare este de a permite fișierelor (de exemplu bc.exe) să devină programe care se execută (și care atunci capătă denumirea de procese).

Una din funcțiile auxiliare acestui scop este gestiunea resurselor calculatorului (memorie, etc.) în așa fel încît simultan sau succesiv să poată exista mai multe procese distincte. (N-a fost dintotdeauna așa! În anii cincizeci porneai un calculator pentru a face o singură treabă; după aceea trebuia să-l stingi. Am avansat ceva.)

MS-DOS oferă de pildă un apel de sistem numit system(), prin care un utilizator poate indica nucleului un fișier, al cărui conținut este un șir de instrucțiuni pe care îl dorește executat.

E simplu sa folosești system(), dar nucleul are o treabă foarte grea de făcut. El trebuie să găsească pentru noul proces un loc în memorie (poate le dă pe cele vechi afară), să citească întregul fișier (am văzut că asta nu e deloc simplu), și apoi să sară la prima instrucțiune citită.

Lucrurile se complică și mai tare dacă programul nou citit trebuie să se execute în paralel cu altele (nu e grozav să nu trebuiască să aștepți pînă programul care tipărește la imprimantă termină de scos totul ca să lucrezi cu programul de scris texte?). O astfel de coexistență a mai multor procese simultan se numește ``multitasking'' în engleză. Utilizatorii de MS-DOS nu au văzut așa ceva, dar cei de Windows, da.

Metoda prin care nucleul implementează de obicei multitasking-ul se numește, tot în engleză, ``time sharing'' (punere în comun a timpului) și este extrem de interesantă. Să ne aplecăm asupra ei și să vedem cum se poate ea obține din instrucțiunile primitive ale mașinii.

Ideea de bază este simplă: toate calculatoarele au un ``ceas'' (un periferic) care generează periodic întreruperi. Fiecare întrerupere cauzează, după cum am spus, întreruperea (cum altfel?) programului care tocmai se execută, memorarea PC-ului din această clipă, și saltul la o anumită procedură asociată întreruperii. În mod normal toate aceste proceduri, care servesc la tratarea întreruperilor, sunt tot parte a nucleului (numele lor englezesc este ``interrupt handlers'').

Cum ziceam, vine întreruperea de ceas; tocmai se executa programul nr.1. Se sare la procedura de tratare a întreruperii din nucleu, care se uită să vad;a cine se executa: programul nr.1. ``Aha!'' zice nucleul, ``pentru moment îi ajunge.'', după care pune la loc sigur valoarea PC-ului (salvată de întrerupere), și a celorlalți regiștri. Apoi ia din alt loc valoarea pe care o salvase pentru PC-ul programului nr.2, și face un salt acolo. Și tot așa. Rezultatul este executarea interclasată a instrucțiunilor celor două programe.

Lucrurile se petrec foarte frumos astfel; dacă programele nu accesează nici o zonă de memorie sau fișier sau periferic în comun, fiecare are impresia că este singurul care se execută pe acel calculator. Programele nu trebuie scrise într-un fel special pentru a putea coexista, de aceea sarcina programatorului este mult mai simplă.

Inversa operației de creere a unui proces este ``moartea''. Aceasta este realizată de nucleu scoțînd procesul defunct de pe lista proceselor care se vor executa și eliberînd memoria alocată lui (plus alte resurse, cum ar fi fișiere, etc.).

Memoria virtuală

Poate am cerut prea mult de la programe mai sus, cînd am zis că ele nu trebuie să acceseze nimic în comun. Memoria este una singură (de obicei), și este cam greu să scrii un program în așa fel încît să se poată împăca cu oricare altul.

Dar nucleul ne sare din nou în ajutor! Orice sistem de operare mai evoluat (categorie la care MS-DOS nu se ridică cu toată bunăvoința în ziua de azi) transformă și accesul la memorie în așa fel încît procesele să nu se poată influența în moduri nedorite. Mecanismul pe care îl descriem (numit ``memorie virtuală'') este extrem de util chiar și în cazul în care nu avem multitasking.

Ideea este de a folosi UMM (care pentru asta a și fost făcută) pentru a da impresia fiecărui proces care se execută pe mașină că toată memoria este a lui, de la adresa 0 la adresa infinit, dacă se poate, și a nimănui altcuiva, chiar dacă lucrurile nu stau de fapt de loc așa.

Soluția este de a înghesui mai multe procese simultan în memoria disponibilă fizic, și de a traduce fiecare acces la adresa X a unuia din procese la o altă adresă.

Cum ``un desen face cît o sută de cuvinte'' hai să vedem un exemplu extrem de simplificat:

  procesul nr.1                 procesul nr.2
----------------------      -------------------------
|0                  N|      |0                     M|  adrese virtuale
----------------------      -------------------------

vvvvvvvvvvvvvvvvvvvvvv      vvvvvvvvvvvvvvvvvvvvvvvvv  UMM traduce

-----------------------------------------------------
|0                  N        N+1                 N+M|  adrese fizice
-----------------------------------------------------

Cînd procesul nr.1 face o referință la ceea ce el crede că este adresa 0, această referința este trimisă de UMM chiar la adresa 0 din memoria fizică. Pe de altă parte, adresa 0 a procesului nr.2 este trimisă la adresa N+1 din memoria fizică.

Reamintiți-vă că în realitate cele două procese nu se execută niciodată simultan1, ci pe rînd, cînd le sună ceasul. De fiecare dată cînd comută de la un proces la altul (deci cînd tratează întreruperea de la ceas), nucleul trebuie să schimbe și felul în care UMM traduce adresele. Acest lucru îl face UC discutînd cu UMM cam în același fel în care discută cu un controler: îi transmite tot felul de parametri.

Mecanismul simplu al memoriei virtuale poate fi exploatat și în feluri mult mai exotice. De exemplu se pot executa în time-sharing procese al căror total de memorie necesară depășește cantitatea existentă. Cum? Simplu: cînd un proces este înlocuit cu altul, părți din memoria ocupată de el sunt mutate pe disc (de exemplu într-un fișier). Cînd vine din nou la rînd, acele părți sunt aduse la loc în memoria fizică. (Discul, de obicei, este mult mai mare decît memoria.) Această tehnică se numește ``swapping''.

Ea este cu două tăișuri, pentru că scrierea/citirea de pe disc este mult mai lentă decît cea din memorie, și nu-ți poți permite ca la fiecare zecime de secundă să te muți la un alt program, iar mutarea să dureze 10 secunde. Nucleul trebuie să fie zgîrcit în astfel de mutări.

Deocamdată atît despre memoria virtuală.

Securitatea

Izolarea proceselor în zone de memorie disjuncte face sarcina programatorului mai ușoară și calculatorul mai eficient: se pot rula simultan programe ale unor utilizatori diferiți, care nu se vor incomoda unul pe altul nicicum. Un program nu poate scrie/citi din memoria altuia, pentru ca nici una din adresele pe care el le referă nu este tradusă de UMM în vreo adresă a celuilalt.

Acesta e unul dintre aspectele securității utilizării calculatoarelor, și este iarăși una din sarcinile nucleului. Sisteme ca MS-DOS sau Windows, care sunt concepute pentru a permite unui singur utilizator să folosească la un moment dat calculatorul, sunt mai laxe din punct de vedere al securității oferite: în definitiv îți dai cu ciocanul peste propriile degete. În realitate lucrurile nu stau chiar așa, o dovadă fiind proliferarea virușilor, care sunt procese care fac daune și se multiplică modificînd fișiere; ele profită de slăbiciunea sistemului în această privință.

Securitatea are multe aspecte, plecînd de la a asocia fiecare proces cu un utilizator, un utilizator cu niște drepturi, fiecare obiect (fișier, terminal, etc.) cu un utilizator, și de a verifica dacă procesele accesează numai ceea ce trebuie și cînd trebuie. Modelul interpretorului se arată iarăși util.

Operațiile pe care le poate face un proces sunt împărțite în două categorii: ``sigure'' și ``periculoase''. Operațiile sigure nu vor putea niciodată cauza neplăceri altcuiva decît procesului care le execută, așa că pot fi executate oricînd. Un exemplu este adunarea a două numere.

Operațiile nesigure ar putea fi folosite întotdeauna pentru scopuri malefice de către un proces. Toate operațiile de acces la memorie intră în această categorie, precum și toate operațiile de comunicație cu perifericele (dacă oricine poate scrie pe disc poate șterge lucruri care nu-i aparțin). Orice executare a unei operații nesigure este atent supervizată de către nucleu. Iată în ce fel:

Modul nucleu și modul utilizator

Bine, dar nu putem trăi fără operații de acces la periferice! Nucleul însuși are nevoie de așa ceva. Pentru a rezolva această dilemă, UC moderne au cel puțin două moduri de lucru: modul nucleu (kernel) și modul utilizator (user), plus o pereche de instrucțiuni pentru a trece dintr-un mod în celălalt. (Nefericită supraîncărcare a cuvîntului ``nucleu'', dar nu o coincidență, după cum vom vedea.) În modul nucleu orice operație este permisă. În modul utilizator operațiile ilegale declanșează excepții. Instrucțiunile de schimbare a modului sunt ele însele ilegale, ca și cele de control al întreruperilor. Ce este o excepție și cum o folosește nucleul pentru a implementa securitatea?

O excepție (trap) este de obicei un caz special de întrerupere: un obiect cu care ne-am mai întîlnit. ``Special'' pentru că trap-ul nu este generat de un periferic, ci de UMM sau chiar de UC. În rest însă totul arată ca o întrerupere ordinară: salvarea PC-ului, saltul la rutina de tratare.

Ca totul să fie în regulă, mai trebuie aranjat ca orice întrerupere să cauzeze automat trecerea în mod nucleu a UC.

Să asamblăm componentele:

  1. cînd calculatorul pornește, UC este în modul nucleul. Nucleul este citit de pe disc;

  2. nucleul construiește tabelele cu adresele procedurilor care tratează întreruperile (interrupt handlers) și trap-urile; face în așa fel încît întreruperile și trap-urile să treacă UC automat în mod nucleu;

  3. cînd nucleul vrea să predea controlul unui proces, trece mașina în mod utilizator și apoi pune PC-ul la adresa de unde se va executa acel proces;

  4. procesul se execută; fie că face ceva ilegal, fie că vine întreruperea de ceas (sau o alta), se trece în mod nucleu, și se sare la procedura de tratare a întreruperii, care este parte din nucleu;

  5. procedurile de tratare a trap-urilor investighează starea procesului care tocmai se executa cînd trap-ul a fost generat. Din starea memoriei, PC-ul salvat, și natura trap-ului (acces ilegal la memorie, instrucțiune de intrare/ieșire, etc.) handler-ul poate deduce dacă acea operație a fost într-adevăr ilegală. Dacă da, procesul respectiv este probabil ``omorît''.

Schema este foarte simplă, și are următoarele consecințe (dacă sistemul este bine scris):

Natura nucleului

Putem simboliza situația așa cum o înțelegem acum astfel:

                    --------------------------
                    |                        |
                    |    proces(e)           |
                    |                        |
                    |----O1------            |
                    |           |            |
                    |   nucleu  |            |
                    |           |            |
                    -----O2--O3------O3-------
                    |                        |
                    |     hardware           |
                    --------------------------

Am desenat un singur proces, pentru că impresia fiecăruia este că mașina îi aparține în totalitate.

Asta s-ar citi cam așa: procesele rulează ``deasupra'' nucleului dar și mașinii hardware, iar nucleul rulează deasupra hardware-ului direct. Ce înseamnă ``X deasupra lui Y''? Înseamnă că X folosește instrucțiuni sau funcții oferite de Y.

O1
sunt apelurile de sistem -- instrucțiuni oferite de nucleu proceselor.
O2
sunt instrucțiunile nesigure -- acestea pot fi executate numai de către nucleu.
O3
sunt instrucțiunile sigure -- care pot fi executate direct de către procese, dar, firește, și de către nucleu.

Nucleul nu folosește în această schemă niciodată instrucțiuni oferite de procese (există sisteme în care acest lucru nu este adevărat întotdeauna).

Vedeți, noțiunea de ``nucleu'' nu este foarte corectă la acest nivel, în care discutăm structura nucleului însuși: în realitate este vorba de o singură UC, care este succesiv în mod nucleu și utilizator. În fiecare mod UC poate executa anumite categorii de instrucțiuni. ``Nucleul'' este o abstracție: este o porțiune de cod (și date), care este executată din cînd în cînd: atunci cînd utilizatorul o cere, sau atunci cînd este nevoie de serviciile sale, de exemplu cînd vine o întrerupere.

Tabela de procese

Nucleul este deci o colecție de proceduri, care se invocă fie de către utilizator, prin apeluri de sistem, fie prin întreruperi sau trap-uri. În acest sens este o entitate pasivă, al cărei singur sens este de a servi procesele utilizatorilor.

Putem însă privi nucleul și invers: ca pe un dispecer, care își mută atenția de la un proces la altul, și îl ``ajută'' să se execute. Pentru acest scop el posedă o structură de date care descrie informațiile de care are nevoie pentru a identifica fiecare proces. Este practic un array mare de tot de structuri identice, una pentru fiecare proces. Acest array se cheamă ``tabela de procese''.

Ce trebuie să fie într-una din structuri? Răspunsul, evident, depinde foarte mult de sistemul de care vorbim, dar putem extrage un ``cel mai mare numitor comun''. Să aruncăm o privire deci asupra unor posibile cîmpuri; iată deci cum și-ar putea descrie nucleul un proces:

Unele dintre cîmpuri sunt evidente, alte vor fi descifrate în secțiunile care urmează.

Nucleul are tot timpul o variabilă globală numită procesul_curent, și care indică dintre cîmpurile array-ului care este cel folosit în clipa de față. Acesta este procesul care tocmai se execută. Cînd un proces se întrerupe, informațiile sunt salvate în căsuța lui. Cînd un proces se (re)pornește informațiile sunt preluate din căsuța sa, iar variabila procesul_curent este pusă să arate spre această căsuță.

Despre nucleu se spune la un moment dat că ``se execută în contextul procesului X'' dacă variabila procesul_curent are valoarea X în acel moment. În general asta înseamnă că procesul X tocmai a făcut un apel de sistem.

Apelurile de sistem

Ce este deci un apel de sistem? Este o ``instrucțiune'' pusă la dispoziția utilizatorului de către nucleu. Cum se realizează concret? De obicei printr-o instrucțiune ilegală, sau o instrucțiune specială a UC, care este folosită în acest scop. Hai să presupunem că UC are o instrucțiune numită sys_call, care se comportă întocmai ca o instrucțiune ilegală: generează un trap. Atunci un proces face un apel de sistem în felul următor:

  1. pune într-un anumit loc convenit dinainte în memoria lui personală (de exemplu pe stivă, sau într-o anumită variabilă) informațiile care descriu apelul de sistem:

  2. execută instrucțiunea sys_call.

    Ce se întîmplă apoi?

  3. sys_call generează un trap;

  4. UC trece in mod nucleu și începe să execute procedura asociată acelui trap;

  5. procedura aceasta investighează (căutînd în zona convenită) codul apelului; găsește write_file; sare la o procedură din nucleu care se ocupă de scrierea în fișiere;

  6. procedura aceasta (pe care o putem numi abuziv tot write_file) se uită tot în locul convenit după argumente; le extrage;

  7. procedura încearcă să se execute cu aceste argumente; se calculează cilindru, sector, etc., și controlerul de disc este rugat să extragă informația cerută;

  8. procesul curent este suspendat și marcat ca așteptînd completarea unui apel de sistem; informațiile despre apelul de sistem care tocmai se execută sunt salvate în căsuța din tabela de procese asociată lui;

  9. se marchează într-o structură de date asociată controlerului faptul că i s-a dat de lucru pentru acest proces;

  10. un alt proces este pus în execuție, ca să nu se piardă timpul așteptînd resurse lente;

  11. timpul trece....

  12. mai tîrziu, în contextul altui proces (pentru că cel despre care vorbeam este încă suspendat), controlerul de disc generează o întrerupere;

  13. procedura asociată întreruperii investighează informația asociată controlerului pe care o scrisese la pasul 9; vede pentru ce proces a lucrat acesta;

  14. procedura asociată întreruperii preia de la controler rezultatele (codul de eroare, de exemplu) și le pune în căsuța procesului care a făcut cererea, la descrierea apelului de sistem în curs de execuție;

  15. procesul suspendat este marcat din nou gata pentru execuție;

  16. cînd eventual este selectat pentru a fi rulat din nou acest proces, rezultatele sunt copiate din tabela de procese într-un alt loc convenit dinainte pentru a primi răspunsul;

  17. se sare la adresa indicată de PC-ul salvat al acestui proces, continuîndu-se astfel execuția lui.

Partea frumoasă este că deși procesul crede că nu s-a întîmplat nimic altceva decît că i s-a executat o procedură-apel-de-sistem, în realitate între timp poate sute de alte procese au rulat și au făcut treabă. Asta da, eficiență!

Observați că lucrul cerut de un proces se desfășoară în două etape:

Reentranța

Lucrurile se desfășoară ca și cum nucleul este o bucată de cod executabil comună tuturor proceselor. Cînd procesele fac apeluri de sistem, de fapt UC execută cod din nucleu. Codul din nucleu este deosebit de codul ordinar al unui proces prin faptul că accesul la el se poate face numai prin niște puncte foarte precis controlate. Nu se poate sări la o adresă din nucleu; în spațiul de adrese al unui proces (practic) nu există nici o adresă care să fie a nucleului. Saltul la codul nucleului se face prin trap-uri, care schimba nivelul UC de la ``utilizator'' la ``nucleu'' și modifică tabelele de translatare ale UMM în așa fel încît adresele din nucleu devin accesibile.

Pe de altă parte situația nu este simetrică, pentru că procedurile din nucleu au nevoie să acceseze memoria unui proces: de exemplu procedura write_file descrisă mai sus trebuie să ia din spațiul de memorie al procesului datele de scris și să le trimită la perifericul pe care se află fișierul. (Firește că nucleul va trebui să fie atent ca nu cumva să scoată afară pe disc tocmai zona de memorie în care lucrează controlerul în timp ce procesul este suspendat.)

Schema este deci următoarea: procesele se execută mai tot timpul în mod utilizator, din cînd în cînd însă au nevoie de serviciile nucleului (sau survine o întrerupere). Atunci procesele continuă să se execute, dar în mod nucleu, iar codul care se execută este parte din nucleu (gîndiți-vă din nou la nucleu ca la o bibliotecă de funcții și va fi mai clar).

       ....suspendate.....   curent 
       |     |     |     |     |        cod procese
       |     |     |     |     |
       |     |     |     |     |<-
       |     |     |     |     |
                  [ ]<-                 cod nucleu comun proceselor
           ->     [ ]     <-
     ->           [ ]                   <- = PC

Nucleul este deci un fel special de cod, care se poate afla ``de mai multe ori'' în execuție la un moment dat. Mai exact, un proces se execută, iar toate celelalte sunt oprite, iar PC-urile lor punctează toate undeva prin codul nucleului.

Acest gen de cod, care se află simultan în corpul mai multor procese se numește cod reentrant. Codul reentrant este foarte greu de scris, pentru că nu se comportă deloc așa cum suntem obișnuiți cu programele ordinare. De exemplu în codul reentrant variabilele globale nu pot fi folosite decît cu foarte mare grijă.

O procedură din nucleu nu poate folosi pur și simplu o variabilă globală pentru a pasa parametri o alteia pe care o cheamă: dacă tocmai atunci survine o întrerupere care lucrează și ea tocmai cu acea variabilă? Între două instrucțiuni consecutive s-ar fi putut executa alte cîteva mii, din cine știe ce parte a nucleului, ca rezultat al apariției întreruperii!

Programele scrise de utilizator sunt complet izolate de întreruperi (nu simt deloc efectele lor directe) datorită nucleului, și de aceea sunt mult mai ușor de scris.

Coexistența mai multor procese care se execută interclasat mai are efecte foarte interesante. Operații care sunt foarte clare în contextele unor procese izolate devin foarte șubrede cînd se combină. Cel mai simplu exemplu: este relativ limpede ce înseamnă că un proces deschide un fișier: nucleul pregătește niște structuri de date pentru accesul ulterior mai rapid la fișier. Este de asemenea clar ce se întîmplă dacă un proces șterge un fișier: zona de date de pe disc alocată fișierului trebuie să dispară. Dar ce se întîmplă dacă un proces deschide un fișier și altul îl șterge? Dacă primul a scris deja ceva ce se întîmplă cu datele lui? În ce va continua el să scrie, șamd.? Răspunsul la astfel de întrebări nu este deloc evident, și trebuie făcută o convenție care să trateze aceste cazuri, întotdeauna în același mod. Este tot treaba nucleului să extindă semantica (semnificația) operațiilor și pentru cazul cînd operațiile se combină.

Erorile

Nucleele trebuie să fie foarte fiabile. Nucleul se execută în mod nucleu, și are acces la toată puterea mașinii: poate comunica cu orice periferic, poate accesa orice zonă de memorie. Nucleul trebuie să fie foarte atent, pentru că el se execută în contextele feluritor procese: nu cumva un proces să-l înșele dîndu-i niște argumente ciudate la un apel de sistem, care să-l facă să o ia razna. De exemplu un proces ar putea cere un write_file, dar da ca adresa a datelor o adresă care este înafara memoriei alocate lui. Nucleul nu trebuie să încerce să citească de acolo, pentru că privilegiile sale ar putea duce la o catastrofă.

Procedurile din nucleu care execută apelurile de sistem vor verifica întotdeauna dacă procesul care a cerut ceva are dreptul să facă asta, și dacă argumentele date au sens.

Adesea nucleul are de a face cu erori: fie cereri imposibil de executat, fie defecțiuni ale hardware-ului sau controlerelor. Ce trebuie să facă în acest caz? Nu poate pur și simplu opri calculatorul în întregime zicînd ``eroare: nu e vîrîtă discheta''.

Erorile cauzate de cereri ilegale sunt relativ ușor de rezolvat, pentru că nucleul raportează proceselor care le-au făcut că ceva nu e în regulă cu argumentele. Procesele se vor descurca cu această indicație de eroare. Există însă erori care nu sunt vina proceselor, sau care nu pot fi atribuite cuiva anume: de pildă apariția unui pachet eronat pe o rețea. Aceste erori nu pot fi nici ignorate, pentru că ar putea avea consecințe fatale.

Metoda folosită este ca fiecare funcție să verifice erorile care ar fi putut apărea la fiecare operație invocată, să încerce să le remedieze singură dacă poate, sau dacă nu să le raporteze funcției care a chemat-o.

Nucleele au de aceea nevoie de o metodă de raportare a erorilor: ``error logging''. Cea mai simplă variantă este ca erorile să fie scrise într-un fișier, unde administratorii de sistem să le poată inspecta, pentru a remedia defecțiunile care nu pot fi remediate de software.

Comunicația inter-proces

În fine, una din ultimele slujbe ale nucleului, după ce a încercat să izoleze cît mai bine procesele unele de altele, este de a le permite să comunice, dar în moduri foarte controlate, în așa fel încît să unul să nu poată influența pe altul mai mult decît este el dispus să se lase influențat.

Nucleele pun la dispoziție apeluri de sistem pentru transferul de date între procese. Cele mai comune se folosesc de fișiere, dar pot exista metode mult mai exotice, ca zone de memorie comună mai multor procese sau mesaje.

Cîteodată ca sarcină a nucleului (de exemplu în BSD UNIX) este și implementarea protocoalelor de comunicație între calculatoare diferite. Prin acestea mai multe nuclee pun la dispoziția proceselor un mijloc de comunicație inter-proces care permite transmiterea datelor între procese aflate pe mașini diferite.

Micro-nuclee și monoliți

Există două tendințe mari în lumea calculatoarelor: de a pune cît mai mult în nucleu, sau de a pune cît mai puțin. A doua tehnologie dă naștere micro-nucleelor. Un micro-nucleu încearcă să conțină cît mai puține funcții: management-ul proceselor, foarte puțină grijă de memorie, dar mecanisme puternice de comunicație inter-proces.

Funcțiile necesare tuturor utilizatorilor sunt atunci oferite de procese speciale, numite servere. De exemplu putem imagina un nucleu care nu oferă nici un fel de operații pe fișiere. O operație de genul write_file ar putea fi implementată de un server special pentru fișiere, căruia procesele doritoare (clienții) îi trimit prin mesaje informațiile necesare.

Cealaltă metodă, care este cea mai răspîndită actualmente, folosește nuclee monolitice. Orice serviciu care implică un grad oarecare de pericol (ar putea fi folosit pentru a influența alte procese) este oferit direct de nucleu. Windows95 și UNIX sunt sisteme tipice monolitice. Exemple de micronuclee: Windows NT, Mach, Chorus.

Sistemele de operare distribuite încearcă să transforme mai multe calculatoare într-o singură mașină virtuală; utilizatorul nu ar trebui să fie preocupat de locul în care procesele lui se execută. În tehnologia curentă (încă la început) sistemele distribuite se bazează pe micro-nuclee care oferă funcții speciale. Sistemele uzuale erau proiectate ca și cum calculatorul pe care rula nucleul era singurul; mecanismele de comunicație inter-proces pe alte mașini erau adăugiri care nu se potriveau foarte bine cu felul în care nucleele lucrează. În sistemele distribuite fiecare nucleu este proiectat de la început cu ideea de a lucra într-o lume populată de multe alte nuclee, cu care poate colabora, dar în care nu poate avea întotdeauna încredere.

Un nucleu poate să aibă încredere în procesele care rulează pe mașina pe care el se află, pentru că ele au luat naștere numai datorită lui însuși, și au obținut toate resursele prin operații aflate strict sub controlul său. Aceasta nu mai este adevărat pentru procesele aflate pe alte mașini.

Ce a mai rămas

Un sistem de operare -- se spune -- trebuie să ofere mult mai mult decît serviciile nucleului. Ce anume mai face parte dintr-un sistem de operare, și nu este nucleu:



Footnotes

... simultan1
Dacă avem de-a face cu un multiprocesor -- un calculator cu mai multe procesoare -- două procese se pot executa simultan, dar în esență funcționarea memoriei virtuale este aceeași