Sistemul Virtual de Fișiere

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

ianuarie 1998

Subiect:
VFS: Virtual File System
Cunoștințe necesare:
cunoștințe generale despre sistemele de fișiere; familiaritate cu arhitectura unui sistem de fișiere;
Cuvinte cheie:
vnod, sistem de fișiere virtual, montare, modularitate


Contents



Interfață și implementare

Ingineria programării ne învață că putem ascunde un același obiect sub o sumedenie de operații diferite, și alternativ, că putem inventa seturi de operații distincte pentru a opera asupra unei aceleiași entități.

Avem deci o oarecare independență între implementarea unui obiect (de pildă o stivă poate fi implementată ca o listă sau ca un vector) și interfața unui obiect, care este setul de operații pe care le efectuăm asupra lui (de exemplu o stivă va oferi operații de genul ``pune element (push)'', ``extrage element (pop)'', ``verifică dacă sunt elemente (empty?)'').

Aparent paradoxal, tradiția ne învață că mai importantă decît implementarea unei creaturi informatice este interfața ei. De ce? Din motive de compatibilitate. Atîta vreme cît un obiect are aceeași interfață (și un același comportament), toți cei care care îl folosesc pot rămîne neschimbați, chiar dacă implementarea obiectului se schimbă. De aceea este extrem de important să proiectăm interfețele cum trebuie de la început.

Putem vedea importanța acestui precept la scară industrială într-un exemplu izbitor. Proiectanții procesorului 8086 de la Intel nu au putut rezista tentației de a introduce în setul de instrucțiuni al procesorului o mulțime de operații exotice (de exemplu operații pe șiruri de numere zecimale cu cifre pe cîte 4 biți), pentru că aveau destui tranzistori nefolosiți pe circuitul integrat. Și-au zis: ``în definitiv ce ne costă?''. Eroare fatală. Setul de instrucțiuni este interfața unui procesor, și ca atare trebuie să rămînă neschimbat chiar atunci cînd arhitectura internă (implementarea) procesorului se schimbă. Așa că aceste instrucțiuni pe care nu le folosește (practic) nimeni există și în setul de instrucțiuni din 80486, Pentium, Pentium Pro și Pentium II. Și știți de ce nu există procesoare Pentium la mai mult de 300Mhz, dar există procesoare Alpha la 600Mhz? Din cauza setului de instrucțiuni: anumite instrucțiuni Pentium pur și simplu nu pot fi implementate foarte rapid (una din ele fiind cea de mai sus).

Asta este drama: interfața unui produs longeviv tinde să supraviețuiască implementării. Din cauza aceasta cel care proiectează o interfață trebuie să fie extrem de grijuliu, pentru că are de luptat cu un adversar colosal: timpul. El trebuie să anticipeze evoluția unei interfețe și a utilizării ei.

Vom vedea în acest articol o interfață excelent proiectată cu peste un sfert de secol în urmă, care a evoluat și supraviețuit tuturor încercărilor la care a fost supusă. Este vorba de interfața (setul de operații) cu fișiere în sistemul de operare Unix1.

Fișierul ca abstracțiune

Un fișier în Unix este un simplu container de date; un fel de array de octeți de o lungime (teoretic) arbitrară. Independent de modul în care este implementat un fișier operațiile pe el se fac cu un set extrem de redus de operații esențiale (open(), close(), read(), write(), lseek()) la care se adaugă o sumedenie de operații secundare ca importanță sau care sunt folosite pentru a manipula directoare. Să ne reamintim pe scurt cum se folosesc aceste operații:

Avem un exemplu de folosire a funcțiilor de acces la fișiere în programul de mai jos, care copiază fișierul ``sursa'' în fișierul ``destinatie''.

#include <unistd.h>     /* pentru open(), exit() */
#include <fcntl.h>      /*        O_RDWR */
#include <errno.h>      /*        perror() */

void fatal(char * mesaj_eroare)
{
        perror(mesaj_eroare);
        exit(1);
}       

