Tesi Completa - Università degli Studi di Modena e Reggio

Transcript

Tesi Completa - Università degli Studi di Modena e Reggio
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