File system unionali: Implementazioni, parte I


Questo articolo ti è stato portato dagli abbonati di LWN

Gli abbonati a LWN.net hanno reso possibile questo articolo – e tutto ciò che lo circonda. Se apprezzate il nostro contenuto, per favore comprate un abbonamento e rendete possibile la prossima serie di articoli.

25 marzo 2009

Questo articolo è stato contribuito da Valerie Aurora (ex Henson)

Nell’articolo della scorsa settimana, ho esaminato i casi d’uso, i concetti di base e i comuni problemi di progettazione dei file system di unione. Questa settimana, descriverò diverse implementazioni di unioning file system in dettaglio tecnico. I file system di unioning che coprirò in questo articolo sono le uniondirectory di Plan 9, gli union mount di BSD, gli union mount di Linux. Il prossimo articolo coprirà unionfs, aufs, e possibilmente uno o due altri filesystem di unioning, e concluderà la serie.

Per ogni file system, descriverò la sua architettura di base, le caratteristiche e l’implementazione. La discussione dell’implementazione si concentrerà in particolare sui whiteout e sulla lettura delle directory. Concluderò con uno sguardo agli aspetti di ingegneria del software di ogni implementazione; per esempio, la dimensione e la complessità del codice, l’invasività e l’onere per gli sviluppatori di file system.

Prima di leggere questo articolo, potreste voler controllare il resoconto appena pubblicato da Andreas Gruenbacher del workshop Union Mount tenuto lo scorso novembre. È un buon riassunto delle caratteristiche dei file system di unione che sono più urgenti per gli sviluppatori di distribuzioni. Dall’introduzione: “Tutti i casi d’uso a cui siamo interessati si riducono fondamentalmente alla stessa cosa: avere un’immagine o un filesystem che viene usato in sola lettura (o perché non è scrivibile, o perché non si vuole scrivere sull’immagine), e fingere che questa immagine o filesystem sia scrivibile, memorizzando le modifiche da qualche altra parte.”

Plan 9 union directories

Il sistema operativo Plan 9 (sfogliare il codice sorgente qui) implementa l’unione nel suo speciale Plan 9way. Nelle directory di unione di Plan 9, solo lo spazio dei nomi delle directory di primo livello viene unito, non le sottodirectory. Senza i vincoli imposti dagli standard UNIX, le directory di unione di Plan 9 non implementano i whiteout e non schermano nemmeno le voci duplicate: se lo stesso nome di file appare in due file system, viene semplicemente riportato due volte negli elenchi delle directory.

Una directory di unione del Piano 9 viene creata così:

 bind -a /home/val/bin/ /bin

Questo causerebbe che la directory/home/val/binvenga montata “dopo” (l’opzione-a)/bin; altre opzioni sono di mettere la nuova directory prima di quella esistente, o di sostituire interamente la directory esistente. (Questo mi sembra uno strano ordinamento, poiché mi piace che i comandi nel miobin/personale abbiano la precedenza sui comandi a livello di sistema, ma questo è l’esempio dalla documentazione di Plan 9). Brian Kernighane spiega uno degli usi delle directory di unione: “Questo meccanismo di uniondirectory sostituisce il percorso di ricerca delle convenzionali shell UNIX. Per quanto vi riguarda, tutti i programmi eseguibili sono in /bin”. Le uniondirectory possono teoricamente sostituire molti usi dei fondamentali blocchi di costruzione UNIX dei collegamenti simbolici e dei percorsi di ricerca.

Senza whiteout o eliminazione di duplicati, readdir()le uniondirectory sono banali da implementare. Gli offset delle voci delle directory dal file system sottostante corrispondono direttamente all’offset in byte della voce della directory dall’inizio della directory stessa. Una directory unita è trattata come se il contenuto delle directory sottostanti fosse concatenato insieme.

Il piano 9 implementa un’alternativa a readdir() che vale la pena notare, dirread().dirread() restituisce strutture di tipo Dir, descritte nella pagina man di stat(). La parte importante di Dir è il membro Qid. Un Qid è:

…una struttura contenente i campi path e vers: path è garantito essere unico tra tutti i nomi di percorso attualmente sul file server, e vers cambia ogni volta che il file viene modificato. Il percorso è un long long (64 bit, vlong) e la vers è un unsigned long (32 bit, ulong).