int main(void)
{
        int miner_sursa, miner_destinatie;
        int copiat;
        char buf[1024];

        miner_sursa = open("sursa", O_RDONLY);
        miner_destinatie = open("destinatie", O_WRONLY | O_CREAT, 0644);
        if (miner_sursa < 0 ||
            miner_destinatie < 0)
                fatal("Nu pot deschide un fisier");
        lseek(miner_sursa, 0, SEEK_SET);
        lseek(miner_destinatie, 0, SEEK_SET);
        while ((copiat = read(miner_sursa, buf, sizeof(buf)))) {
                if (copiat < 0)
                        fatal("Eroare la citire");
                copiat = write(miner_destinatie, buf, copiat);
                if (copiat < 0)
                        fatal("Eroare la scriere");
        }
        close(miner_sursa);
        close(miner_destinatie);
        return 0;
}

Vnodul: un i-nod virtual

Un sistem de operare de tip Unix poate opera simultan cu mai multe tipuri de sisteme de fișiere. De pildă toate sistemele suportă pe lîngă un sistem de fișiere pe un disc local un sistem de fișiere la distanță numit NFS: network file system, creat de firma SUN. Tot firma Sun Microsystems a construit mecanismele necesare suportării unei varietăți de fișiere implementate complet diferit simultan de către un singur nucleu (de fapt nu cei de la Sun au inventat noțiunea, însă implementarea lor, descrisă în cele ce urmează, a devenit practic un standard). Terminologia folosită in acest articol este deci cea propusă de Sun, deși exemplele de cod C vor fi din sistemul de operare Linux.

Cheia constă în următorul fapt: toate sistemele de fișiere prezintă aceeași interfață. Cu alte cuvinte, utilizatorul (și vom vedea că și nucleul în interiorul său) acționează cu exact aceleași funcții asupra tuturor fișierelor, indiferent că se află pe un disc local sau pe unul la distanță sau pe un floppy DOS, etc.; programul de mai sus va fi scris în exact același fel pentru toate aceste cazuri.

În interiorul nucleului fiecare fișier este reprezentat printr-o structură de date numită vnod: un nod de informații virtual. În articolul anterior, despre structura sistemului de fișiere din Unix, din PC Report din decembrie 1996, am văzut că sistemul de fișiere tradițional din Unix folosea pe disc o structură de date numită inod pentru a descrie atributele fișierelor. Vnodul este o generalizare a inodului, care este însă rezidentă în memoria calculatorului, și nu pe un disc, și cu care nucleul reprezintă fiecare fișier deschis.

Vnodul conține două feluri de cîmpuri în interiorul său:

Structura ar arăta deci (în principiu) cam așa:

struct vnode {
    cimpuri_independente;
    union {
        struct msdos_inode_info msdos_i;
        struct nfs_inode_info   nfs_i;
        struct sysv_inode_info  sysv_i;
        ....
    } u;
};

Fiecare structură din uniunea u conține atribute specifice fiecărui sistem de fișiere în parte. Dacă aveți un sistem Linux (o variantă de Unix ale cărei surse sunt disponibile gratuit oricui) puteți vedea definiția vnodului în fișierul de surse al nucleului /usr/src/linux/include/linux/fs.h (din păcate Linux folosește pentru aceste structuri de date -- cel puțin versiunile pînă la 2.0.30 -- alte nume decît restul lumii; numele structurii este struct inode în loc de vnode, dar nu o să lăsăm asta să ne descurajeze).

Cel mai important cîmp independent de structură este o structură cu pointeri spre funcții. Ca să fim concreți vom folosi tot sursa Linux, unde numele structurii este struct inode_operations (numele ei corect ar fi struct vnode_operations).

Definiția acestei structuri se găsește în același fișier și este extrem de interesantă; schematic arată așa (am simplificat un pic pentru motive pedagogice):

struct inode_operations {
        int (*lseek) (struct inode *, struct file *, off_t, int);
        int (*read) (struct inode *, struct file *, char *, int);
        int (*write) (struct inode *, struct file *, char *, int);
        int (*open) (struct inode *, struct file *);
        void (*release) (struct inode *, struct file *);
        int (*fsync) (struct inode *, struct file *);
        int (*create) (struct inode *,const char *,int,int,struct inode **);
        int (*lookup) (struct inode *,const char *,int,struct inode **);
        int (*link) (struct inode *,struct inode *,const char *,int);
        int (*unlink) (struct inode *,const char *,int);
        ....
}

