Mardi 1er février 2005 |
misc.ml
, pour nous permettre de lire ou d'écrire des entiers
dans des chaînes de caractères. En pratique, on stockerait les entiers
sous formes binaires. Cependant pour simplifier la mise au point, on les
représentera en ascii sous forme décimale avec des 0 en tête. Ainsi, 7
sera représenté par la chaîne 0007
. (Comme la taille des fichiers sera
représentée par un entier sur le disque, on sera donc limité à des
fichiers de taille inférieure à 10000 octets. Pour aller au delà, il
suffira de revenir à une représentation binaire.)read_int : string -> int -> int
telle que
read_int
str offset retourne l'entier stocké en ascii
dans la chaîne de caractères
str à partir de la position offset.
Ainsi, read_int "0123456789"
4 retournera l'entier 4567.
On retournera l'entier −1 plutôt que d'échouer si la
sous-chaîne ne représente pas un entier.
(On pourra utiliser int_of_string
et String.sub
).
write_int : int -> string -> int -> unit
telle que l'appel write_int
n str offset écrive
l'entier n dans la chaîne str en
commençant à la position offset.
(On pourra utiliser la fonction Printf.sprintf
avec le format
"%04d"
et la fonction String.blit
.)
misc.ml
la déclaration de type suivant
permettant de reporter différents types d'erreurs de façon uniforme.type system_error = | EIO (* Erreur de bas niveau lors d'un accès au disque *) | EACCESS (* Erreur d'accès *) | EISDIR (* Répertoire trouvé où un fichier régulier est attendu *) | ENOTDIR (* Fichier régulier trouvé où un répertoire est attendu *) | EINVAL (* Argument invalide *) | EEXIST (* Un fichier existe déjà *) | ENOENT (* Un fichier manque *) | EBUSY (* Un fichier est occupé *) | ENOSPC (* Plus d'espace sur le disque *) exception System of system_error * string * string let system_error err call mes = raise (System (err, call, mes)) exception Implementation of string let implementation_error s = raise (Implementation s) |
Implementation
pour reporter des erreurs fatales liées
à des erreurs d'implantation (des cas de figures qui ne devraient pas
arriver dans un usage normal de la librairie en mode utilisateur).System
pour reporter les erreurs à l'utilisateur
de la librairie. On utilisera en particulier les cas suivants:
.... | EIO (* Erreur de bas niveau lors d'un accès au disque *) | EACCESS (* Erreur d'accès *) | EISDIR (* Répertoire trouvé où un fichier régulier est attendu *) | ENOTDIR (* Fichier régulier trouvé où un répertoire est attendu *) | EINVAL (* Argument invalide *) | EEXIST (* Un fichier existe déjà *) | ENOENT (* Un fichier manque *) | EBUSY (* Un fichier est occupé *) | ENOSPC (* Plus d'espace sur le disque *) ... |
misc.ml
d'autres fonctions d'usage général
telles que try_finalize
ou really_read
, etc.
block_size
) et
terminé par la taille des blocs écrite sur 4 octets. La taille du
fichier représentant le disque, mesurée en octets, devra donc être
un multiple de la taille des blocs augmenté de 4. Les blocs devront
avoir au moins un taille minimale min_block_size
de 32
octets.disk.mli
et disk.ml
.Opendisk
qui prend une structure avec une
composante name contenant le chemin désignant le fichier
représentant le disque et rend ce fichier visible comme un disque.
module type DISK = sig val block_size : int val block_nb : int val read_block : int -> string -> int -> unit val write_block : int -> string -> int -> unit val close : unit -> unit val print : unit -> unit end module Opendisk (F : sig val name : string end) : DISK |
Opendisk
, commencer par écrire un
champ disk
(visible seulement dans le module) qui
contient un descripteur ouvert sur le fichier représentant le disque.block_size
et block_nb
qui
contiennent la taille et le nombre total de blocs du disque.lseek_block : int -> unit
qui place la position du
descripteur disk
juste avant le bloc dont le numéro est passé en
argument.close
qui désactive le disque (en fermant le
descripteur). Les appels suivants à read_block
et write_block
devront
produire une erreur. Pour cela, on pourra ajouter une référence alive
permettant de vérifier que le disque est toujours en état de marche
avant chaque accès.read_block : int -> string -> int -> unit
telle que
read_block
n buf offset transfert le bloc n du
disque dans le tampon buf à partir de l'octet offset. On
vérifiera que la fonction est appelée avec des paramètres valides. En cas
d'erreur de lecture ou d'écriture, on reportera une erreur EIO
.write_block : int -> string -> int -> unit
telle que write_block
n buf offset
effectue le transfert du contenu de buf à partir de
l'octet offset vers le bloc n du disque. En cas
d'erreur de lecture ou d'écriture, on reportera un erreur EIO
.print_block
et print
suivantes : let print_block s n = let buffer = String.create block_size in read_block n buffer 0; for i = 0 to block_size - 1 do if buffer.[i] = '\000' then buffer.[i] <- '@' done; Printf.printf "%04d%s" n s; for i = 0 to block_size / 4 - 1 do Printf.printf "%s " (String.sub buffer (i * 4) 4); done; print_newline() let print () = print_newline(); Printf.printf " I-NB I-FN B-FL N-FN (super block)\n"; Printf.printf " KIND LINK SIZE ... (inode)\n"; for n = 0 to block_nb -1 do print_block ": " n done;; |
print
.
Format
qui prend un disque et retourne
uniquement une fonction mkfs
permettant de le formatter.
module Format (D : Disk.DISK) : sig val mkfs : int -> unit end |
Mount
où la signature USERFS
représente l'ensemble des fonctions de manipulation d'un système de
fichiers quelconque, disponibles pour l'utilisateur. Ce foncteur devra
échouer si le disque n'est pas formaté :
module Mount (D : Disk.DISK) : USERFS;; |
USERFS
complète qui devra être implémentée à la fin de
cette section est la suivante (on l'étendra dans la section 4 pour manipuler les répertoires) :
module type USERFS = sig open Common (* drapeaux d'ouverture de fichier *) type open_flag = O_RDWR | O_CREAT type file_descr type file_perm = int val openfile : string -> open_flag list -> file_perm -> file_descr val close : file_descr -> unit (* mode de positionnement. *) type seek_command = SEEK_SET | SEEK_CUR | SEEK_END val lseek : file_descr -> int -> seek_command -> int val read : file_descr -> string -> int -> int -> int val write : file_descr -> string -> int -> int -> int val ftruncate : file_descr -> int -> unit val stat : string -> stats val sync : unit -> unit val umount : unit -> unit end;; |
Unix
de même nom.inodes
et les blocks
. Ces
opérations plus élémentaires sont fournies dans un foncteur
Alloc
, qui comme les modules Format
et Mount
, est abstrait
par rapport à un module (D : Disk.DISK)
, i.e prend en argument un
tel module. C'est le module Alloc
que nous allons implanter en premier.USERFS
, nous allons
créer un ensemble de fichiers common.ml
, cache.ml
, simplefs.mli
et simplefs.ml
. file_kind
et stats
qui sont les mêmes pour tous les
systèmes de fichiers sont placés dans un fichier common.ml
afin
d'être partagés simplement entre les différents modules.
open Misc (* définitions communes à tous les systèmes de fichiers *) type file_kind = S_REG | S_DIR type stats = { st_dev : int ; st_ino : int ; mutable st_kind : file_kind ; mutable st_nlink : int ; mutable st_size : int ; };; |
DISK
et fournies par le foncteur Make
qui renvoie un type
CACHE
. Ce dernier contient un module Blk
, un module Inode
et un
module Super
. Cette interface et son implantation sont fournies dans
le fichier cache.ml. Dans cette implantation, les
blocs et les inodes sont cachés dans une table de hachage et écrits
sur le disque lorsqu'ils ne sont plus utilisés. CACHE
est la suivante :
open Misc open Common module type CACHE = sig module Blk : sig val block_size : int (* taille des blocs *) type blk (* type opaque pour représenter les blocs en mémoire, il contient entre autres, le numéro du bloc et un tampon associé *) val bread : int -> blk (* prend en argument un numéro de bloc et retourne sa représentation mémoire. Indique au cache que la représentation mémoire est utilisée. *) val bzeros : int -> blk (* prend en argument un numéro de bloc et retourne sa représentation mémoire remplie de zéros *) val read_string : blk -> int -> string -> int -> int -> unit (* [ read_string b boff s soff len ] place dans la chaîne de caractères [s], à partir de la position [soff], [len] caractères lus, à partir de la position [boff] dans le bloc représenté en mémoire par [b] *) val write_string : string -> int -> blk -> int -> int -> unit (* [ write_string s soff b boff len ] place dans le bloc représenté par [b], à partir de la position [boff], [len] caractères lus dans la chaîne de caractères [s] à partir de la position [soff] *) val read_int : blk -> int -> int (* [ read_int b boff ] retourne l'entier dont les 4 octets de la représentation binaire ont été récupérés, à partir de la position [boff], dans le bloc représenté par [b] *) val write_int : int -> blk -> int -> unit (* [ write_int i b boff ] place les 4 octets de la représentation binaire de l'entier [i] dans le bloc représenté par [b] à partir de la position [boff] *) val brelse : blk -> unit (* Indique au cache que la représentation mémoire du bloc passé en argument n'est plus utilisée. Doit être exécuté après un bread sur le même bloc. *) val sync : unit -> unit (* copie sur le disque, tous les blocs dont la représentation en mémoire ne correspond pas à celle sur le disque *) end module Inode : sig type inode = { mutable reference_number : int; (* nombre de références actives : descripteurs ouverts *) stats : stats; (* informations de statut *) blocktbl : int array (* table des blocs *) } (* type utilisé pour la représentation des inodes en mémoire *) val blocktbl_offset : int (* offset de la table des blocs dans l'inode *) val blocktbl_size : int (* taille de la table des blocs *) val max_file_size : int (* taille maximale d'un fichier *) val iget : int -> inode (* retourne la représentation mémoire de l'inode dont le numéro est passé en argument et incrémente son compteur de références. *) val iput : inode -> unit (* décrémente le nombre de référence sur l'inode. L'inode peut être écrit sur le disque et son cache récupéré lorsqu'il n'est plus référencé. *) val sync : unit -> unit (* synchronise l'ensemble des représentations mémoire des inodes avec celles sur le disque *) val busy : unit -> int (* retourne false si tous les inodes sont libres, true sinon *) end module Super : sig val root_inode : int (* numéro du bloc contenant l'inode de la racine du système de fichier *) val inode_nb : int (* nombre total d'inodes dans le système de fichiers *) val sfstype : unit -> bool (* vérifie que le système de fichiers est au format "sfs0" *) val force_sfstype : unit -> unit (* déclare le système de fichiers au format "sfs0" *) val inode_nb_ref : int ref (* nombre d'inodes utilisés *) val last_free_inode : int ref (* le dernier inode libre trouvé *) val free_block_list : int ref (* liste des blocs libres *) val free_block_nb : int ref (* nombre de blocs libres *) val sync : unit -> unit (* synchronise le super-bloc en mémoire avec le disque *) end val block_nb : int (* nombre total de blocs (réguliers et inodes) *) val close : unit -> unit (* débranche le disque brutalement *) end |
module Make (D : Disk.DISK) : CACHE |
bread
et
brelse
, d'une part, et iget
et iput
, d'autre part, doivent être
bien parenthésés pour chaque inode n et pour chaque bloc n. De
plus, dans le cas des blocs, on n'autorise pas l'imbrication des
parenthèses, ie. un bloc n ouvert ne peut pas être ouvert une
deuxième fois avant d'avoir été refermé.umount
qui exige que tous les blocs aient étés libérés.
Une erreur au moment de umount
peut être due au programme test, si
un descripteur n'a pas été refermé, ou à la bibliothèque si le
parenthèsage n'a pas été bien respecté.simplefs.mli
réduit à :
module type USERFS = sig end;; module Mount (D : Disk.DISK) : USERFS;; |
simplefs.ml
, le début
d'implantation du foncteur Alloc
qui est abstrait par rapport à un
cache (et donc un disque) et qui implantera les fonctions d'allocations
de blocs et d'inodes pour notre système de fichiers :
module Alloc (C : Cache.CACHE) = struct open Common open C.Blk open C.Inode open C.Super (* Définitions propres à l'accocation des blocs et inodes [À COMLÉTER] *) |
end |
System
lui aussi
abstrait par rapport au cache, à placer également dans le fichier
simplefs.ml
.
module System (C : Cache.CACHE) = struct module A = Alloc (C) open Common open C.Blk open C.Inode open C.Super open A (* Définitions propres au système de fichiers [À COMLÉTER] *) |
end;; |
simplefs.ml
une première implantation, vide,
du module vide USERFS
:
module type USERFS = sig end |
module Mount (D : Disk.DISK) : USERFS = struct module C = Cache.Make (D) let () = if not (C.Super.sfstype()) then system_error EINVAL "Mount: fstype" "" module F = System(C) include Common include F end;; |
Mount
fournit à l'utilisateur une restriction
des fonctionalités implémentées (principalement) par le module système.
Par la suite, il suffira de compléter la signature USERFS
au fur et à
mesure que les définitions auront été implémentées dans le module
System
, sans avoir à changer la définition du module Mount
lui-même.
Remarquez que le module Mount
vérifier que le disk est bien formatté et
échoue dans le cas contraire.simplefs.mli
et simplefs.ml
:
ocamlc -c misc.ml disk.mli disk.ml common.ml cache.ml |
simplefs.mli
ou simplefs.ml
:
ocamlc -c simplefs.mli simplefs.ml |
make simplefs |
make showdisk ./showdisk Simplefs.dbg |
make mestests |
watch -n 1 ./showdisk Simplefs.dbg |
Start subshell
en répondant à la question Caml toplevel to run:
par:
ocaml unix.cma misc.cmo disk.cmo common.cmo cache.cmo simplefs.cmo |
mestests.ml
une à
une. L'exécution de D.sync()
écrit les caches sur le disque, ie. dans le
fichier, donc les modification devraient aussitôt apparaître dans le
terminal qui imprime en permanence l'état du fichier vu comme un disque.Alloc
et System
petit à
petit. 0
représente le type de l'inode
(0
= S_REG
et 1
= S_DIR
); l'entier à l'indice
1
contient le nombre de liens (« durs ») vers cet inode; l'entier à
l'indice 2
contient la taille du fichier en octet et les autres
entiers correspondent à la table des blocs (numéros des blocs
constituant le fichier). Il n'y a pas d'indirection dans la table des
blocs. La taille maximale d'un fichier est donc : (D.block_size / 4 - 3) * D.block_size
. Inode
qui est défini
dans le fichier cache.ml
que vous avez récupéré abstrait l'accès et
le cache des inodes comme décrit ci-dessus./
. On pourra donc court-circuiter les fonction
d'allocation/déallocation des inodes que l'on considérera seulement
dans la partie 4.System.namei : string -> inode
(ie. une fonction
namei
dans le module System
) qui pour l'instant « prend » et retourne
l'inode de la racine si le nom passé en argument est /
et sinon échoue
(on pourra/devra utiliser iget
).System.stat
qui retourne les informations sur le
fichier dont le nom est passé en argument (pour l'instant toujours
/
). Cette fonction doit prendre soin de « rendre » tout inode
« pris » et doit recopier la structure stats
avant de la retourner
afin d'éviter que l'utilisateur ne puisse en la modifiant modifier
celle du « système » (pour plus de sûreté, on aurait dû déclarer un
autre type stats
isomorphe à Common.stat mais avec des champs non
mutables)./
du disque
Simplefs.dbg
.
System.file_descr
qui doit contenir une
valeur de type inode
, une position dans le fichier et un état (ouvert ou
fermé) codé sur un booléen. type open_flag = O_RDWR | O_CREAT |
System.openfile
qui
n'accepte que les flags O_RDONLY
et O_CREAT
(en même temps) et
n'accepte que le chemin /
. Les permissions sont ignorées (elles sont
incluses dans la signature pour rester compatible avec la librairie
Unix
). La fonction openfile
devra également incrémenter le nombre
de référence à l'inode du fichier (au travers d'un appel à iget
).
correspondant au fichier: ainsi l'inode du fichier sera conservé dans le
cache tant que le descripteur sera ouvert.
System.close
qui ferme un descripteur de
fichier. À l'inverse de la fonction openfile
elle devra décrémenter
le nombre de références à l'inode (au travers d'un appel à iput
).
Lorsque le nombre de référence est nul, la fonction iput
relachera l'inode
en le sauvant sur le disque de façon transparente.
System.read
dont le comportement doit être
similaire à celui de la fonction Unix.read
. Dans un premier temps,
on pourra ignorer les blocs vides qui sont associés au bloc 0
dans
la table des blocs (i.e. on supposera qu'il n'y a pas de tels blocs).
On fera particulièrement attention à lever des exceptions pour tous
les cas d'erreur.Simplefs
, écrire un petit programme exécutable
testread
qui prend un nom de disque et un chemin sur la ligne de commande
et affiche le contenu du fichier désigner par le chemin sur le dique.
On devra donc pouvoir exécuter:
./testread Simplefs.dbg / | diff - testread.outet ne trouver aucune différence. On considérera une erreur comme fatale (le programme retournant alors un code d'erreur non nul). On pourra s'inspirer de la fonction
copyfile
vue dans le cours.
inode_nb
(fixé une fois pour toutes au formatage, ici 1), le dernier inode
libre trouvé last_free_inode
(utilisé comme point de départ pour
chercher les inodes libres), la tête de la liste des blocs libres
free_block_list
et le nombre de bloc libres free_block_nb
. Le
reste du bloc est utilisé pour stocker une chaîne de caractères
identifiant le système de fichiers (ici on utilise sfs0
).
Super
, il reste à
écrire les fonctions de gestion des blocs libres et des inodes libres.Alloc.alloc_block : unit -> bid
qui retourne le
numéro du premier bloc libre et met à jour la liste des blocs libres
comme vu en
cours
2 page 36. S'il n'y a plus de bloc libre, la fonction lève une
exception ENOSPC
. Alloc.free_block : bid -> unit
qui libère le bloc dont le
numéro est passé en argument et l'ajoute dans la liste des blocs libres.
Alloc.itrunc
telle que Alloc.itrunc inode size
affecte la taille size
au fichier associé à l'inode inode
. Si la
valeur de size
est supérieure à la taille du fichier, la table des
blocs sera remplie avec le bloc 0
, si elle est inférieure, les
blocs du fichiers au delà de size
seront désalloués.System.ftruncate
qui prend un descripteur de
fichier plutôt qu'un inode en premier argument. Contrairement à la
précédente, celle-ci ne s'appliquer qu'aux fichiers réguliers. (La fonction
ftruncate
ne modifie pas la position courante de lecture/écriture.)System.lseek
comparable à la fonction lseek
du
module Unix
. Attention ! Lorsque lseek
déplace la position courante au
delà de la fin du fichier, la taille du fichier n'augmente pas et son
occupation disque n'est pas modifiée. En revanche, dès qu'une écriture aura
lieu celle-ci augmentera la taille et l'occupation disque du fichier. Les
blocs intermédiaires ne seront pas alloués, un 0
est placé dans la table
des blocs de l'inode pour indiquer que le bloc n'a pas été
alloué. Sémantiquement, un bloc 0 représente un bloc dont tous les
caractères sont nuls ('\000'
), donc lorsque le bloc sera effectivement
alloué, pour écrire dedans, on veillera bien à initialiser le bloc avec des
caractères nuls.System.write
dont le comportement doit être
similaire à celui de la fonction Unix.read
. Attention ! Il ne faut
pas oublier d'allouer les blocs qui n'ont pas encore été alloués.Alloc.sync
qui écrit le contenu du système de
fichiers dans le fichier. On prendra soin de bien synchroniser toutes
les données représentées en mémoire avec celles de notre « disque ».USEFS.umount
qui effectue un appel à sync
, vérifie
que plus aucun fichier n'est ouvert (en utilisant Inode.busy
) avant de
fermer le disque. System.mkfs: int -> unit
qui prend en argument le
nombre d'inodes, et initialise le disque avec un système de fichiers ne
contenant que la racine. Pour cela, il faut définir le nombre d'inodes, les
initialiser (on pourra utiliser la fonction iput
en lui
passant une structure de type inode
créée manuellement et pas avec iget),
chaîner les blocs libres (voir section 3) dans la liste des
blocs libres, créer un fichier racine et écrire le super-bloc.S_REG
, et on pourra exiger que le node d'inode soit exactement 1 (on
traitera le cas général dans la partie suivante).simplefs.mli
:
module Format (D : Disk.DISK) : sig val mkfs : int -> unit end |
simplefs.ml
:
module Format (D : Disk.DISK) = System (Cache.Make(D)) |
USERFS
du module Simplefs
est (presque) un
sous-ensemble de la librairie Unix
, tout script de manipulation du (seul)
fichier /
de notre système de fichier peut également être écrit
en utilisant le module Unix
à la place du module Simplefs
./
système de fichier Simplefs.dbg
avec son effet sur (une copie) d'un vrai fichier Unix Simplefs.out
dont les
contenus sont au départ identique.OK
ou FAILED
.fsck
, mais ils devrait éliminer la plupart des
erreurs. Lorsqu'un test échoue, vous avez une idée de la fonction en cause,
vous pouvez alors refaire des test plus petits et plus spécialisés, et insérer
des ordres d'impression du disque pour comprendre ce qui s'est mal passé.USERFS
avec les
valeurs suivantes (que l'on ajoutera petit à petit) :
type dir_handle val opendir : string -> dir_handle val readdir : dir_handle -> string val closedir : dir_handle -> unit val mkdir : string -> file_perm -> unit val unlink : string -> unit |
Alloc.ialloc: file_kind -> inode
qui cherche un inode libre, le « prend », c'est-à-dire augmente son compteur
de référence, et le destine à être un fichier
(pour l'instant de taille vide) dont la sorte est passée en premier
argument.Alloc.iput
destinée à remplacer la fonction
C.Inode.iput
qui, lorsqu'un inode
est effectivement désalloué
(ie. lorsqu'à la fois son compteur de référence et le nombre de liens sont
nuls) désalloue également les blocs qu'il contient.
On redonnera aux inodes libres le statut de fichier ordinaire (S_REG
).read
, write
et
lseek
déjà écrites pour manipuler le contenu des fichiers. let dirent_size = 16 let filename_max_size = 12 |
'.'
et '..'
. Ainsi, chaque fois qu'un répertoire est créé, son
nombre de liens initial est 2 et le nombre de liens du répertoire
parent est incrémenté. Lorsqu'une entrée du répertoire est supprimée,
on place le caractère '\000'
(représenté pas \
dans le dessin) dans
le premier caractère de l'entrée. Cette entrée libre peut être
utilisée ultérieurement si une nouvelle entrée est créée dans le
répertoire, mais le répertoire n'est jamais compacté a priori.type dir_handle = file_descr |
USERFS
ne révèle pas leur identité. System.closedir
.internal_readdir
qui prend un descripteur ouvert
sur un répertoire et retourne l'entrée suivante du répertoire, sous la
forme d'une paire (nom, n) où n est le numéro d'inode du fichier
désigné par nom dans ce répertoire.System.readdir
.namei
qui permet de trouver et de « prendre » un inode déterminé par
un chemin complet (ici toujours absolu). On pourra (si on le souhaite)
écrire les fonctions auxiliares suivantes : val open_inode : inode -> bool -> inode val equal_dirent : string -> string -> bool val find_name_in_dir_inode : inode -> string -> inode val split_name : string -> string val relative_namei : inode -> name |
System.mkdir
. Pour cela, on pourra utiliser des
fonctions auxiliares suivantes :
(* [fill_buffer_with_dirent buffer name n] rempli [buffer] avec le nom [name] et le numéro d'inode [n] *) val fill_buffer_with_dirent : string -> string -> int -> unit (* [add_dirent inode name n] ajoute au répertoire [inode] l'entrée de nom [name] et de numéro d'inode [n] *) val add_dirent inode -> string -> int |
System.unlink
et une nouvelle version de System.openfile
.System.opendir
.System.mkfs
pour créer un répertoire à la
racine plutôt qu'un fichier ordinaire lorsqu'il y a plusieurs inodes.
Attention à la particularité de la racine. On exigera qu'il reste au moins
autant de blocs que d'inodes.
Ce document a été traduit de LATEX par HEVEA