Perché questo è interessante? Uno dei motivi per cui readdir() è così difficile da implementare è che restituisce il membro d_off di struct dirent, un unico off_t (32 bit a meno che l’applicazione non sia compilata con supporto per file grandi), per marcare la voce della directory dove un’applicazione dovrebbe continuare a leggere alla prossima chiamata readdir(). Questo funziona bene finché d_off è un semplice byte di offset in un file piatto di meno di 232 byte e le voci di directory esistenti non vengono mai spostate – non è il caso di molti filesystem moderni (XFS, btrfs, ext3 con indici htree). Il 96-bit Qid è un segnaposto molto più utile del 32 o 64-bit off_t. Per un buon riassunto dei problemi coinvolti nell’implementazione di readdir(), leggete l’eccellente post di TheodoreY. Ts’o sull’argomento nella mailing list di btrfs.

Dal punto di vista dell’ingegneria del software, le directory di unione di Plan 9 sono un paradiso. Senza whiteout, eliminazione di voci duplicate, complicati offset di directory, o fusione di namespace oltre la directory di primo livello, l’implementazione è semplice e facile da mantenere.Tuttavia, qualsiasi implementazione pratica di file system di unione per Linux (o qualsiasi altro UNIX) dovrebbe risolvere questi problemi. Per i nostri scopi, le directory di unione di Plan 9 servono principalmente come ispirazione.

BSD union mounts

BSD implementa due forme di unione: l’opzione"-o union"del comandomount, che produce una directory di unione simile a quella di Plan 9, e il comandomount_unionfs, che implementa un file system di unione più completo con whiteout e fusione dell’intero spazio dei nomi. Ci concentreremo su quest’ultimo.

Per questo articolo, usiamo due fonti per i dettagli specifici dell’implementazione: l’implementazione originale BSD union mount come descritto nel documento USENIX 1995Unionmounts in 4.4BSD-Lite, e la pagina man e il codice sorgente di FreeBSD7.1 mount_unionfs. AltriBSD possono variare.

Una directory può essere montata per unione sia “sotto” che “sopra” una directory esistente o un montaggio per unione, finché il ramo superiore di una unione scrivibile è scrivibile. Sono supportati due modi di whiteout: o un whiteout viene sempre creato quando una directory viene rimossa, o viene creato solo se un’altra voce di directory con quel nome esiste attualmente in un ramo sotto il ramo scrivibile. Sono supportati tre modi per impostare la proprietà e la modalità dei file copiati. Il più semplice ètransparent, in cui il nuovo file mantiene lo stesso proprietario e modo dell’originale. La modalità masquerade rende i file copiati di proprietà di un particolare utente e supporta una serie di opzioni di montaggio per determinare la modalità del nuovo file.La modalità traditional imposta il proprietario all’utente che ha eseguito il comando di montaggio unione, e imposta la modalità secondo la umask al momento del montaggio unione.

Ogni volta che una directory viene aperta, viene creata una directory con lo stesso nome sul livello superiore scrivibile se non esiste già. Dall’articolo:

Creando le directory ombra in modo aggressivo durante la ricerca, l’unionfilesystem evita di dover controllare ed eventualmente creare la catena di directory dalla radice del supporto al punto di una copia.Poiché lo spazio su disco consumato da una directory è trascurabile, la creazione di directory quando vengono attraversate per la prima volta sembrava una migliore alternativa.

Come risultato, un "find /union" risulterà nella copia di ogni directory (ma non delle voci di directory che puntano a non-directory) nel livello scrivibile. Per la maggior parte delle immagini di file system, questo userà una quantità trascurabile di spazio (meno che, per esempio, lo spazio riservato all’utente root, o quello occupato dagli inode inutilizzati in un filesystem di tipo FFS).

Un file viene copiato fino al livello superiore quando viene aperto con permesso di scrittura o gli attributi del file vengono cambiati. (Poiché le directory vengono copiate quando vengono aperte, la directory che le contiene è garantita esistere già sul livello scrivibile). Se il file da copiare ha più hard link, gli altri link vengono ignorati e il nuovo file ha un numero di link pari a uno. Questo può rompere le applicazioni che usano gli hard link e si aspettano che le modifiche attraverso un nome di link vengano visualizzate quando si fa riferimento attraverso un diverso hard link. Tali applicazioni sono relativamente poco comuni, ma nessuno ha fatto uno studio sistematico per vedere quali applicazioni falliranno in questa situazione.