Vnodul conține deci și un pointer spre o structură care conține pointeri către funcțiile care trebuie să opereze cu inodul însuși!

Vom reveni mai jos asupra folosirii vnodurilor.

Partea independentă de arhitectură

Ce fel de atribute sunt prezente în orice vnod? Putem să ne facem o idee în Linux privind în fișierul fs.h indicat mai sus; printre altele: discul (perifericul) pe care se află acest fișier, numărul acestui inod pe acel periferic, drepturile și posesorul fișierului, data modificării, etc. Mai sunt prezente structuri de date necesare nucleului pentru operații pe vnod: semafoare pentru sincronizarea accesului proceselor la vnod, liste înlănțuite de hash pentru căutarea rapidă a vnodurilor în memorie, structura cu funcțiile care operează asupra vnodului, descrisă mai sus.

Partea dependentă de arhitectură

Desigur, fiecare sistem de fișiere ține informațiile de care are nevoie în vnod în partea care-i este rezervată. De exemplu, pentru un sistem de fișiere Unix clasic (descris într-un articol mai vechi), vnodul va conține lista blocurilor de pe disc care aparțin fișierului.

Această listă de blocuri nu va fi prezentă în cazul vnodurilor pentru fișiere de tip NFS, pentru că acestea nu sunt prezente pe un disc pe calculatorul local, ci pe unul la distanță. Dimpotrivă, un vnod pentru un fișier NFS va conține informații suficiente pentru a comunica cu serverul care deține fișierul.

