Mihai Budiu -- mihaib+, at cs.cmu.edu
http://www.cs.cmu.edu/~mihaib/
18 octombrie 1997
Sistemul de operare Unix s-a impus în lumea calculatoarelor datorită multor calități remarcabile. Acest articol va încerca să ilustreze una dintre ele, și anume uniformitatea remarcabilă a operațiilor. Vom ilustra cum o singură parte a sistemului de operare, și anume sistemul de fișiere integrează într-un mod uniform o multitudine de funcții independente cum ar fi: stocarea informației, numirea ei, protejarea informației prin controlul accesului, numirea perifericelor și controlul accesului la periferice, iar în sistemele moderne Unix chiar și numirea proceselor, operațiile cu procese și controlul accesului la procese. Partea frumoasă este că toate aceste lucruri diferite se fac folosind un număr mic de mecanisme: protecția fișierelor, perifericelor și proceselor se face de către nucleu în exact același fel. Această uniformitate face sistemul mai ușor de folosit și de implementat și mai flexibil.
Într-un articol anterior am prezentat structurile de date pe care le folosește sistemul de operare Unix pentru a implementa operațiile cu fișiere (structuri a căror totalitate poartă denumirea de ``sistem de fișiere''). O oarecare familiaritate cu conținutul acelui articol se poate dovedi utilă, dar (sper că) nu este absolut necesară. Articolul acela este disponibil din pagina de web a autorului în postscript. Articolul de față este o continuare independentă a celuilalt. Sperăm ca peste cîtăva vreme să augumentăm această suită cu un al treilea articol despre implementarea sistemului de fișiere din Unix, care este extrem de interesantă.
Am folosit acest articol și ca un pretext pentru a trece în revistă totalitatea apelurilor de sistem din Unix care operează cu fișiere. E un lucru foarte instructiv să înțelegem cum orice operație ne putem imagina se poate sintetiza numai din aceste operații primitive puse la dispoziție de nucleu.
Un răspuns superficial la întrebarea ``la ce folosește un sistem de fișiere (în Unix -- dar și în alte multe sisteme de operare)?'' ar fi ``la stocarea informației.'' Răspunsul este cu siguranță corect, dar incomplet. Sistemul de fișiere mai are și multe alte funcționalități esențiale, cîteodată foarte departe de misiunea aceasta. Le vom enumera pe scurt, le vom discuta fugar, și vom vedea apoi cum unele dintre aceste funcțiuni se manifestă în sistemul de operare Unix.
Funcțiuni ale sistemelor de fișiere:
Ultimele două aspecte nu vor fi discutate în acest articol.
Celorlalte le vom consacra grosso modo cîte o secțiune.
Să aruncăm o scurtă privire pe lista de apeluri de sistem care operează cu fișiere (un apel de sistem este o funcțiune oferită de sistemul de operare). Dăm în tabela 1 o listă completă (sortată alfabetic) a apelurilor de sistem care operează cu fișiere, din sistemul de operare SunOS 4.1.3, o versiune de Unix extrem de populară de la firma Sun Microsystems.
Apel | Funcțiune | Categorie |
access | verifică dreptul de acces | A |
chdir | schimbă directorul curent | D |
chmod | schimbă drepturile de acces | A |
chown | schimbă posesorul | A |
chroot | schimbă directorul rădăcină | D |
close | închide un fișier | F |
creat | crează un fișier | FD |
execve | execută un fișier | -- |
fchmod | ca și chmod | A |
fchown | ca și chown | A |
fcntl | operații speciale | A |
flock | ``încuie'' fișier | A |
fstat | ca și stat | A |
fsync | salvează conținutul din cache al fișierului | F |
ftruncate | ca și truncate | F |
getdents | citește conținutul directorului | D |
ioctl | operații speciale cu fișiere speciale | S |
link | dă încă un nume unui fișier | D |
lseek | mută cursorul la o nouă adresă în fișier | F |
lstat | ca și stat | A |
mkdir | face un nou director | D |
mkfifo | face un fișier special ``țeavă'' | S |
mknod | face un fișier special | S |
mmap | transformă acest fișier într-o zonă de memorie | F |
open | deschide un fișier pentru acces | F |
read | citește din fișier date | F |
readlink | citește o legătură simbolică | D |
rename | schimbă numele unui fișier | D |
rmdir | șterge un director | D |
stat | citește atributele unui fișier | A |
symlink | crează un nou nume pentru un fișier | D |
sync | salvează cache-ul pe disc | -- |
tell | unde e cursorul în fișier? | F |
truncate | reduce lungimea fișierului | F |
umask | schimbă drepturile cu care se crează fișiere | A |
unlink | șterge un nume al unui fișier | D |
write | scrie date în fișier | F |
Am clasificat apelurile de sistem după tipul operației în categoriile descrise în tabela 2.
|
Funcțiunea cea mai evidentă a unui sistem de fișiere este de a organiza datele în fișiere. Acestea pot avea proprietăți felurite în diferite sisteme de operare. În Unix fișierele propriu-zise (adică nu directoarele sau alte varietăți ciudate pe care le discutăm mai jos) sunt simple array-uri de octeți din punct de vedere al sistemului de operare; utilizatorul își organizează în aceste array-uri datele cum dorește. Acest model este valabil și în MS-DOS și Windows. În plus fișierele mai sunt caracterizate de niște atribute a căror semnificație este importantă pentru sistemul de operare. Atribute tipice sunt: numele fișierelor, de care utilizatorul se folosește pentru a indica fișierele asupra cărora vrea să facă operațiile, posesorul fișierului, data la care fișierul a fost creat, drepturile de acces la fișier, etc. Atributele unui fișier sunt memorate de Unix într-o structură de date numită inod, de la ``information-node''.
Atributele sunt oarecum secundare funcției principale a fișierelor, cea de a memora date; fișierele puteau fi folosite pentru a memora date chiar și fără a avea la-ndemînă atribute. Sistemul de operare însă menține atributele pentru că s-a observat că funcționalitatea pe care acestea o oferă este foarte utilă. Vom vedea că atributele sunt unul dintre mecanismele prin care sistemul de fișiere poate fi folosit și pentru alte scopuri.
Să trecem în revistă rapid operațiile care se ocupă doar de manipularea datelor; operațiile cu atribute vor fi explorate într-o secțiune ulterioară.
În tabelul 3 sunt extrase din tabelul 1 operațiile de categoria ``F'', care lucrează direct cu conținutul fișierelor.
|
După cum vedeți operațiile sunt relativ clare, poate mai puțin mmap(), de care ne vom ocupa altă dată, într-un articol referitor la sistemul de memorie virtuală (această funcție, de altfel foarte importantă, este o aplicație imediată a sistemului de memorie virtuală).
Putem face mai multe lucruri cu un fișier: îl putem crea, îl putem scurta, ne putem plimba prin el și putem citi/scrie în el ``petice''. Funcția fsync() ne asigură că fișierul ajunge pe disc; sistemele de operare încearcă să țină cît mai mult timp informațiile în memorie pentru că discul este foarte lent comparativ cu memoria și orice acces la el este foarte costisitor. Este demn de observat că doar open(), creat() și truncate() lucrează cu fișiere date prin nume; celelalte funcții lucrează cu fișiere deja deschise cu open() anterior, și care sunt indicate cu rezultatul dat de funcția open(). Cînd open() deschide un fișier returnează un număr întreg care se numește descriptor de fișier: ``file descriptor'', sau ``file handle'' (în traducere ``mîner de fișier''), folosit de alte funcții pentru a indica fișierul. Verificarea dreptului de acces se face o singură dată, la deschiderea fișierului. Toate celelalte operații sunt apoi mult mai rapide.1
Funcția open() poate crea noi fișiere, ca și creat().
Sistemul de directoare are ca misiune de a ordona fișierele într-o ierarhie și de a le da nume care să fie convenabile utilizatorilor. Un director este un obiect al cărui ``conținut'' este format din nume de fișiere și din nume de alte directoare. Directoarele mai conțin pentru fiecare nume și numărul inodului care-i corespunde (articolul citat în introducere vorbește mai mult despre asta). Presupunem că cititorului îi este cunoscută noțiunea de ``cărare'' (path) care descrie un lanț de directoare care duce la un fișier (de exemplu /usr/home/mihaib/data/articles/2fs.tex este cărarea care duce la fișierul în care tocmai scriu acest articol în această clipă.
Să trecem rapid în revistă în tabelul 4 operațiile (luate din tabelul 1) care lucrează cu directoare.
|
Funcția creat() apare din nou, pentru că schimbă conținutul unui director atunci cînd crează un fișier. mkdir() este echivalentul lui creat(), doar că face un nou director.
Un fișier în Unix poate avea mai multe nume. Primul nume apare cînd fișierul este creat, următoarele se pot adăuga cu funcția link(). Un nume al unui fișier poate fi schimbat cu rename(), sau poate fi șters cu unlink(). rmdir() este echivalentul lui unlink() pentru directoare.
În fine, în Unix există o clasă specială de fișiere numite legături simbolice (symbolic links), al căror conținut este numele altui fișier (o cărare). Aceste fișiere se crează cu apelul symlink().
Dacă fișierul A este o legătură simbolică la B, atunci orice acces la A este transformat de nucleu într-un acces la B. Pentru că un read() din fișierul A ne arată automat conținutul fișierului B, avem la dispoziție funcția specială, readlink(), cu care putem vedea la ce fișier punctează o legătură simbolică.
Legăturile simbolice sunt foarte asemănătoare cu numele fișierelor, dar există o serie de diferențe subtile pe care le vom discuta altă dată. Ele au fost create pentru că, deși un fișier poate avea mai multe nume create cu link(), ele toate trebuie să se afle pe același disc. Pentru a putea avea nume alternative pentru un fișier pe alte discuri decît cel pe care se află fișierul, se folosesc legăturile simbolice.
URL-urile (Universal Resource Locators), cu care în Internet se indică paginile de web, sunt o extensie a noțiunii de legătură simbolică: pe lîngă numele unui fișier ele indică și numele unui calculator plus eventual numele unui protocol, astfel:
http:// | www.cs.cmu.edu | /afs/cs/user/mihaib/ |
protocolul de comunicații | calculatorul | cărarea |
Facem acum o observație banală, dar crucială: pentru a putea face orice operație cu un fișier trebuie să cunoaștem o cărare la el (pot exista mai multe, pentru că un fișier poate avea mai multe nume); singura metodă pentru a specifica un fișier este printr-o cărare!
De aici rezultă imediat o serie de consecințe foarte importante. Una dintre ele este că un fișier care nu are nume nu este accesibil nicicum. Din cauza asta în Unix fișierele nu pot fi șterse (nu există nici un apel de sistem pentru asta! verificați); ele dispar atunci cînd ultimul lor nume este șters cu unlink().
Dar cele mai importante consecințe vor apărea abia atunci cînd vom vorbi despre protecție. Înainte de asta trebuie însă să vedem cum se manipulează atributele.
Toate atributele unui fișier (mai puțin numele) sunt ținute de Unix în inod. (Numele sunt ținute în directoare.) În tabela 5 sunt trecute operațiile puse la dispoziție de către nucleu pentru a opera cu atribute.
|
Funcția fcntl() (file control: controlul fișierelor) face o sumedenie de lucruri diferite în funcție de valoarea celui de-al doilea argument: află dacă fișierul e încuiat, modifică modul de lucru cu fișierul (de exemplu poate aranja ca fiecare octet scris să meargă imediat spre disc, fără să stea în cache), poate pune alte încuietori, etc. O funcție complicată care deocamdată nu ne interesează prea tare.
stat() citește într-o structură de date atributele fișierului.
Pentru secțiunea următoare ne interesează mai ales să știm că următoarele sunt atribute ale unui fișier:
Funcțiile access(), umask(), [f]chmod() operează cu drepturile, funcțiile [f]chown() cu posesorul.
Cum implementează sistemul de operare Unix protecția accesului la fișiere?
În primul rînd trebuie să ne fie clar cine poate accesa fișierele. Fișierele sunt accesate numai de procese (programele care sunt în curs de execuție). Nu utilizatorii lucrează cu fișiere2, ci programele lor. Fiecare program are o identitate moștenită de la programul care a identificat utilizatorul cînd acesta a tastat parola. Pe toate programele care s-au născut din programul căruia i-am tastat parola mea, scrie undeva ``ăsta e al lui mihaib'' (numele meu de utilizator).
Atributul ``posesor'' al unui fișier este de aceeași natură. Cînd un proces ștampilat ``mihaib'' crează un fișier (cu open(), mknod(), mkdir() sau creat()), acel fișier capătă aceeași ștampilă, care devine atributul ``posesor''. Comanda chown() poate fi folosită pentru a schimba posesorul unui fișier.
În cele ce urmează simplificăm puțin discuția restrîngînd atenția la drepturile ``utilizatorilor''. După cum știți Unix poate agrega utilizatori în grupuri, dar funcționarea sistemului de protecție este asemănătoare pentru grupuri ca pentru indivizi.
Cum observam mai sus, orice comandă care operează asupra unui fișier trebuie să manipuleze întîi o cărare la acel fișier (pentru a putea scrie într-un fișier, el trebuie întîi deschis, și atunci se indică prin cărare). Unix-ul clasic distinge doar trei tipuri de operații pe fișiere: citire, scriere și executare.
Regula de protecție pentru accesul la un fișier în Unix este atunci foarte simplă:
Un proces poate opera asupra unui fișier indicat printr-o cărare dacă are drept de inspecție în toate directoarele din cărare, și dacă are dreptul de a face operația dorită asupra ultimei componente din cărare.
Observați un lucru foarte interesant: decizia dacă un proces are dreptul să facă ceva cu un fișier depinde numai de drepturile procesului pe fiecare componentă a cărării. Decizia nu depinde nicidecum de conținutul fișierului, de pildă. Decizia nu depinde (aproape) nicidecum de tipul fișierului.
Chiar dacă fișierul este un director, un fișier de date, un fișier special (vom vedea ce sunt astea mai jos), sau o legătură simbolică, dreptul de acces se verifică în același fel (pentru toate fișierele care compun cărarea):
Observați că datorită faptului că un fișier poate avea mai multe cărări, accesul pe diferite cărări poate avea restricții diferite pentru un același proces!
Lăsăm ca exercițiu cititorului ilustrarea aplicațiilor acestui mecanism de protecție. Exemplele abundă: directoarele fiecărui utilizator au drepturi mai mari pentru posesor, fișierele executabile de către toți utilizatorii nu pot fi modificate (decît de administrator), (deci virușii nu se pot propaga!), se pot folosi directoare publice pentru fișiere temporare (/tmp), poșta electronică poate fi stocată în fișiere ordinare fără frică de violare a secretului corespondenței, serverele de comunicație în rețea (ex.: ftp, www) lucrează pe o ierarhie de directoare izolată de cea principală a sistemului (cu chroot()3), etc.
Sa ne uităm încă odată la tabelul 3.
Observați că ceea ce un proces știe despre conținutul unui fișier este accesibil numai prin funcțiile acestea: open(), close(), read(), write(), lseek() (celelalte funcții sunt mai puțin importante și nu neapărat necesare; operațiile lor principale ar putea fi sintetizate din cele înșirate aici).
Această interfață este foarte generală, pentru că nu presupune nimic despre organizarea internă a fișierelor. La o adică ne putem imagina aceste operații mergînd pe orice ``container'' cu date. Aceste operații sunt o interfață abstractă pentru un depozit de date.
Această putere de expresie a interfeței a fost sesizată de proiectanții Unix-ului, la începutul anilor '70, și este premeditată. În Unix o mulțime de alte obiecte pe lîngă fișiere sunt accesate folosind aceste operații!
Unix oferă noțiunea de ``pipe'': o țeavă între două procese. Există două feluri de țevi, dar acum ne vom ocupa doar de una dintre ele, de ``țeava numită'' (named pipe). Aceasta are un nume ca un fișier, dar se comportă oarecum diferit: datele pot fi citite o singură dată; citirea ``consumă'' datele. Partea frumoasă este că o astfel de țeavă poate fi manipulată tot cu apelurile indicate mai sus... nimic din interfață nu obligă un fișier să păstreze datele scrise!
O astfel de țeavă se poate crea cu apelul de sistem mkfifo(path, drepturi);. Dacă am creat un astfel de ``fișier'', toate celelalte operații pe el se efectuează cu aceleași funcții: open() deschide țeava și verifică drepturile de acces, close() o închide, write() scrie în țeavă (și blochează scriitorul pînă apare un cititor), iar read() citește din țeavă (sau blochează cititorul pînă apare un scriitor). Apelul de sistem lseek() nu se poate folosi cu astfel de fișiere.
Natura diferită a țevii nu este vizibilă pentru procese, pentru că interfața este aceeași. Această interfață este extrem de puternică.
Ca un divertisment, care nu are prea mare legătură cu subiectul articolului, vom arăta cum se poate folosi o ``țeavă numită'' pentru a depista pe un calculator legat în Internet cine face finger ca să afle informații despre noi.
Ideea este că atunci cînd cineva face finger adresa_mea@cs.cmu.edu demonul local de finger citește din directorul meu fișierul cu numele .plan. Dacă fac acest fișier o țeavă numită și pun un proces să scrie în țeavă, acest proces va fi blocat pînă cînd cineva face un finger spre mine. Acest proces va fi deblocat de demonul de finger. Un script shell ca cel următor face acest lucru:
cd rm -f .plan mknod .plan p # creaza teava while /bin/true; do echo "No plan" > .plan netstat | grep finger >>ma-cauta done
Comanda echo va fi blocată pînă apare un cititor. După ce apare un cititor (demonul fingerd), se va executa imediat comanda netstat, care va arăta toate conexiunile de rețea deschise în clipa aceea, inclusiv cea a demonului fingerd de pe mașina locală cu programul finger de pe mașina de la distanță. În fișierul ma-cauta se va depune adresa mașinii de unde se face finger. (Mai mult de atît este ceva mai greu de aflat.)
Dar să ne întoarcem la drepturile noastre.
Am văzut că abstracția de fișier poate îmbrăca mai multe obiecte: nu numai fișiere, ci și țevi. Oare nu există și alte creaturi care să poată fi manipulate prin această interfață?
Ba da, ba da. O mulțime. De exemplu: toate perifericele.
În Unix toate perifericele conectate la calculator au un nume de fișier și sunt operate ca fișiere! Aceste fișiere sunt numite ``fișiere speciale'', și există două mari categorii: fișiere speciale de tip ``caracter'' și fișiere de tip ``bloc''. Diferența între ele nu ne interesează aici4.
Tot ceea ce e periferic în Unix: mouse-ul, tastatura și ecranul (numite împreună ``consolă''), terminalele conectate și modemurile, memoria video, discurile și unitățile de bandă, cdrom-ul și placa audio, toate acestea sunt ascunse de Unix sub aceeași abstracție, asupra căreia se operează cu open(), close(), read(), write(), lseek() și ioctl().
Singura funcție nou-venită este ioctl(), care seamănă cu fcntl(), pe care am prezentat-o mai sus. Diferența este că ioctl(), de la ``I/O control'' (controlul intrării și ieșirii) se folosește numai pentru fișiere speciale, iar fcntl() pentru fișiere ordinare. Funcția aceasta este o șmecherie care permite utilizatorilor să facă toate operațiile pe care nu le pot face cu celelalte funcții. Cu alte cuvinte, nu toate perifericele se ``ascund'' bine sub abstracția prezentată, și atunci avem nevoie de o funcție specială.
Deocamdată să lăsăm pe ioctl() la o parte și să vedem mai multe despre fișierele speciale.
În sistemele Unix tradiționale toate fișierele speciale sunt strînse în directorul /dev, de la ``devices'' (aparate). În sistemele moderne există o întreagă ierarhie de directoare în directorul acesta: directoare pentru discuri, directoare pentru benzi, etc.
Iată o porțiune din directorul /dev de pe o stație de lucru Sun cu SunOS 4.1.3 (fac o selecție; sunt peste 550 de fișiere!):
crw-rw-rw- 1 root 69, 0 May 6 17:55 audio crw-rw-rw- 1 root 26, 0 May 6 17:55 bwone0 crw--w--w- 1 mihaib 0, 0 Oct 19 14:06 console lrwxrwxrwx 1 root 9 May 6 17:37 modem -> /dev/ttya crw-rw-rw- 1 root 11, 0 May 6 17:55 des brw-rw-rw- 1 root 16, 0 May 6 17:51 fd0a crw-rw-rw- 1 root 104, 0 May 6 17:55 lightpen crw-rw-rw- 1 root 13, 0 May 6 17:51 mouse crw------- 1 root 37, 40 May 6 17:51 nit crw-rw-rw- 1 root 30, 0 May 6 17:55 rmt0 crw-rw-rw- 1 root 30, 1 May 6 17:55 rmt1 crw-r----- 1 root 17, 0 May 6 17:51 rsd0a crw-r----- 1 root 17, 1 May 6 17:51 rsd0b crw-rw-rw- 1 root 2, 0 Sep 25 11:25 tty crw-rw-rw- 1 root 12, 0 Oct 19 14:44 ttya crw-rw-rw- 1 root 12, 1 May 6 17:51 ttyb ^ ^^ ^ tipul major minor
Numele fișierelor speciale nu sunt prea bine standardizate; fiecare sistem va numi un anumit periferic în alt fel. Tipul mai sus este ``c'' pentru fișiere de tip ``caracter'' și ``b'' pentru fișiere de tip ``bloc''. Pe sistemul meu fișierele de mai sus corespund la (tabela 7):
|
Faptul că fiecare periferic oferă o interfață de fișier are următoarele consecințe:
Firește, ca și țevile, fișierele speciale nu există fizic pe disc; pe disc există doar numele acestor fișiere precum și atributele. ``Conținutul'' acestor fișiere este ``calculat'' de nucleu. Vom reveni la acest lucru.
Să vedem în acțiune abstracția de fișier în cazul perifericelor.
Dacă vă uitați cu atenție mai sus veți observa că ``fișierul'' console îi aparține utilizatorului mihaib (eu), și nu lui root, ca toate celelalte. Asta pentru că atunci cînd eu am făcut ``login'' la consolă, fișierul special care este consola mi-a fost trecut în posesie mie de programul ``login''. În felul acesta, toate procesele lansate de mine, care au ``ștampila'' mihaib pe ele pot scrie și citi de la consolă (un lucru foarte util)!
De asemenea veți observa că drepturile pentru alți utilizatori permit scrierea la consolă! Acest lucru permite altor utilizatori să-mi trimită mesaje folosind comanda write a shell-ului, a cărei implementare nu face altceva decît să deschidă fișierul /dev/console și să scrie mesajele.
Adesea se fac bancuri proaste pe un sistem pe care lucrează mai mulți utilizatori: unul găsește un astfel de terminal al unui coleg permisiv și aruncă o tonă de date spre el: cat /usr/include/*.h >/dev/console. Pentru a nu permite așa ceva, pot face chmod 600 /dev/console. (Pot face același lucru cu comanda mesg n.)
Iată deci cum sistemul de fișiere constrînge accesul la periferice și comunicația între procese. Iar acest lucru este ``pe gratis'': același mecanism care era folosit pentru fișiere ordinare merge și aici.
În general accesul la periferice este restrîns, pentru că altfel utilizatori răutăcioși ar putea cauza stricăciuni. Dacă aveți drepturi de administrator pe o mașină puteți citi toate discurile direct, bloc cu bloc, direct ``din'' fișierele speciale. Pe mașina asta aș putea cu od /dev/sd0a. Dacă aveți un Linux și vreți să încercați, discurile se numesc de obicei /dev/hd0_.
Și așa mai departe. Programele care arhivează pe bandă pur și simplu deschid fișierul /dev/rmt0 și scriu apoi ce au de scris acolo. Serverul de ferestre X Windows deschide (cu open()) fișierele speciale /dev/bwone0 și /dev/mouse și scrie, respectiv citește de acolo. Aceste programe nu au nici o instrucțiune magică pentru a opera cu benzi sau cu ecranul: știu doar să scrie și să citească din ``fișiere'', și în plus știu ce semnificație are ``conținutul'' fișierelor (adică valorile scrise) pentru hardware.
Cum funcționează aceste ``fișiere''? Pentru implementarea fiecăruia din ele nucleul conține o cantitate impresionantă de ``soft'', numită ``driver'' (șofer?). Pe un sistem Unix driverele pot lua mai mult de jumătate din codul întregului sistem de operare!
Fiecare fișier special are printre atribute două valori, numite ``major device number'' și ``minor device number''. Acestea sunt cele două coloane de numere care apar în listingul de mai sus.
Cînd nucleul este compilat, înauntru are un array mare de structuri care corespund fiecărui driver. De exemplu căsuța 0 din array corespunde driver-ului de consolă, căsuța 17 discurilor iar căsuța 13 mouse-ului. (Am citit numărul ``major'' din listing).
Aceasta este și asocierea dintre numele de fișier și programele care implementează abstracția de fișier: cînd un proces deschide fișierul /dev/console, nucleul observă din atributele fișierului stocate în inod, că este vorba de un fișier special cu major-ul 0, deci roagă driver-ul 0 să manipuleze toate operațiile pe acel fișier. Acel driver va face anumite acțiuni cînd se va scrie în fișier, care acțiuni vor duce la afișarea de informații pe ecran. Cum se face acest lucru, depinde foarte tare de hardware-ul în chestiune.
La ce folosește numărul minor? De exemplu sistemul de mai sus are două benzi: rmt0 și rmt1. Atunci, pentru că în hardware sunt identice, pentru ele există un singur driver. Driver-ul va opera însă cu una sau alta din unitățile de bandă, în funcție de numărul minor al fișierului: scrierile la rmt0 vor merge pe o banda, iar cele la rmt1 la cealaltă.
/------------------------->banda 1 /dev/rmt1 | inod | | driver | | driver | | Array |---------| | | consola | | banda | | de | posesor | | |_________|__....___|___^_______|_...__| drivere | drepturi| | 0 1 17/ 18 | ..... | | / | major 17|----|--------------------------/ | minor 1|----/ | | | tip=c | |---------| | | In sistemul | In nucleu de fisiere | <-------|------>
Pentru ca schema să funcționeze trebuie ca atributele (major-minor) ale fișierelor speciale să fie corect alocate pentru a corespunde ordinii în care sunt driverele compilate în nucleu. Administratorul de sistem este cel care crează noi fișiere speciale, cu comanda mknod, și trebuie să fie grijuliu să mențină o asociere corectă cînd crează noi fișiere speciale (și adaugă noi drivere în nucleu).
Observați că deja un periferic ``seamănă'' foarte puțin cu un fișier: nu poți depune date pe care apoi să le extragi în mouse, de pildă. Totuși perifericul este un dispozitiv care manipulează informație, deci interfața generică este potrivită.
Se poate merge și mai departe cu ideile. La ce altceva se poate ``aplica'' această interfață? Ce fel de funcționalitate îmi mai poate da sistemul fără a introduce abstracții noi?
Foarte multe alte obiecte cu aceeași interfață pot fi construite. Pe lîngă fișierele speciale care corespund unor periferice concrete, în directorul /dev mai există o mulțime de alte fișiere speciale care corespund unor periferice virtuale, sau unor ``pseudo-periferice''. Acestea sunt niște aparate imaginare, sintetizate în întregime în software.
Iată cîteva:
crw------- 1 root 16, 0 May 6 17:51 klog crw-r--r-- 1 root 3, 1 May 6 17:51 kmem crw-r--r-- 1 root 3, 0 May 6 17:51 mem crw-rw-rw- 1 root 3, 2 Oct 19 20:50 null crw-rw-rw- 1 root 21, 0 Oct 19 20:51 ptyp0 crw-rw-rw- 1 root 21, 1 Oct 19 20:36 ptyp1 crw-rw-rw- 1 root 20, 0 Oct 19 20:51 ttyp0 crw-rw-rw- 1 root 20, 1 Oct 19 20:36 ttyp1 crw-rw-rw- 1 root 15, 0 Apr 7 1997 win0 crw-rw-rw- 1 root 15, 1 Apr 7 1997 win1 crw-rw-rw- 1 root 3, 12 May 6 17:51 zero
Ele corespund următoarelor funcțiuni:
|
De exemplu win0 reprezintă o fereastră de pe ecran; aceasta se comportă ca un terminal în miniatură, dar nu există fizic; este o noțiune emulată software. Un pseudo-periferic, deci.
/dev/mem este chiar o imagine directă a memoriei calculatorului. Citind astfel octetul 1000 din /dev/mem:
{ char c; int i = open("/dev/mem", O_RDONLY); lseek(i, 1000); read(i, &c, 1); close(i); }
vom afla ce se află în memoria fizică a mașinii la adresa 1000! (Firește, presupunînd că drepturile de acces ne permit acest lucru.)
Alte fișiere bizare: /dev/null este un fișier de lungime 0, în care orice se scrie se pierde (nu are nici un efect). Este extrem de util într-o sumedenie de circumstanțe. De pildă dacă nu vrem să vedem erorile care pot fi generate de o comandă, putem să rugăm comanda să scrie aceste erori ``în'' fișierul /dev/null: ls -R /var/spool/ 2>/dev/null.
Încă odată: protecția accesului la pseudo-periferice se face în același fel, de către codul din sistemul de fișiere care caută în directoare.
Prezentăm aici o altă spectaculoasă extensie a noțiunii de fișier (sau mai precis o altă construcție care folosește interfața cu care se accesează fișierele): un sistem de fișiere în care se află... procese!
Această interfață a fost propusă în 1991 pentru sistemul Unix de la AT&T, System V, inițial pentru a facilita depanarea, dar a evoluat într-o metodă generală de interacțiune cu procesele care se execută pe un calculator.
Există o oarecare variabilitate în implementarea acestei construcții, așa că exemplul pe care îl prezentăm ar putea diferi oarecum pe sisteme specifice.
Lucrurile stau cam așa: există un director /proc, iar în acest director există cîte un sub-director pentru fiecare proces care se execută pe sistem, numit ca identificatorul numeric al procesului (adică procesul ``1'' are un director numit ``1''). În directorul fiecărui proces se află mai multe ``fișiere'' care descriu procesul respectiv. Pentru un proces putem avea următoarele fișiere:
Acest sistem de fișiere este folosit de debuggere: un debugger care vrea să depaneze procesul 100 va deschide fișierul /proc/100/as și va scrie la adresa dorită (cu lseek(), write()) codul unui breakpoint!
Multe alte operații pot fi făcute pe fișiere în acest fel. Linux are în plus în /proc pentru fiecare proces o listă a tuturor fișierelor pe care procesul le are deschise.
Protecțiile pe directoarele și fișierele din /proc sunt puse în așa fel încît interacțiunile între procese independente sunt strict controlate. De exemplu directorul care corespunde fiecărui proces aparține utilizatorului care a lansat procesul.
Faptul că putem folosi interfața read-write-open-close-lseek-ioctl pentru creaturi atît de variate în constituție, de la fișiere pînă la pseudo-periferice ne face să ne gîndim dacă nu poate fi folosită ca un mijloc universal de acces la informație.
Această interfață derivă simultan o fenomenală putere din generalitatea ei, pentru că orice tip de date poate fi văzut pînă la urmă ca o succesiune de octeți, dar și o fenomenală slăbiciune, pentru că semnificația acestor octeți trebuie stabilită prin alte mijloace decît cele ale interfeței. Ce înseamnă să scriu un x ``în'' fișierul /dev/bwone0, asta numai cel care a scris driver-ul pentru placa grafică poate ști (și cel care a citit documentația, desigur).
Funcția ioctl(), despre care n-am spus mare lucru în acest articol, este folosită pentru a simula toate funcțiunile care nu se pot obține cu ușurință doar cu funcțiile citate. Un exemplu de acțiune care se obține cu un ioctl() și care este greu de implementat doar cu celelalte funcții: pentru un driver al unității de bandă există o comandă ioctl() care îi spune să deruleze banda înapoi.
Varianta diametral opusă de proiectare este prezentă în interfața driver-elor de ``terminal'' din sistemele de operare Windows: o sumedenie de apeluri de sistem diferite pentru fiecare operație grafică, de la desenatul unui cerc, la șters fereastra. Este greu de arbitrat între cele două alternative. Lăsăm cititorul să gîndească la meritele fiecărei scheme.
Revenind la interfața uniformă din Unix: în tehnologia orientată pe obiecte această situație se rezumă astfel: avem o clasă de bază abstractă, care are ca metode virtuale open(), close(), read(), write(), ioctl() și lseek(). Fișierele, perifericele, pseudo-perifericele și descrierile proceselor sunt toate clase derivate din această clasă de bază.
Deși sistemul de operare Unix tradițional (și chiar și variantele moderne) este scris în C, astfel de ``reminiscențe'' ale programării orientate pe obiecte abundă. Sperăm să putem vorbi în alte articole despre: variatele tipuri de sisteme de fișiere pe care Unix le poate folosi (în Linux există cel puțin 10!), despre sistemul de memorie virtuală și despre STREAMS. Toate aceste construcții folosesc aceeași tehnologie, de a manipula a mulțime de obiecte diferite ca natură dar folosind o unică interfață.