I whiteout sono implementati con uno speciale tipo di voce di directory, DH_WHT. Le voci di directory whiteout non fanno riferimento ad alcun inode reale, ma per una facile compatibilità con le utilità di file system esistenti come fsck, ogni voce di directory whiteout include un finto numero di inode, il numero di whiteout riservato WINO. Il file system sottostante deve essere modificato per supportare il tipo di voce di directory whiteout. Le nuove directory che sostituiscono una voce whiteout sono marcate come opache attraverso un nuovo attributo inode “opaque”, in modo che i lookup non viaggino attraverso di esse (anche in questo caso richiedendo un supporto minimo dal file system sottostante).

Le voci di directory duplicate e i whiteout sono gestiti nell’implementazione dello spazio utentereaddir(). Al opendir()tempo, la libreria C legge la directory tutta in una volta, rimuove i duplicati, applica i whiteout, e mette in cache i risultati.

I supporti unionali diBSD non tentano di gestire i cambiamenti ai rami sotto il ramo superiore scrivibile (sebbene siano permessi). Il modo in cui viene gestito rename() non è descritto.

Un esempio dalla pagina man mount_unionfs:

 The commands mount -t cd9660 -o ro /dev/cd0 /usr/src mount -t unionfs -o noatime /var/obj /usr/src mount the CD-ROM drive /dev/cd0 on /usr/src and then attaches /var/obj on top. For most purposes the effect of this is to make the source tree appear writable even though it is stored on a CD-ROM. The -o noatime option is useful to avoid unnecessary copying from the lower to the upper layer.

Un altro esempio (notando che credo che il controllo dei sorgenti sia meglio implementato fuori dal file system):

 The command mount -t unionfs -o noatime -o below /sys $HOME/sys attaches the system source tree below the sys directory in the user's home directory. This allows individual users to make private changes to the source, and build new kernels, without those changes becoming visible to other users.

Linux union mounts

Come BSD union mounts, Linux union mounts implementa l’unione del file system nel livello VFS, con alcuni supporti minori dai sistemi di file sottostanti per whiteout e tag directory opachi. Esistono diverse versioni di queste patch, scritte e modificate da Jan Blunck, Bharata B. Rao e Miklos Szeredi, tra gli altri.

Una versione di questo codice unisce solo le directory di primo livello, simile alle directory union di Plan 9 e all’opzione BSD -o unionmount. Questa versione di union mount, a cui mi riferisco come uniondirectory, è descritta in dettaglio in un recente articolo su LWN di Goldwyn Rodrigues e nel recente post di Miklos Szeredi di un set di patch aggiornato. Per il resto di questo articolo, ci concentreremo sulle versioni di union mount che uniscono il fullnamespace.

Linux union mount è attualmente in fase di sviluppo attivo. Questo articolo descrive la versione rilasciata da Jan Blunck contro Linux2.6.25-mm1, util-linux 2.13, ed e2fsprogs 1.40.2. I set di patch, della serie asquilt, possono essere scaricati dal sito ftp di Jan:

Patch del kernel: ftp://ftp.suse.com/pub/people/jblunck/patches/

Utilities: ftp://ftp.suse.com/pub/people/jblunck/union-mount/

Ho creato una pagina web con i link alle versioni git delle suddette patch e un po’ di documentazione in stile HOWTO http://valerieaurora.org/union.

Un’unione viene creata montando un file system con il flagset MS_UNION (i flag MS_BEFORE, MS_AFTER e MS_REPLACE sono definiti nel codebase mount ma attualmente non utilizzati). Se viene specificato il flag MS_UNION, allora il file system montato deve essere di sola lettura o supportare i whiteout. In questa versione di union mount, il flag di union mount è specificato dall’opzione “-o union” a mount. Per esempio, per creare un’unione di due file system loopbackdevice, /img/ro e /img/rw, dovreste eseguire:

 # mount -o loop,ro,union /img/ro /mnt/union/ # mount -o loop,union /img/rw /mnt/union/