Pentru cazul sistemului de operare Linux, există pentru fiecare tip de sistem de fișiere care poate fi prezent în nucleu cîte un fișier header cu numele de tipul include/linux/*_fs_i.h care conține structura de date privată respectivului sistem de fișiere; aruncați o privire prin ele.

Să facem în figura 1 un desen ca să rezumăm situația așa cum este ea acum.

Figure 1: Vnodul
\begin{figure}\centerline{\epsfxsize=10cm\epsffile{vnod.eps}}\end{figure}

În limbajul programării orientate pe obiecte situația se descrie foarte sumar astfel: în nucleu există o clasă de bază virtuală numită vnod prin care nucleul reprezintă orice fișier; toate metodele acestei clase sunt pur virtuale (metodele sunt funcțiile din structura inode_operations). Fiecare tip de fișier particular suportat de un nucleu este o clasă derivată din această clasă de bază.

Vnoduri speciale

În articolul din decembrie 1997 am explicat faptul că în Unix sub abstracția de fișier se ascund multe alte creaturi: țevile (pipes), perifericele (fișierele speciale), pseudo-perifericele și chiar procesele! Cu alte cuvinte toate aceste obiecte sunt accesate prin aceeași interfață, care include funcțiile descrise mai sus, read(), close(), etc.

În interior nucleul reprezintă toate aceste obiecte în același fel, și anume prin vnoduri. În consecință, pentru nucleu fiecare periferic ``deschis'' este un vnod; în particular o partiție de disc este reprezentată intern în nucleu tot ca un vnod.

Sistemul de fișiere ca abstracțiune

Toată povestea anterioară se repetă aproape identic pe un alt nivel: nucleul Unix se bazează pe lîngă abstracția de fișier pe cea de sistem de fișiere. Structura prin care se reprezintă în nucleu un sistem de fișiere se numește Sistem de Fișiere Virtual (Virtual File System, VFS). Putem citi definiția structurii pentru Linux în același fișier: include/linux/fs.h, unde (în terminologie specifică Linux) structura de date se cheamă struct super_block.

Dacă un vnod reprezintă un fișier individual care a fost deschis, un VFS reprezintă o întreagă partiție care a fost montată în sistemul de fișiere (secțiunea următoare este consacrată acestei operații).

Tot așa cum vnodul are o serie de metode (funcții) care operează asupra lui, VFS conține un pointer spre o serie de funcții globale ale unui sistem de fișiere. Pentru Linux acestea sunt declarate în același fișier, în structura struct super_operations; cele mai importante operații sunt cele care citesc/scriu de pe o partiție un inod. Iată un fragment din această structură:

struct super_operations {
        void (*read_inode) (struct inode *);
        void (*write_inode) (struct inode *);
        void (*put_inode) (struct inode *);
        void (*put_super) (struct super_block *);
        void (*write_super) (struct super_block *);
        void (*statfs) (struct super_block *, struct statfs *);
        .....
};

Montarea

Cum ajung mai multe sisteme de fișiere (nu neapărat diferite arhitectural) să fie folosite simultan de un sistem Unix? În DOS sau Windows cînd se indică un fișier se indică (poate implicit) și partiția de disc unde fișierul se află (ex.: cu C:).

În Unix lucrurile stau puțin altfel: toate sistemele de fișiere disponibile utilizatorilor sunt montate (cu comanda mount) de către administratorul de sistem, de obicei cînd calculatorul boot-ează. Comanda aceasta (care se folosește de un apel de sistem cu același nume) are două argumente importante: un nume de partiție, și un nume de director2.

De exemplu, presupunînd că am un hard disc numit /dev/hda2, pot să montez (dacă am drepturi de administrator) sistemul de fișiere aflat pe el peste directorul /mnt cu comanda:

mount /dev/hda2 /mnt

Să observăm în trecere că faptul că numai administratorul are dreptul de a monta sisteme de fișiere restrînge mult posibilitatea de propagare a virușilor: un utilizator neprivilegiat nu poate accesa dischete sau discuri străine.

Ce înseamnă montarea? Poate fi montată numai o partiție formatată și pe care se află un sistem de fișiere. Prin montare directorul rădăcină al acelei partiții este identificat cu directorul indicat la comanda mount, în cazul nostru /mnt. După execuția comenzii, de la directorul /mnt în jos se va afla întregul arbore de directoare de pe partiția /dev/hda2.

Am văzut cîteva lucruri foarte simple; am putea spune aproape banale. Uluitor este de pildă faptul că în interior nucleul folosește o singură structură de date, vnodul, pentru a reprezenta zeci de creaturi diferite:

Toate aceste obiecte sunt reprezentate în același fel pentru că oferă fiecare (un subset) al aceleiași interfețe, bazată pe read(), write(), open(), close(), ioctl().

Acum vom încerca să vedem cum funcționează aceste abstracții. Să urmărim deci o serie de operații în sistemul de fișiere.

Sistemul de fișiere în acțiune

Vom parcurge mai multe etape:

  1. Vom vedea de unde știe nucleul să manipuleze atîtea feluri de sisteme de fișiere și obiecte diferite;
  2. Vom vedea cum apare prima partiție montată;
  3. Vom vedea cum celelalte partiții sunt montate;
  4. Vom vedea cum se execută o operație asupra unui fișier care trebuie să traverseze o cărare pe mai multe partiții.

Partea spectaculoasă este că, datorită interfeței identice a tuturor fișierelor din sistem, codul care operează cu fișiere poate fi scris în mare măsură complet independent de natura fișierelor, fie ele DOS, Unix sau NFS sau altceva. Vom vedea că cea mai mare parte a operațiilor se petrec într-un nivel software care se comportă ca un comutator gigantic, care pe măsură ce acționează asupra unor fișiere aflate pe partiții diferite comută între codul feluritelor sisteme de fișiere. Acest comutator se numește Virtual File System Switch, și se abreviază cîteodată cu vfssw.

Plasamentul acestui cod în interiorul nucleului este simbolizat în figura 2.

Figure 2: Sistemul de fișiere în nucleu
\begin{figure}\centerline{\epsfxsize=10cm\epsffile{vfs.eps}}\end{figure}

Compilarea nucleului

Fiecare sistem de fișiere își aranjează altfel datele pe disc; anumite sisteme ca NFS cer colaborarea unui client și a unui server pentru a oferi servicii de fișiere. Fiecare este implementat prin alte proceduri, chiar dacă oferă același set de operații. Administratorul de sistem hotărăște la compilarea nucleului care din sistemele de fișiere disponibile vor face parte din nucleu. Fiecare pune la dispoziție un set de funcții pentru manipularea structurii vfs (cele din structura struct super_operations de mai sus) și pentru manipularea fișierelor însele (vnoduri).

În cazul sistemului de operare Linux fiecare sistem de fișiere are sursele în propriul lui arbore de directoare plecînd din /usr/src/linux/fs/.

Boot-area

Cînd sistemul de operare bootează, înainte de lansarea proceselor, se execută o secțiune de inițializări în care sunt chemate procedurile de inițializare ale tuturor subsistemelor nucleului; fiecare driver se inițializează și apoi sistem de fișiere are ocazia să se inițializeze.

Înainte de a lansa orice proces nucleul montează prima partiție, partiția rădăcină. Această partiție a fost configurată de administratorul de sistem la construirea sistemului. Pe această partiție se găsesc cele mai importante directoare ale sistemului, fără de care acesta nu poate funcționa. Acestea sunt:

/dev/
care conține toate numele fișierelor speciale și inodurile lor; aici fiecare hard-disc are un nume și un număr major și minor3 care indică nucleului ce partiție fizică corespunde fiecărui nume;

/etc/
care conține toate fișierele de configurare ale sistemului și scripturile care se execută la inițializare;

/bin/
care conține toate fișierele executabile esențiale, printre care shell-ul, comanda mount, utilitare de reparat discul, etc.

După montarea partiției nucleul deschide directorul rădăcină care va rămîne deschis pînă la oprirea sistemului. Asta înseamnă că îi alocă un vnod pe care îl inițializează corespunzător și pe care îl păstrează în memorie; o variabilă globală punctează la acest vnod. (Aceste operații sunt executate de nucleul Linux în funcția mount_super() pe care o puteți găsi prin surse într-un loc depinzînd de versiunea nucleului pe care o aveți.)

Din clipa asta sistemul este funcțional.

Să vedem mai departe cum procedează nucleul pentru a monta o nouă partiție în arborele de directoare și cum nucleul procedează pentru a deschide un fișier.

Montarea

Să presupunem că avem doar o partiție montată, cea rădăcină. Să vedem ce face nucleul la executarea comenzii:

mount -t msdos /dev/hda1 /mnt

care îi cere să monteze o partiție cu sistem de fișiere de tip MS-DOS peste directorul /mnt.

Nucleul face următoarele operații:

  1. Descifrează tipul sistemului de fișiere pentru a ști care porțiune de cod (dintre multiplele sisteme de fișiere ale nucleului) o să manipuleze structurile de date de pe această partiție;

  2. Deschide directorul indicat (/mnt); asta înseamnă citirea de pe disc a inodului acelui director într-un vnod din memorie; acest vnod va rămîne deschis atîta vreme cît sistemul de fișiere va fi montat deasupra lui;

  3. Cheamă funcția de inițializare corespunzătoare acestui sistem de fișiere (msdos), care funcție construiește structura VFS (numită în Linux super_block) și îi inițializează cîmpurile, de obicei citind primele sectoare de pe hard-disc din partiția indicată (/dev/hda1);

  4. Deschide fișierul /dev/hda1, care este un fișier special de tip bloc corespunzînd unei partiții de disc;

  5. Deschide directorul rădăcină al partiției montate (/dev/hda1) într-un vnod în memorie;

  6. Marchează faptul că cele două vnoduri (ale directorului /mnt și al rădăcinii partiției noi) practic ``coincid'': asta va permite traversarea unor cărări de genul /mnt/tmp, care încep pe o partiție și se termină pe alta.

Puteți citi codul apelului de sistem mount(2) pentru Linux în fișierul /usr/src/linux/fs/super.c; pe lîngă cele spuse codul mai face o sumedenie de verificări pe care le-am sărit.

Deschiderea unui fișier

Există două mari categorii de apeluri de sistem care operează cu fișiere: apeluri care primesc cărări (path) către un fișier, și care primesc un descriptor de fișier (mîner, handle). Descriptorii am văzut că se obțin deschizînd un fișier cu open(), căreia i se dă o cărare.

Pe scurt, pentru a opera asupra unui fișier trebuie întîi parcursă o cărare pînă la el. După cum știm există două feluri de cărări: relative la directorul curent al procesului sau absolute, care pornesc de la rădăcina sistemelor de fișiere (cele din urmă se scriu începînd cu semnul /). Diferența constă doar în vnodul de la care pornește operația de traversare a cărării: într-un caz este vnodul directorului curent al procesului, care este permanent menținut de nucleu într-o variabilă asociată procesului, iar în celălalt caz directorul rădăcină, care este menținut într-o altă variabilă asociată procesului (în Unix un proces poate să-și schimbe ceea ce crede că este rădăcina întregului arbore de directoare cu apelul de sistem chroot(2)).

Pentru nucleu deschiderea unui fișier înseamnă:

Starea inițială

Să revizuim însă structurile de date ale nucleului așa cum se prezintă ele în această clipă; ele sunt înfățișate în figura 3.

Figure 3: Structurile de date în nucleu
\begin{figure}\centerline{\epsfxsize=10cm\epsffile{structuri.eps}}\end{figure}

Funcția namei

Funcția din nucleu care traduce o cărare într-un vnod în acest fel se numește în mod tradițional în sistemele derivate din Berkeley Unix (și în Linux) namei(), datorită faptului că traduce un nume de fișier într-un i(v)nod. (Numele funcției în sisteme descendente din System V de la AT&T este lookuppn(), de la lookup path name.) Într-un pseudo-cod funcția namei() arată cam așa (cu detalii e mult mai complicată):

vnode * namei(char * carare, vnode * start)
{
        char * componenta;
        vnode * curent = start;

        while (carare) {
                if (carare && !DIRECTOR(curent)) 
                        return NULL;
                componenta = extrage_prima_componenta(carare);
                carare = elimina_prima_componenta(carare);
                if (!strcmp(componenta, "..") &&
                    curent->montat_pe) {
                        if (curent != ROOT_VNODE)
                                curent = curent->montat_pe;
                        continue;
                }
                if (curent->montat_sub) {
                        curent = curent->montat_sub;
                        continue;
                }
                curent = curent->operatii->lookup(curent, componenta);
                if (!curent) 
                        return NULL;
        }
        return curent;
}

Să vedem pas cu pas cum operează nucleul pentru a deschide fișierul /mnt/tmp/a.

  1. Este invocată funcția namei("mnt/tmp/a", ROOT_VNODE);

  2. Pentru că mai avem componenente în cărare trebuie ca curent să fie un vnod de director; se verifică acest lucru;

  3. Este extrasă prima componentă din cărare (mnt); cărarea rămîne tmp/a;

  4. curent = ROOT_VNODE; peste curent nu e montat nimic;

  5. Se cheamă indirect funcția curent->operatii->lookup(curent, "mnt"), care caută în directorul dat de vnodul curent un fișier cu numele mnt și returnează vnodul său de pe aceeași partiție (partiția rădăcină în cazul nostru); aceasta este noua valoare a lui curent; dacă vnodul căutat nu se afla anterior căutării în memorie, funcția lookup() îl alocă și îl citește de pe disc; funcția lookup() construiește și structura de ->operatii din cadrul vnodului, păstrînd operațiile de la directorul în care se caută (dacă două vnoduri sunt pe aceeași partiție sunt deci din același sistem de fișiere și deci au aceleași funcții de acces!);

  6. Se reia bucla while;

  7. Pentru că pe directorul /mnt este montată partiția /dev/hda1, valoarea lui curent->montat_sub este chiar vnodul care este rădăcina acestei partiții (care este în memorie); curent ia această valoare iar bucla se reia prin execuția lui continue;

  8. În clipa asta curent punctează la un vnod dintr-un sistem de fișiere complet diferit (de tip MS-DOS); pe nucleu însă acest lucru nu-l interesează, pentru că vnodul are aceleași operații, inclusiv lookup (chiar dacă ele sunt implementate într-un mod complet diferit);

  9. Într-un mod perfect similar se extrage prima componentă a cărării, tmp care este căutată (cu apelul funcției curent->operatii->lookup(curent, "tmp") în directorul al cărui vnod este curent; se schimbă valoarea lui curent;

  10. O ultimă trecere prin buclă va găsi la fel vnodul fișierului a și-l va încărca în memorie; acesta este rezultatul funcției namei().

O operație pe un fișier

Cheia în implementarea funcției namei este apelul indirect de funcție curent->operatii->lookup(). Vom înțelege la ce folosește acest apel privind modul în care nucleul execută o operație pe un fișier deja deschis; să vedem un posibil cod al apelului de sistem write():

int write(int miner, char * buffer, unsigned cantitate)
{
        struct vnode * v;
        struct file * f;

        f = proces_curent->fisiere_deschise[miner];
        v = f->vnode;
        return v->operatii->write(v, buffer, cantitate);
}

(Am eliminat toate testele de corectitudine a argumentelor.) Nucleul întîi indexează într-un array al procesului curent pentru a găsi vnodul v al fișierului deschis anterior al cărui ``mîner'' a fost returnat utilizatorului (rolul structurii struct file nu ne interesează deocamdată; vom reveni asupra ei într-un alt articol). Apoi nucleul cheamă din nou indirect funcția write, așa cum apare ea între operațiile asociate vnodului găsit.

Frumusețea acestei scheme este următoarea: fiecare sistem de fișiere își organizează altfel datele pe disc; Unix folosește o schemă complicată în care fișierele sunt descrise prin inoduri, MS-DOS descrie blocurile fișierelor printr-o structură numita FAT (file access table), etc. Dar atîta vreme cît ambele sisteme de fișiere pun la dispoziție o funcție care scrie într-un fișier date, nu contează prea tare că această funcție este complet diferit implementată pentru cele două sisteme. Important este că are aceeași interfață!

Fiecare vnod poartă cu el din momentul în care este adus în memorie propriul lui vector de operații. Vnodurile pentru sisteme Unix și vnodurile pentru sisteme de fișiere MS-DOS au ambele o operație write(), care primește aceleași argumente și returnează aceleași rezultate, chiar dacă intern se comportă complet diferit.

Din această cauză utilizatorii pot trata în Unix fișiere de naturi foarte diferite (și nu numai fișiere) ca pe obiecte de același tip.

Concluzie

Există o oarecare independență între operațiile pe care le putem face asupra unui obiect (interfața sa) și modul în care acele operații sunt realizate (implementarea). O interfață bine proiectată poate avea consecințe dramatice.

Cu siguranță flexibilitatea acestei interfețe este unul dintre ingredientele care a asigurat succesul sistemului de operare Unix și a paradigmelor sale. Avem aici un exemplu splendid de modularitate: părți complet diferite constitutiv au aceeași interfață încît pot fi practic substituite una alteia, ca niște bucăți de Lego.

Mai mult de jumătate din sursele C ale unui nucleu sunt drivere (pentru Linux 2.0.30 asta înseamnă aproape o jumătate de milion de linii de cod!). Nici un om nu poate înțelege atît de mult cod. Dar datorită modularității nimeni nu trebuie să înțeleagă toate piesele: trebuie doar să cunoști interfețele; implementarea poate fi oricare. Înțelegînd interfețele înțelegi și cum funcționează întregul.



Footnotes

... Unix1
Am prezentat într-un articol din PC Report din decembrie 1997 această interfață pe larg; o copie a articolului este disponibilă din pagina de web a autorului. Vom relua însă aici ideile esențiale.
... director2
Articolul citat mai sus despre funcționarea sistemelor de fișiere din Unix, din PC Report din decembrie 1997 explică cum se poate numi o partiție folosind un nume ca al unui fișier, și cum nucleul operează cu partițiile. Vom vedea mai jos că nucleul trebuie să aibă de la bootare montată cel puțin o partiție, numită partiția rădăcină (root filesystem) ca să poată continua cu celelalte.
... minor3
Despre numerele majore și minore vedeți articolul citat anterior.