Il Virtual File System
(aprile 1997)

Pubblicato con il permesso del Linux Journal

Questo articolo delinea l'idea del VFS e dà una panoramica di come il kernel di Linux accede al suo filesystem. Le informazioni qui riportate si riferiscono a Linux 2.0.x (per ogni x) e 2.1.y (per y fino almeno a 18). Il modulo di esempio, invece, funziona solo con le versioni 2.0.y.

Il File

Nei sistemi Unix, il ``file'' è l'oggetto più utilizzato: un pathname unico identifica ogni file all'interno di un sistema. Ogni file si comporta come ogni altro file nel modo in cui viene utilizzato e modificato: le stesse chiamate di sistema e gli stessi comandi funzionano con qualsiasi file. Questo succede indipendentemente dal supporto fisico che contiene l'informazione e dal modo in cui l'informazione è organizzata sul supporto. L'astrazione dal dispositivo in cui l'informazione è immagazzinata viene realizzata richiedendo il trasferimento dati ai vari device driver; l'astrazione dal modo in cui l'informazione è organizzata viene ottenuta in Linux tramite il VFS.

Come fa Unix

Linux vede il suo filesystem nello stesso modo di Unix: adotta il concetto di super-blocco, inode, directory e file come vengono usati in Unix. L'albero dei file che viene visto in un determinato momento dipende da come le differenti parti vengono assemblate; ogni ``parte'' in questo caso è rappresentata da una partizione di disco rigido o un altro dispositivo che viene ``montato'' nel sistema. Mentre credo che non ci siano problemi sull'operazione di mount di un albero, credo sia il caso di dare alcune spiegazioni sui concetti di super-blocco, inode, directory e file.

Orientazione agli oggetti

Mentre la lista precedente descrive l'organizzazione teorica dell'informazione, un sistema operativo deve essere in grado di gestire differenti modi di organizzare l'informazione sul disco. Anche se in teoria è possibile cercare una disposizione ottimale delle informazioni ed usare questa struttura per tutti i dischi, la maggior parte degli utenti di calcolatori hanno bisogno di avere accesso ai loro dischi senza bisogno di riformattarli, e talvolta devono poter montare volumi via NFS attraverso la rete, ed a volte addirittura usare quegli strani CD e floppy i cui nomi di file non possono eccedere 8+3 caratteri.

Il problema di poter gestire differenti formati di dati in maniera trasparente è stato affrontato trasformando i super-blocchi, gli inode e i file in ``oggetti'': ogni oggetto dichiara un insieme di operazioni che possono essere usate su di lui. Il kernel eviterà di avere grossi costrutti switch per poter avere accesso a differenti modi di strutturare l'informazione sul disco, e nuovi tipi di filesystem potranno essere aggiunti o rimossi a run-time.

Tutta l'idea del VFS, perciò, è implementata tramite insiemi di operazioni che agiscono su tali oggetti. Ogni oggetto include una struttura dati che elenca le operazioni per agire su di lui, e la maggior parte di tali operazioni (funzioni C) ricevono come argomento un puntatore ``self'' come primo argomento, permettendo perciò la modifica dell'oggetto stesso.

In pratica, un super-blocco contiene un campo struct super_operations *s_op, un inode contiene struct inode_operations *i_op ed un file contiene struct file_operations *f_op.

Tutta la gestione dei dati e la bufferizzazione che viene effettuata dal kernel Linux è indipendente dal formato effettivo dei dati immagazzinati: ogni comunicazione con il supporto di immagazzinamento avviene attraverso una delle strutture operations. Il ``tipo di filesystem'', poi, è il modulo software che si occupa di tradurre le operazioni sull'effettivo meccanismo di immagazzinamento dei dati -- sia esso un dispositivo a blocchi, una connessione di rete (NFS) o virtualmente qualunque altro mezzo per salvare e recuperare dati. Questi moduli software che implementano i tipi di filesystem posso far parte del kernel che viene lanciato o essere compilati come moduli caricabili dinamicamente tramite insmod o kerneld.

L'implementazione attuale di Linux permette di utilizzare i moduli per tutti i tipi di filesystem utilizzati tranne il filesystem root (almeno il filesystem root deve essere montato prima di essere in grado di caricare un file nel kernel). In effetti, il meccanismo initrd permette di caricare un modulo prima di montare il filesytem root, montando temporaneamente come root un ram-disk. Questa ultima tecnica e' solitamente solo utilizzata nei dischetti di installazione.

In questo articolo utilizzo l'espressione ``modulo'' per riferirmi sia ad un modulo caricabile dinamicamente sia ad un decodificatore di filesystem che faccia parte del kernel.

In sintesi, la gestione dei file avviene come descritto qui sotto, e come rappresentato in figura:


Virtual file system image

La figura è anche disponibile in postscript come lj-vfs.ps.


Tipici problemi implementativi

I meccanismi descritti qui sopra per accedere ai dati dei filesystem sono staccati dalla disposizione fisica dei dati sul disco e sono progettati per gestire tutte le semantiche Unix che riguardano i filesystem.