Ogni union mount crea unstruct union_mount:

 struct union_mount {atomic_t u_count;/* reference count */struct mutex u_mutex;struct list_head u_unions;/* list head for d_unions */struct hlist_node u_hash;/* list head for searching */struct hlist_node u_rhash;/* list head for reverse searching */struct path u_this;/* this is me */struct path u_next;/* this is what I overlay */ };

Come descritto inDocumentation/filesystems/union-mounts.txt, “Tutte le strutture union_mount sono memorizzate in due tabelle hash, una per la ricerca del prossimo livello inferiore dello stack union e una per la ricerca inversa del prossimo livello superiore dello stack union.”

I whiteout e le directory opache sono implementati in modo molto simile a BSD. Il file system sottostante deve supportare esplicitamente i whiteout definendo l’operazione di inode .whiteout per le directory (attualmente, i whiteout sono implementati solo per ext2, ext3 e tmpfs). Le implementazioni ext2 ed ext3 usano il tipo di voce di directory whiteout, DT_WHT, che è stato definito in include/linux/fs.h per anni ma non utilizzato al di fuori del file system Coda finora. Un numero riservato di inode whiteout, EXT3_WHT_INO, è definito ma non ancora usato; le voci whiteout attualmente allocano un inode normale. Un nuovo inodeflag, S_OPAQUE, è definito per marcare le directory come opache.Come in BSD, le directory sono marcate opache solo quando sostituiscono una voce whiteout.

I file sono copiati in alto quando il file è aperto per la scrittura. Se necessario, ogni directory nel percorso del file viene copiata nel ramo superiore (copia su richiesta delle directory). Attualmente, il copy up è supportato solo per file e directory regolari.

readdir() è uno dei punti più deboli dell’attuale implementazione. È implementato allo stesso modo di BSD union mountreaddir(), ma nel kernel. Il campo d_off è impostato all’offset all’interno della directory corrente sottostante, meno le dimensioni delle directory precedenti. Le voci di directory dalle directory sotto il livello superiore devono essere controllate rispetto alle voci precedenti per duplicati o cancellazioni. Come attualmente implementato, ogni chiamata di sistema readdir() (tecnicamente, getdents()) legge tutte le precedenti voci di directory in una cache del kernel, poi confronta ogni voce da restituire con quelle già presenti nella cache prima di copiarle nel buffer utente. Il risultato finale è che readdir() è complesso, lento, e potenzialmente alloca una grande quantità di memoria del kernel.

Una soluzione è prendere l’approccio BSD e fare la cache, il whiteout e l’elaborazione dei duplicati nello userspace. Bharata B. Raois sta progettando un supporto per l’union mount readdir() in glibc (lo standard POSIX permette che readdir() sia implementato a livello libc se la nuda chiamata di sistema del kernel non soddisfa tutti i requisiti). Questo sposterebbe l’uso della memoria nell’applicazione e renderebbe la cache persistente. Un’altra soluzione sarebbe quella di rendere la cache in-kernel persistente in qualche modo.

Il mio suggerimento è di prendere una tecnica da BSD union mounts ed estenderla: copiare proattivamente non solo le voci di directory per le directory, ma tutte le voci di directory dai file system inferiori, processare i duplicati e i whiteout, rendere la directory opaca, e scriverla su disco. In effetti, state processando le voci della directory per gli whiteout e i duplicati alla prima apertura della directory, e poi scrivendo la “cache” risultante delle voci della directory sul disco. Le voci della directory che puntano ai file sui file system sottostanti devono significare in qualche modo che sono voci “fall-through” (il contrario di un whiteout – richiede esplicitamente di cercare un oggetto in un file system inferiore). Un effetto collaterale di questo approccio è che i whiteout non sono più necessari.

Un problema che deve essere risolto con questo approccio è come rappresentare le voci di directory che puntano a file system inferiori. Si presentano diverse soluzioni: la voce potrebbe puntare a un numero di inode riservato, il file system potrebbe allocare un inode per ogni voce ma contrassegnarlo con un nuovo attributo S_LOOKOVERTHERE inode, potrebbe creare un collegamento simbolico a un obiettivo riservato, ecc. Questo approccio userebbe più spazio sul file system sovrastante, ma tutti gli altri approcci richiedono l’allocazione dello stesso spazio in memoria, e generalmente la memoria è più cara del disco.

