UNIVERSITÀ DEGLI STUDI DI MODENA E REGGIO EMILIA Facoltà di Ingegneria – Sede di Modena Corso di Laurea in Ingegneria Informatica Analisi di alcune implementazioni moderne di file systems: Ext3, ReiserFS e WinFS Relatore: Tesi di Laurea di: Prof.ssa Letizia Leonardi Alessandro Davolio Correlatore: Ing. Luca Ferrari __________________________________________________________________________ Anno accademico 2004-2005 1 SOMMARIO INTRODUZIONE 4 1 6 1.1 1.2 1.3 1.4 2 2.1 2.2 2.3 2.4 2.5 3 3.1 3.2 3.3 3.4 3.5 3.6 4 4.1 4.2 4.3 4.4 4.5 4.6 5 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 REQUISITI DI UN FILE SYSTEM MODERNO STRUTTURA DI BASE E MASSIMA CAPACITÀ DI PARTIZIONE GARANZIA D’ INTEGRITÀ DEI DATI PROBLEMI DI OCCUPAZIONE DEL DISCO IN SISTEMI MULTIUTENTE PRESERVARE I DATI PRIVATI DA MODIFICHE NON AUTORIZZATE IL FILE SYSTEM VIRTUALE DI LINUX (VFS) DOVE REPERIRE IL CODICE COLLEGARE UN FILE SYS TEM AL KERNEL CONNETTERE UN FILE SYSTEM AL DIS CO TROVARE UN FILE OPERAZIONI SUGLI INODE IL FILE SYSTEM EXT3 7 9 11 12 15 17 19 20 22 23 27 STRUTTURA FISICA GESTIONE DELLE DIRECTORY ATTRIBUTI ESTESI ED ACL QUOTE DISCO IN EXT3 J OURNALING ALTRI PARTICOLARI DI EXT3 27 30 32 34 36 39 IL FILE SYSTEM REISERFS 41 ORGANIZZAZIONE DELLA MEMORIA STRUTTURA FISICA IMPLEMENTAZIONE DI STREAM DI DATI E DI ATTRIBUTI J OURNALING REPACKING FUTURE CARATTERISTICHE DEL FILE SYSTEM WINFS 41 44 47 48 50 51 53 M EMORIZZAZIONE DEI DATI: IL MODELLO NTFS MODELLI DEI DATI TIPI E SOTTOTIPI PROPRIETÀ DEGLI OGGETTI E CAMPI DATI LINGUAGGIO DI DEFINIZIONE DEGLI SCHEMI RELAZIONI USARE IL MODELLO DATI DI WINFS ADO.NET E WINFS NOTIFICAZIONI 2 54 58 58 59 60 61 62 62 63 6 6.1 6.2 7 CONFRONTO PRESTAZIONALE 64 MONGO B ENCHMARK SLOW B ENCHMARK 64 68 CONCLUSIONI 69 APPENDICE A – GLI ALBERI DI DATI 71 A.1. DEFINIZIONI A.2. CHIAVI A.3. B ILANCIAMENTO DELLA STRUTTURA 72 73 74 APPENDICE B: ACCESS CONTROL LISTS (ACLS) 77 B.1. EVOLUZIONE DEI PERMESSI D’ACCESSO NEI FILE SYS TEM B.2. STRUTTURA DI UNA ACL B.3. ALGORITMO DI CONTROLLO DEGLI ACCESSI 77 78 79 APPENDICE C – LINKED LISTS 81 RIFERIMENTI BIBLIOGRAFICI 84 3 Introduzione Un file system è quella parte del sistema operativo che consente all’utente di interagire con i dati immagazzinati nei supporti di memoria secondaria: esso rappresenta quindi l’interfaccia tra i dati fisici in queste memorie ed il resto del sistema operativo, oltre che con gli applicativi che utilizzano questi dati. In questo elaborato si analizzeranno alcune implementazioni di file system, e si andrà a vedere in che modo questi interagiscano con il resto del sistema operativo e con gli applicativi di più alto livello, tenendo conto di tutti i requisiti che un moderno software di questo tipo si trova a dover soddisfare. I file system che saranno presi in analisi sono tre: ?? Ext3 ?? ReiserFS ?? WinFS Ext3, che assieme a ReiserFS viene utilizzato con i sistemi operativi UNIX e Linux, è un file system di larghissimo impiego, e rappresenta un ottimo esempio di file system moderno sviluppato utilizzando il meccanismo dell’open-source. ReiserFS, anch’esso software distribuito liberamente, è stato però principalmente sviluppato dai programmatori della Namesys, sua azienda produttrice, ed è un software molto più giovane del primo che presenta molte caratteristiche interessanti. WinFS è adottato dal nuovo sistema operativo della Microsoft (Longhorn), ed è ancora in fase di test, ma promette grosse innovazioni per quanto riguarda la gestione dei dati. Di tutti e tre questi file system saranno prese in analisi le principali caratteristiche, ma per problemi di disponibilità di codice sorgente e documentazione, un confronto diretto sarà fatto solo tra i primi due elencati. Il seguente elaborato è composto di sei capitoli e tre appendici, e dopo questa breve introduzione si andranno ad analizzare nel primo capitolo le caratteristiche e i requisiti che deve fornire un moderno file system: oggi, infatti, un software di questo tipo deve risolvere tutte le problematiche che concernono la gestione dei dati immagazzinati in memorie di massa di dimensioni sempre maggiori, e quindi diventa importante poter recuperare velocemente le informazioni richieste attraverso opportune strategie di gestione della memoria secondaria. Un altro problema molto sentito oggi, è quello della sicurezza delle informazioni memorizzate: un sistema informatico deve garantire 4 l’integrità dei dati presenti sui propri supporti di memoria sia rispetto ad un danneggiamento fisico (che può essere dovuto a blackout, crash del sistema operativo, o altri motivi che non sono direttamente collegati con operazioni eseguite dall’utente), che rispetto all’alterazione dei dati dovuta ad accessi e modifiche effettuati su questi ultimi da utenti non autorizzati. I moderni sistemi di elaborazione si trovano spesso ad operare con dati memorizzati su supporti di diversa natura, che possono archiviare i dati con meccanismi anche molto diversi tra loro (per esempio dischi rigidi o CD); diventa quindi importante il fatto che un file system possa essere gestito dal resto del sistema operativo in modo trasparente e indipendente dal tipo di dispositivo da cui esso va a recuperare i dati. Un file-system dovrà quindi presentarsi rispetto al codice sovrastante come un insieme uniforme d’istruzioni, con le quali quest’ultimo può interagire con tutti i dati ad esso accessibili, indipendentemente dal tipo di supporto in cui sono memorizzati. Nel secondo capitolo sarà quindi presentata l’interfaccia con cui i sistemi operativi UNIX- like si relazionano con i file systems, soddisfacendo quindi la necessità di trasparenza del codice rispetto alle applicazioni di livello più alto. I tre capitoli successivi, e cioè il terzo il quarto ed il quinto, riportano l’analisi effettuata sui tre file systems sopra elencati, mentre il sesto capitolo riporta i risultati di due benchmark eseguiti per confrontare Ext3 e ReiserFS, la cui documentazione è stata reperita sulla rete internet. 5 1 Requisiti di un file system moderno In questo capitolo verranno prese in analisi le principali caratteristiche ed i requisiti che un file system deve soddisfare per rispondere alle necessità dei moderni sistemi di elaborazione. Per fare ciò, saranno in primo luogo analizzate, in linea di massima, le caratteristiche e gli usi dei computer odierni. Al giorno d’oggi i computer sono utilizzati per le applicazioni più svariate, ed un calcolatore general-purpose spesso viene utilizzato con programmi anche molto diversi tra loro (ad esempio video editing, gestione di database, programmi di contabilità, eccetera), e non si è quindi in grado di conoscere a priori il carico di lavoro che la macchina dovrà sopportare. Non potendo conoscere le condizioni di lavoro in cui un computer andrà ad operare, non si è in grado neanche di conoscere quali componenti (sia hardware che software) saranno sottoposti ad un maggiore stress da lavoro. Per questo motivo i progettisti (anche in questo caso sia hardware che software), negli ultimi anni, hanno cercato di sviluppare i singoli componenti di una macchina in modo che si possano interfacciare con gli altri apparati secondo standard ben definiti, e senza così doversi preoccupare degli altri componenti con i quali andranno ad interagire. Concentrandosi sull’aspetto software, un esempio di come gli sviluppatori abbiano perseguito questa politica è rappresentato dalla stratificazione del codice: tutti i sistemi sono strutturati secondo livelli crescenti, in cui il codice di livello più basso è quello che comanda direttamente la macchina, ed il codice più alto è quello più astratto e con cui interagisce l’utente. In una struttura di questo tipo, ogni strato (layer) di software comunica con lo strato superiore mettendo a sua disposizione un certo numero di funzioni standard da esso interpretabili. Per quanto riguarda la gestione dei dati nelle memorie secondarie, e più nello specifico i file-system, ci si accorge che anche questi imple mentano diversi meccanismi per garantire la sicurezza delle informazioni rispetto a problemi molto diversi tra loro. Nei paragrafi seguenti saranno illustrati i principali problemi, riguardanti la sicurezza dei dati, che un progettista di file-system si trova a dover risolvere. 6 1.1 Struttura di base e massima capacità di partizione Per dare un’idea di base di come un file-system organizzi i dati memoria, si può affermare che esso divide quest’ultima in una serie di blocchi di dimensione finita nei quali va ad immagazzinare i dati. Le informazioni che vengono immagazzinate dentro i blocchi possono essere principalmente di tre tipi: ?? Dati archiviati dall’utente e dalle applicazioni che operano sul sistema: sono i dati generici dei quali fanno uso gli utenti ed i programmi installati (che per fare alcuni esempi possono essere il contenuto di un file di testo oppure un brano audio). ?? Dati riguardanti l’organizzazione della struttura gerarchica della memoria visibile agli occhi dell’utente: per fare un esempio sono tutte le formazioni che dicono quanti e quali file sono presenti in un direttorio, e dove questi si trovano fisicamente in memoria. Questo tipo di dati deve rappresentare un’immagine istantanea della memoria secondaria, e per questo, molte di queste informazioni vengono ricalcolate ed usate nella memoria volatile a run-time. ?? Dati riguardanti la struttura fisica del file-system (metadati): sono tutti quei dati che non sono di diretta utilità per l’utente e non sono ad esso accessibili, ma sono utilizzati dal file-system e dal sistema operativo per gestire in modo corretto tutte le informazioni immagazzinate dall’utente e dagli applicativi. Questi dati sono ad esempio il numero di blocchi in cui è diviso il disco rigido, la dimensione d’ogni blocco, o una descrizione riassuntiva dell’utilizzo di ogni blocco, e nella maggior parte dei file-system sono memorizzati in un apposito blocco, a cui ha accesso soltanto il sistema operativo, chiamato superblocco. Questa distinzione fatta per i tipi di dati spesso vale anche per i blocchi di memoria secondaria, in quanto questi sono utilizzati in modo diverso a seconda di dove si trovano all’interno dell’albero dei direttori. Questo albero, che rispecchia l’organizzazione in file e cartelle che appare agli occhi dell’utente, utilizza ciascun blocco o come nodo o come foglia. I nodi sono quei blocchi che memorizzano tutte le informazioni riguardanti la strut tura gerarchica del file system: essi conterranno quindi i riferimenti ad altri nodi di livello a loro direttamente inferiore, oppure potranno contenere i riferimenti alle foglie, che sono i blocchi che vengono utilizzati per immagazzinare i dati che sono utilizzati da gli 7 applicativi (file). Esistono poi blocchi speciali riservati a contenere soltanto metadati (il superblocco appunto, con le eventuali sue copie), che sono al di fuori dell’organizzazione gerarchica dei direttori, e piuttosto ne gestiscono l’utilizzo. Per ulteriori informazioni riguardanti gli alberi si faccia riferimento all’appendice A. Il problema della massima dimensione di partizione per un file-system è un problema che viene affrontato nelle prime fasi di progetto, in quanto esso va ad influenzare parametri che difficilmente possono essere gestiti in modo automatico dal calcolatore a runtime, come la lunghezza in byte degli indirizzi fisici dei dati presenti in memoria: decidendo per esempio che lo spazio d’indirizzamento del file system che si vuole progettare sarà di quattro byte, si va a limitare automaticamente a 232 il numero massimo di indirizzi allocabili, ma si va anche stabilire che qualunque oggetto che fa riferimento a dati presenti memoria dovrà contenere un campo di indirizzo della dimensione di 4 byte (32 bit). Oltre allo spazio d’indirizzamento, esiste poi tutta una serie di parametri che possono andare a ridurre notevolmente lo spazio per i dati archiviati dall’utente, nel caso in cui ci si trovasse a lavorare con applicazioni che archiviano soltanto file di dimensione molto ridotta: questi parametri sono tutte le informazioni aggiuntive riguardanti i file che si vogliono memorizzare (e che non sono di diretto interesse per l’utente), come ad esempio i permessi d’accesso, la data di creazione, l’autore, gli attributi, e la stringa contenente il nome del file. Tutti questi parametri hanno una loro precisa occupazione di memoria e, nel loro insieme, in certi casi possono occupare più spazio su disco di quello occupato dai dati d’interesse per l'utente (in altre parole il contenuto del file stesso). Un altro problema strettamente correlato con la dimensione dei dati che non sono d’interesse diretto dell’utente, è il numero massimo di file memorizzabili all’interno di un direttorio. Nei file system odierni, le informazioni aggiuntive riguardanti i file (indirizzo fisico da cui recuperare i dati) sono raggruppati in sequenze di dati chiamati “descrittori” del file stesso; e sono questi descrittori che, memorizzati consecutivamente all’interno di un blocco direttorio, vanno a descrivere i file contenuti all’interno del direttorio stesso. Per aume ntare il numero massimo di file memorizzabili all’interno di un direttorio, il progettista può scegliere di fare due cose: ?? Aumentare la dimensione di blocchi in cui viene divisa la memoria: in questo modo viene reso disponibile più spazio in cui memorizza re i descrittori dei file ed allo 8 stesso tempo vengono velocizzare le operazioni di I/O, in quanto le testine del disco rigido dovranno fare meno salti tra un blocco e l’altro per accedere al contenuto di un file. D’altro canto, però, blocchi di maggiore dimensione implicano un maggiore spreco di spazio su disco. Bisogna considerare che mediamente l’ultimo blocco allocato ad un file è di solito occupato solo per la metà del suo spazio, quindi più un blocco diventa grande, maggiore sarà lo spazio sprecato ne ll’ultimo blocco d’ogni file. ?? Diminuire la dimensione delle informazioni che si vogliono memorizzare nei descrittori: questa soluzione permette sì di memorizzare un maggior numero di file all’interno di un direttorio, ma allo stesso tempo diminuisce drasticamente il numero d’informazioni aggiuntive che possono essere memorizzate come riferimento ad un file. Bisogna però ricordare che la maggior parte dei file-system, per ovviare a questo problema, utilizza una soluzione intermedia: si sceglie in pratica di memorizzare all’interno del descrittore soltanto le informazioni che devono essere recuperate in modo più veloce, e si decide di collocare tutti i dati che sarebbero letti solo in caso d’interazione con il contenuto del file (come ad esempio i permessi d’accesso, che possono essere letti in caso di richiesta di accesso contenuto del file) assieme al contenuto del file stesso, cioè i dati generici archiviati da utente ed applicazioni. 1.2 Garanzia d’integrità dei dati Un altro requisito che i file system devono soddisfare, è quello di garantire l’integrità dei dati anche dopo blackout o altri arresti improvvisi del sistema che possono lasciare incompiute le operazioni di I/O. Per prima cosa bisogna osservare che in caso di riavvio dopo blackout, i dati che potrebbero risultare corrotti o danneggiati possono essere sia i dati utente sia quelli riguardanti il file system in sè; è quindi necessario provvedere a meccanismi che assicurino sia i dati dell’utente sia i dati ad uso ristretto del sistema operativo. Nel caso in cui un arresto improvviso del sistema dovesse danneggiare il superblocco, nel migliore dei casi, il sistema operativo si troverebbe ad interpretare (se i dati danneggiati risultassero per caso interpretabili dalla macchina) informazioni che non rispecchiano l’organizzazione e l’uso della memoria, col rischio di danneggiare le 9 rimanenti porzioni di dati che non sono stati coinvolti in precedenza. Per ovviare a questo problema la maggior parte dei file system mantiene almeno una copia del superblocco, o comunque di tutte le informazioni sulla struttura della memoria, in settori del disco lontani dal superblocco stesso; questo per scongiurare che le testine del disco rigido, durante l’arresto, possano danneggiare le copie nel caso queste fossero posizioniate in settori adiacenti il superblocco. Una soluzione di questo tipo garantisce nella maggior parte dei casi che il sistema riesca a recuperare i dati riguardanti l’organizzazione delle informazioni su disco, ma allo stesso tempo crea un’elevata ridondanza della metadata; cosa che però non va ad influire molto sullo spazio messo a disposizione per i dati utente, viste le piccole dimensioni occupate dalle informazioni strutturali rispetto alle dimensioni dei dischi rigidi in questo momento in commercio. Un’altra soluzione che viene utilizzata per controllare la presenza di errori all’interno dei blocchi sono le “bitmap”: esse sono dei record di dimensione variabile (dipendente dimensione del blocco stesso) che rappresentano l’utilizzo dei vari byte (oppure altra misura di memoria utilizzata dal sistema operativo) del blocco stesso; in questi record, generalmente, ogni bit rappresenta un’unità di memoria del blocco, ed il fatto che un bit sia uguale a 1 o a 0 sta ad indicare l’utilizzo o meno della relativa unità di memoria del blocco (dove per unità di memoria s’intende appunto la misura di memoria utilizzata dal S.O., qualunque essa sia). Una soluzione di questo tipo può essere adottata anche nel superblocco, per descrivere sia l’utilizzo del superblocco stesso che l’uso, nel loro complesso, di tutti gli altri blocchi di memoria. Un metodo già da tempo adottato per garantire l’integrità delle informazioni in memoria è quello di eseguire automaticamente, al riavvio della macchina dopo un arresto non previsto, le utility per il controllo dell’integrità dei dati messe spesso a disposizione assieme dal sistema operativo (ad esempio scandisk per i sistemi Microsoft, oppure e2fsck per le partizioni formattate con Ext2): l’utilizzo di questi programmi, però, si sta rivelando sempre meno pratico per via delle dimensioni sempre maggiori dei dischi rigidi odierni. Questo tipo di programmi, generalmente, non compie ricerche mirate dei dati danneggiati, ma esegue scansioni complete della memoria, cercando blocco per blocco gli eventuali dati danneggiati, aumentando così il tempo necessario ad eseguire una scansione completa proporzionalmente alle dimensioni della memoria stessa. 10 Un metodo per garantire l’integrità dei dati che ha preso piede negli ultimi anni, consiste nel registrare periodicamente le operazioni di I/O eseguite su disco all’interno di un log (registro, diario), in modo da avere sempre sotto controllo quali dati sono stati realmente scritti in memoria secondaria. In linea di massima questi meccanismi funzionano tutti allo stesso modo: prima di eseguire un’operazione di scrittura su disco, il file-system registra una copia dei dati che andranno ad essere scritti nella porzione di memoria adibita a registro, dati che saranno tolti dal registro soltanto dopo che l’operazione di scrittura è stata eseguita. In questo modo, nel caso di arresti non previsti di sistema, il file-system può usare il registro per controllare quali operazioni di scrittura sono rimaste eventualmente incompiute, andando così a ripristinare integrità dei dati. Una soluzione di questo tipo risulta essere molto più funzionale sui sistemi odierni dell’utilizzo degli appositi programmi per il controllo d’integrità, ma allo stesso tempo deve far fronte a tutti i problemi d’integrità dei dati nel registro che non riguardano tutto il resto della memoria in caso di blocchi improvvisi: il file-system deve essere quindi in grado di “capire“ quali operazioni di scrittura sono andate a buon fine, indipendentemente dal fatto che il registro sia o meno stato danneggiato durante l’arresto. Un altro problema che questo tipo di registri deve affrontare riguarda la scrittura su disco di file molto estesi: il fatto di porre sul registro tutti i file che devono essere scritti, indipendentemente dalla loro dimensione, causa uno spreco di memoria ed un rallentamento delle operazioni di scrittura (con questo meccanismo un file viene scritto due volte in memoria secondaria ) proporzionali alla dimensione del file stesso; bisogna quindi porre un limite massimo alla dimensione dei file scrivibili in registro, ed allo stesso tempo definire una politica di trattamento della scrittura dei file di dimensioni superiori. 1.3 Problemi di occupazione del disco in sistemi multiutente Un altro dei problemi che i file-system si trovano a dover risolvere riguarda l’utilizzo della memoria di massa da parte dei vari utenti di uno stesso sistema. Il centro della questione non è tanto quello di garantire un equo spazio a tutti gli utenti, ma principalmente è l’impedire che l’intero disco rigido venga occupato dai dati dei vari utilizzatori, impedendo così la possibilità di eseguire operazioni di manutenzione o di ripristino di informazioni su disco da parte dell’amministratore. Le soluzioni per 11 ovviare a questo problema sono numerose, e ciascun file-system ne adotta alcune in particolare; in questo paragrafo ci si limiterà ad elencare le caratteristiche delle principali di queste. Il metodo più banale per ovviare a questo problema è quello di riservare permanentemente uno spazio minimo di memoria, di dimensione stabilita, all’amministratore di sistema; in questo modo si lascia ciascun utente libero di utilizzare tutte le risorse di sistema disponibili, lasciando un minimo di spazio riservato al superutente per le operazioni d’emergenza, nel caso il disco si riempisse. Una soluzione più raffinata della precedente consiste nel riservare a ciascun utente opportune porzioni di memoria, con le quali memorizzare i propri dati personali: queste porzioni prendono il nome di “quote disco” (disk quo tas). Assieme alle quote disco può essere poi definita una serie di politiche, riguardanti l’uso di queste ultime, spesso gestite dai livelli superiori del sistema operativo, e non direttamente dal file-system: viene spesso permesso di condividere le quote disco tra utenti dello stesso gruppo, oppure viene dato il permesso a certi utenti di poter usufruire delle quote disco di altri, ecc. Accanto alle politiche di gestione delle quote si affiancano poi le politiche d’archiviazione dei dati: la stragrande ma ggioranza dei file-system odierni permette di definire il direttorio di lavoro e quello di partenza di ciascun utente, in modo che questi possa archiviare i propri dati soltanto in questi spazi definiti, a cui possono avere accesso soltanto altri utenti autorizzati; si può anche decidere di utilizzare le politiche in modo combinato, riservando tutta la quota disco di un singolo utente soltanto all’interno della propria cartella di lavoro, in modo che esso non abbia la possibilità di archiviare i suoi dati fuori da quest’ultima. 1.4 Preservare i dati privati da modifiche non autorizzate Accanto al problema delle quote disco e delle politiche di gestione dello spazio utente, si affiancano tutti i problemi che riguardano la garanzia dell’integrità e della privatezza dei dati rispetto ad accessi effettuati da utenti non autorizzati; ecco perchè la maggior parte dei sistemi operativi multiutente prevede la possibilità di limitare l’accesso ai contenuti dei file. 12 Tutti i sistemi di sicurezza delle memorie secondarie, utilizzati dai sistemi operativi, ruotano attorno ai concetti di utente, gruppo, permesso e proprietà. Per utente s’intende ogni singolo utilizzatore, che è automaticamente identificato attraverso un codice univoco chiamato “identificativo di protezione” (security identifier - SID); codice che viene utilizzato dal sistema anche per identificare i profili utente, che raccolgono tutte le informazioni e le impostazioni personalizzate sull’utilizzo del sistema di ogni utente. Per gruppo s’intende invece un generico insieme d’utenti, accomunato dal fatto di possedere le stesso codice identificativo di gruppo (group identifier – GID), che viene memorizzato all’interno di ciascun profilo utente. Se si definisce il termine “proprietà” come l’insieme di tutte le operazioni che sono eseguibili su un documento o, in generale, un file, il termine permesso assume il significato di relazione tra utente o gruppo, e proprietà di un documento: un permesso di lettura, per esempio, è la relazione di autorizzazione all’azione di lettura di un determinato file, concessa ad un utente oppure ad un gruppo. I sistemi di controllo d’accesso ai dati degli odierni sistemi operativi hanno, in generale, alcune caratteristiche in comune: ?? Accesso discrezionale ad oggetti da proteggere: il proprietario di un oggetto, ad esempio un file o una cartella, è in grado di concedere o negare l’autorizzazione ai vari utenti, per controllare come e da chi l’oggetto viene utilizzato. ?? Ereditarietà delle autorizzazioni: gli oggetti possono ereditare l’autorizzazione dall’oggetto che li contiene, ad esempio un file può ereditare le autorizzazioni della cartella in cui è contenuto. ?? Privilegi di amministratore: è possibile controllare gli utenti o i gruppi che possono eseguire funzioni amministrative e apportare modifiche che influiscono sulle risorse di sistema. ?? Controllo di eventi di sistema: è possibile utilizzare delle funzionalità di controllo per individuare eventuali tentativi di elusione della protezione o per creare un itinerario di controllo. ?? Utilizzo di liste di controllo accesso (access control list – acl): le acl sono liste ordinarie di regole che vengono usate per prendere una decisione, ad esempio se permettere o meno ad un certo utente l’accesso ad un file; ciascuna regola esprime una o più proprietà dell’oggetto da valutare (ad esempio l’autore, il nome o l’indirizzo di un file), e se queste proprietà sono verificate essa indica 13 quale decisione prendere 1 . Queste strutture non trovano impiego soltanto all’interno di file system, ma sono largamente usate per la gestione degli accessi nei dispositivi di rete: esse, infatti, possono essere per esempio utilizzate nella configurazione dei firewall o come controlli di smistamento dei pacchetti passanti sui router. 1 Per una trattazione più ampia di questo tema si faccia riferimento all’appendice B. 14 2 Il File System Virtuale di Linux (VFS) Prima di iniziare ad analizzare due file system utilizzabili su sistemi Linux, è necessario dare un paio di nozioni riguardo a come il sistema operativo dispone di un’unica interfaccia in grado di relazionarsi in modo trasparente con i file system che sono utilizzati sulla memoria, e in modo indipendente da questi ultimi. In Linux, l’accesso a tutti i file avviene attraverso il Virtual Filesystem Switch, o VFS [VFS]. Questo è uno strato di codice interposto tra il file-system ed il resto del sistema operativo, che implementa le operazioni generiche di file system richieste dal sistema collegandole con lo specifico codice necessario per gestirle (codice che sarà necessariamente diverso a seconda del file system utilizzato). Per ragioni di comodità, nei seguenti capitoli, si farà riferimento al Virtual Filesystem Switch anche con i termini “file system virtuale” e “switch virtuale”. La figura 2.1 mostra come il Kernel di sistema operativo intercetti tutte le richieste d’accesso ai dati eseguite dalle applicazioni, le passi al VFS che, associando ad esse il corretto codice a seconda del file system su cui i dati si trovano, formula delle richieste di accesso al file system appropriate. Tutte le richieste d’accesso ai dati sono gestite utilizzando un buffer dati virtuale (buffer cache), le cui operazioni in uscita vanno ad accedere alle informazioni per mezzo dei driver che comandano le memorie fisiche. Tutte queste operazioni sono eseguite in modo del tutto trasparente rispetto alle applicazioni di più alto livello, in quanto l’elaborazione delle richieste avviene a livello di Kernel. 15 Figura 2.1: Funzionamento del VFS all’interno del kernel Linux. Per meglio chiarire il funzionamento del VFS, in questo capitolo saranno utilizzate parti di codice che lo switch usa per gestire il funzionamento del file system Ext2. Il modo con cui il VFS interagisce con il codice degli altri file system è del tutto analogo a quello riportato nei paragrafi successivi. Tutto il VFS interagisce con i file system presenti sulle memorie secondarie utilizzando due entità diverse (e le relative strutture dati che le descrivono, dichiarate nel file linux/fs.h): ?? Il superblocco, descritto nella struttura super_block, che contiene la descrizione del superblocco (e quindi di tutto il file system) e fornisce una serie di metodi al sistema operativo per interagire con esso. ?? L’inode, definito nell’omonima struttura, che contiene la descrizione di un file o direttorio a cui si vuole accedere (in Linux le directory sono interpretate dal sistema come particolari tipi di file contenenti una serie di riferimenti, “inode number”, ad altri file). Ogni inode rappresenta un file, e contiene tutte le informazioni necessarie alla gestione del contenuto del file stesso (indirizzo fisico, nome, lunghezza, ecc.), ed è identificato da un preciso “inode number” 16 che permette di rintracciarlo all’interno di una tabella in cui sono memorizzati tutti gli inode presenti sul file system (e di conseguenza anche tutti i file). Un esempio di come il VFS utilizza gli inode per organizzare i dati all’interno delle directory è mostrato in figura 2.2: ogni direttorio contiene un riferimento alla tabella degli inode per ogni file in esso contenuto, e per accedere ai dati del file sarà quindi necessario andare a leggere il contenuto dello specifico inode nella tabella. L'inode table, infatti, contiene l'elenco di tutti gli inode (quindi di tutti i file) sulla partizione (questo per i file system fisici, di cui si tratta nel presente elaborato), e per ognuno di questi vengono memorizzati uno o più riferimenti alla corrispondente voce di tabella all’interno dei direttori. Figura 2.2: Utilizzo della tabella degli inode per organizzare i file all'interno delle directory. Assieme a queste entità entrano anche in gioco le strutture dati ad esse correlate, che forniscono le operazioni che il sistema può eseguire sulle entità stesse; strutture che sono principalmente super_operations, inode_operations e file_operations, ed altre che sono comunque contenute in linux/fs.h. 2.1 Dove reperire il codice Il codice sorgente del VFS si trova nel sottodirettorio fs/ dei sergenti del kernel di Linux, assieme ad altre parti di codice correlate, come la buffer cache ed il codice per interagire con tutti i formati di file eseguibile. Ogni specifico file system è contenuto in una directory inferiore; per esempio, i sorgenti del file system Ext2 sono contenuti in fs/ext2/. 17 La tabella 2.1 riporta il nome dei file del direttorio fs/, e dà per ciascuno di essi una breve descrizione. La colonna centrale, chiamata system, vuole indicare a quale sottosistema il file è (principalmente) dedicato: ?? EXE significa che i file è utilizzato per gestire ed interagire con i file eseguibili ?? DEV significa che viene utilizzato come supporto ai driver per i vari dispositivi installati sulla macchina ?? BUF significa gestione della buffer cache. ?? VFS significa che il file è parte del file system virtuale, e delega alcune funzionalità al codice specifico di ogni file system ?? VFSg indica che il codice presente nel file è completamente generico e non delega mai parte le sue funzioni a codice specifico di ogni file system. File system Funzione binfmt_aout.c EXE Esecuzione degli eseguibili di tipo a.out binfmt_elf.c EXE binfmt_java.c EXE Esecuzione dei file eseguibili di tipo ELF Esecuzione dei file java e delle applets binfmt_script.c EXE block_dev.c DEV Esecuzione degli script di tipo # e ! Funzioni read(), write(), e fsync() per blocchi generici buffer.c BUF Gestione della buffer cache, che memorizza i blocchi letti dai dispositivi. dcache.c VFS devices.c DEV dquot.c VFS exec.c VFSg fcntl.c fifo.c file_table.c filesystems.c inode.c ioctl.c La directory cache, che memorizza i nomi dei direttori durante le ricerche. Funzioni per il supporto di dispositivi generici, come ad esempio i registri Supporto generico per la gestione delle quote disco. Supporto generico per i file eseguibili. Le funzioni di call si trovano nei files binfmt_*. Supporto per la gestione dei descrittori dei file con la funzione VFSg fcntl(). VFSg Gestione del buffer FIFO per l’accesso ai dischi. VFSg Lista dinamicamente estensibile dei files aperti dal sistema. Tutti il file system precompilati sono richiamati da questo file VFS attraverso la funzione init_name_fs(). VFSg Lista dinamicamente estensibile degli inode aperti dal sistema. Primo livello per l’handling dei controlli di I/O; VFS successivamente l’handling viene passato al file system o al driver interessato, se necessario. 18 locks.c VFSg Supporto per le varie operazioni di locking dei file Riempie l’inode una volta fornito il percorso. Implementa VFS diverse system calls collegate ai nomi dei file. namei.c Ottimizzazione per gestire il sistema nel caso no n si usino le quote disco noquot.c VFS open.c pipe.c read_write.c VFS Contiene system calls, comprese open(), close(), and vhangup(). VFSg Implementazione delle pipes. VFS read(), write(), readv(), writev(), lseek(). readdir.c select.c VFS VFS Contiene diverse interfacce usate per la lettura delle directory Le basi per la system call select() stat.c VFS super.c VFS Supporto per stat() e readlink() Supporto per superblocco, mount()/umount(). filesystem registry, Tabella 2.1: Breve descrizione del contenuto dei file che compongono il codice del VFS Linux. 2.2 Collegare un file system al kernel Per poter utilizzare i dati presenti in un particolare file system, in UNIX e Linux, sono necessarie due operazioni: la registrazione del file system ed il montaggio della partizione dati. Registrare un file system significa fornire al VFS le caratteristiche del file system che si vuole utilizzare, come ad esempio il tipo di file system, in modo che lo switch virtuale sia poi in grado di reperire il codice necessario per gestire quel particolare tipo di partizione. Se si cerca nel codice di ogni file system la funzione init_name_fs(), si vede che essa contiene poche linee di codice. Per esempio, nel file system Ext2, la funzione è come segue (da fs/ext2/super.c): int init_ext2_fs(void) { return register_filesystem(&ext2_fs_type); } Tutto quello che la funzione svolge è registrare il file system nel sistema operativo attraverso la struttura ext2_fs_type: static struct file_system_type ext2_fs_type = { ext2_read_super, "ext2", 1, NULL }; 19 ext2_read_super è un puntatore a funzione che indica al sistema operativo l’indirizzo in cui si trova la funzione per la lettura del superblocco nei file system di tipo Ext2 (operazione necessaria per montare correttamente un qualsiasi file system). “ext2” è il nome del tipo di file system, che è usato (ad esempio quando si digita il comando mount … -t ext2) per determinare quale file system utilizzare per montare un disco rigido. “1” indica che il file system richiede un dispositivo di memoria su cui operare (a differenza per esempio dei file system di rete, che non si relazionano direttamente con una memoria di massa, ma recuperano i dati, attraverso un’interfaccia di rete, da una memoria remota), e NULL è necessario per riempire lo spazio che verrà utilizzato per mantenere una linked list di tutti i file system nel registro del VFS, contenuto in fs/super.c. 2.3 Connettere un file system al disco Il resto della comunicazione tra il codice del file system ed il Kernel non avviene finché non viene montato un dispositivo (partizione dati, o intero disco rigido) che utilizza quel tipo di file system. Quando si monta un dispositivo contenente un file system di tipo Ext2, viene chiamata la funzione ext2_read_super() che, andando a leggere i dati contenuti nel superblocco, fornisce al sistema operativo i dati riguardanti la struttura fisica della partizione. Se la lettura del superblocco avviene con successo e la funzione è in grado di collegare lo specifico file system al disco che si vuole montare, essa riempie la struttura super_block con diverse informazioni che includono un puntatore alla struttura chiamata super_operations, che a sua volta contiene puntatori a funzioni volte alla manipolazione dei superblocchi; in questo caso, puntatori a funzioni specifiche di Ext2. Il superblocco è la parte di memoria che definisce la struttura dell’intero file system su di un dispositivo, e le operazioni che riguardano il file system nella sua interezza (al contrario delle operazioni che riguardano i singoli file) sono considerate operazioni di superblocco. La struttura dati super_operations contiene puntatori a funzione volte alla manipolazione degli inode, del superblocco, e che notificano o cambiano lo stato del file system nel suo complesso (statfs() e remount()). 20 Ecco com’è definita la struttura super_operations nel file system virtuale (da linux/fs.h): struct super_operations { void (*read_inode) (struct inode *); 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 *); }; Come si nota, tutti i dati definiti dalla struttura sono puntatori a funzione, che andranno a contenere l’indirizzo di memoria a cui reperire le funzioni che saranno usate dal VFS per interagire con i dati sulle memorie di massa. Sotto viene riportata la dichiarazione più semplice di istanza di questa struttura nel file system Ext2, che rappresenta le funzioni che questo file system fornisce allo switch virtuale associandole a super_operations (vedi in fs/ext2/super.c): static struct super_operations ext2_sops = { ext2_read_inode, NULL, ext2_write_inode, ext2_put_inode, ext2_put_super, ext2_write_super, ext2_statfs, ext2_remount }; Quando un file system viene connesso al disco (ed il modulo che si prende carico di questo compito corrisponde al file sorgente fs/super.c), la funzione do_umount() chiama read_super la quale, a sua volta, termina chiamando (nel caso si stia utilizzando il file system Ext2) ext2_read_super(), funzione che ritorna il superblocco al resto del sistema operativo. Il superblocco ritornato contiene un 21 riferimento alla struttura ext2_super_operations; esso include anche molti altri dati, che possono essere reperiti nello specifico all’interno della definizione di struct super_block in include/linux/fs.h. 2.4 Trovare un file Una volta che il file system è stato montato correttamente, è possibile accedere ai file che sono presenti al suo interno. I passi da compiere in questo caso sono due: utilizzare il nome del file per trovare l’inode da esso puntato, e quindi accedere all’inode. Quando il file system virtuale cerca il nome di un file o di un direttorio, esso include nella ricerca anche il suo percorso, operazione che viene fatta automaticamente dal sistema se il nome dell’oggetto da trovare non è un nome assoluto (cioè non ha inizio col carattere ‘/’). Per trovare gli oggetti in memoria, il sistema operativo utilizza il codice specifico del file-system in cui i dati sono memorizzati; esso esamina il percorso del file un componente per volta (le varie componenti di un percorso sono separate dal carattere ‘/’). Se il componente preso in esame è una directory, allora il componente successivo sarà cercato all’interno del direttorio appena trovato. Ogni componente che viene identificato, indipendentemente dal fatto che sia un file o un direttorio, ritorna un “inode number” che lo identifica univocamente, ed attraverso il quale sono resi disponibili i contenuti dell’oggetto. Se eventualmente l’oggetto trovato risultasse essere un collegamento simbolico ad un altro file, allora il VFS inizierebbe una nuova ricerca di file, il cui nome è quello restituito dal collegamento simbolico. Al fine di prevenire cicli di ricerca ricorsivi infiniti, viene posto un limite alla profondità dei collegamenti simbolici: il Kernel seguirà soltanto un certo numero di collegamenti ricorsivi prima di interrompere l’operazione segnalando un errore. Solo dopo che il VFS ed il file system hanno recuperato il numero di inode dal nome del file (compito svolto dalla routine namei(), in namei.c) è possibile accedere ai contenuti dell’inode. La funzione iget() trova e ritorna al sistema operativo l’inode identificato dall’inode number che gli viene fornito come parametro; la funzione iput() è invece utilizzata dal sistema per rilasciare l’accesso ad un inode che non deve più essere utilizzato. Queste funzioni, in linea di principio, sono simili a malloc() e free(), con l’unica 22 differenza che più processi possono mantenere simultaneamente l’accesso ad uno stesso inode, inoltre viene mantenuto un conteggio dei processi attivi che fanno uso di quest’ultimo, in modo da poter conoscere quando l’inode può essere effettivamente rilasciato dal file-system. Il file handle 2 che viene passato al codice di una generica applicazione dopo la richiesta di accesso ad un file, è in realtà un numero di tipo integer che indica l’offset (distanza dall’inizio di tabella) sulla tabella dei file per trovare la voce cercata; la voce della tabella a cui l’applicazione può accedere in questo modo, contiene l’inode number che era stato cercato dalla funzione namei() fino a quando il file non viene chiuso dal processo o è il processo stesso a terminare. Quando un qualsiasi processo fa una qualsiasi operazione su di un file utilizzando i “file handle”, essa va in realtà a manipolare ed interagire con il relativo inode. 2.5 Operazioni sugli inode L’inode number e la struttura di inode stessa non possono essere creati dal VFS stesso, esse devono essere fornite dal file system, in quanto è questa ultima parte di codice che gestisce l’interazione del resto del sistema operativo con i dati presenti in memoria secondaria. Di seguito viene riportato in che modo il file system virtuale ottiene il numero di inode partendo dal nome del file a cui si vuole accedere. Il VFS inizia la ricerca della prima directory contenuta nel percorso, che dal suo nome ricava il rela tivo inode; a questo punto l’inode è utilizzato per trovare il direttorio3 successivo contenuto nel percorso, e così via fino ad esaurire i componenti del percorso. Quando in questo modo la ricerca raggiunge la fine, il sistema ricava l’inode relativo al file o al direttorio che stava cercando. Una ricerca di questo tipo può iniziare solo se il VFS ha a disposizione un inode di partenza, cosa che viene fornita al momento del montaggio del file system dal superblocco, attraverso un puntatore ad inode contenuto in quest’ultimo chiamato s_mounted, che contiene un riferimento ad una struttura di tipo inode per l’intero file system. Questo inode è allocato quando il file system viene 2 Un file handle è, in genere, una struttura dati che permette ad un’applicazione di alto livello di accedere ed eventualmente modificare i dati presenti in un file; è il meccanismo con cui il codice del sistema operativo permette alle applicazioni di utilizzare i dati nelle memorie, ed è fornito dal sistema operativo a seguito della richiesta di accesso ad un file da parte di un’applicazione. 3 Vedi il paragrafo relativo alla gestione dei direttori nel capitolo su Ext3. 23 montato, e rimosso dalla memoria all’atto dello smontaggio. L’inode s_mounted rappresenta il direttorio di root nei file system Linux, e quindi anche in Ext2 ed Ext3, e partendo da questo possono essere ritrovati gli altri inode presenti in memoria. Ogni inode ha inoltre un puntatore ad una struttura a sua volta composta di puntatori a funzione, struttura che prende il nome di inode_operations. L’elemento chiamato lookup() è quello che viene usato dal VFS per recuperare un altro inode presente sullo stesso file system. In generale un file system ha soltanto una funzione lookup(), che viene utilizzata per ogni inode presente su di esso, ma è anche possibile avere diverse funzioni di lookup() che possono sono utilizzate su un’unica partizione; il file system proc consente questa molteplicità perché su di esso esistono direttori che hanno funzioni differenti. La struttura inode_operations è la seguente (definita in linux/fs.h). 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 (*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); 24 }; La maggior parte di queste funzioni è direttamente riferita ad una precisa chiamata di sistema (system call). In Ext2 e in Ext3, direttori, file e link simbolici hanno differenti istanze di inode_operations (cosa verificata anche in molti altri sistemi operativi), esse sono reperibili nei seguenti file: ?? fs/ext2/dir.c contiene la struttura ext2_dir_inode_operations ?? fs/ext2/file.c contiene la struttura ext2_file_inode_operation ?? fs/ext2/symlink.c contiene ext2_symlink_inod_operations Esistono poi diverse system call riguardanti file (e direttori) che non sono comprese all’interno della struttura inode_operations, e si trovano invece all’interno della struttura file_operations : struttura che contiene tutte le funzioni che operano specificamente sui file e sul loro contenuto, piuttosto che sugli inode; la dichiarazione della struttura è riportata di seguito. 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); }; 25 Al di là degli esempi presentati in questo capitolo, dunque, secondo il modello VFS, il ruolo di uno specifico file system è quello di fornire un superblocco, una lista di inode (uno per ogni file presente), e di fornire un’implementazione che supporti le operazioni richieste dal sistema operativo (come appunto apertura, lettura, scrittura su file), cosa che viene fatta attraverso le strutture e le routine prese in analisi in questo capitolo. Le strutture file_operations, inode_operations e super_operations , una volta che il file system viene montato, andranno a contenere tutti i riferimenti che servono al sistema operativo per reperire le funzioni che implementano le operazioni sui file per quel determinato tipo di file system. 26 3 Il File system Ext3 In questo capitolo sarà preso in analisi il file system ext3, diretto discendente di ext2, dal quale eredita gran parte del suo codice sorgente e lo mantiene in sostanza immutato. Questo filesystem, infatti, rappresenta un adeguamento alle necessità odierne di un file system già molto stabile e versatile, a cui sono state aggiunte parti di codice per aumentarne le funzionalità, la più importante delle quali, come si vedrà in seguito, è l’implementazione di un journaling layer. Il modo con cui ext3 interagisce con i livelli più alti del sistema operativo è esattamente lo stesso che viene utilizzato in ext2, l’unica cosa che cambia, ovviamente, sono i direttori in cui i sorgenti dei due file system possono essere reperiti. 3.1 Struttura fisica Il file system è costituito di tanti gruppi di blocchi (block groups) di memoria secondaria, che non hanno però necessariamente una corrispondenza diretta con i blocchi fisici sulla memoria di massa, specialmente da quando i dischi rigidi sono stati ottimizzati per le operazioni di lettura sequenziale e quindi tendono a nascondere la loro struttura fisica al sistema operativo che li gestisce[vfs/ext2]. La struttura fisica del file system è rappresentata nella figura 3.1. BOOT Sector Superblocco 0 1024 Gruppo di Gruppo di Gruppo di Gruppo di Gruppo di blocchi 1 blocchi 2 blocchi 3 blocchi 4 blocchi n 2048 Copia del Descrittore Superblocco di gruppo Block Bitmap Inode Bitmap Parte della inode Table Blocchi Dati Figura 3.1: Organizzazione della memoria nei file system Ext3. 27 Il punto di partenza per il file system è sempre il superblocco, che è una struttura dati di 1024 bytes allocata ad una distanza di 1024 bytes dall’inizio della partizione; il resto della memoria viene diviso in vari gruppi di blocchi. Ogni gruppo di blocchi contiene una copia delle informazioni fondamentali di controllo della memoria (superblocco ed i descrittori di file system), ed inoltre contiene una parte del file system: una block bitmap, una inode bitmap, una parte della tabella degli inode (inode table) ed i blocchi dati. La tabella degli inode non viene quindi memorizzata tutta in un particolare settore di memoria, ma è suddivisa nei vari block groups in modo che ogni file si trovi nei limiti del possibile nello stesso gruppo di blocchi del corrispondente inode, così da ridurre al minimo gli spostamenti delle testine del disco durante le richieste d’accesso ai contenuti di un file. L’utilizzo dei gruppi di blocchi rappresenta un vantaggio anche in termini di affidabilità del sistema, visto che la replicazione delle strutture dati di controllo in ogni block group consente un facile recupero del superblocco nel caso questo risulti essere corrotto o danneggiato. La presenza della copia del superblocco in ogni gruppo può essere impostata attraverso l’uso della flag SPARSE_SUPERBLOCK, che a seconda del suo stato notifica o meno la presenza del superblocco all’interno di un block group. In questo modo è possibile diminuire lo spreco di memoria evitando di mantenere una copia del superblocco in ogni gruppo, memorizzandola soltanto all’interno di alcuni. Il descrittore di gruppo contiene le informazioni riguardanti la struttura del gruppo di blocchi a cui appartiene, ed è una struttura dati formata dai seguenti elementi: Nome campo tipo di commento dato Contiene l’indirizzo del blocco in cui è bg_block_bitmap ULONG immagazzinata la block bitmap per questo gruppo. Contiene bg_inode_bitmap ULONG l’indirizzo in cui è memorizzata l’inode bitmap per questo gruppo. 28 Contiene ULONG bg_inode_table l’indirizzo in cui è memorizzata la inode table per questo gruppo. Conteggio dei blocchi liberi di questo bg_free_blocks_count USHORT gruppo Conteggio degli inode liberi di questo bg_free_inodes_count USHORT gruppo Numero di inode del gruppo che sono bg_used_dirs_count USHORT bg_pad USHORT padding bg_reserved ULONG[3] padding La dimensione del directory descrittore può essere calcolata come (sizeof(Ext3_group)*numero_di_gruppi)/dimensione_blocco . La block bitmap è una serie di bit che indica quali blocchi del gruppo sono stati allocati e quali no (ogni blocco è rappresentato da un bit) ed, analogamente, l’inode bitmap rappresenta quali voci, della parte di inode table presente nel gruppo, sono state allocate ad inode e quanti “slot” rimangono liberi per l’allocazione di nuovi file. La dimensione in byte della block bitmap, per esempio, può essere calcolata nel seguente modo: (blochi_per_gruppo/8)/dimensione_blocco arrotondando per eccesso entrambe le divisioni [spec]. In Ext3 le directory sono gestite come Linked Lists 4 (liste collegate) di lunghezza variabile: ogni elemento della lista contiene l’inode number, la dimensione dell’elemento stesso, la lunghezza del nome ed il nome del file stesso (in Linux sia file che directory vengono trattati dal sistema operativo come files). Utilizzando questo meccanismo che consente elementi di lista di lunghezza variabile è possibile utilizzare lunghi nomi di file senza sprecare spazio nelle directory. La struttura di un elemento presente in una directory (directory entry) è mostrata nella tabella 3.1: 4 Per una spiegazione esaustiva sul significato di linked list si vada all’appendice C. 29 Inode number Dimensione elemento Lunghezza del nome Nome del file Tabella 3.1: Struttura di una directory entry. Come esempio, la tabella 3.2 mostrerà la struttura di un direttorio contenente i files: file1, long_file_name, e f2. i1 16 05 file1 i2 40 14 long_file_name i3 12 02 f2 Tabella 3.2: esempio di directory Ext3. La dimensione dei blocchi di memoria non è fissa, ma può essere modificata dall’amministratore di sistema utilizzando l’utility tune2fs, che permette di sceglierne la dimensione (tipicamente 1024, 2048 o 4096 bytes). L’utilizzo di grandi blocchi di memoria, come già in precedenza affermato, velocizza le operazioni di I/O ma allo stesso tempo comporta uno spreco di spazio su disco, in quanto questo file system non permette che uno stesso blocco sia utilizzato da più file, lasciando così l’ultimo blocco assegnato ad ogni file statisticamente mezzo vuoto. 3.2 Gestione delle directory Il file system Ext3 eredita dal suo predecessore una meccanismo di gestione dei direttori basato sulle linked lists, come spiegato nel paragrafo precedente. Un sistema di questo tipo, tuttavia, non consente di utilizzare tutti gli algoritmi di bilanciamento e ottimizzazione della ricerca che sarebbero possibili utilizzando una struttura ad albero, ed allo stesso tempo non rende il file system immune da perdite di dati dovute alla possibile corruzione di nodi vicini alla radice, che potrebbero rendere irraggiungibile buona parte dei files presenti in memoria. Per ovviare a questi problemi è stato sviluppato, e reso disponibile a partire dalla release 2.4 del kernel Linux, un sistema di organizzazione ad albero bilanciato per i direttori chiamato “HTree” [HTree]. Questo sistema identifica ogni blocco con una hash key di 32 bit, ed ogni chiave fa riferimento ad una serie d’oggetti immagazzinati in una foglia dell’albero. Il fatto che i 30 riferimenti interni, che sono anch’essi memorizzati all’interno di blocchi di memoria, siano di lunghezza fissa di 8 byte, garantisce un elevato fanout per l’albero (utilizzando blocchi di memoria di 4KB possono essere memorizzati più di 500 riferimenti per blocco); e i due livelli di nodi, di cui è al massimo composto l’albero, sono sufficienti per poter memorizzare più di 16 milioni di file che hanno una lunghezza del nome di 52 caratteri. Questo sistema è stato studiato in modo da garantire una perfetta compatibilità all’indietro, e funziona in modo parallelo al sistema d’indirizzamento lineare basato sulle liste dati; il sistema, infatti, utilizza la struttura HTree soltanto per ordinare ed avere la possibilità di fare ricerche veloci all’interno dell’albero, ma per eseguire le operazioni sui files esso usa il tradizionale riferimento ad inodes e blocchi di memoria. Per rendere tutto ciò possibile, le foglie dell’albero sono state progettate in modo da essere identiche ai blocchi contenenti le directory utilizzate nel vecchio meccanismo, ed i blocchi interni contenenti strutture dati di 8 byte appaiono ai kernel che non supportano gli HTree come directory entries cancellati. Un altro vantaggio dovuto a quest’attenzione alla compatibilità viene dal fatto che in questo modo la struttura ad albero diviene estremamente robusta: nel caso in cui uno dei nodi interni dovesse essere corrotto, il kernel potrebbe rintracciare i vari files e le directory utilizzando il sistema tradizionale basato sulle linked lists. L’utilizzo di questo algoritmo d’ordinamento ha portato ad un aumento di prestazioni, nell’accesso a directory contenenti un gran numero di files, fino a 50 o 100 volte rispetto ai tempi impiegati dalle precedenti versioni che non utilizzano questo meccanismo; e l’unico svantaggio viene notato quando si usa il comando readdir(), che ritorna i file presenti in un direttorio ordinati secondo la propria hash key e provoca una lettura degli inode contenenti i dati dei file in ordine del tutto casuale, senza ottimizzare quindi i salti delle testine del disco rigido tra un gruppo di blocchi e l’altro. Una soluzione a questo problema che è tuttora allo studio è quella di associare direttamente ad ogni inode la relativa hash key, e raggruppare quindi gli inodes in memoria per chiave; più nello specifico, la soluzione a cui si sta lavorando, è quella di allocare ogni nuovo inode all’interno di un possibile range di inode liberi d’ampiezza dipendente dalla dimensione del direttorio in cui il file si trova, e quindi scegliere un inode libero per l’allocazione in modo dipendente dalla hash key del file. 31 3.3 Attributi estesi ed ACL In questa sezione si discuterà di come sono implementate le access control lists in Linux, ed in particolare sul file system ext3 [POSIX-ACL]. Per via di altre estensioni del kernel che traggono vantaggio dal poter associare parti di informazioni ai files, Linux e la maggior parte degli altri sistemi UNIX- like implementa le ACL sottoforma di attributi estesi (Extended Attributes, o più brevemente EAs) dei files. Un attributo esteso è un pezzo d’informazione relativa ad un file (esterna però al file, non facente parte del suo contenuto, che è utilizzato dai processi utente) che non viene memorizzata all’interno del relativo descrittore, ma è allocata in parti ben definite del disco; più precisamente esso è una coppia nome- valore associata permanentemente ad un file, simile ad una variabile ambiente di un processo. Per rendere disponibili i vantaggi delle ACL ai processi utente, il sistema operativo mette a disposizione delle system calls specifiche per gli attributi estesi, che fungono quindi da interfaccia tra kernel e processi. La scelta di progetto più facile e lungimirante per implementare gli attributi estesi, è quella di associare ad ogni file una directory nascosta contenente un file per ogni attributo relativo (file che ovviamente contiene il valore dell’attributo), ma una soluzione di questo tipo comporterebbe un grande spreco di spazio su disco per l’allocazione di nuovi blocchi per file e direttori, oltre che una perdita di tempo dovuta alle operazioni di accesso ad ogni singolo file-attributo; ecco perché la maggior parte dei sistemi operativi utilizza metodi implementativi differenti. Come definito nel kernel Linux, ogni inode contiene un campo chiamato i_file_acl; se questo campo non è uguale a zero, esso contiene allora il numero del blocco di memoria in cui sono memorizzati gli attributi estesi associati all’inode: attributi che indipendentemente dal tipo sono composti da un nome (che identifica l’attributo stesso) ed un valore associato, inoltre tutti gli attributi estesi associati ad uno stesso inode devono essere memorizzati all’interno dello stesso blocco. La figura 3.2 mostra in che modo il sistema associa agli inode gli attributi estesi: diversi inode possono condividere lo stesso blocco attributi (se hanno uguali attributi), ma i blocchi contenenti gli attributi estesi non si trovano necessariamente nello stesso gruppo di blocchi in cui è memorizzato il relativo inode. 32 I1: i_file_acl= 216 I2: i_file_acl=218 I3: i_file_acl=216 I4 : i_file_acl=NULL I5:…. Inode Table Data Blocks Blocco dati 216, che memorizza attributi estesi Data Block 217 Blocco dati 218, che memorizza attributi estesi Data Blocks Figura 3.2: Uso di i_file_acl per memorizzare il riferimento al blocco contenente gli attributi. Per ottimizzare l’utilizzo dello spazio su disco, il file system consente a diversi inode che hanno identici attributi estesi di condividere lo stesso blocco attributi su disco; il numero di inode che fanno riferimento ad un unico blocco è controllato attraverso un contatore di riferimenti ad inode presente nel blocco stesso. La condivisione dei blocchi attributi è implementata in modo trasparente rispetto all’utente: Ext3 tiene automaticamente traccia dei blocchi attributi recentemente aperti, ed inoltre utilizza una tabella che tiene conto dei blocchi ordinandoli per numero e per riassunto (checksum) dei contenuti (tabella che è implementata come una hash table di doppie linked lists, per numero e contenuti). Un singolo blocco attributi può essere condiviso da un numero massimo stabilito di 1024 inodes, questo per limitare il danno causato in caso di danneggiamento del singolo blocco, che si ripercuoterebbe su di un numero limitato di files. Quando vengono modificati gli attributi di un file che utilizza la condivisione del blocco, viene utilizzato un meccanismo di copy-on-write per allocare un nuovo blocco attributi da associare al file in questione; questo salvo che i nuovi attributi del file non corrispondano a quelli presenti su di un blocco già esistente che può essere condiviso. L’implementazione corrente di questi attributi richiede che tutti quelli associati ad uno stesso file siano contenuti all’interno di un unico blocco, cosa che va ad influenzare la dimensione di un singolo attributo, visto che la dimensione dei blocchi, in ext3 può essere di 1, 2, o 4KB. Il grosso limite di questo tipo di implementazioni viene alla luce nelle partizioni in cui tutti i file tendono ad avere set differenti di attributi, all’interno delle quali si ha un notevole spreco di spazio su disco, sia per i blocchi in sè che per memorizzare la tabella riassuntiva utilizzata dal sistema. Inoltre la memorizzazione degli attributi di un nuovo file, in un caso di questo tipo, comporterebbe un grosso carico di lavoro eseguito dal sistema per scorrere l’intera hash table per cercare un blocco corrispondente e, 33 eventualmente, allocare una nuova voce in tabella nel caso non si trovassero blocchi da poter mettere in condivisione. 3.4 Quote disco in Ext3 Come già accennato nel capitolo 2, un moderno file system deve provvedere alla gestione dello spazio su disco tra i vari utenti. Il kernel Linux definisce un’interfaccia standard, attraverso il VFS, con la quale interagisce con tutti i file system che supportano le quote disco, che sono in ogni caso trattate alla stregua di tutti gli altri dati in memoria: queste sono memorizzate all’interno di appositi file (appartenenti a blocchi di memoria contraddistinti da un apposito header, descrittore), chiamati quota file, ai quali si può accedere attraverso un inode. Il superblocco contiene sempre un riferimento ai quota file, in modo che il sistema operativo possa avviare il meccanismo di gestione delle quote al momento dell’inizializzazione del file system. I file che il kernel mette a disposizione per gestire questa funzione, si trovano nel direttorio /include/linux dei codici sorgenti del kernel, e sono i seguenti: ?? quota.h: definisce le strutture dati ed i parametri che sono utilizzati nelle comunicazioni tra il VFS ed i file system, in particolare viene dichiarata la struttura struct dquot_operations che contiene tutte le operazioni necessarie per la gestione del file quota su disco. ?? quotaio_v1.h e quotaio_v2.h: definiscono le strutture che il VFS utilizza per definire il formato del quota file e dei blocchi contenenti i dati. In particolare sono contenute le strutture che fungono da descrittore dei quota file e dei quota block. ?? quotaops.h: contiene le definizioni delle operazioni eseguibili sulle quote disco. Dopo che il sistema quote è stato configurato, le routine contenute in questa libreria associano le funzioni di gestione delle quote del VFS al corretto codice sorgente. Il file system ext3 definisce il proprio meccanismo di gestione delle quote disco all’interno del file /fs/Ext3/super.c contenuto nei sorgenti del kernel Linux: in esso sono ridefinite le strutture dquot_operations e quotactl_ops (dichiarate anche in quota.h) in modo da associare ad ogni funzione puntata dalle strutture la specifica funzione del file system, come riportato di seguito: 34 static struct dquot_operations ext3_quota_operations = { .initialize .drop = ext3_dquot_initialize, = ext3_dquot_drop, .alloc_space = dquot_alloc_space, .alloc_inode = dquot_alloc_inode, .free_space = dquot_free_space, .free_inode = dquot_free_inode, .transfer = dquot_transfer, .write_dquot = ext3_write_dquot, .acquire_dquot = ext3_acquire_dquot, .release_dquot = ext3_release_dquot, .mark_dirty = ext3_mark_dquot_dirty, .write_info = ext3_write_info }; static struct quotactl_ops ext3_qctl_operations = { .quota_on = ext3_quota_on, .quota_off = vfs_quota_off, .quota_sync = vfs_quota_sync, .get_info = vfs_get_dqinfo, .set_info = vfs_set_dqinfo, .get_dqblk = vfs_get_dqblk, .set_dqblk = vfs_set_dqblk }; In linea generale questo file system mette a disposizione per default all’amministratore di sistema (root) il 5% dei blocchi in cui è suddiviso il disco, in modo da consentire all’amministratore di poter uscire facilmente da eventuali situazioni in cui i processi utente riempiono il disco rigido. Questo tipo di gestione delle quote deriva direttamente da UNIX, e per questo motivo viene utilizzato come criterio base per molti file system Linux [vfs/ext2]. Per gestire le quote disco tra i vari utenti, questo file system utilizza gli strumenti che sono messi a disposizione dal sistema operativo che, come detto in precedenza, riesce in questo modo ad utilizzare un sistema di dialogo con i vari file system del tutto trasparente agli occhi dell’utente (attraverso il VFS). 35 3.5 Journaling La vera innovazione di questo file system rispetto alla sua versione precedente (ext2) consiste nell’aver implementato un meccanismo di registrazione delle operazioni di I/O su disco all’interno di un apposito file che funge appunto da registro (journal, giornale, diario); in questo modo diventa possibile garantire l’integrità del file system in caso di crash del sistema operativo o di blackout improvvisi [JFS]. Ext3 implementa uno strato (layer) di codice completamene indipendente dal software sottostante, chiamato journaling layer o JFS, che funziona da interfaccia tra il file system vero e proprio ed il resto del sistema operativo: esso registra tutte le operazioni di scrittura che avvengono su disco e le raggruppa in una serie di transazioni (transactions) sul proprio registro, che saranno successivamente scritte definitivamente nel file system. Più in particolare, il file system vero e proprio non comunica con il journaling layer se non attraverso le transazioni, ciascuna delle quali rappresenta un’operazione di aggiornamento dei dati atomica, cioè un’operazione di aggiornamento che viene eseguita interamente oppure non viene eseguita. La struttura e l’organizzazione del sistema di journaling sono esemplificate nella figura 3.3: il journaling layer cattura soltanto le operazioni di scrittura in memoria, e le raggruppa in transazioni all'interno del journal file, per poi scriverle in memoria. Le letture da disco, invece, avvengono in modo tradizionale, senza coinvolgere parti di codice aggiuntive. Applicativi di alto livello Lettura dei dati Scrittura Dati Journaling layer Riversamento in memoria delle transazioni Scrittura Transazioni Blocchi dati del File system Transazione Transazione Transazione Transazione 1 2 3 n Journal File Figura 3.3: Organizzazione del sistema di journaling di Ext3. L’atomicità delle operazioni di scrittura del file system, e quindi l’integrità dello stesso, è garantita dal fatto che il JFS registra al suo interno tutte le operazioni di scrittura richieste dal sistema operativo: in caso di blackout quindi l’unica parte di disco che 36 rischia di non essere consistente per il sistema operativo è il registro delle transazioni, che però viene gestito autonomamente con opportuni algoritmi dal JFS. L’atomicità della scrittura di una singola transazione dal journal al file system è invece assicurata dal fatto che le transazioni sono progettate in maniera e con una dimensione tale da poter garantire la loro intera scrittura su disco anche in caso di blackout: con i dischi rigidi attualmente in commercio, anche se venisse a mancare l’alimentazione, la testina di scrittura potrebbe completare l’operazione recuperando l’energia necessaria dall’inerzia di rotazione del supporto di memoria. Ext3 lascia all’utente la possibilità di scegliere in che modo memorizzare il registro transazioni, occupandosi poi della sua gestione una volta scelta la collocazione: il registro può essere memorizzato come file del file system sfruttando la normale organizzazione degli inode, oppure può trovarsi all’interno di un range di blocchi di memoria prestabilito e ad esso riservati; è inoltre possibile che il journal file si trovi all’interno del file system che esso gestisce, ma magari su di un altro disco rigido o su una partizione differente, è anche possibile fare in modo che diverse partizioni formattate in ext3 condividano il medesimo journal file, della cui gestione si occuperà soltanto il relativo JFS. Ogni transazione scritta su registro viene contraddistinta da un apposito tag della lunghezza di 512 byte, che serve ad identificare il blocco di memoria in cui si trova come blocco riservato al journal file, e che contiene inoltre il numero identificativo della transazione a cui appartiene. I 512 byte che servono da tag vengono scritti solo dopo che sono stati memorizzati in registro tutti gli altri dati appartenenti alla transazione, in modo da poter distinguere, in caso di blackout o altri imprevisti, le transazioni che sono state memorizzate correttamente (e sono quindi pronte per essere scritte nel file system) da quelle rimaste incomplete che saranno rimosse dal registro. Il JFS assegna al file di registro uno spazio ben definito su disco, spazio che può essere ampliato in caso di necessità attraverso opportune system call, utilizzando la memoria virtuale che il sistema operativo mette a disposizione. In linea di massima, però, il numero di transazioni che possono essere memorizzate nel journal file non è infinito, ed occorre un meccanismo che liberi lo spazio necessario per le nuove transazioni da registrare. Per ovviare a questo problema il journal file è progettato alla stregua di un buffer FIFO (first in first out): quando diventa necessario liberare spazio sul file, il journaling layer scrive su disco le transazioni più vecchie presenti su registro, marca queste ultime come transazioni eseguite (operazione, questa, necessaria per avere la 37 certezza di quali operazioni sono state eseguite in caso di arresti improvvisi di sistema) e le cancella dal file, liberando spazio per registrare nuove transazioni; questo processo prende il nome di “checkpointing” delle transazioni. Per la scrittura su registro di file di grandi dimensioni, il JFS spezza l’operazione in tante transazioni che vengono via via scritte sul file system proprio attraverso il checkpointing, meccanismo che è del tutta analogo al comando truncate() di Linux. Il sistema di journaling deve essere in grado di gestire anche operazioni di aggiornamento continuo di un numero limitato di blocchi di memoria, per fare questo esso funge da immagine del file system rispetto al VFS ed al sistema operativo in generale: in casi come questo non è garantito che ogni singolo aggiornamento diventi effettivo nel file system, ma è il JFS che si prende carico di memorizzare ognuno di questi ultimi all’interno del registro, e di fare da “immagine” della parte di memoria secondaria da aggiornare rispetto al VFS ed al resto del sistema operativo, per poi riversare nel file system gli aggiornamenti solo periodicamente, snellendo così il carico di lavoro del disco rigido e della CPU; l’unico modo per garantire l’ordinamento e la sequenzialità degli aggiornamenti è quello di aprire un file in modalità O_SYNC, oppure eseguire l’operazione di fsync() su di esso. Il journaling layer è inoltre in grado di gestire i blackout o gli arresti di sistema in tutta una serie di casi particolari come garantire l’aggiornamento dei quota files in seguito ad una scrittura da parte di un determinato utente, garantire la cancellazione di un file o di una directory qualsiasi e l’eliminazione dei dead link, che sono quei file che sono stati cancellati dal disco (hanno un link count uguale a zero) ma sono ancora presenti in memoria perché utilizzati da uno o più processi al momento dell’arresto di sistema. In definitiva si può affermare che il sistema di journaling che ext3 mette a disposizione rappresenta un grosso vantaggio, perchè garantisce di avere un file system sempre consistente ed evita all’utente l’uso di utility come fsck, che per controllare la consistenza di partizioni di dimensioni sempre maggiori impiegherebbero una quantità di tempo spropositata. I particolari del journaling layer su cui si sta ancora lavorando riguardano la trasparenza del JFS rispetto all’utente e lo snellimento del carico di lavoro del JFS stesso. Sul piano della struttura fisica ext3 differisce dalla sua versione precedente solo per la presenza del journal file, ed una partizione di memoria che utilizza ext3 può essere rimontata utilizzando ext2 senza che quest’ultimo trovi il file system incoerente, perché il file di registro viene visto come un normale file. 38 Si sta quindi lavorando ad una funzione di tune2fs che consenta all’utente di aggiungere un inode arbitrario, riservato ad un journal file di dimensione definita ad una partizione ext2 esistente, senza che esso appaia come file nel file system, e quindi senza che l’utente possa eliminarlo o modificarlo arbitrariamente. L’altra grossa modifica che è in corso riguarda il tipo di journaling che il JFS implementa: nella versione attuale i dati che sono inseriti nelle transazioni riguardano gli aggiornamenti che vengono fatti nell’intero file system, in altre parole il registro memorizza gli aggiornamenti sia nei dati utente che nei metadati, così da poter eseguire una semplice operazione di write-behind per aggiornare il file system. Un meccanismo di questo tipo, però, comporta carichi di lavoro molto elevati per la macchina (nonostante gli aggiornamenti del file system siano comunque eseguiti nei momenti in cui la CPU ed il disco non sono in pieno utilizzo), e ci si sta quindi muovendo nella direzione di eseguire un aggiornamento soltanto dei metadati, quando la situazione ovviamente lo permette. Un esempio di metadata-only journaling può essere quello dell’allocazione di un blocco ad un particolare file: nel caso di metadata journaling, sul registro verrebbe scritto quale blocco deve essere contrassegnato come “in uso” e a quale file deve essere aggiunto un puntatore a quel blocco, transazione che occuperebbe uno spazio di pochi kilobyte su registro, mentre l’attuale implementazione di ext3 eseguirebbe un’intera copia dei blocchi che verrebbero modificati. 3.6 Altri particolari di Ext3 In aggiunta alle caratteristiche standard messe a disposizione da Unix, Ext3 supporta alcune estensioni che non sono normalmente presenti nei file system Unix [vfs/ext2]. Questo file system implementa i link simbolici veloci (fast symbolic links), che non memorizzano il nome del file di riferimento all’interno del blocco di memoria ma all’interno dell’inode stesso, evitando sprechi di memoria e velocizzando le operazioni di lettura dei link (non c’è bisogno di accedere ai blocchi dati per recuperare il file di riferimento). Un tipo di file di recente introduzione è il file immutabile, un tipo di file, cioè, che può essere soltanto letto: nessun utente è in grado di scriverlo o di cancellarlo. I file di tipo append-only, invece, possono essere aperti in modalità di scrittura, ma ogni dato aggiunto viene inserito in coda al file, tipo che è molto utile per i file che possono 39 soltanto crescere di dimensione come i process log; come i file immutabili, questi non possono essere cancellati o rinominati. Ext3 si avvantaggia dell’utilizzo della buffer cache per accedere al disco compiendo delle pre- letture (readaheads): quando un blocco deve essere letto, il codice del kernel richiede la lettura di diversi blocchi di memoria ad esso contigui. I readaheads sono normalmente effettuati durante le letture sequenziali dei dati, e questo file system le estende anche alla lettura dei direttori. I gruppi di blocchi sono utilizzati dal file system per raggruppare insieme inode e dati: il codice del kernel prova sempre ad allocare blocchi dati per un file nello stesso gruppo in cui è memorizzato il relativo inode; questo per ridurre al minimo gli spostamenti della testina del disco rigido per ricercare ad accedere ai contenuti del file. Anche durante le operazioni di scrittura, ext3 prealloca fino a 8 blocchi dati adiacenti a quello che deve essere scritto, in modo da tenere i data block appartenenti allo stesso file i più vicini possibile tra loro. Con questa tecnica si raggiungono hit rate del 75% in preallocazione anche in partizioni con poco spazio libero residuo, e si velocizzano le operazioni di lettura sequenziale dei dati. 40 4 Il file system ReiserFS Questo file system, sviluppato dal team diretto dal dott. Hans Reiser, presenta nella sua attuale versione, la quarta, diverse caratteristiche implementative che lo rendono molto efficace nell’adempiere ai requisiti discussi nel capitolo 2 [reiser]. Come per tutti i file system che utilizzano il VFS per relazionarsi con il sistema operativo, ReiserFS si interfaccia con il codice di livello più elevato attraverso il superblocco, gli inode ed i file; entità, queste, che sono gestite ai livelli più bassi in modo autonomo dal codice del file system. Ogni oggetto che viene creato all’interno della partizione è gestito da un apposito plugin, che contiene al suo interno tutte le funzioni necessarie per eseguire le operazioni riguardanti quel tipo di oggetto. Esistono quindi file plugins, directory plugins, e plugin per gestire tutti gli elementi memorizzati; un’implementazione di questo tipo permette di aggiornare le funzionalità riguardanti un singolo aspetto del sistema operativo senza dover stravolgere l’intero codice, sono poi messi a disposizione dei controlli che permetteranno di scegliere quale plugin utilizzare per operare su di un particolare tipo d'oggetto (cosa che, salvo interventi dell’utente, è eseguita in modo automatico dal sistema). 4.1 Organizzazione della memoria ReiserFS divide le partizioni su disco in blocchi di 4KB, collegati tra loro attraverso una struttura ad albero di dimensione finita. Una divisione di questo tipo velocizza l’accesso ai dati da parte delle applicazioni che utilizzano il comando mmap() per accedere direttamente ad i contenuti dei file in memoria, in quanto questa routine necessita che i dati letti siano allineati di 4KB, rendendo necessarie operazioni di allineamento nel caso i dati fossero divisi in modo diverso. Tutti i blocchi vengono poi organizzati in una struttura ad albero finito, in cui solo i rami di livello più basso contengono riferimenti alle foglie, che sono i blocchi che contengono i dati utente. Il sistema riesce a recuperare ogni oggetto che viene memorizzato nell’albero tramite l’uso delle hash key che vengono associate a questi ultimi, e tutti gli oggetti che vengono memorizzati in un blocco sono ordinati al suo interno per chiave. 41 La ricerca di un oggetto all’interno dell’albero parte dalla radice (da cui possono essere raggiunti tutti gli altri nodi), e prosegue nei vari nodi; man mano che la ricerca si muove da un nodo all’altro, per scegliere in quale sottoalbero proseguire, il codice controlla le chiavi: per ogni puntatore ad un sottoalbero esiste una chiave limite sinistra (left delimiting key, per sinistra s’intende di livello più alto, navigare un albero da sinistra a destra significa percorrerlo dalla radice verso le foglie), ed i puntatori ai sottoalberi e i sottoalberi stessi sono ordinati per chiave limite sinistra, che corrisponde alla chiave più piccola che identifica gli oggetti presenti nel sottoalbero. Ogni sottoalbero contiene oggetti la cui chiave limite sinistra corrisponde alla chiave limite sinistra del relativo puntatore (puntatore al sottoalbero), e riassumendo le informazioni in questo modo diventa possibile scegliere quale sottoalbero esplorare, senza dover più usare algoritmi di navigazione di tipo ricorsivo. In particolare, lo Unix directory plugin presente nel codice del file system, analogamente alle specifiche del VFS, implementa le directory come una lista di “directory entries” (elementi presenti nella directory), ciascuna contenente un nome ed una chiave. Quando viene dato un nome come parametro di ricerca da risolvere, il directory plugin trova la directory contenente il nome in questione e di conseguenza ritorna la chiave presente nella relativa directory entry. A questo punto la chiave può essere utilizzata dal codice che gestisce la memorizzazione dei dati nell’albero per trovare tut te le parti del file che è stato precedentemente nominato. La chiave limite destra, anch’essa memorizzata all’interno di ogni sottoalbero, è la più grande di tutte le chiavi, e riassume il percorso dalla radice fino al punto dell’albero a cui si è arrivati. Il contenuto di ogni nodo nell’albero è ordinato per chiave; di conseguenza anche l’intero albero è ordinato per chiave, e per una chiave data si conosce subito il percorso da fare per trovare almeno un elemento da essa contraddistinta. Dato che una chiave intera non identifica un file ma un particolare byte all’interno di quest’ultimo, durante le ricerche, il codice ritornerà soltanto la parte di chiave necessaria ad identificare il file nella sua interezza. Come in ext3, per ragioni di efficienza e di fanout, nei nodi interni non vengono memorizzati dati, ma soltanto puntatori ai sottoalberi con le relative chiavi di ricerca, e relegando i dati utente soltanto all’interno delle foglie dell’albero, come mostrato in figura 4.1: ciascun nodo dell’albero è composto di campi dati preformattati contenenti, appunto, la hash key ed il riferimento ai blocchi contenenti i dati relativi ad ogni 42 oggetto. I blocchi contenenti i dati (foglie) possono contenere più di un oggetto (block sharing), e ciascuno di questi ultimi è inglobato all’interno di un proprio “item”, contenitore formattato, contenente i dati dell’oggetto, la chiave identificativa ed altre informazioni necessarie alla corretta gestione dei contenuti. Figura 4.1: Struttura dei nodi in ReiserFS. ReiserFS organizza i dati secondo un particolare tipo d’albero bilanciato chiamato “dancing tree” (albero danzante). In opposizione ai normali alberi bilanciati binari, che tendono ad essere bilanciati in ogni istante, il dancing tree è progettato in modo da eseguire le operazioni di bilanciamento soltanto quando il carico di lavoro per la macchina è minore, oppure solo in seguito ad un riversamento di dati su disco. L’idea dietro a questo sistema è quella di velocizzare le operazioni con i file posticipando le operazioni di ottimizzazione dell’albero, e scrivendo i dati su disco solo quando necessario, visto che scrivere su supporto rigido è un’operazione molto più lenta che scrivere i dati in RAM. Per questo motivo, le operazioni di ottimizzazione sono eseguite meno di frequente, ma d’altra parte possono risultare più pesanti delle ottimizzazioni che sarebbero eseguite se si applicassero gli algoritmi di bilanciamento ad ogni scrittura. Il dancing tree permette inoltre di fondere insieme il contenuto di diversi nodi la cui capacità non è utilizzata appieno; questa funzionalità è utilizzata in risposta ad eventuali richieste di memoria da parte del sistema, ed ogni volta che i dati vengono scritti su disco come riversamento in memoria di una transazione completata. Con la stessa idea di fondo viene utilizzato un altro accorgimento per ridurre il tempo perso in scritture su disco: la procrastinazione degli aggiornamenti su disco. Con questo metodo si esegue il maggior numero possibile di aggiornamenti dei dati direttamente in RAM, per poi riversare tutto su disco solo quando si ha a disposizione una versione definitiva di quello che sarà il nuovo layout di memoria. 43 In ReiserFS ogni oggetto può essere visto sia come file che come directory allo stesso tempo: se si accede ad esso come file lo si potrà manipolare come sequenza di byte, se lo si apre come directory si possono ottenere i files e tutti gli oggetti in esso contenuti; rendere possibile questi tipi di accesso è necessario per poter implementare stream ed attributi. Per implementare un file Unix regolare, con la relativi metadati, sono utilizzati un plugin per il corpo del file, un directory plugin per trovare i file plugin che verranno usati per interpretare i metadati. Altra particolarità di questo file system è la possibilità di memorizzare file nascosti: questi sono immagazzinati all’interno dell’albero come normali directory entries, ma non sono visibili all’utente con il normale comando readdir; questo è molto utile per permettere alle applicazioni l’accesso ai propri file nascosti, senza disturbare l’utente permettendogli di vedere ed accedere a file di applicazione quando questo non è necessario. 4.2 Struttura fisica La memorizzazione dei dati sulle partizioni ReiserFS si avvale del concetto di item (elemento). Un item è un contenitore dati di formattazione predefinita che è contenuto all’interno di un singolo nodo, e permette in questo modo di gestire lo spazio all’interno dei nodi. In questo file system ogni item ha una chiave identificativa, un offset da inizio nodo a cui il corpo dell’item comincia, una lunghezza del corpo ed un pluginID che identifica il tipo di item che è stato memorizzato. I nodi che contengono item sono chiamati nodi formattati (formatted nodes), mentre i nodi che non contengono questi elementi sono chiamati foglie non formattate (unfleaves, unformatted leaves); questi ultimi si possono trovare soltanto a fine albero, in quanto possono essere utilizzati per memorizzare dati appartenenti ad un file, in porzioni di memoria chiamati extent: un extent è una sequenza di blocchi contigui non formattati che appartengono allo stesso oggetto. Un puntatore ad extent contiene il numero del primo blocco e la lunghezza totale della sequenza, in modo da non sprecare spazio per memorizzare riferimenti per ogni singola unfleave; e visto che ciascun extent appartiene ad un solo oggetto, può essere memorizzata una sola chiave per ognuno di essi, per poi calcolare la hash key d’ogni byte all’interno degli stessi. Grazie a questo 44 meccanismo s’inizia a risparmiare spazio all’interno dei nodi per tutti gli extent più grandi di due blocchi. Ritornando sui nodi formattati, Reiser4 definisce diversi tipi di item, per contenere i vari tipi d’informazioni: ?? static_stat_data: contiene proprietario, permessi, ultimo accesso, creazione, ultima modifica, e numero di link appartenenti ad un file. ?? cmpnd_dir_item: contiene le directory entries (elementi contenuti nei direttori) e le chiavi dei file a cui ogni entry fa riferimento. ?? Puntatori ad extent: spiegati sopra. ?? Puntatori a nodi: spiegati nel paragrafo precedente, contengono anche la chiave limite del nodo a cui fanno riferimento. ?? bodies (parti di file): contiene gruppi di dati appartenenti ad un file che non sono abbastanza grandi da riempire un singolo nodo. Di seguito è riportato la forma con cui sono definite le strutture dati utilizzate da ReiserFS: La foglia non formattata, che è l’unico nodo che non ha un proprio header (descrittore), è una semplice sequenza di byte contigui. .................................................................................................................................................... La struttura dell’item è la seguente: Item_Body . . separated . . Item_Head Item_Key Item_Offset Item_Length Item_Plugin_id Una foglia formattata ha la seguente struttura: Block_Head Item_Body 0 Item_Body 1 - Item_Body n 45 Free Space Item_Headn - Item_Head1 Item_Head0 Un nodo interno all’albero, appartenente cioè ad un ramo, ha la seguente struttura: Block_Head Item_Body 0 --- NodePointer0 Item_Body n NodePointern Free Space Item_Headn - - - Item_Head0 I nodi più esterni, che contengono puntatori ai dati (foglie dell’albero), sono chiamati “ramoscelli” (twig): Block_Hea d Item_Body 0 Item_Body 1 Item_Body 2 Item_Body 3 Item_Body n Free NodePointe ExtentPointe NodePointe ExtentPointe NodePointe Spac r0 rn r1 r2 r3 e Item_Hea Item_He dn ad0 Come accennato in precedenza, questo file system memorizza i riferimenti ai dati soltanto nei nodi di livello più basso (twig, che sono gli unici che possono quindi contenere extent pointers), questa scelta consente di utilizzare algoritmi meno pesanti nelle operazioni di bilanciamento dell’albero. Altra peculiarità di questo file system è la possibilità di condividere blocchi tra più oggetti: un singolo blocco non viene assegnato ad un singolo file, ma può contenere dati appartenenti a diversi oggetti. Questo sistema, che permette di ottenere un’efficienza nell’utilizzo dello spazio su disco del 94%, è utilizzato per i file di grandi dimensioni la cui coda non riesce a riempire un intero blocco, e per tutti i file la cui dimensione è minore di quella di un singolo blocco; tutte queste informazioni vengono incapsulate negli items, in modo da poter essere identificate all’interno di ogni blocco. Questa funzionalità rende però molto lunghe le operazioni d’accesso ad un particolare file memorizzato in un blo cco condiviso, in quanto diventa necessario eseguire una scansione del blocco per posizionarsi all’offset a cui iniziano i dati: per questo motivo Namesys (l’azienda che ha sviluppato questo file system) consiglia di disabilitare questa funzione nel caso si dovessero utilizzare applicazioni che richiedono un utilizzo intensivo delle risorse della macchina. 46 4.3 Implementazione di stream di dati e di attributi ReiserFS utilizza soltanto due elementi, con i quali è in grado di realizzare gli stream di dati e tutti gli attributi associati ad ogni oggetto. Un file è una sequenza di byte contraddistinta da un nome. Una directory è uno spazio semantico che collega i nomi ad una serie di oggetti presenti nella directory stessa. Nella versione precedente di questo file system esistevano anche gli attributi dei file, che erano dati esterni al file, che però ne descrivevano le caratteristiche; un esempio di questi attributi sono i permessi d’accesso, le ACL, o la data di ultima modifica. Ogni attributo richiedeva la sua AP I, e creare nuovi attributi creava complessità e problemi di compatibilità nel codice. Dato che in Reiser4 i file possono essere interpretati anche come directory, diventa possibile implementare i normali attributi come singoli files. Per accedere ad un particolare attributo di un file, ora è necessario avere il nome del file, seguito dal separatore di directory ‘/’ ed il nome dell’attributo. Un file tradizionale è ora implementato in modo da possedere alcune caratteristiche proprie dei direttori; esso contiene dei file al suo interno (se visto come directory) che sono i file contenenti gli attributi associati al file stesso. Gli elementi necessari a comporre stream ed attributi per mezzo di file e directory sono: ?? Api efficienti per file di piccole dimensioni. ?? Metodi di memorizzazione che siano efficienti anche per piccoli file (tra cui descrittori di dimensioni contenute per evitare sprechi di memoria dovuti al fatto che i descrittori possono essere più grandi dei file a cui sono associati). ?? Plugin che siano anche in grado di comprimere un file che funge da attributo in un singolo bit. ?? File che si comportino anche come directory quando richiesto. ?? Associazioni d’appartenenza, inclusa l’aggregazione dei file. ?? Ereditarietà. ?? Transazioni. ?? Directory entries nascosti (file nascosti). Caratteristiche queste, che non erano presenti nella versione 3, e sono state realizzate in questa versione. 47 Il file system mette a disposizione la system call Reiser4(), che implementa tutte le caratteristiche necessarie per accedere alle ACL come file/directory; queste caratteristiche comprendono l’apertura e chiusura di transazioni, l’effettuare una sequenza di I/O in una singola system call, e la possibilità di accedere ai file senza dover utilizzare il descrittore (cosa, questa, ne cessaria per I/O di piccole dimensioni che siano efficienti). Il principale svantaggio dovuto a quest’implementazione degli attributi consiste nel fatto che ReiserFS, in questa versione, non rende disponibile la possibilità di condividere gli attributi tra i vari file, ed inoltre il file system pone un limite massimo di 64KB alla dimensione di ogni file attributo [POSIX-ACL]. 4.4 Journaling Analogamente ad Ext3, anche questo file system implementa un registro al cui interno memorizzare tutte le operazioni di I/O su disco, in modo da garantire l’integrità dei dati anche dopo arresti improvvisi di sistema. A differenza del file system preso in analisi nel capitolo precedente, però, ReiserFS utilizza un journaling soltanto dei metadati e non di tutti i dati che dovranno effettivamente essere scritti in memoria (questo, ovviamente, viene fatto quando la situazione lo permette). Anche in questo caso le operazioni di I/O sono raggruppate in transazioni memorizzate in un registro invisibile all’utente; una transazione mantiene intatto il contenuto dei blocchi che andranno ad essere modificati fino al momento in cui essa viene riversata su disco, ed è una lista di operazioni di scrittura che dovranno essere eseguite in sequenza sui blocchi del file system interessati. I blocchi che vengono letti da disco per poi essere modificati sono chiamati dirty (sporchi), e sono divisi in due parti, relocate ed overwrite, ognuna delle quali viene gestita in maniera differente. ?? I blocchi appartenenti al relocate set sono quelli che saranno scritti in una nuova posizione in memoria, senza che i vecchi dati siano quindi sovrascritti da quelli aggiornati. ?? I blocchi appartenenti all’overwrite set sono quelli che devono necessariamente essere scritti nella loro posizione originale, e che no n appartengono al relocate set. In pratica questo set raggruppa tutti i blocchi che hanno un corrispondente in 48 memoria che si vuole venga sovrascritto dai nuovi dati, più tutti quei blocchi per cui la sovrascrittura è la migliore politica di layout nonostante il costo della doppia scrittura su disco (deve essere aggiornato anche il riferimento nel nodo superiore a quello preso in considerazione). Notare che il superblocco è posizionato nell’albero dei direttori come radice della struttura (che contiene un puntatore al nodo di root utente), e come i blocchi che contengono le bitmap dell’utilizzo del disco, esso non ha riferimenti ad un ramo più alto, e sarà quindi sempre parte dell’overwrite set. La scelta di adottare una politica di journaling di questo tipo dipende dal fatto che, in genere, sovrascrivere blocchi dati già precedentemente allocati è più dispendioso in termini di operazioni necessarie rispetto allo scrivere i dati in blocchi “puliti”. Inoltre bisogna ricordare che il codice non valuta la dimens ione dei dati che andranno ad essere scritti in memoria. Nel caso di overwrite dei blocchi con una mole di dati aggiornati maggiore dell’originale, quindi, si dovrebbe riscrivere e riempire il blocco originale per poi creare comunque un riferimento ad un blocco libero che ospiterà la rimanente parte dei nuovi dati da memorizzare. Un’esemplificazione del meccanismo di divisione dei dati relocate/overwrite è mostrata in figura 4.2. Applicativi di alto livello Scrittura Dati Nodo interno Aggiornamento Chiave e riferimento Foglia Contenente i dati originali Creazione riferimento Journaling Layer Blocco dati vuoto Relocate Set Overwrite Set Figura 4.2: Funzionamento del journaling layer di ReiserFS. L’ultima azione eseguita dal codice quando viene completata la memorizzazione di una transazione in registro, è quella di creare un riferimento ad essa all’interno del 49 superblocco, così che anche in caso di crash, alla lettura del superblocco, la transazione ultimata sia riversata nel file system. Reiser4 utilizza alcuni espedienti per ottimizzare il journaling per evitare, quando possibile, un numero eccessivo di scritture su disco; questi accorgimenti sono il copyon-capture, e lo steal-on-capture. Il copy-on-capture permette di ottenere una copia di un blocco di memoria appartenente ad una transazione, in modo da permettere all’applicazione richiedente di avere a disposizione i dati aggiornati per poter eseguire le proprie elaborazioni. Questo meccanismo permette di accedere ai dati che stanno per essere aggiornati senza dover accedere due volte al file system e dover attendere che quest’ultimo sia aggiornato. Lo steal-on-capture è basato sull’idea scrivere in memoria soltanto l’ultimo di tutti gli aggiornamenti che devono essere fatti su di una seria di blocchi, evitando tutte le scritture intermedie che sarebbero poi riaggiornate. Questo sistema esclude da riversamento su disco tutte le transazioni che hanno in corso di completamento, o già presente su registro, una transazione più recente che deve andare ad aggiornare gli stessi blocchi di memoria. Una ottimizzazione che riguarda il carico di lavoro della CPU è invece chiamata “Encryption on commit”, realizzata per velocizzare le operazioni di scrittura su disco di dati in modo criptato. Si è deciso di ridurre il numero di volte in cui la CPU deve usare gli algoritmi di cifratura, facendo sì che scrivesse dati criptati non ad ogni invocazione di write(), ma soltanto all’atto del riversare in memoria i contenuti delle transazioni. Evitando di scrivere le informazioni criptate già all’interno del registro transazioni, si risparmiano le operazioni necessarie alla CPU per criptare informazioni che no n saranno riversate in memoria perché riaggiornate all’interno del journal dal meccanismo di steal-on-capture, velocizzando così i tempi d’elaborazione. Questo sistema di codifica dei dati è automaticamente attivato quando si va a scrivere dati in un nodo che ha impostato il flag CONTAINS_ENCRYPTED_DATA come attivo. 4.5 Repacking Un altro modo per velocizzare le operazioni d’accesso ai file, che permette di tirarsi fuori dal conflitto di prestazioni dovuto al tempo richiesto per le operazioni di bilanciamento in proporzione al tempo guadagnato nelle operazioni di ricerca, è quello di utilizzare un repacker. Un repacker è un programma di utilità che esegue una 50 deframmentazione del file system ed è inoltre in grado di eseguire un ordinamento dell’albero in entrambi i sensi di percorrenza (quello messo a disposizione da Namesys cambia alternativamente verso di percorrenza ad ogni avvio). In generale l’uso di questo tipo di programmi può essere utile perché, in media, l’80% dei dati presenti in una partizione di memoria rimane inalterato per lunghi periodi di tempo; ed una volta ottimizzata questa parte di dati, si restringono le operazioni di I/O su di una porzione più ristretta di memoria, velocizzando le stesse. 4.6 Future caratteristiche del file system Per quanto riguarda l’organizzazione della memoria secondaria, sono in corso di sviluppo due principali caratteristiche. La prima di queste è la possibilità di specificare un tipo d’ordinamento dei nomi attraverso una funzione arbitraria fornita o specificata dall’utente, già al momento della lettura dei dati, in modo da non dover richiamare una diversa routine per ordinare i dati che sono letti su disco. L’altra caratteristica che deve essere implementata riguarda la compressione delle chiavi memorizzate all’interno dei nodi, così da poter aumentare ulteriormente il fanout di albero, visto che si restringerebbe lo spazio occupato da ciascun riferimento memorizzato nei nodi. Le innovazioni riguardanti la sicurezza delle informazioni saranno, anche in questo caso, princ ipalmente due: la possibilità di permettere ad un utente di modificare i dati entro certi limiti predefiniti, e la possibilità di aggregare i file che hanno le stesse caratteristiche di sicurezza. La prima caratteristica sarà implementata attraverso due plugin che implementeranno le limitazioni di scrittura. Una limitazione sarà invocata prima della scrittura di un file; se il codice non ritornerà errori, allora sarà consentito l’aggiornamento dei dati. Il primo plugin che implementerà questa funzionalità sarà sottoforma di funzione del kernel, caricabile in memoria come modulo, che permetterà la scrittura solo se il file sarà contraddistinto dalle stringhe “secret” o “sensitive”, ma non “top-secret”. L’altro plugin, che eseguirà le stesse operazioni, sarà però sottoforma di programma perl residente in un file eseguito all’interno dello spazio utente; questo per poter aggirare la scarsa flessibilità delle funzioni di kernel. Saranno quindi implementati limiti di scrittura che possono essere eseguiti sia dal kernel che dallo spazio utente, sottoforma di applicazioni; in particolare, i controlli di scrittura eseguibili dall’utente saranno dei 51 plugin che eseguono la procedura di controllo contenuta in un file specificato come una normale applicazione, e passeranno come input a quest’ultima i dati che devono essere scritti, come output di processo, nel caso l’applicazione non riportasse errori. L’aggregazione di file che hanno le stesse caratteristiche di sicurezza è importante per garantire flessibilità nella loro gestione: può succedere che sia presente in memoria un gran numero di file che rappresentano logicamente una sola entità nei confronti della gestione della loro sicurezza (per esempio sono soggetti agli stessi controlli d’accesso e scrittura), e sarebbe quindi desiderabile avere un singolo punto di controllo per la gestione complessiva dei loro attributi di sicurezza. Un'altra caratteristica non ancora implementata riguarda l’auditing: si realizzerà una “infrastruttura” di supporto per plugin che siano in grado di notificare all’amministratore o ad altri utenti quando, ad esempio, viene effettuato un accesso ad un particolare file, così da poter aumentare ulteriormente il livello di sicurezza su software proprietario o su file riservati. ReiserFS fornirebbe quindi il supporto per la gestione di una libreria di plugin che operano in questo campo, in modo da rendere più snella la loro realizzazione e più semplice il loro utilizzo. 52 5 WinFS Nella recente versione di Microsoft Windows, avente il nome non ancora ufficiale di “Longhorn”, Microsoft presenta un nuovo file system chiamato WinFS, che è basato sul vecchio file system Microsoft NTFS per quanto riguarda la parte di memorizzazione dei dati. Nonostante la scarsità di documentazione pubblicata sull’argomento, e la difficoltà nel reperire i sorgenti del file system, in quanto software protetto da copyright, nei seguenti paragrafi saranno esposte le caratteristiche principali di questo sistema, che implementa meccanismi di gestione dati, come la possibilità di memorizzare informazioni non all’interno di file, che sono del tutto nuove in questo campo [WFS]. WinFS, oltre a soddisfare i requisiti di un normale file system, estende ai file i servizi d’indicizzazione, ordinamento, streaming dei dati e di ricerca, che sono comuni ai database; i primi studi per una gestione dei dati di questo tipo, sono stati eseguiti dagli sviluppatori Apple per i sistemi operativi MAC OS X, e questo file system, che amplia di molto queste funzionalità, rappresenta un proseguimento degli studi in questo campo. WinFS estende le potenzialità delle API messe a disposizione, rendendole in grado di operare su dati puri, non incapsulati all’interno di files, come registrazioni audio, appuntamenti, contatti, persone, documenti, immagini, e-mail, ecc. I dati che sono memorizzati possono anche essere strutturati o semi-strutturati; il file system mette a disposizione una “infrastruttura software” per organizzare, cercare e condividere tutte queste informazioni. Per organizzare le informazioni su disco, invece del tradizionale albero gerarchico, WinFS utilizza un grafo diretto aciclico (direct acyclic graph, DAG) che è un insieme di oggetti immagazzinati e relazioni tra essi, che sono memorizzati come un database relazionale, in grado di fornire supporto per memorizzare ogni tipo di relazione, gerarchica e non. In questo modo diventa possibile trovare oggetti usando come parametro di ricerca il valore di una loro proprietà, o anche il valore di una proprietà di un altro oggetto ad essi collegato da una qualsiasi relazione. Nella figura 5.1 viene riportata l’organizzazione e la struttura del file system WinFS: la gestione dei dati a livello più basso è basata sul file system NTFS; i dati su disco vengono poi gestiti da un motore di database relazionale, che tratta ogni oggetto secondo uno specifico modello dati. A fianco dei 53 modelli dati viene messo il codice che implementa tutte le operazioni ed i servizi necessari al resto del sistema operativo. Tutti i dati in memoria di massa, una volta classificati secondo i modelli dati predefiniti vengono passati ad una serie di servizi di livello più alto, che li tratta e li classifica secondo le entità e le regole definite in appositi schemi dati. Al livello superiore WinFS mette a disposizione tre API per utilizzare i dati come oggetti canonici, oppure per trattarli usando gli standard T/SQL o XML. Figura 5.1: Struttura ed organizzazione gerarchica dei componenti di WinFS. 5.1 Memorizzazione dei dati: il modello NTFS Come detto precedentemente WinFS utilizza l’organizzazione e la suddivisione del disco già adottata da NTFS, in quanto questa rappresenta di per sé una base stabile su cui costruire l’infrastruttura per una gestione dei dati di tipo relazionale [NT]. Una partizione dati formattata in NTFS è composta, come mostrato in figura 5.2, da un settore di boot, dalla tabella generale dei file (Master File Table, MFT), che contiene le informazioni riguardanti tutti i file e le directory presenti in memoria, da diversi file utilizzati dal sistema per gestire il file system e dallo spazio assegnato ai file. Figura 5.2: Struttura generale delle partizioni NTFS. 54 Il settore di boot inizia sempre dal settore 0 e può essere lungo fino a 16 settori, esso contiene principalmente il superblocco e le istruzioni in linguaggio macchina per le prime operazioni d’avvio del sistema. Il superblocco, in questo file system contiene anche l’indirizzo a cui è allocata la MFT, e l’indirizzo a cui è allocata la sua immagine (da utilizzare nel caso la prima copia risulti corrotta), in quanto NTFS consente di memorizzare la file table ad indirizzi diversi per evitare di dover scrivere questi dati su settori danneggiati. La tabella 5.1 mostra la struttura del settore di boot: il primo record contiene l'istruzione di jump per la lettura del codice necessario ad inizializzare il sistema, mentre l'extended BPB rappresenta un'estensione del superblocco, ed insieme a quest'ultimo contiene le informazioni riguardanti la struttura fisica della partizione. Byte Offset Lunghezza record Nome record 0x00 3 bytes Jump Instruction 0x03 LONGLONG OEM ID 0x0B 25 bytes BPB (superblocco) 0x24 48 bytes Extended BPB 0x54 426 bytes Bootstrap Code 0x01FE WORD End of Sector Marker Tabella 5.1: Organizzazione del settore di boot. La tabella 5.2 mostra invece la struttura del superblocco. Byte Lunghezza Valore Offset record esemplificativo 0x0B WORD 0x0002 Bytes Per Settore 0x0D BYTE 0x08 Settori Per Cluster 0x0E WORD 0x0000 Settori Riservati 0x10 3 BYTES 0x000000 always 0 0x13 WORD 0x0000 non utilizzato da NTFS 0x15 BYTE 0xF8 Media Descriptor 0x16 WORD 0x0000 always 0 0x18 WORD 0x3F00 Settori Per Traccia 0x1A WORD 0xFF00 Numero di testine 0x1C DWORD 0x3F000000 Settori Nascosti 55 Nome del record 0x20 DWORD 0x00000000 non utilizzato da NTFS 0x24 DWORD 0x80008000 non utilizzato da NTFS 0x28 LONGLONG 0x4AF57F0000000000 Settori Totali Logical Cluster Number for 0x30 LONGLONG 0x0400000000000000 the file $MFT (file contenente la MFT) Logical Cluster Number for 0x38 LONGLONG 0x54FF070000000000 the file $MFTMirr (imagine della MFT) Clusters per Segmento 0x40 DWORD 0xF6000000 0x44 DWORD 0x01000000 Clusters Per Index Block 0x48 LONGLONG 0x14A51B74C91B741C Numero di serie di Volume 0x50 DWORD 0x00000000 riservato ai filr Tabella 5.2: Struttura del superblocco NTFS. Questa tabella mostra come la master file table e la relativa immagine siano memorizzate in appositi file posizionati ad un offset predefinito in un cluster specificato nei campi di superblocco. Ogni file in una partizione NTFS è rappresentato da un record nella MFT. NTFS riserva i primi 16 record di questa tabella per informazioni speciali. Il primo record descrive, infatti, la MFT stessa, ed il secondo contiene un riferimento all’immagine della tabella, in modo da poter recuperare le informazioni sulla sua struttura anche nel caso il primo record risulti corrotto. L’indirizzo a cui recuperare l’immagine della MFT è specificato nel settore di boot, ed una copia di quest’ultimo viene sempre memorizzata nel centro logico del disco. Il terzo record della tabella è il file di registro utilizzato dal sistema di journaling, che anche questo file system adotta. I record dal diciassettesimo in poi sono assegnati ad ogni file e directory presente in partizione (anche in NTFS le directory sono un particolare tipo di file). La struttura della MFT è rappresentata in figura 5.3: i record relativi ai file di grandi dimensioni hanno sempre uno o più riferimenti ai blocchi di memoria contenenti i dati. La MFT contiene anche un riferimento alla propria immagine, da usare in caso di corruzione della prima. 56 Figura 5.3: Struttura della Master File Table di NTFS . La MFT alloca una certa quantità di spazio per ogni record in essa presente, e gli attributi di un file sono sempre scritti all’interno del record stesso. File e directory di piccole dimensioni (tipicamente non più grandi di 1500 bytes, questo però dipende anche dagli attributi ad essi associati), possono però essere interamente memorizzati all’interno della tabella, velocizzando così le operazioni di I/O su di essi nel caso siano di dimensioni tali da poter essere contenuti in un solo record assieme ad attributi e descrittore. Le directory di grandi dimensioni, non potendo essere contenute in un singolo record sono organizzate in alberi bilanciati: ogni record ha dei riferimenti a dei blocchi di memoria che contengono gli elementi presenti nella directory che non possono essere memorizzati nel record stesso. Tutta questa organizzazione della memoria funge da impalcatura su cui poggiano le parti più astratte del file system, che saranno descritte nei paragrafi successivi. 57 5.2 Modelli dei dati Per poter implementare tutte queste funzionalità, il file system necessita di conoscere i tipi di dati che deve gestire, e realizza ciò attraverso i modelli di dati. Un modello di dati deve fornire i concetti per descrivere strutture dati e la loro organizzazione. Esempi di modelli di dati sono i modelli Entity-Relationship e l’organizzazione dei file system tradizionali; analogamente, il modello dati di WinFS fornisce i seguenti concetti per descrivere le strutture dati e le organizzazioni che implementa: ?? Tipi e sottotipi ?? Proprietà e campi ?? Relazioni ?? Gerarchizzazione ed ereditarietà ?? Estensibilità 5.3 Tipi e sottotipi In questo file system, un tipo di dato rappresenta un modello per un’istanza d’oggetto, ed è composto da una o più proprietà (campi dati): per esempio il tipo “Persona” può avere un campo denominato “Nome”, e “Peter” può essere un’istanza del tipo “Persona”. D’altra parte, però, un tipo di dato di WinFS può avere un supertipo ed uno o più sottotipi che esprimono le relazioni di ereditarietà e di ordinamento gerarchico, così come il tipo “Documento” ha come supertipo il tipo “Oggetto” e come sottotipo, tra gli altri, il tipo “Media”. Ogni istanza di un tipo di dato viene serializzata all’interno del file system, e ad essa è associato un apposito spazio semantico (namespace) che definisce in che modo questa istanza può essere ricercata e identificata. Tutti i tipi di dato di WinFS sono derivati dal tipo “Base.Item”, ed è permessa soltanto l’ereditarietà singola tra i tipi: quando un tipo di dato è derivato da un altro, esso eredita tutti i campi dati del secondo, ed il file system non consente di ereditare campi dati da altri tipi di dato. Gli elementi che compongono i tipi di dato sono chiamati elementi innestati (nested elements, o anche elementi annidati) e sono ciò che più si avvicina alle semplici strutture dati o ai record. Tutti gli elementi innestati devono essere derivati dal tipo “Base.NestedElement”, e possono soltanto essere utilizzati come campi dati di un tipo WinFS. 58 5.4 Proprietà degli oggetti e campi dati Come già detto, tutti i tipi di dato sono composti da uno o proprietà e campi. Un campo o una proprietà possono essere o un tipo di dato scalare definito da WinFS oppure un oggetto di tipo “Nested Element”. Il punto di partenza del modello dati di questo file system sono i tipi scalari, e la tabella 5.3 mostra tutti i tipi che sono supportati, con il loro tipo corrispondente SQL e .NET: Tabella 5.3: Corrispondenza tra i dati scalari di tipo WinFS, SQL e CLR. Il sistema di definizione dei tipi permette che ad un singolo campo dati siano associati più valori (collezioni), così che, per esempio, una persona possa avere più indirizzi di casa o più di un recapito telefonico. Tutte le proprietà ed i campi devono esprimere dei vincoli riguardanti il loro contenuto, e questo viene implementato attraverso le restrizioni, che per esempio possono esprimere che il valore di un campo può o non può essere NULL. 59 5.5 Linguaggio di definizione degli schemi Questo file system introduce il concetto di linguaggio di definizione degli schemi, che serve per definire tutti i tipi di dato da esso utilizzati. Come parte dell’implementazione di WinFS, il sistema operativo “Longhorn” fornisce una serie di schemi predefiniti che definiscono alcuni tipi d’oggetto e NestedElement di base. Ogni tipo di dato è definito all’interno di uno schema, e di seguito viene riportato un esempio di come si può definire il tipo “Indirizzo” (Address): <Type Name="Address" MajorVersion="1" MinorVersion="0" ExtendsType="Core.CategorizedNestedElement" ExtendsVersion="1"> <Field Name="AddressLine" Type="WinFSTypes.nvarchar(1024)" Nullable="true" MultiValued="false" TypeMajorVersion="1"></Field> <Field Name="PrimaryCity" Type="WinFSTypes.nvarchar(49)" Nullable="true" MultiValued="false" TypeMajorVersion="1"> </Field> <Field Name="SecondaryCity" Type="WinFSTypes.nvarchar(49)" Nullable="true" MultiValued="false" TypeMajorVersion="1"><Notes></Notes> </Field> <Field Name="CountryRegion" Type="WinFSTypes.nvarchar(1024)" Nullable="true" MultiValued="false" TypeMajorVersion="1"></Field> <Field Name="PostalCode" Type="WinFSTypes.nvarchar(16)" Nullable="true" MultiValued="false" TypeMajorVersion="1"></Field> ... </Type> 60 Per definire un nuovo tipo di dato si può definire un nuovo schema oppure si può estendere uno schema esistente; questi due meccanismi sono alla base dell’estensibilità dei dati supportata da questo file system. 5.6 Relazioni WinFS utilizza il concetto di relazione per esprimere il collegamento di un oggetto con un altro, e tutte le relazioni sono definite da un tipo origine ed un tipo obiettivo (source e target type); questi tipi vengono poi traslati nelle istanze degli oggetti che devono essere collegati come parte dell’istanza di relazione. Tutte le relazioni sono dipendenti dal tipo sorgente, ed una relazione, per esistere, richiede per forza l’esistenza di un tipo di dato sorgente; se l’istanza di tipo di dato che funge da sorgente viene eliminata, allora anche la relazione sarà eliminata. Una relazione senza istanza obiettivo, infatti, può esistere, e viene chiamata relazione di tipo “dangling”. In questo file system sono definiti due tipi di relazioni: holding (di mantenimento) e reference (di riferimento). Le relazioni di tipo holding non possono essere dangling, e controllano la vita del loro obiettivo: l’istanza obiettivo coinvolta nella relazione continua ad esistere fin quando rimane obiettivo di almeno una relazione qualsiasi. La relazione holding maggiormente degna di nota è la relazione “membro di una directory” tra un tipo file (target) ed un direttorio (source): in WinFS uno stesso file può essere contenuto in diverse directory, quindi ci possono essere differenti relazioni che hanno quest’ultimo come obiettivo. Se si elimina una di queste relazioni, l’istanza del file non esisterà più nella directory che rappresenta la sorgente della relazione appena eliminata, ma il file continuerà ad essere presente in memoria fino a quando rimarrà in relazione con un’altra istanza di directory. Le relazioni di riferimento non controllano la vita dell’istanza che hanno come obiettivo, e possono essere stabilite anche tra istanze residenti su dischi o partizioni diverse. Così come tutti i tipi di dato, le relazioni possono avere proprietà e campi che possono essere di tipo scalare o “NestedElement”: per esempio una relazione tra una persona ed un’istanza di ufficio può avere come campi la data d’inizio e la data di fine, o anche l’orario di lavoro. 61 5.7 Usare il modello dati di WinFS L’API messa a disposizione dal file system permette di utilizzare gli oggetti attraverso il linguaggio di tipo CLR oppure in linguaggio di tipo COM nativo, così da permettere agli sviluppatori di aggiungere potenzialità come query, navigazione ed eventi dato alle loro applicazioni. Tutti i tipi di Windows definiti come schema dati WinFS hanno una loro classe corrispondente nell’API, chiamata “data class”; in Longhorn sono definiti 28 schemi dati con relative classi nell’application program interface di WinFS. Possono essere inoltre definiti nuovi schemi o estendere quelli già esistenti per poi generare le classi equivalenti di tipo .NET e COM per mettere a disposizione delle applicazioni questi nuovi tipi. Per potenziare le capacità di ricerca, il sistema utilizza il linguaggio OPATH, un linguaggio di ricerca ed ordinamento degli oggetti che permette di eseguire ricerche complesse all’interno delle partizioni di memoria; linguaggio che per fare questo implementa la possibilità di usare filtri che utilizzano la corrispondenza di particolari proprietà o campi di un oggetto. Nei tradizionali linguaggi di programmazione, le relazioni tra gli oggetti venivano espresse tramite i campi di una classe; con WinFS diventa possib ile, per esempio, ricercare gli autori dei documenti attraverso query che coinvolgono le relazioni tra gli oggetti. 5.8 ADO.NET e WinFS Oltre al linguaggio OPATH, questo file system fornisce anche un’API per accedere ai dati in memoria con query di tipo OLEDB e ADO.NET. Il componente principale di quest’API per l’accesso relazionale ai dati è il linguaggio T-SQL, che permette di effettuare query basate su SQL all’interno della base di dati (partizione di memoria) di WinFS. Utilizzando questa funzione si possono ricercare ed utilizzare i dati come all’interno di un database relazionale, e si mette a disposizione delle applicazioni un maggior controllo sui dati presenti in memoria secondaria ed una maggiore capacità di ricerca. 62 5.9 Notificazioni Questo file system memorizza un gran numero d’oggetti tra loro correlati, e in certi casi può essere utile poter controllare le modifiche che sono eseguite su alcuni di essi; per fare ciò WinFS implementa un meccanismo capace di notificare all’utente quando sono eseguite delle modifiche o quando viene eliminato un oggetto. Le notificazioni di questo file system sono analoghe a quelle di tipo CLR, e la classe principale per poter utilizzare questo sistema è la classe “ItemWatcher”. 63 6 Confronto Prestazionale Di seguito sono riportati i risultati di due test, che sono stati pubblicati sul sito internet di Namesys (www.namesys.com), per mettere a confronto le prestazioni di Ext3 e Reiser4. Non verranno trattate le prestazioni di WinFS per via della difficoltà a reperire il file system (che al momento della stesura di questa tesi non è ancora stato rilasciato in una versione ufficiale, ma è ancora in fase di test), ed anche perché mettendolo a confronto con altri file system tradizionali, per avere termini di paragone comuni, si dovrebbero prendere in considerazione solo le prestazioni riguardanti l’I/O di generici file, senza valutare le performance fornite da tutte le altre funzionalità che WinFS implementa, come per esempio la ricerca di oggetti utilizzando il modello Entity/Relationship, o i servizi d’indicizzazione. I risultati sotto riportati sono stati ottenuti utilizzando due benchmark diversi, uno per valutare le prestazioni nel gestire molti file di piccole dimensioni, chiamato Mongo, ed uno, lo Slow benchmark, per confrontare i file system nella scrittura di grossi file in memoria. Entrambi i programmi possono essere reperiti all’indirizzo http://www.namesys.com/benchmarks/. Dei due programmi che sono stati utilizzati il primo, Mongo, è stato scritto da Namesys, e la scelta di utilizzare un benchmark scritto dagli sviluppatori di Reiser4 è dovuta alla completezza del programma, che in un’unica esecuzione può valutare le prestazioni nel compiere tutte le normali operazioni sui file che un file system si trova normalmente a dover affrontare. L’altro benchmarck, invece, è un programma standard scritto da Jon Burgess, che viene utilizzato come termine di confronto anche con altri file systems. 6.1 Mongo Benchmark Mongo è uno script per Linux che funziona nel modo seguente: 1. All’avvio dello script vengono create n partizioni di altrettanti file system diversi, che vengono specificati a riga di comando. Una volta eseguito ciò, il programma si compone di 8 fasi diverse, in ciascuna delle quali sono valutate le prestazioni dei file system creati. 64 2. La prima fase consiste nella creazione di un albero di lunghezza casuale che viene riempito da files la cui dimensione varia da 0 ad un valore massimo stabilito in modo dipendente da una funzione di distribuzione predefinita. In altre parole, tra tutti i file che si andranno a creare, il numero maggiore di essi avrà una dimensione che ricade attorno ad un valore prestabilito, mentre gli altri che si scostano da questo valore saranno in percentuale sempre minore man mano che ci si allontana dal valore di riferimento. 3. La fase successiva è quella di copia: finita la creazione dell’albero vengono confrontati i tempi impiegati dai vari file system per la copia dei file creati. Questa operazione non è eseguita creando un semplice riferimento ad inode già esistenti, ma viene effettuata copiando ed allocando “ex-novo” in blocchi liberi di memoria i file precedentemente creati. Una copia di questo tipo comporta la lettura (e scrittura) non soltanto degli inode ma anche di tutti i blocchi dati appartenenti ai file. 4. Nella fase di append lo script legge i nomi dei file creati ed aggiunge in coda ad essi un numero di byte calcolato come dimensione_del_file * fattore_standard_di_aggiunta. Queste operazioni possono essere eseguite anche utilizzando l’opzione fsync(). 5. La fase successiva è quella di modify: il programma legge tutti i file e modifica in essi un numero di byte calcolato come dimensione_del_file * fattore_standard_di_modifica, partendo da una posizione casuale all’interno del file. Anche questa operazione può essere effettuata usando fsync(). 6. La fase di overwrite, che segue quella di modifica, sovrascrive i file utilizzando la routine impiegata nella fase precedente, utilizzando però in fattore_standard_di_modifica=1, andando quindi a modificare (sovrascrivere) tutti i byte di ciascun file. 7. La fase di read, poi legge tutti i file creati in un ordine prestabilito, e mette a confronto i tempi impiegati. 8. La fase di stats, invece, valuta la velocità d’esecuzione del comando “find – type f” su tutte le partizioni create. In altre parole in questa fase viene valutata la velocità nella ricerca di tutti i file che sono stati creati, in quanto il comando “find –type f” cerca tutti i file regolari memorizzati nella partizione. 9. L’ultima fase eseguita è quella di delete, che valuta la velocità nel rimuovere, tramite il comando “rm –r”, tutti i file creati. 65 Lo script può poi eseguire, se specificato, due fasi speciali che richiedono l’opzione DD_MBCOUNT, che sono la scrittura e lettura di file di grosse dimensioni. Nel test eseguito con questo programma l’81% dei file creati era di dimensione tra 0 e 10KB, mentre la dimensione del restante 19% apparteneva all’intervallo 10-100KB. La macchina su cui è stato eseguito lo script ha le seguenti caratteristiche: Processore Intel Xeon 2.40 GHz con 256 MB di RAM e 1 MB di cache, sul quale viene eseguita la versione 2.6.8.1 del Kernel di Linux. La tabella 6.1 riporta i risultati ottenuti. Ad ogni lettera corrisponde un tipo di file system, e più precisamente: ?? A corrisponde a Reiser4 ?? B corrisponde a Reiser4 montato con l’opzione di solo utilizzo dei blocchi non formattati per contenere i dati appartenenti ad uno stesso file (estensione dei blocchi, o extent). ?? C corrisponde a Reiser3 ?? D corrisponde a Ext3 con l’opzione di mount data=writeback, cioè con l’opzione di journaling rapido (writeback continuo, appunto,e minori garanzie di consistenza dei dati). ?? E corrisponde a Ext3 con l’opzione di mount data=journal, cioè ad Ext3 con journaling completo di dati utente e metadata. ?? F corrisponde a Ext3 con l’opzione di mount data=ordered, in altre parole Ext3 con metadata-only journaling. La tabella mostra la misurazione dei tempi di Reiser4 nella prima colonna, mentre nelle altre è mostrato il valore del rapporto tra i tempi di questo file system rispetto a quelli impiegati dagli altri misurati. I valori in rosso sono quelli in cui il tempo impiegato da Reiser4 è inferiore rispetto a quello dell'altro filesystem (rapporto maggiore di 1), mentre quelli in verde identificano le prove in cui i tempi impiegati da Reiser4 sono maggiori di quelli del file system usato come confronto. 66 REAL_TIME A B/A C/A CREATE 3,796 0,686 1983,000 2592,000 3010,000 2256,000 COPY 9,128 0,672 1674,000 2241,000 2105,000 1819,000 READ 7,815 1007,000 1617,000 1282,000 1295,000 1250,000 STATS 1,008 0,672 1162,000 0,655 0,690 0,162 1264,000 1270,000 1216,000 B/A C/A D/A CREATE 1,301 0,670 0,574 2577,000 2529,000 2802,000 COPY 2,253 0,651 0,550 1694,000 2004,000 1860,000 READ 38.61 1002,000 0,494 0,427 0,432 0,427 STATS 0,480 0,656 0,498 0,459 0,468 0,457 DELETE 2,212 0,651 0,306 0,145 0,149 0,149 B/A C/A D/A E/A F/A CREATE 0,960 0,681 0,243 0,549 0,513 1000,000 COPY 0,667 0,692 0,319 0,460 0,583 0,618 READ 0,545 0,691 0,306 0,361 0,359 0,370 STATS 1,032 0,674 0,408 0,486 0,491 0,484 DELETE 0,766 0,658 1758,000 0,109 0,111 0,116 C/A E/A F/A DELETE 156.84 D/A E/A 0,655 F/A 0,655 CPU_TIME A E/A F/A CPU_UTIL A DISK_USAGE A B/A D/A CREATE 1978440,000 1000,000 1088,000 1108,000 1108,000 1108,000 COPY 3956708,000 1000,000 1088,000 1108,000 1108,000 1108,000 READ 3956708,000 1000,000 1088,000 1108,000 1108,000 1108,000 STATS 3956708,000 1000,000 1088,000 1108,000 1108,000 1108,000 DELETE 4,000 1000,000 0.000 0.000 0.000 0.000 Tabella 6.1: Confronto prestazionale utilizzando lo script Mongo. I risultati mostrati in tabella 6.1 sono stati ottenuti utilizzando il parametro file_size=8192. Questo parametro viene passato al generatore casuale dell’albero, che lo utilizza per elaborare la distribuzione della dimensione dei file che andranno ad essere creati. La prima cosa che salta all’occhio dai risultati dei test è il grande risparmio di spazio su disco che si ottiene grazie al sistema di block sharing di Reiser4, che è circa di tre ordini di grandezza. Dalle altre tabelle, invece, si evince che Reiser4 impiega dei tempi 67 inferiori5 rispetto agli altri file system per compiere le comuni operazioni sui file, ma risulta essere un codice più pesante degli altri, in quanto occupa la CPU per tempi più lunghi e la sfrutta più intensivamente degli altri file system usati come paragone: solo per le operazioni di creazione e copia di un file, infatti, Resiser4 segna tempi d’utilizzo del processore di circa tre ordini di grandezza inferiori rispetto a Ext3. Proprio il fatto che Reiser4 impieghi meno tempo “utente” per eseguire le proprie operazioni nonostante sfrutti per più tempo la CPU, può essere indice di una maggiore integrazione tra le sue varie componenti, che interagiscono tra loro utilizzando interfacce e codice meno complesso, rendendo l’intero codice più uniforme e meno stratificato di quello che può essere quello di Ext3. 6.2 Slow Benchmark I test eseguiti per confrontare le prestazioni di lettura e scrittura di grandi quantità di dati sono stati fatti con il programma slow.c. Questo programma esegue la scrittura e la lettura di uno stream di dati di dimensione specificata, ed una volta terminate queste operazioni, le ripete per testare le prestazioni in lettura/scrittura di 2, 4 e 8 stream dello stesso tipo in parallelo. La dimensione del file che è stato utilizzato per eseguire la prova è di 1 GB, e nei grafici seguenti è riportato il valore medio di throughput per i vari filesystem testati. Di seguito è riportata la configurazione hardware del computer su cui sono stati eseguiti i test, e nei grafici vengono mostrati i risultati delle prove. La configurazione della macchina su cui è stato eseguito il benchmark è la seguente: Processore AMD Athlon 1,40 GHz con 256 Kb di cache, sul quale viene eseguitala versione 2.6.5 del Kernel di Linux, e per eseguire il test si è scelto di scrivere file della dimensione di 1 GB ciascuno (questo esplicitandolo a riga di comando, lanciando il programma con il comando ./slow foo 1000). test : ./slow foo 1000 5 Questo in termini di tempo reale, cioè il tempo che il file system impiega, considerando l’utilizzo concorrente della CPU da parte degli altri applicativi di sistema, per compiere un’operazione agli occhi dell’utente. 68 TEST DI SCRITTURA 30 Ext2 25 Reiser4 MB/s 20 Reiser4 Extents Only 15 10 Ext3 Ordered 5 Ext3 Journal 0 Reiser3 Write 1 Write 2 Write 4 Write 8 Resier4 no tail Numero di stream paralleli Figura 6.1: Risultati del test di scrittura effettuati con slow.c. TEST DI LETTURA 30 Ext2 MB/s 25 Reiser4 20 Reiser4 Extents Only 15 Ext3 Ordered 10 Ext3 Journal Reiser3 5 Resier4 no tail 0 Read 1 Read 2 Read 4 Read 8 Numero di stream paralleli Figura 6.2: Risultati del test di lettura effettuato con slow.c. Le figure 6.1 e 6.2 evidenziano chiaramente il gap prestazionale tra Ext3 e Reiser4, che si manifesta soprattutto quando i file system devono trattare simultaneamente diversi stream di dati, mentre si nota che nel test di lettura di un solo flusso dati, per volta tutti i file system hanno dato più o meno le stesse prestazioni. 7 Conclusioni In conclusione, si può dire che tutti i file system presi in analisi in questo elaborato soddisfano ampiamente le esigenze ed i requisiti illustrati nel secondo capitolo, nonostante ciascuno di essi abbia caratteristiche implementative molto diverse dagli altri. Ext3 ha in sè tutti i pregi e i difetti di essere stato sviluppato molto più di ReiserFS sfruttando l’open source: da un lato può vantare di essere un file system stabile e 69 maturo, nonché forse il più diffuso tra gli utenti Linux, ma d’altra parte paga in termini di prestazioni lo scotto di essere stato sviluppato in modo disomogeneo da molte persone, magari neanche in contatto tra loro. ReiserFS, pur essendo un software libero, può vantare dalla sua parte il fatto di essere stato sviluppato principalmente da un unico team di programmatori (Namesys) fin dalle sue prime versioni, e questo si manifesta in una maggiore integrazione tra le varie parti del software, in modo da ottimizzare al massimo i tempi di esecuzione del maggior numero possibile di operazioni che il codice può eseguire. Questo maggior grado di integrazione ed ottimizzazione lo si nota anche dai risultati dei benchmark: il tempo utente impiegato nell’esecuzione di molte operazioni è minore rispetto a quello impiegato da Ext3 nonostante l’impiego della CPU sia maggiore di quest’ultimo (sintomo, questo, di una maggiore complessità di codice). Il file system che, tra i tre presi in analisi, fornisce all’utente il maggior numero di funzionalità è sicuramente WinFS: la gran parte delle nuove funzioni, secondo la documentazione finora rilasciata, è stata costruita ad un livello superiore rispetto a quelli di gestione della memoria fisica e della sua organizzazione, e questo, probabilmente, si tradurrà in un maggior carico di lavoro per la CPU; inoltre, visto che il file system è ancora in fase di test e quindi soggetto a continue correzioni, non si è ancora in grado di valutare appieno in che modo tutte le funzioni messe a disposizione interagiscono col resto del sistema operativo. Anche se WinFS risulta essere ancora un software molto immaturo, che probabilmente subirà molte modifiche, c’è da dire che questo file system prosegue ed amplia la strada della gestione dei dati su disco alla stregua di un database relazionale, che è stata recentemente sperimentata anche nell’ultima versione del sistema operativo Apple, primo software a sperimentare ed implementare una gestione dei dati di questo tipo. In futuro, infatti, ci sarà da aspettarsi che molti altri file system implementino questo modo di archiviare le informazioni, che, pur essendo ancora da affinare, può fornire una flessibilità nel gestire i dati molto maggiore rispetto a sistemi di tipo tradizionale. Un nuovo obiettivo per i team di sviluppo dei file system, può essere sicuramente quello di costruire una piattaforma per gestire le informazioni su disco come una base di dati (anche implementando il modello relazionale), che sia la più snella e compatta possibile e che, questo soprattutto per i sistemi UNIX- like, sia il più possibile portabile e trasparente, in modo da dover effettuare il minor numero possibile di modifiche al codice di livello superiore, VFS in primis. 70 Appendice A – Gli Alberi Di Dati L’ordinamento dei dati in file e cartelle con cui l’utente si trova a dover operare, è implementato nella maggior parte dei sistemi operativi, Linux compreso, attraverso strutture dati chiamate alberi. 71 A.1. Definizioni Un albero è una struttura gerarchica formata da diversi nodi, ognuno dei quali può contenere dati e riferimenti ad eventuali altri nodi ad esso collegati; ogni albero ha un solo nodo di partenza dal quale è possibile raggiungere tutti gli altri nodi dell’albero scendendo di livello man mano che si attraversa la struttura dati. Una definizione di precisa di albero può essere la seguente: 1. Un albero è un insieme di nodi organizzati in un nodo radice, e in zero o più addizionali insiemi di nodi chiamati sottoalberi. 2. Ognuno dei sottoalberi è esso stesso un albero 3. Nessun nodo dell’albero contiene riferimenti alla radice, ed esattamente un riferimento da un nodo nell’albero punta ad ogni altro nodo che non sia radice dell’albero. 4. Il nodo radice contiene un riferimento ad ognuno dei suoi sottoalberi, riferimento che è un puntatore alla radice del sottoalbero [reiser]. Figura A.1: Un esempio di albero bilanciato di altezza=4 e fanout=3 La figura A.1 mostra un classico esempio d’albero dati, che può essere utilizzato per fornire alcune altre definizioni: ?? Sarà chiamato radice dell’albero il nodo di livello più alto della struttura. ?? Verranno chiamati foglia (leaf) dell’albero tutti i nodi che non contengono riferimenti a sottoalberi (non fungono cioè da radice di un eventuale sottoalbero). 72 ?? Sarà chiamato altezza dell’albero il numero complessivo di livelli che la struttura sviluppa a partire dalla sua radice. ?? Verrà chiamato fanout di un nodo il numero di riferimenti ad altri nodi che esso contiene; analogamente può essere definito il fanout dell’albero, che diventa il numero di riferimenti ad altri nodi che ciascun nodo contiene (foglie escluse), nel caso in cui tutti nodi dell’albero contengano lo stesso numero di riferimenti. Un’eventuale ricerca di dati all’interno dell’albero di figura A.1 partirebbe dal nodo radice (l’unico nodo interno di livello 4), attraverserebbe due livelli di nodi interni, e terminerebbe su di una foglia (che non ha figli) che contiene il dato da ricercare. A.2. Chiavi Nei sistemi informatici, ad ogni oggetto che viene immagazzinato all’interno di questa struttura è assegnata una chiave identificativa, che sarà utilizzata per ritrovare l’oggetto stesso durante ogni ricerca. Considerando che nei file-system un nodo è composto da uno o eventualmente più blocchi di memoria, le chiavi degli oggetti saranno dei numeri univoci di dimensione predefinita, che possono contenere al loro interno più o meno informazioni sull’oggetto a cui fanno riferimento a seconda della loro lunghezza. In molti file system, la generazione delle chiavi per gli oggetti avviene attraverso algoritmi che cercano di includere nella chiave stessa il maggior numero possibile d’informazioni, riguardanti l’oggetto che devono riferire, procedimento che comunque aiuta alla generazione di set di chiavi tutte differenti tra loro 6 . La scelta di identificare gli oggetti presenti nell’albero attraverso delle chiavi offre sicuramente il vantaggio di velocizzare le operazioni di ricerca, ma d’altro canto va ad influire sul fanout dell’albero stesso: a parità di dimensione di blocco, infatti, maggiore sarà la dimensione delle chiavi che il blocco contiene, minore sarà il numero di oggetti a cui il nodo può riferire. 6 Maggiore sarà il numero di informazioni contenute nella chiave, maggiori saranno le possibilità di descrivere tutti i particolari di un oggetto, in modo così da poter generare due chiavi diverse per due oggetti che differiscono anche di poco tra loro per forma e contenuti. 73 A.3. Bilanciamento della struttura Passiamo ora ad analizzare quali meccanismi possono essere utilizzati per velocizzare le ricerca degli oggetti all’interno dell’albero. Esistono due principi di gestione e di organizzazione delle strutture dati che sono comunemente utilizzati in tutti i file system, questi principi sono: ?? Bilanciamento in altezza dell’albero: un albero bilanciato in altezza è una struttura tale che ogni percorso di ricerca che inizi della radice e termini in una qualsiasi delle foglie, ha esattamente la stessa lunghezza (dove per lunghezza s’intende il numero di nodi attraversati dalla radice fino alla foglia). In altre parole questo principio afferma che più si riesce ad uniformare l’altezza delle foglie (che nei file system sono generalmente gli elementi che contengono i dati utente), più i tempi d’accesso ai dati saranno mediamente uniformi, evitando così (nei casi più sfavorevoli) di dover attendere molto più tempo per accedere a dati utilizzati di frequente, rispetto a quello che servirebbe per accedere a dati non tanto utilizzati ma memorizzati all’interno di un nodo molto vicino alla radice. ?? Bilanciamento del fanout (ampiezza) dell’albero: un albero bilanciato in ampiezza è una struttura tale che ogni suo nodo contiene esattamente lo stesso numero di riferimenti a sottoalberi. Una struttura di questo tipo accelera notevolmente le operazioni di ricerca, specialmente quando i file system utilizzano algoritmi di ricerca di tipo non ricorsivo, che navigano l’albero confrontando nodo per nodo le chiavi che questi contengono, fino a risalire all’oggetto da ricercare (a differenza degli algoritmi ricorsivi, che ad ogni nodo istanziano tante nuove funzioni di ricerca quanti sono i riferimenti che il nodo contiene, indipendentemente dal fatto che il percorso porti o no a raggiungere il dato cercato). Con un bilanciamento di questo tipo si va quindi ad uniformare il tempo medio di ricerca degli oggetti, in quanto si va a stabilire che la macchina impiegherà lo stesso tempo per scegliere quale indipendentemente dal nodo dell’albero in cui si trova. 74 percorso seguire Figura A.2: Esempio d’albero non bilanciato. Confrontando la figura A.1 con la figura A.2 balza subito all’occhio la differenza tra alberi bilanciati e non bilanciati: non tutti i nodi dell’albero di figura A.2 hanno lo stesso fanout, e non tutte le sue foglie hanno la stessa distanza dalla radice. Nella figura A.3 è mostrato un esempio di come un albero sbilanciato possa rallentare le operazioni di ricerca dei dati. A B Albero 1 Albero 2 C D Figura A.3: Esempi di albero bilanciato e fortemente sbilanciato. I due alberi di figura A.3 hanno entrambi lo stesso numero di nodi (15), il primo, però ha altezza=4 e fanout=2, mentre il secondo ha altezza massima=8 e fanout medio=2 (non tutti i blocchi hanno lo stesso numero di riferimenti ad altri blocchi sottostanti). Se si dovesse compiere una ricerca all’interno delle due strutture, supponendo che ogni nodo contenga sia dati che chiavi e riferimenti, si avrebbe che: ?? Per reperire un qualsiasi dato all’interno dell’albero 1 bisognerebbe effettuare un massimo di tre accessi a sottoalberi e foglie. 75 ?? Per recuperare le informazioni nell’albero 2 si potrebbe eseguire anche un solo accesso (da A a B), ma nel peggiore dei casi (per leggere un dato contenuto nel blocco D o nell’altro dello stesso livello) bisognerebbe effettuare 7 accessi ai blocchi sottostanti; e già per accedere ai dati contenuti nel blocco C sarebbe necessario un numero di el tture maggiore del numero massimo possibile nel primo albero. Tutto ciò che è stato detto in precedenza, trova una diretta corrispondenza nel modo in cui i file system gestiscono l’organizzazione dei dati in file e cartelle. La radice dell’albero viene in genere rappresentata come la directory di più alto livello, dalla quale si diramano tutte le altre e, generalmente, i riferimenti (ed eventualmente anche i dati) che sono contenuti all’interno dei nodi sono rappresentati come tutti gli elementi che una cartella contiene. La scelta di relegare i dati soltanto nelle foglie, riservando così ai nodi il solo scopo di contenere il riferimento ad altri dati, solo di recente è diventata comune alla maggior parte dei file system, in quanto rappresenta un ottimo modo per aumentare il fanout (e quindi il massimo numero d’oggetti memorizzabili in un direttorio) ma allo stesso tempo va ad aumentare l’altezza dell’albero. Questo tipo d’implementazione della struttura prende il nome di B+Tree, ed è usata da tutti i file system di recente sviluppo (Ext3 e ReiserFS compresi), in quanto risponde alla necessità di poter realizzare direttori che contengono un numero sempre maggiore di file. Un altro accorgimento per alleggerire il carico di lavoro della macchina, è quello di eseguire le operazioni di bilanciamento dell’albero soltanto quando la CPU non è occupata da altri applicativi, oppure solo quando strettamente necessario: un sistema di questo tipo prende il nome di Dancing Tree o B*Tree, e va a memorizzare le informazioni senza perseguire una politica di mantenimento del bilanciamento, e relega tutte le operazioni per ribilanciare la struttura in un secondo momento (per questo motivo può capitare che il carico di lavoro per eseguire queste operazioni sia molto pesante, qua ndo, invece, bilanciando l’albero ad ogni singola scrittura, si hanno molteplici operazioni sull’albero ma circa tutte con lo stesso peso). Il Dancing Tree, tra i file system presi in analisi, è adottato solo da ReiserFS, in quanto gli altri eseguono il bilanciamento ad ogni scrittura dell’albero. 76 Appendice B: Access Control Lists (ACLs) Di seguito sarà illustrato il metodo con cui i file system implementano la gestione dei permessi d’accesso ai file; questo metodo è un ampliamento ed una generalizzazione degli standard precedenti, e prende il nome di Access Control List (ACL) [POSIXACL]. B.1. Evoluzione dei permessi d’accesso nei file system Tradizionalmente tutti i sistemi che supportano la famiglia di standard POSIX (portable operating system interface, adottata anche da UNIX e Linux), implementano un sistema di permessi molto semplice ma altrettanto potente: ogni oggetto presente nel file system è associato a tre set di permessi che ne definiscono i criteri d’accesso per le classi proprietario (owner), gruppo proprietario (owning group) ed altri utenti (others), ed ogni set contiene i permessi di lettura (r), scrittura (w) ed esecuzione (x). Uno schema di questo tipo viene implementato usando soltanto nove bit più alcuni altri per gestire funzioni speciali, ed è sufficiente per gestire la maggior parte dei casi d’uso degli oggetti presenti in memoria. Con l’evoluzione delle tecnologie software si è resa necessaria una maggiore flessibilità di quella permessa da un sistema di questo tipo, e sono stati sviluppati programmi in grado di aggirare le sue numerose limitazioni, producendo d’altro canto un notevole aumento della complessità di questi programmi e un conseguente aumento dei bachi in esso presenti. Un classico esempio di limitazione imposta dal tradizionale sistema di permessi è il fatto che soltanto agli amministratori di sistema è consentito creare e modificare i gruppi, e la maggior parte delle applicazioni sviluppate per consentire ai normali utenti di effettuare modifiche dei permessi sui propri documenti contiene bug che, se sfruttati, possono compromettere la sicurezza di tutti i dati in memoria. La necessità di sviluppare un sistema di permessi più sicuro e flessibile ha portato alla realizzazione delle Access Control Lists (ACLs), che sono definite come strumento standard nella famiglia POSIX 1003. 77 B.2. Struttura di una ACL Una Access Control List è costituita da una serie di voci (entry), ciascuna rappresentante una classe di utenti, che definisce per ogni classe i tradizionali permessi di lettura, scrittura ed esecuzione (-r-w-x) sull’oggetto. Le ACL contenenti soltanto le tre classi base owner, group e other sono dette ACL in forma minima (minimal ACL), e sono equivalenti ai permessi d’accesso di vecchio stampo, mentre quelle contenenti altre voci sono dette ACL estese (extended ACL) e contengono una maschera (mask) e un qualsiasi numero di utenti o gruppi d’utenti, ciascuno definito da un nome. La tabella B.1 mostra la tipica struttura di una ACL estesa. Tipo di Voce Forma Testuale Proprie tario (owner) user::rwx Singolo Utente Definito user:nomeutente:rwx Gruppo Proprietario (owning group) group::rwx Singolo Gruppo Definito group:nomegruppo:rwx Maschera (mask) mask::rwx Altri Utenti (other) other::rwx Tabella B.1: Struttura di una ACL estesa. Le voci di singolo utente definito e di singolo gruppo definito sono assegnate alla classe gruppo proprietario, a cui appartiene già la voce “owning group”, presente obbligatoriamente in tutte le ACL. Per processarle si fa uso della voce mask: quando un utente registrato nella ACL come Utente Definito o Gruppo Definito, richiede di eseguire un’operazione sul relativo oggetto, il sistema di controllo dell’ACL permette soltanto le operazioni che compaiono sia nella maschera che nella voce utente/gruppo, come nell’esempio mostrato in tabella B.2. Tipo di Voce Forma Testuale Permessi Singolo Utente Definito user:GiorgioRomani:r-x r-x Maschera mask::rw- rw- Permessi Effettivi r-- Tabella B.2: Uso della voce “mask” per definire i diritti finali delle voci "Singolo Utente Definito" e "Singolo Gruppo Definito". 78 B.3. Algoritmo di controllo degli accessi Quando un processo richiede l’accesso ad un oggetto presente nel file system viene eseguito un algoritmo composto di due passi, il primo dei quali è selezionare la voce che più identifica il processo richiedente. Le varie voci di lista sono lette nel seguente ordine: proprietario, utenti definito, gruppi (proprietario e gruppi definiti), altri utenti (other), e solo una di queste voci sarà utilizzata per l’accesso. Il secondo passo controlla se la voce trovata contiene sufficienti permessi d’accesso per il processo richiedente. Un singolo processo può appartenere ad uno o più gruppi, quindi più di una voce di gruppo può essere usata per gestire i permessi; se qualcuno dei possibili gruppi contiene i permessi richiesti, allora il sistema permetterà l’accesso all’oggetto, se invece nessuna delle possibili voci identificate permette l’accesso, allora al processo non sarà permesso di accedere ai contenuti ric hiesti. L’algoritmo di controllo degli accessi può essere descritto in pseudo-codice come segue: ?? Se l’userID del processo richiedente corrisponde all’userID del proprietario, allora verranno utilizzati i permessi del proprietario (owner). ?? Altrimenti se l’userID del processo corrisponde all’userID di un utente definito nella lista, allora la relativa voce nell’ACL ne determinerà i permessi. ?? Altrimenti se una delle groupID del processo corrisponde con l’ID dell’owner group, e la relativa voce dell’ACL contiene i permessi richiesti, allora l’accesso è consentito ?? Altrimenti se una delle groupID del processo corrisponde con l’ID di uno dei gruppi definiti nella lista, e la relativa voce dell’ACL contiene i permessi richiesti, allora l’accesso è consentito. ?? Altrimenti se una delle groupID del processo corrisponde con l’ID di uno dei gruppi definiti nella lista o con il gruppo proprietario, e nessuna delle voci corrispondenti nella lista contiene il permesso richiesto, allora l’accesso viene negato. ?? Altrimenti la voce “other” determina i permessi d’accesso. ?? Se la voce selezionata nella lista è la “owner” o “other”, e contiene i permessi richiesti, allora l’accesso viene garantito. 79 ?? Altrimenti se la voce selezionata nella lista corrisponde ad un utente o gruppo definito nella stessa, allora si esegue il confronto con i permessi definiti nella maschera: se sia la voce corrispondente che la maschera contengono il permesso richiesto, allora l’accesso è garantito. ?? Altrimenti l’accesso viene negato. Con l’utilizzo di questo strumento diventa quindi possibile definire permessi di accesso specifici per singoli utenti, cosa che risulta molto utile per grandi sistemi multiutente con diversi livelli gerarchici, e diventa anche possibile fornire il potere di gestire i permessi di accesso a determinati oggetti ad utenti diversi dall’amministratore di sistema. 80 Appendice C – Linked Lists Questo tipo di struttura dati permette di memorizzare collezioni di uno stesso tipo di dato, indipendentemente dalla lunghezza finale della collezione stessa. Una lista collegata (linked list) è formata da una serie di tanti elementi dello stesso tipo, ai quali si può accedere soltanto sequenzialmente, collegati tra loro attraverso dei riferimenti. Ed è proprio l’utilizzo dei riferimenti ad altri elementi che differenzia le linked list dagli array di dati: questi ultimi sono memorizzati all’interno di un blocco unico contenente tutti i dati ad essi appartenenti; e per navigarli basta spostarsi di un offset 7 di byte predefinito, così da trovare in questo modo l’elemento successivi o precedente a quello appena letto nell’array. Gli elementi appartenenti ad una stessa linked list non sono necessariamente memorizzati in zone di memoria tra loro adiacenti, e per accedere a ciascuno di essi bisogna spostarsi all’indirizzo di memoria al quale è memorizzato l’elemento precedente o successivo; all’interno d’ogni elemento appartenente alla lista diventa quindi necessario inserire un campo dati aggiuntivo contenente l’indirizzo di memoria al quale può essere recuperato l’elemento successivo della lista. Un esempio di dichiarazione di linked list è riportato di seguito: struct linkedlist { /* Dichiarazione dei tipi di Dato appartenenti alla Struttura */ struct linkedlist *prossimo_elemento; struct linkedlist *elemento_precedente; }; 7 Per leggere l’elemento successivo di un array, è necessario spostarsi avanti in memoria, facendo un salto (offset) di dimensione pari alla grandezza del singolo elemento memorizzato nell’array, in quanto ogni elemento è memorizzato in una zona adiacente al suo precedente e successivo. 81 Per navigare una linked list è poi necessario utilizzare un puntatore alla struttura dati nel quale viene memorizzato il riferimento all’elemento di partenza, che è utilizzato per accedere agli altri elementi della lista attraverso un reindirizzamento: struct linkedlist *puntatore; puntatore=puntatore->prossimo_elemento; Quando viene allocato un nuovo elemento appartenente ad una linked list, è necessario identificare quell’elemento come coda della lista, cosa che viene di solito eseguita ponendo il valore del puntatore all’elemento successivo ad un valore nullo. Altra cosa necessaria, quando si aggiunge un elemento in coda alla lista, è assegnare un riferimento alla nuova coda nell’elemento precedente, cambiando il valore del puntatore da NULL all’indirizzo a cui è memorizzata la nuova struttura. Come si può ben notare, è la presenza dei puntatori all’elemento successivo e precedente che permette una navigazione della lista in entrambe le direzioni, ma in molti casi uno dei puntatori può essere omesso nella dichiarazione: nel caso risulti necessaria la navigazione in un solo senso basterà includere assieme al puntatore all’elemento successivo un riferimento alla testa della lista (primo elemento), in modo da poter ricominciare a scorrere la lista dall’inizio in caso di necessità. La figura C.1 mostra la strut tura di due linked lists, una con riferimenti all’elemento successivo e precedente, l’altra con riferimenti solo agli elementi successivi. Elemento1 Variabile 1 Variabile 2 Variabile 3 Elemento 1 Elemento 2 Elemento 3 Variabile 1 Variabile1 Variabile1 Variabile1 Variabile 2 Variabile2 Variabile2 Variabile2 Varibile3 Varibile3 Varibile3 Elemento 2 Variabile 3 NULL Elemento 3 Variabile 1 NULL Variabile 2 Variabile 3 NULL Figura C.1: esempi di linked lists con riferimenti agli elementi successivi e precedenti, e con riferimenti solo agli elementi successivi. Nella figura C.1 risulta subito evidente che queste strutture dati consentono soltanto una loro navigazione di tipo sequenziale: per accedere, cioè, all’n-esimo elemento di lista 82 bisognerà prima accedere agli altri n-1 elementi; una volta avuto accesso ad un elemento, per accedere all’elemento ad esso adiacente, bisogna leggere e posizionarsi all’indirizzo di memoria contenuto nel relativo puntatore. Per contrassegnare un certo elemento come capo o coda della lista, si assegna all'apposito puntatore il valore NULL, mentre i puntatori degli elementi interni contengono sempre l’indirizzo dell’elemento precedente o successivo. 83 Riferimenti Bibliografici [HTree] Tratto da Directory Indexing of ext2/3 - Mingming Cao 26-7-2005 http://ext2.sourceforge.net/2005-ols/paper- html/node3.html [JFS] Tratto da EXT3, Journaling Filesystem Conferenza del 20/7/2000 tenuta dal Dr. Stephen Tweedie http://olstrans.sourceforge.net/release/OLS2000-ext3/OLS2000-ext3.html [MSDN] Progettazione di applicazioni distribuite con Visual Studio .NET - Controllo di Accesso. Tratto da http://msdn.microsoft.com/library/ita/default.asp?url=/library/ITA/vsent7/html/vxconac cesscontrol.asp [NT] New Technology File System, designed for Windows NT, 2000, XP http://www.ntfs.com/ntfs-partition-boot-sector.htm http://www.ntfs.com/ntfs- mft.htm [POSIX-ACL] Tratto da POSIX Access Control Lists on Linux, di Andreas Grunbacher - SuSE Labs, SuSe Linux AG www.suse.com [reiser] Reiser4 Overview, tratto da www.namesys.com [vfs/ext2] Tratto da Design and Implementation of the Second Extended Filesystem di: Rémy Card, Theodore Ts'o, Stephen Tweedie Pubblicato agli atti del primo simposio internazionale su Linux in Olanda. ISBN 90-367-0385-9 http://web.mit.edu/tytso/www/linux/ext2intro.html [spec] Tratto da John's spec of the second extended filesystem http://uranus.it.swin.edu.au/~jn/explore2fs/es2fs.htm 84 [VFS] A Tour of the Linux VFS - 1996 Michael K. Johnson, [email protected] http://www.tldp.org/LDP/khg/HyperNews/get/fs/vfstour.html [WFS] WinFS Data Model - Jesús Rodríguez http://www.c-sharpcorner.com/Longhorn/WinFS/WinFSDataModel.asp 85