Purtroppo, però, non tutti i tipi di filesystem supportano tutte le funzioni descritte. In particolare non tutti i tipi hanno il concetto di inode, nonostante il kernel identifichi ogni file tramite un numero di inode unsigned long. Se la disposizione dei dati non ha il concetto di inode, il codice che implementa readdir() e read_inode() deve inventare un numero di inode per ciascun file immagazzinato sul disco.

Una tecnica tipica per scegliere il numero di inode è l'utilizzo dell'offset del blocco di controllo del file all'interno dell'area dati del filesystem, assumendo che i file siano identificati da qualcosa che può essere chiamato blocco di controllo. Il filesystem iso9660, per esempio, usa questa tecnica per creare un numero di inode associato ad ogni file.

Il filesystem /proc, d'altro canto, non si appoggia su alcun supporto fisico per estrarre i suoi dati, ed usa perciò numeri predefiniti per i file standard (come /proc/interrupts), ed assegna numeri dinamici per gli altri file. Il numero di inode associato ad ogni file è immagazzinato nella struttura dati associata ad ogni file allocato dinamicamente.

Un altro tipico problema che si incontra nell'implementazione di un tipo di filesystem è la gestione delle limitazioni nelle capacità di immagazzinamento dell'informazione. Per esempio, come reagire quando un utente prova a rinominare un file con un nome più lungo del massimo consentito in quel particolare filesystem, o quando si prova a modificare il tempo di accesso di un file all'interno di un filesystem che non ha il concetto di tempo di accesso.

In questi casi il codice ritornerà il valore -ENOPERM, che significa ``Operation non permitted''. La maggior parte delle funzioni del VFS, come tutte le chiamate di sistema ed un certo numero di altre funzioni del kernel, ritornano zero o un numero positivo in caso di successo, e un numero negativo in caso di errore. I codici di errore ritornati dalle funzioni del kernel sono il negato di uno dei valori definiti in asm/errno.h.

File dinamici in /proc

Vorrei mostrare adesso un po' di codice per giocare con il VFS, ma è abbastanza difficile inventare un filesystem abbastanza piccolo da stare in questo articolo. La scrittura di un nuovo filesystem è sicuramente un compito interessante, ma una implementazione completa include 39 funzioni di tipo ``operazione''. In pratica, c'è veramente bisogno di costruire un altro tipo di filesystem giusto per il gusto di farlo?

Fortunatamanete, il filesystem /proc come definito all'interno del kernel permette ai moduli di giocare con le strutture interne del VFS senza il bisogno di registrare un tipo di filesystem completamente nuovo. Ogni file all'interno di /proc può dichiarare le sue inode_operations e file_operations, ed è perciò in grado di sfruttare tutte le caratteristiche del VFS. L'interfaccia per la creazione di file /proc è abbastanza facile da poter essere presentata qui, senza andare troppo nel dettaglio. I file /proc dinamici vengono chiamati così perchè il loro numero di inode viene allocato dinamicamente al momento della creazione del file, invece di essere estratto da una tabella di inode o essere generato da un numero di blocco.

In questa parte dell'articolo costruiremo un modulo chiamato burp, che sta per ``Bella ed Utile Risorsa per Provare''. Non mostrerò qui nel testo tutto il codice del modulo in quanto la struttura interna di ciascun file che verrà creato non è direttamente collegata con il tema di questo articolo. L'intero modulo, burp.c, può comunque essere compilato e provato da chiunque abbia accesso come root su di una macchina Linux.

La struttura principale usata nella costruzione dell'albero dei file in /proc è struct proc_dir_entry: una di tali strutture è associata a ciascun file all'interno di /proc e viene usata per tenere traccia dell'albero dei file. Le operazioni readdir() e lookup() di default relative al filesystem utilizzano un albero di struct proc_dir_entry per restituire informazioni al processo nello spazio utente.

Il modulo burp, equipaggiato con le strutture necessarie, crea tre file: /proc/root è il dispositivo a blocchi associato alla partizione di root del sistema; /proc/insmod è un'interfaccia per caricare/scaricare i moduli senza bisogno di diventare root; /proc/jiffies legge il valore corrente del contatore dei jiffies (cioè il numero di interruzioni del clock a partire dall'avvio del sistema). Questi tre file non hanno nessun valore reale e servono solo a mostrare come vengono usate le file_operations e le inode_operations. Come si nota, burp è in effetti un ``Banale Utilizzo delle Risorse di Proc''. Per evitare che la trattazione diventi troppo noiosa, non descriverò qui i dettagli del caricamento/scaricamento del modulo: tali dettagli sono già stati descritti nei precedenti articoli del Pluto Journal.

La creazione e la distruzione di un file in /proc viene effettuata chiamanto le seguenti funzioni:


proc_register_dynamic(struct proc_dir_entry *where,
                      struct proc_dir_entry *self);
proc_unregister(struct proc_dir_entry *where, int inode);

