Sto iniziando una serie di post su ZFS. Cercherò di fornire il più possibile diagrammi architetturali ed esempi di codice. La serie è rivolta a chi vuole saperne di più sull'architettura generale del filesystem, in particolare su ZFS (OpenZFS).
Per ora mi limiterò all'implementazione e all'architettura su Illumos, il fork open source di OpenSolaris.
Cominciamo con l'astrazione di livello superiore, il Virtual File System. Come la maggior parte dei sistemi Unix, Illumos utilizza un framework chiamato Virtual File System (VFS) che rappresenta un livello di astrazione sotto il quale possono essere implementati più filesystem concreti. Una delle prime versioni di VFS è stata introdotta in SunOS 2.0 con l'introduzione di NFS (Network file system). È bene sapere che l'implementazione su Illumos è stata in realtà la prima mai realizzata per quanto riguarda i file system virtuali. L'implementazione di SunOS è stata quella utilizzata nella prima versione commerciale di un sistema operativo Unix. Sistema Unix V release 4.
VFS consente di astrarre quasi tutti gli oggetti come file e filesystem. Citeremo alcune delle categorie di file system oggi in uso:
All'interno di VFS ci sono due concetti chiave. Il primo è il file virtuale, astratto come oggetto vnode, mentre il secondo è l'oggetto virtual file system o vfs. Un vnode fornisce funzioni relative ai file e vfs fornisce funzioni relative al file system. In Illumos le operazioni di vnode sono definite in usr/src/uts/common/sys/vnode.h e sono contenute nella struttura vnodeops_t.
/*
* Operazioni sui vnodi. Nota: i file system non devono mai operare direttamente
* su una struttura 'vnodeops' -- cambierà nelle versioni future! Essi
* devono usare vn_make_ops() per creare la struttura.
*/
typedef struct vnodeops {
const char *vnop_name;
VNODE_OPS; /* Firme di tutte le operazioni dei vnodi (vops) */
} vnodeops_t;
Per citare alcune delle operazioni dei vnodi disponibili: open, close, read, write, seek, sync. Tutte le firme delle funzioni si trovano in VNODE_OPS.
Le funzioni vnode e vfs delegano alle implementazioni concrete del filesystem. Tutte le funzioni relative ai file raggiungono il livello vfs/vnode attraverso una chiamata di sistema e da lì vengono indirizzate all'implementazione appropriata del filesystem. Il diagramma seguente mostra l'architettura di livello superiore del VFS.
I file sono referenziati nello spazio di processo tramite i descrittori di file. I descrittori di file sono numeri interi, nello specifico un int di tipo C. Ad esempio, i 3 descrittori di file standard POSIX corrispondenti ai 3 flussi: STDIN - 0, STDOUT - 1 e STDERR - 2. I descrittori di file vengono assegnati quando il file viene aperto e poi liberati quando il file viene chiuso.
Ogni processo ha un proprio elenco di file. Le informazioni sui file per ogni processo in Illumos sono conservate in una struttura chiamata uf_info_t. Questa si trova in usr/src/uts/common/sys/user.h.
/*
* Informazioni sui file per processo.
*/
typedef struct uf_info {
kmutex_t fi_lock; /* vedere sotto */
int fi_badfd; /* descrittore di file difettoso # */
int fi_action; /* azione da intraprendere in caso di utilizzo di un fd non corretto */
int fi_nfiles; /* numero di voci in fi_list[] */
uf_entry_t *volatile fi_list; /* elenco di file corrente */
uf_rlist_t *fi_rlist; /* elenchi di file ritirati */
} uf_info_t;
Gli elenchi di file sono indicizzati dal descrittore di file intero. Vediamo ora come si può arrivare normalmente a questa informazione, l'elenco di file corrente. In usr/src/uts/common/sys/user.h è definita la struttura user_t che contiene tutti i dati per processo relativi a un utente. Attraverso user_t, accedendo al suo campo u_finfo (di tipo uf_info_t), si accede alle informazioni sui file per processo e all'elenco dei file correnti. Un buon esempio è nel codice di Dtrace (dtrace.c): uf_info_t *finfo = &curthread->t_procp->p_user.u_finfo. Esistono strumenti che permettono di vedere l'elenco fi_list in base all'ID del processo, come ad esempio pfiles.
L'elenco dei file contiene elementi del tipo uf_entry_t. La definizione di uf_entry_t si trova ancora una volta nello stesso file usr/src/uts/common/sys/user.h.
/*
* Voce nell'elenco dei file aperti per processo.
* Nota: solo alcuni campi vengono copiati in flist_grow() e flist_fork().
* Questo è indicato tra parentesi nei commenti dei membri della struttura.
*/
typedef struct uf_entry {
kmutex_t uf_lock; /* blocco per-fd [mai copiato] */
struct file *uf_file; /* puntatore a file [grow, fork] */
struct fpollinfo *uf_fpollinfo; /* stato del poll [grow] */
int uf_refcnt; /* LWP che accedono a questo file [grow] */
int uf_alloc; /* allocazione del sottoalbero destro [grow, fork] */
short uf_flag; /* flag di fcntl F_GETFD [grow, fork] */
short uf_busy; /* il file è allocato [grow, fork] */
kcondvar_t uf_wanted_cv; /* in attesa di setf() [mai copiato] */
kcondvar_t uf_closing_cv; /* in attesa di close() [mai copiato] */
struct portfd *uf_portfd; /* associata alla porta [crescere] */
/* Evitare la falsa condivisione - pad alla granularità di coerenza (64 byte) */
char uf_pad[64 - sizeof (kmutex_t) - 2 * sizeof (void*) -
2 * sizeof (int) - 2 * sizeof (short) -
2 * sizeof (kcondvar_t) - sizeof (struct portfd *)];
} uf_entry_t;
Come possiamo vedere, uf_entry_t contiene il puntatore al file nel suo campo: uf_file. Diamo un'occhiata più avanti nel percorso, per vedere il riferimento tra il puntatore al file e il vnode ad esso collegato. In Illumos la definizione della struct file risiede in usr/src/uts/common/sys/file.h.
/*
* Una struttura di file viene allocata per ogni chiamata open/creat/pipe.
* L'uso principale è quello di contenere il puntatore di lettura/scrittura associato a
* ogni file aperto.
*/
typedef struct file {
kmutex_t f_tlock; /* blocco a breve termine */
ushort_t f_flag;
ushort_t f_flag2; /* flag extra (FSEARCH, FEXEC) */
struct vnode *f_vnode; /* puntatore alla struttura vnode */
offset_t f_offset; /* puntatore a caratteri di lettura/scrittura */
struct cred *f_cred; /* credenziali dell'utente che l'ha aperto */
struct f_audit_data *f_audit_data; /* dati di audit del file */
int f_count; /* conteggio dei riferimenti */
} file_t;
Dal puntatore al file si può ora raggiungere il vnode collegato a livello di sistema attraverso il campo f_vnode. Più processi possono avere riferimenti allo stesso vnode. Siamo quasi alla fine del livello del filesystem virtuale, mentre ci spostiamo verso l'implementazione concreta del filesystem. L'ultima barriera è fondamentalmente il vnode. La definizione di vnode risiede in usr/src/uts/common/sys/vnode.h.
typedef struct vnode {
kmutex_t v_lock; /* protegge i campi del vnodo */
uint_t v_flag; /* flag del vnode (vedi sotto) */
uint_t v_count; /* conteggio dei riferimenti */
void *v_data; /* dati privati per fs */
struct vfs *v_vfsp; /* ptr al VFS contenente */
struct stdata *v_stream; /* flusso associato */
enum vtype v_type; /* tipo di vnodo */
dev_t v_rdev; /* dispositivo (VCHR, VBLK) */
...
} vnode_t;
Il punto di ingresso nell'implementazione specifica del filesystem è il campo v_data all'interno della struttura vnode_t. Avendo ZFS sotto di sé, v_data punterà a uno znode. In ZFS lo znode è l'equivalente dell'inode UFS. Il v_data viene lanciato in una struttura znode_t attraverso la macro: #define VTOZ(VP) ((znode_t *)(VP)->v_data). Maggiori informazioni su znode e sull'implementazione delle operazioni posix di ZFS nei prossimi post.
Facciamo un breve riepilogo del percorso del codice per arrivare dai dati utente a livello di processo all'implementazione del livello Posix di ZFS.
proc_t processo;
user_t p_user = process->p_user; // la struttura dell'utente
uf_info_t file_info = p_user->u_finfo; // informazioni sui file per processo
uf_entry_t file_entry = file_info->fi_list[0]; // Questo nel caso in cui abbiamo fd 0
file_t p_file = file_entry->uf_file; // puntatore alla struttura del file
vnode_t file_vnode = p_file->f_vnode; // il vnode
(znode_t *)file_vnode->v_data // ha raggiunto lo znode ZFS
Il prossimo post si addentrerà nel livello posix di ZFS e nel percorso del codice di alcune chiamate sys.
Buona codifica.