Funcționarea sistemului de fișiere din Unix

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

18 octombrie 1997

Subiect:
Funcțiuni speciale ale sistemului de fișiere în sistemul de operare Unix
Cunoștințe necesare:
oarecare familiaritate cu sistemul de operare Unix
Cuvinte cheie:
atribute, protecție, interfață, periferic, apel de sistem


Contents




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.

Funcțiunile sistemului de fișiere

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

Table 1: Apelurile de sistem pentru fișiere din SunOS.


Am clasificat apelurile de sistem după tipul operației în categoriile descrise în tabela 2.


Table 2: Tipuri de apeluri de sistem pentru fișiere
Categorie Semnificație Apeluri Operații distincte
A operații cu atribute 11 7
F operații cu conținutul fișierelor 10 9
D operații cu directoare 11 11
S operații cu fișiere ``speciale'' 3 3
-- nu e o operație propriu-zisă pe fișiere 2 2
Total 37 32


Fișiere

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.

Operațiile cu conținutul fișierelor

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.


Table 3: Apeluri de sistem pentru conținutul fișierelor.
Apel Sintaxa Descriere
open fd = open(path, mod) deschide un fișier specificat prin nume (path) pentru acces în modul indicat; întoarce un număr întreg (descriptor) pentru a manipula ulterior fișierul
close close(fd) închide un fișier deschis cu open
creat creat(path, drepturi) crează un fișier cu un nume specificat
truncate truncate(path, lungime) redu lungimea fișierului dat prin nume la cea indicată
ftruncate ftruncate(fd, lungime) ca și truncate, doar că fișierul e indicat prin descriptor
lseek lseek(fd, deplasare, relativ) mută cursorul de scriere/citire la o nouă adresă în fișier
tell tell(fd) află unde e cursorul în fișier
read read(fd, memorie, lungime) citește din fișier date în memorie (de la poziția unde e cursorul curent)
write write(fd, memorie, lungime) scrie date din memorie în fișier (la poziția dată de cursorul curent)
fsync fsync(fd) salvează conținutul fișierului care era păstrat în cache
mmap ... transformă acest fișier într-o zonă de memorie; scrierile în această zonă de memorie se vor reflecta în fișier, iar citirile din memorie se vor face din acel fișier. Argumentele sunt prea complicate ca să le discutăm acum.


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().

Directoare

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.


Table 4: Apeluri de sistem pentru directoare
Apel Sintaxa Descriere
chdir chdir(path) schimbă directorul curent pentru acest proces
chroot chroot(path) schimbă directorul rădăcină pentru acest proces
getdents getdents(fd, memorie, lungime) citește conținutul directorului în memorie
creat creat(path, drepturi) crează un fișier cu numele indicat
mkdir mkdir(path, drepturi) fă un nou director cu numele indicat
rmdir rmdir(path) șterge un director care nu conține fișiere
link link(path1, path2) dă încă un nume (path2) fișierului cu numele path1
unlink unlink(path) șterge un nume al unui fișier
rename rename(path1, path2) schimbă numele unui fișier din path1 în path2
symlink symlink(path1, path2) crează un nou nume pentru un fișier
readlink readlink(path, memorie, lungime) citește la cine merge o legătură simbolică


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.

Operațiile cu atribute

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.


Table 5: Apeluri de sistem pentru manipularea atributelor.
Apel Sintaxa Descriere
access access(path, drepturi) verifică dacă fișierul path poate fi accesat în modul indicat (de către procesul curent)
umask umask(masca) indică drepturile de acces pe care le vor avea fișierele create de acum încolo (cu open, creat, mkdir)
chmod chmod(path, drepturi) schimbă drepturile de acces la fișierul indicat
fchmod fchmod(fd, drepturi) ca și chmod, pe un fișier deschis
chown chown(path, utilizator, grup) schimbă posesorul unui fișier
fchown fchown(fd, utilizator, grup) ca și chown
stat stat(path, memorie) citește toate atributele unui fișier
fstat fstat(fd, memorie) ca și stat
lstat lstat(path, memorie) ca și stat, doar că citește atributele unei legături simbolice
flock flock(fd, permisiuni) ``încuie'' sau descuie un fișier (interzice -- pentru sincronizare -- accesele altor procese)
fcntl fcntl(fd, comanda, argumente) operații speciale...


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:


Table 6: Atribute importante ale fișierelor.
Posesorul
Drepturi de acces
Tipul (director, etc.)


Funcțiile access(), umask(), [f]chmod() operează cu drepturile, funcțiile [f]chown() cu posesorul.

Protecție

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.

Abstracția de fișier; operațiile pe fișiere

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!

Țevile cu nume

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.)

Perifericele în Unix: fișierele ``speciale''

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


Table 7: Fișiere speciale.
audio placa de sunet
bwone0 placa grafică alb-negru (Black-and-White ONE)
console consola (tastatură, ecran)
des integratul care criptează DES (Data Encription Standard)
fd0a primul floppy disk
lightpen creionul optic
mouse șoarecul
nit placa de rețea (Network Interface Tap)
rmt0 banda nr. 0 (Remote Mag Tape)
rsd0a primul disc SCSI, partiția A (Raw Scsi Disc 0)
ttya primul terminal (mai precis portul serial; TeleTYpe)


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).

Pseudo-perifericele (pseudo-devices)

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:


Table 8: Fișiere speciale.
klog depozit pentru mesajele de eroare ale nucleului
kmem memoria virtuală vizibilă nucleului
mem memoria fizică a calculatorului
null ``canalul fără fund'' (sink)
ttyp0 terminal virtual 0; master
ptyp0 terminal virtual 0; sclav
win0 fereastra 0
zero șirul infinit de zerouri


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.

Sistemul de fișiere /proc

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:

status
informație despre starea procesului (posesori, memorie alocată, etc);
psinfo
informație folosită de comanda ps, pentru a descrie acest proces;
ctl
scriind în acest fișier anumite valori se poate influența comportarea procesului;
map
descrie harta memoriei virtuale a procesului; cum se traduce fiecare adresă virtuală într-una fizică;
as
memoria virtuală alocată procesului.
etc.

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.

Concluzie: programarea orientată pe obiecte

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ță.



Footnotes

... rapide.1
Dezavantajul este că o schimbare a drepturilor de acces nu are nici un efect pentru procesele care au deschis anterior fișierul!
... siere2
Un utilizator poate ``accesa'' discul doar cu un ciocan.
...chroot()3
În Unix nu numai noțiunea de ``director curent'' este alta pentru fiecare proces, dar fiecare proces are propriul lui ``director rădăcină''.
... aici4
Diferența este că cele bloc se pot accesa fizic numai în blocuri de informație de mărime fixă, iar cu cele caracter se pot transfera cantități arbitrare de informație. Altă diferență este că se pot plasa sisteme de fișiere decît pe periferice de tip bloc.