In entrambe le funzioni, where è la directory a cui il nuovo file appartiene: burp utilizza &proc_root come argomento per specificare la root-directory del filesystem. La struttura self, d'altra parte, è dichiarata all'interno di burp.c per ciascuno dei tre file. La definizione di proc_dir_entry è riportata qui sotto.


struct proc_dir_entry {
        unsigned short low_ino;  /* inode number for the file */
        unsigned short namelen;  /* lenght of filename */
        const char *name;        /* the filename itself */
        mode_t mode;             /* mode (and type) of file */
        nlink_t nlink;           /* number of links (1 for files) */
        uid_t uid;               /* owner */
        gid_t gid;               /* group */
        unsigned long size;      /* size, can be 0 if not relevant */
        struct inode_operations * ops; /* inode ops for this file */
        int (*get_info)(char *, char **, off_t, int, int);  /* read data */
        void (*fill_inode)(struct inode *);  /* fill missing inode info */
        struct proc_dir_entry *next, *parent, *subdir; /* internal use */
        void *data;              /* used in sysctl */
};

La parte ``sincrona'' di burp si riduce perciò a tre linee all'interno di init_module() e tre all'interno di cleanup_module(). Tutto il resto viene gestito dall'interfaccia VFS ed è ``event-driven'' per quanto un processo che accede ad un file può essere considerato un evento (si, so che questo modo di pensare le cose è eterodosso, e sconsiglio di usare queste espressioni in ambiti professionali o accademici).

Le tre linee in init_module() assomiglieranno dunque a: "proc_register_dynamic(&proc_root, &burp_proc_root);", mentre quelle in cleanup_module() saranno come "proc_unregister(&proc_root, burp_proc_root.low_ino);".

Il campo low_ino è qui il numero di inode per il file che viene rimosso da /proc, ed è stato dinamicamente assegnato a load-time.

Ma come risponderanno questi file all'azione dell'utente? Vediamo ognuno di essi indipendentemente.

La sessione mostrata qui sotto fa vedere come tali file appaiono e come due di esse funzionano. Il codice incluso successivamente mostra le tre strutture usate per dichiarare i file in /proc. Le strutture non sono state definite completamente in quanto il compilatore C riempie con degli zeri le strutture parzialmente definite senza per questo generare dei messaggi di warning (questa è una caratteristica intenzionale del compilatore).


morgana% ls -l /proc/root /proc/insmod /proc/jiffies
--w--w--w-   1 root     root            0 Feb  4 23:02 /proc/insmod
-r--r--r--   1 root     root           11 Feb  4 23:02 /proc/jiffies
brw-------   1 root     root       3,   1 Feb  4 23:02 /proc/root
morgana% cat /proc/jiffies
0002679216
morgana% cat /proc/modules
burp               1            0
morgana% echo isofs ">" /proc/insmod
morgana% cat /proc/modules
isofs              5            0 (autoclean)
burp               1            0
morgana% echo -isofs ">" /proc/insmod
morgana% cat /proc/jiffies
0002682697
morgana%



struct proc_dir_entry burp_proc_root = {
    0,                  /* low_ino: the inode -- dynamic */
    4, "root",          /* len of name and name */
    S_IFBLK | 0600,     /* mode: block device, r/w by owner */
    1, 0, 0,            /* nlinks, owner (root), group (root) */
    0, &blkdev_inode_operations,  /* size (unused), inode ops */
    NULL,               /* get_info: unused */
    burp_root_fill_ino, /* fill_inode: tell your major/minor */
    /* nothing more */
};

struct proc_dir_entry burp_proc_insmod = {
    0,                  /* low_ino: the inode -- dynamic */
    6, "insmod",        /* len of name and name */
    S_IFREG | S_IWUGO,  /* mode: REGular, Write UserGroupOther */
    1, 0, 0,            /* nlinks, owner (root), group (root) */
    0, &burp_insmod_iops, /* size - unused; inode ops */
};

struct proc_dir_entry burp_proc_jiffies = {
    0,                  /* low_ino: the inode -- dynamic */
    7, "jiffies",       /* len of name and name */
    S_IFREG | S_IRUGO,  /* mode: regular, read by anyone */
    1, 0, 0,            /* nlinks, owner (root), group (root) */
    11, NULL,           /* size is 11; inode ops unused */
    burp_read_jiffies,  /* use "get_info" instead */
};

Il modulo è stato compilato e provato su di un PC, un Alpha ed una Sparc, tutte con un kernel 2.0.x

L'implementazione attuale di /proc ha altre interessanti caratteristiche da offrire, la più interessante delle quali è l'implementazione di sysctl(). L'idea è così interessante che non trova spazio qui, e sarà l'argomento di un articolo in un altro numero del Pluto Journal.

Esempi interessanti

La mia presentazione è finita, ma ci sono vari posti in cui trovare del codice interessante riguardante il VFS. Implementazioni particolarmente interessanti sono le seguenti:

Di Alessandro Rubini

Verbatim copying and distribution of this entire article is permitted in any medium, provided this notice is preserved