Un problema meno pressante con l’attuale implementazione è che i numeri di inode non sono stabili attraverso l’avvio (si veda il precedente articolo sull’unione dei file system per i dettagli sul perché questo è un problema).Se le directory “fall-through” sono implementate allocando un inode per ogni voce di directory sul file system sottostante, allora i numeri di inode stabili saranno un effetto collaterale naturale. Un’altra opzione è quella di memorizzare una mappa di inode persistente da qualche parte – in un file nella directory di primo livello, o in un file system esterno, forse.

Gli hard link sono gestiti – o, più precisamente, non gestiti – nello stesso modo di BSD union mounts. Di nuovo, non è chiaro come molte applicazioni dipendano dalla modifica di un file attraverso un percorso hard-linked e dal vedere i cambiamenti attraverso un altro percorso hard-linked (al contrario del symboliclink). L’unico metodo che mi viene in mente per gestire correttamente questo è quello di mantenere una cache persistente da qualche parte sul disco degli inode che abbiamo incontrato con collegamenti multipli.

Ecco un esempio di come funzionerebbe: Diciamo che avviamo una copia per l’inode 42 e scopriamo che ha un numero di link pari a tre. Creeremmo una voce per il database dei collegamenti rigidi che include l’id del file system, il numero di nodo, il numero di collegamenti e il numero di inode della nuova copia sul file system di livello superiore. Potrebbe essere memorizzato in un file in formato CSV, o come symlink in una directory riservata nella directory principale (ad esempio, “/.hardlink_hack/<fs_id>/42“, che è un link a “<new_inode_num> 3“), o in un vero e proprio database. Ogni volta che apriamo un inode su un file system sottostante, lo cerchiamo nel nostro hard link database; se esiste una voce, diminuiamo il numero di link e creiamo un hard link all’inode corretto sul nuovo file system. Quando tutti i percorsi vengono trovati, il numero di link scende a uno e la voce può essere cancellata dal database. La cosa bella di questo approccio è che l’ammontare dell’overhead è limitato e scompare completamente quando tutti i percorsi agli inode rilevanti sono stati cercati. Tuttavia, questo introduce ancora una quantità significativa di complessità probabilmente non necessaria; l’implementazione BSD mostra che molte applicazioni funzioneranno felicemente con un comportamento hardlink non del tutto corretto.

Attualmente, rename() delle directory attraverso i rami restituisce EXDEV, l’errore per il tentativo di rinominare un file attraverso diversi file system. Lo spazio utente di solito gestisce questo in modo trasparente (dato che deve già gestire questo caso per le directory di diversi file system) e ricade nel copiare i contenuti della directory uno per uno. Implementare la copia ricorsiva rename() delle directory attraverso i rami nel kernel non è un’idea brillante per le stesse ragioni del rinominare attraverso i file system regolari; probabilmente restituire EXDEV è la soluzione migliore.

Dal punto di vista dell’ingegneria del software, gli union mount sembrano essere un ragionevole compromesso tra caratteristiche e facilità di manutenzione. La maggior parte dei cambiamenti VFS sono isolati in fs/union.c, un file di circa 1000 linee. Circa 1/3 di questo file è l’implementazione readdir() del kernel, che sarà quasi sicuramente sostituita da qualcos’altro prima di ogni possibile fusione; le modifiche ai file system sottostanti sono abbastanza minime e necessarie solo per i file system montati come rami scrivibili. Il principale ostacolo alla fusione di questo codice è l’implementazione readdir(). Altrimenti, i manutentori di file system sono stati notevolmente più positivi riguardo ai supporti unionali rispetto a qualsiasi altra implementazione di unioning.

Un bel riassunto dei supporti unionali può essere trovato nelle slide di Bharata B. Rao sui supporti unionali per FOSS.IN.

Il prossimo

Nel prossimo articolo, esamineremo unionfs e aufs, e confronteremo le varie implementazioni dei file system unionali per Linux. Restate sintonizzati!

Voci dell’indice di questo articolo
Kernel Filesystems/Union
Kernel Union mounts
GuestArticles Aurora (Henson), Valerie

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.