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 dimount
di un albero, credo sia il caso di dare alcune spiegazioni sui concetti di super-blocco, inode, directory e file. - Il ``super-blocco'' deve il suo nome alle sue origini storiche, quando la meta-informazione riguardo al disco (o alla partizione) era immagazzinata nel primo settore del disco stesso. Al giorno d'oggi il super-blocco non corrisponde più ad un blocco di dati del disco, ma è ancora la struttura dati che contiene le informazioni riguardo al filesystem di cui fa parte. La struttura dati in Linux si chiama
struct super_block
, e contiene diverse informazioni di gestione, come i flag passati al comandomount
, l'istante nel quale il disco è stato montato e dimensione del dispositivo. Il kernel 2.0 usa un vettore statico di 64 di tali strutture per essere in grado di montare fino a 64 dispositivi. - Un ``inode'' (index-node) è associato ad ogni file. L'inode contiene tutte le informazioni riguardanti un file tranne che il suo nome ed i suoi dati: il proprietario, il gruppo, i permessi e la dimensione dei dati contenuti nel file, come pure il numero di ``link'' relativi a tale file e altre informazioni. L'idea di separare le informazioni dal nome del file e dai suoi dati è quello che permette l'implementazione degli ``hard link'' -- come pure permette di usare le notazione `punto' e `punto-punto' per le directory senza il bisogno di trattare tali nomi come speciali. Un inode all'interno del kernel viene descritto da una
struct inode
. - La ``directory'' è un file che associa i nomi dei file agli inode. Il kernel non usa nessuna struttura dati particolare per rappresentare una directory, che viene trattata come un file normale nella maggior parte delle situazioni. Una directory viene letta e modificata tramite funzioni specifiche a ciascun filesystem, indipendentemente da come l'informazione è immagazzinata sul disco.
- Il ``file'' è qualcosa associato ad un inode. Di solito i file sono aree dati, ma possono anche essere directory, dispositivi, FIFO o socket. Un `` file aperto'' è rappresentato nel kernel da una struttura
struct file
: tale struttura contiene un puntatore all'inode che identifica il file. Le strutturefile
vengono create dalle chiamate di sistema comeopen()
,pipe()
esocket()
, esse vengono condivise tra processo padre e figlio attraverso la chiamatafork()
.
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:
La figura è anche disponibile in postscript come lj-vfs.ps.
struct file_system_type
è una struttura che dichiara solo il nome del filesystem ed una funzioneread_super()
. Quandomount
viene eseguito, la funzione riceve informazioni riguardo il dispositivo che viene montato e deve riempire una strutturasuper_block
. La funzione deve anche caricare l'inode della directory root del filesystem all'interno disb->s_mounted
, dovesb
è il super-blocco che viene riempito. Il campo aggiuntivorequires_dev
viene usato da ciascun tipo di filesystem per dichiarare se tale tipo ha bisogno di un dispositivo a blocchi oppure no: per esempio, NFS e/proc
non usano un dispositivo a blocchi, mentreext2
eiso9660
si. Dopo che il super-blocco viene riempito, la strutturafile_system_type
non viene più usata; solo il super-blocco conterrà un puntatore a tale struttura per poter essere in grado di ritornare informazioni all'utente (/proc/mounts
è un esempio di tale informazione di ritorno). La struttura è definita come segue:struct file_system_type { struct super_block *(*read_super) (struct super_block *, void *, int); const char *name; int requires_dev; struct file_system_type * next; /* there's a linked list of types */ };
- La strutture
super_operations
viene usata dal kernel per leggere e scrivere gli inode, salvare le informazioni del super-blocco sul disco e raccogliere statistiche (per poter rispondere alle chiamate di sistemastatfs()
efstatfs()
). Quando un filesystem viene infine smontato, l'operazioneput_super()
viene chiamata -- nel lessico del kernel, ``get'' vuol dire `alloca e riempi', ``read'' vuol dire riempi e ``put'' vuol dire `rilascia'. Lesuper_operations
dichiarate da ciascun tipo di filesystem sono le seguenti:struct super_operations { void (*read_inode) (struct inode *); /* fill the structure */ int (*notify_change) (struct inode *, struct iattr *); 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 *, int); int (*remount_fs) (struct super_block *, int *, char *); };
- Dopo la creazione di una copia in memoria di un inode, il kernel agirà su di lui tramite le sue proprie operazioni.
struct inode_operations
è il secondo insieme di operazioni che viene dichiarato da ciascun tipo di filesystem, e sono elencate qui sotto: come si vede tali operazioni si occupano principalmente della gestione dell'albero delle directory. Le operazioni di gestione delle directory fanno parte delle operazioni relative agli inode perchè l'implementazione di una appositadir_operations
sarebbe risultato in esecuzioni condizionali aggiuntive per ciascun accesso al filesystem. Invece si è scelto di far fare il controllo degli errori all'interno di ciascuna operazione, e le operazioni che hanno solo senso per le directory si rifiuteranno di operare su altri tipi di file. Il primo campo delle operazioni sugli inode definisce le operazioni per agire sui file regolari: se invece l'inode si riferisce ad una FIFO, un socket oppure un dispositivo, allora verranno usate le operazioni specifiche per questi file. Le operazioni sugli inode sono elencate sotto: la versione 2.0.1 del kernel ha cambiato la definizione direname()
rispetto alla versione 2.0.0.struct inode_operations { struct file_operations * default_file_ops; 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); int (*symlink) (struct inode *,const char *,int,const char *); int (*mkdir) (struct inode *,const char *,int,int); int (*rmdir) (struct inode *,const char *,int); int (*mknod) (struct inode *,const char *,int,int,int); int (*rename) (struct inode *,const char *,int, struct inode *, const char *,int, int); /* this from 2.0.1 onwards */ int (*readlink) (struct inode *,char *,int); int (*follow_link) (struct inode *,struct inode *,int,int,struct inode **); int (*readpage) (struct inode *, struct page *); int (*writepage) (struct inode *, struct page *); int (*bmap) (struct inode *,int); void (*truncate) (struct inode *); int (*permission) (struct inode *, int); int (*smap) (struct inode *,int); };
- Infine, le
file_operations
specificano come agire sui dati all'interno di un file regolare: le operazioni implementano i dettagli di basso livello delle chiamate di sistemaread()
,write()
,lseek()
e le altre funzioni che agiscono sui dati. Siccome la stessa strutturafile_operations
viene usata per accedere ai dispositivi, essa contiene anche alcuni campi che hanno solo senso per i dispositive a carattere e a blocchi. E` interessante notare che la versione 2.1 del kernel ha cambiato i prototipi diread()
,write()
edlseek()
in modo da permettere un'estensione maggiore degli offset nei file. Le operazioni come appaiono in Linux-2.0 sono mostrate qui sotto.struct file_operations { int (*lseek) (struct inode *, struct file *, off_t, int); int (*read) (struct inode *, struct file *, char *, int); int (*write) (struct inode *, struct file *, const char *, int); int (*readdir) (struct inode *, struct file *, void *, filldir_t); int (*select) (struct inode *, struct file *, int, select_table *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct inode *, struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); void (*release) (struct inode *, struct file *); int (*fsync) (struct inode *, struct file *); int (*fasync) (struct inode *, struct file *, int); int (*check_media_change) (kdev_t dev); int (*revalidate) (kdev_t dev); };
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.
/proc/root
è un dispositivo a blocchi. Il suo `modo' deve perciò avere acceso il bitS_IBLK
, le sueinode_operations
dovranno essere quelle dei dispositivi a blocchi e il suo numero di dispositivo dovrà essere quello del filesystem root attuale. Siccome il numero di dispositivo associato all'inode non è parte diproc_dir_entry
, il campofill_inode
deve essere usato. Il numero di dispositivo del filesystem root verrà estratto dalla tabella delle partizioni attualmente montate./proc/insmod
è un file scrivibile: necessità perciò delle suefile_operations
in modo da dichiarare la sua funzione di scrittura. Questo file dichiara perciò le sueinode_operations
che puntano alle suefile_operations
. Ogni volta che la sua funzionewrite()
viene invocata, il file chiede a kerneld di caricare o scaricare il modulo il cui nome è stato scritto. Il file è scrivibile da chiunque, ma questo non è un gran problema in quanto caricare un modulo non significa accedere all'hardware che questo controlla, e cosa può essere caricato è ancora controllato da/etc/modules.conf
, di proprietà di root./proc/jiffies
è molto più facile: il file viene solo letto. La versione 2.0 e le più recenti del kernel offrono una interfaccia semplificata per i file in sola lettura: il puntatore a funzioneget_info
all'interno diproc_dir_entry
, viene usato per richiedere il riempimento di una pagina di dati ogni volta che il file viene letto. Perciò,/proc/jiffies
non ha bisogno di dichiarare le sue propriefile_operations
oinode_operatins
, ma usa semplicementeget_info()
. Questa funzione chiama poisprintf()
per convertire il numero interojiffies
in una stringa.
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:- Ovviamente, il filesystem
/proc
: è abbastanza semplice da capire in quanto non è critico per le prestazioni e nemmeno troppo pieno di cose, tranne che per l'implementazione di sysctl. - Il filesystem ``umsdos'': anche questo è parte del kernel ufficiale, e si appoggia sul filesystem
msdos
. Questo modulo implementa soltanto alcune delle operazioni del VFS per aggiungere nuove possibilità ad un filesystem vecchio ed inefficiente. - Il modulo ``userfs'': disponibile sia da
tsx-11
che dasunsite
sottoALPHA/userfs
. La versione 0.9.3 funziona con Linux-2.0. Il modulo definisce un nuovo filesystem che usa programmi esterni per recuperare i dati. Applicazioni interessanti sono il filesystem FTP ed un filesystem in sola lettura per montare i file tar compressi. Nonostante l'utilizzo di programmi per ottenere dati da un filesystem sia molto pericoloso e possa portare ad un blocco del sistema, l'idea è abbastanza interessante. - ``supermount'': il filesystem è disponibile su
sunsite
e mirror. Questo tipo di filesystem è in grado di montare dispositivi rimuovibili come i dischetti e i CD e gestisce la rimozione dei dischi senza costringere l'utente a smontare e rimontare la periferica. Il modulo lavora controllando un altro tipo di filesystem facendo in modo di tenere il dispositivo smontato quando non viene utilizzato. L'operazione è trasparente all'utente. - ``ext2'': il filesystem extended-2 è lo standard per Linux da qualche anno a questa parte. È codice molto difficile ma che val la pena di leggere per chi è interessato a vedere come un filesystem reale è implementato. Questo modulo contiene anche degli agganci per interessanti trovate riguardanti la sicurezza, come i flag di file immutabile e append-only. Se un file è immutabile o append-only può essere cancellato solo quando il calcolatore è in single-user, ottenendo perciò una certa sicurezza dagli attacchi via rete.
Se ti è piaciuto l'articolo, iscriviti al feed per tenerti sempre aggiornato sui nuovi contenuti del blog: