SendMail About Copertina indice |
Articoli
Il Virtual File System
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.
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.
mount
di un albero, credo sia il caso di dare alcune
spiegazioni sui concetti di super-blocco, inode, directory e file.
struct super_block
, e contiene
diverse informazioni di gestione, come i flag passati al comando
mount
, 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.
struct inode
.
struct file
: tale struttura contiene un
puntatore all'inode che identifica il file. Le strutture
file
vengono create dalle chiamate di sistema come
open()
, pipe()
e socket()
, esse
vengono condivise tra processo padre e figlio attraverso la chiamata
fork()
.
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 funzione
read_super()
. Quando mount
viene eseguito,
la funzione riceve informazioni riguardo il dispositivo che viene
montato e deve riempire una struttura super_block
. La
funzione deve anche caricare l'inode della directory root del
filesystem all'interno di sb->s_mounted
, dove
sb
è il super-blocco che viene riempito. Il campo
aggiuntivo requires_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, mentre ext2
e iso9660
si. Dopo che il super-blocco viene riempito, la struttura
file_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 */
};
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 sistema statfs()
e
fstatfs()
). Quando un filesystem viene infine smontato,
l'operazione put_super()
viene chiamata -- nel lessico
del kernel, ``get'' vuol dire `alloca e riempi', ``read'' vuol dire
riempi e ``put'' vuol dire `rilascia'. Le
super_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 *);
};
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 apposita
dir_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 di
rename()
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);
};
file_operations
specificano come agire sui
dati all'interno di un file regolare: le operazioni implementano i
dettagli di basso livello delle chiamate di sistema
read()
, write()
, lseek()
e le
altre funzioni che agiscono sui dati. Siccome la stessa struttura
file_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 di
read()
, write()
ed lseek()
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
.
/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 bit S_IBLK
, le
sue inode_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 di
proc_dir_entry
, il campo fill_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 sue file_operations
in modo da dichiarare la sua funzione di scrittura. Questo file
dichiara perciò le sue inode_operations
che
puntano alle sue file_operations
. Ogni volta che la sua
funzione write()
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 funzione get_info
all'interno di
proc_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 proprie file_operations
o
inode_operatins
, ma usa semplicemente
get_info()
. Questa funzione chiama poi
sprintf()
per convertire il numero intero
jiffies
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.
/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.
msdos
. Questo
modulo implementa soltanto alcune delle operazioni del VFS per
aggiungere nuove possibilità ad un filesystem vecchio ed
inefficiente.
tsx-11
che
da sunsite
sotto ALPHA/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.
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.
SendMail About Copertina indice |