Consignes
Le rendu de ce TP se fera uniquement par TPLab et consistera en une archive (zip, tar, etc.) contenant impérativement l'arborescence suivante :
TP3-votre_login/ ├─ RAPPORT ├─ Makefile ├─ question1.c ├─ ... ├─ test_fichier.c ├─ ... └─ sfs/ ├─ Makefile ├─ ... └─ ...
Attention :
-
RAPPORT doit être un fichier texte contenant les réponses aux questions du TP et vos commentaires / remarques / explications pertinents,
-
Makefile doit compiler tous vos fichiers C (vous pouvez vous inspirer de ce fichier Makefile), (si vous ne connaissez pas les fichiers Makefile, je vous conseille de suivre ce tutoriel)
-
votre Makefile doit compiler les programmes C avec les options --std=c99 -Wall -Wextra -pedantic -Werror,
-
un ou plusieurs fichiers C pour chaque question pertinente.
-
votre code pour la deuxième partie (SFS) doit compiler avec le fichier Makefile fourni (options --std=gnu99 -D_FILE_OFFSET_BITS=64 -Wall -Wextra -pedantic -Werror -Wno-unused-parameter)
Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note de TP !
Liens utiles
-
l'archive du TP : sfs.tar
-
encadrant de TP : Pierre.Hyvernat@univ-smb.fr Clovis.Eberhart@univ-smb.fr
1. Programmation et fichiers
N'oubliez pas que vous pouvez utiliser la commande man pour obtenir la documentation des fonctions C des bibliothèques standards :
$ man 3 read
$ man 3 fwrite
Le 3 permet de préciser que vous voulez la documentation des fonctions C. (Par exemple, le shell possède une commande read, et par défaut, man read ouvre cette page de documentation. Vous pouvez trouver la liste des sections avec leur numéro dans la documentation de la commande man...)
Préliminaires
Le langage C offre deux interfaces pour les fichiers :
-
une interface bas niveau avec les fonctions
open
,close
,write
,read
etlseek
; -
une interface haut niveau avec les fonctions
fopen
,fclose
,fwrite
,fread
etfseek
.
Dans le premier cas, les fichiers sont représentés par un entier appelé
descripteur de fichiers, et dans le second cas, par un élément du type
FILE
.
-
Écrivez deux programmes copy.c et fcopy.c qui recopient un fichier vers un autre. Une fois compilé, les exécutables s'utiliseront comme la command cp:
$ ./copy SOURCE DESTINATION $ ./fcopy SOURCE DESTINATION
-
le programme copy.c utilisera les fonctions "bas niveau",
-
le programme fcopy.c utilisera les fonctions "haut niveau".
-
-
Comparez les temps d'execution des deux programmes en fonction du nombre d'octets que vous lisez à chaque fois (un octet à la fois, ou 2 par 2, ou 1024 par 1024, etc.)
Décrivez vos tests et les résultats.
Note : pour la version avec fread
etc., utilisez un
argument size
(deuxième argument pour fread
et
fwrite
) de 1
, et un argument nmemb
(troisième argument pour fread
et fwrite
) égal au
nombre d'octets que vous voulez lire...
La fonction open
prend un troisième argument optionnel qui
précise les droits d'accès à donner au fichier s'il est créé. Lorsque vous
donnez le drapeau O_CREAT
, il est important de donner cet
argument :
int fd = open(..., ... | O_CREAT, 0644);
0644
(644 en octal) donne les
droit de lecture et d'écriture à l'utilisateur courant et les droits de
lecture aux autres.
Vous pouvez trouver une description complète des droits d'accès avec
$ man sys_stat.h
1.1. stdin
, stdout
et stderr
Au lancement d'un processus, 3 descripteurs de fichiers spéciaux sont initialisés :
-
0
pour l'entrée standard (STDIN_FILENO
), associé auFILE*
stdin
, -
1
pour la sortie standard (STDOUT_FILENO
), associé auFILE*
stdout
, -
2
pour la sortie d'erreur (STDERR_FILENO
), associé auFILE*
stderr
.
stdin
est ouvert en lecture et est associé au périphérique
"clavier", et stdout
et stderr
sont ouvert en
écriture et sont associés au périphérique "terminal".
-
Il est possible de remplacer
printf
par un appel àwrite
/fwrite
. Testez les deux versions possibles du programme suivant et commentez le résultat :#include <unistd.h> #include <stdio.h> #include <string.h> #define NB 1024 int main() { char *msg = "Hello world!"; int zero = 0; int oops; // utilisation de ``write`` write(STDOUT_FILENO, msg, strlen(msg)); /* // utilisation de ``fwrite`` fwrite(msg, 1, strlen(msg), stdout); */ oops = 1/zero; return oops; }
-
Décrivez le fonctionnement de la fonction
fflush
.
1.2. Blocs
-
Si vous travaillez sur les machines de l'université, le répertoire /tmp/ est accessible en lecture et contient un système de fichiers avec inodes. Vous pouvez faire des tests dans ce répertoire.
-
Si vous travaillez sur votre portable, vous pouvez facilement créer un système ext? dans un fichier en utilisant les commandes
$ dd if=/dev/zero of=fs.ext2 bs=4096 count=1024 $ sudo mkfs.ext2 fs.ext2 $ mkdir EXT2 $ sudo mount fs.ext2 EXT2
Le répertoire EXT2 contiendra alors un système de fichier ext2 !
Vous pouvez remplacer ext2 par d'autre systèmes de fichiers comme ext3, ext4, fat...
Chaque partition d'un support de stockage utilise un système de fichiers. Sous Linux, le système de fichier le plus courant est ext4 (successeur de ext2 et ext3).
Tous les systèmes de fichiers "interactifs" découpent les fichiers en blocs de taille fixe. La taille d'un fichier peut donc vouloir dire deux choses distinctes :
-
le nombre d'octets de données du fichier : c'est ce qui est affiché par la commande ls -lh,
-
le nombre d'octets occupés par le fichier sur le support : c'est ce qui est affiché par la commande ls -sh.
-
Faites (et décrivez) quelques expériences simples pour deviner la taille des blocs de votre système de fichier. (Vous pouvez faire ces expériences sur plusieurs supports / partitions...)
-
Essayer de créer un fichier qui nécessite des blocs à accès indirect. Comment vous y prenez vous ?
Sur un système Unix (ext? ou similaire), les répertoires occupent en général 4ko sur le disque (la taille d'un bloc).
-
expliquez pourquoi on ne peut pas avoir de répertoire occupant moins d'un bloc sur le disque
-
essayez d'obtenir un répertoire qui occupe plus d'un bloc sur le disque. Comment vous y prenez vous ?
1.3. Taille des répertoires
-
Quelle est la taille des données d'un répertoire ext2/3/4 ? (commande ls -lhd DIR)
-
Quelle est la taille occupée sur le disque par répertoire ext2/3/4 ? (commande ls -shd DIR)
-
Est-ce que la taille des fichiers contenus dans le répertoire impacte la taille occupée sur le disque par le répertoire ?
-
Ajoutez des fichiers dans un répertoire ext2/3/4 jusqu'à ce que sa taille augmente. Qu'elle est sa nouvelle taille ?
-
Supprimez les fichiers contenus dans le répertoire. Qu'elle est maintenant sa taille ?
1.4. Taille des liens symboliques
La commande suivante permet de créer un lien symbolique LINK pointant vers le chemin TARGET
$ ln -s TARGET LINK
-
Qu'elle est la taille occupée sur le disque par un lien symbolique pointant vers un "petit" chemin ?
Qu'elle est la taille occupée sur le disque par un lien symbolique pointant vers un "grand" chemin ?
-
À quoi correspond la taille des données (affichée par ls -l) pour un lien symbolique ?
-
À votre avis, comment est stocké la cible d'un lien symbolique ?
1.5. Fichiers creux
En général, la taille des données est plus petite que la taille occupée sur le support, qui est toujours un multiple de la taille des blocs.
Les fichiers creux (sparse files en anglais) sont une exception. Un tel fichier peut contenir des trous. Le plus simple pour créer un fichier creux est de suivre la procédure suivante :
-
ouvrir un fichier en écriture,
-
écrire quelques octets au début (facultatif),
-
se déplacer pour dépasser la fin du fichier,
-
écrire quelques octets au nouvel emplacement,
-
fermer le fichier.
-
Écrivez un petit programme C qui permet de créer des fichiers creux.
Une fois compilé, votre programme s'utilisera comme suit :
$ ./sparse FILENAME TOTAL_SIZE
-
Testez votre programme sur plusieurs partitions pour voir celles qui autorisent de tels fichiers.
Décrivez comment vous procédez.
-
Expérimentez avec la commande cp et l'argument --sparse, et expliquez ce que vous observez...
La commande shell truncate (et l'appel système
POSIX truncate
) permet de modifier la taille d'un fichier :
-
si on augmente la taille, les blocs ajoutés au fichier sont des "trous",
-
si on diminue la taille, les blocs à la fin du fichier sont désalloués.
2. Un système de fichiers tout simple
La seconde partie du TP consiste à regarder les sources d'un système de fichiers très simple, créé pour l'occasion, appelé SFS (Simple FileSystem). L'objectif est de rajouter quelques fonctionnalités à ce système de fichiers.
Il s'agit d'un système de fichiers basé sur une notion très simple d'inode. Les "partitions" SFS sont en fait simplement stockées dans des fichiers standards. Un volume SFS peut donc être vu comme une archive contenant une arborescence de fichiers / répertoires.
Pour compiler SFS, vous devez avoir la bibliothèque de développement libfuse. Sous Debian / Ubuntu,
$ sudo apt-get install libfuse-dev
devrait suffire.
2.1. Petite visite guidé du code fourni
L'archive contenant le code fourni se trouve ici.
2.1.1. Architecture du projet
Le répertoire sfs contient les fichiers suivants
- Makefile
-
pour compiler les sources
- sfs_parameters.h
-
contenant les paramètres fixes des systèmes de fichiers SFS. Vous pouvez si vous le souhaitez changer les valeurs des trois paramètres de base.
- sfs_log.[ch]
-
contenant une petite fonction de "log" pour enregistrer des informations de debug dans un fichier (sfs.log par défaut)
- sfs_volume.[ch]
-
contenant la gestion de la partition SFS (contenue en fait dans un fichier Unix)
- sfs_low_level.[ch]
-
pour le code bas niveau : manipulation des blocs et des inodes, écriture et lecture, etc. C'est essentiellement dans ce fichier que vous devrez travailler.
- sfs_posix.[ch]
-
contenant le code haut niveau : Autant que possible, les fonctions dans ces fichiers utilisent le même prototype que les appels système correspondant aux appels système POSIX usuels. Par exemple, la fonction
sfs_write
utilise le même prototype que la fonctionwrite
... - sfs_shell.[ch]
-
contenant un petit shell interactif pour manipuler les systèmes de fichiers SFS
- sfs_fuse.[ch]
-
contenant les définitions nécessaires pour utiliser un système de fichiers SFS avec libfuse
2.1.2. Inodes
SFS est basé sur le type d'inodes suivant :
typedef struct inode {
int type; // inode type: S_IFREG, S_IFDIR, S_IFLNK
int size; // size, in bytes of the corresponding file
int nlinks; // number of hard links to this inode (0 means the inode is free)
int blocks[NB_DIRECT_BLOCKS+2]; // blocks for the data (last 2 blocks for single / double indirection blocks)
} inode_t;
Les constantes S_IFREG
, S_IFDIR
,
S_IFLNK
et NB_DIRECT_BLOCKS
sont définies dans le
fichiers sfs_volume.h.
Chaque inode est représenté par un entier (type int
), avec les
constantes suivantes :
-
l'inode
0
n'est pas utilisé, -
l'inode
1
(constanteBADBLOCKS_INODE
) pourrait servir à référencer les blocs défectueux, -
l'inode
2
(constanteROOT_INODE
) est l'inode de la racine de la partition.
2.1.3. Blocs de données
Les blocs de données sont représentés par des tableaux :
// size (in bytes) of blocks
#define BLOCK_SIZE 512
// type synonym for "raw" bytes
typedef unsigned char byte;
// type synonym for blocks
typedef byte block_t[BLOCK_SIZE];
2.1.4. Fonctions importantes
Les blocs et les inodes sont représentés par des entiers (positifs). Les
fonctions sfs_get_inode
/ sfs_get_block
prennent un
argument entier et renvoie un pointeur vers l'inode / bloc correspondant.
Attention, ces fonctions ne font aucune vérification sur l'entier qui leur est fourni. Si vous les utilisez avec des entiers trop grands (ou négatifs), le résultat n'est pas spécifié, et votre programme risque de provoquer une erreur de segmentation.
2.1.5. Utilisation "bas niveau"
La compilation produit un exécutable sfs_shell qui lance un interpréteur pour diverses commandes sur les systèmes de fichiers SFS :
-
mkfs FILE construit un système de fichiers SFS dans le fichier Unix standard FILE
-
mount FILE ouvre le système de fichiers SFS contenu dans le fichier Unix standard FILE
Il est possible de préciser un fichier Unix contenant un système de fichiers SFS sur la ligne de commandes avec
$ ./sfs_shell FILE
Le fichier FILE sera alors automatiquement monté...
Les commandes disponibles dans sfs_shell sont brièvement décrites par la commande help (ou ?). Il s'agit essentiellement des commandes du fichiers sfs_low_level.c. Vous pouvez par exemple :
-
afficher un inode
-
afficher le contenu d'un bloc (en mode data, adresses ou entrées de répertoire)
-
afficher la bitmap des blocs libres
-
...
Le shell SFS ne gère pas l'historique des commandes. Pour cela, vous pouvez utiliser l'utilitaire rlwrap pour lancer le shell SFS:
$ rlwrap ./sfs_shell [FILE]
2.1.6. Utilisation "haut niveau", libfuse
La compilation produit également un exécutable sfs_fuse.
libfuse est une bibliothèque pour créer des systèmes de fichiers sans modifier le noyau Linux. Il suffit de programmer les fonctions de bases comme open, read, mkdir, etc. et il est possible de manipuler un système de fichier "virtuel" (comme SFS) comme un système de fichiers réel.
Si FILE est un fichier Unix contenant un système de fichiers SFS et si DIR est un répertoire vide, on peut monter le système SFS dans le répertoire avec la commande :
$ ./sfs_fuse -o use_ino FILE DIR
Toutes les opérations usuelles dans le répertoire DIR seront automatiquement répercutées sur le système de fichiers contenu dans FILE.
Vous pourrez notamment utiliser votre shell habituel pour manipuler des fichiers SFS...
Pour enregistrer les modifications dans le fichier contenant le système SFS, il faut démonter le système de fichiers avec la commande
$ fusermount -u DIR
Le répertoire sfs contient un script test-fuse.sh qui permet de
-
compiler SFS
-
créer un volume SFS dans TEST.sfs
-
monter le volume TEST.sfs dans le répertoire TEST.
La commande sfs_fuse envoie des informations dans le fichier sfs.log du répertoire courant. Il est conseillé de regarder le contenu de ce fichier en lançant la commande
$ tail -f ./sfs.log
dans un autre terminal...
2.2. Un premier exercice
-
Compilez le code fourni et lancez le shell SFS :
$ rlwrap ./sfs_shell
-
Créez un système de fichiers dans le fichier TEST.sfs
(NULL)% mkfs TEST.sfs
-
Regarder le contenu de l'inode 2 (la racine)
(TEST.sfs)% I 2
et regarder le contenu du premier bloc de données associé :
(TEST.sfs)% H ??? (TEST.sfs)% E ???
Dans les commandes précédentes, ??? doit être le numero du premier bloc de données de la racine. La première commande affiche le contenu du bloc en hexadécimal et la seconde affiche le contenu du bloc lorsque ses octets sont interprétés comme des entrées de répertoires (type
dir_entry_t
défini dans sfs_low_level.h). -
Initialisez un inode de fichier vide avec la commande
(TEST.sfs)% create_file
et vérifier que l'inode utilisé a bien le type regular file, et une taille de 0 octet.
Combien de blocs de données sont utilisés par cet inode ?
-
Ajoutez cet inode dans le repertoire / avec la commande
(TEST.sfs)% add_entry 2 NAME ???
N'oubliez pas d'utiliser le numéro d'inode approprié et de choisir un nom pour le fichier.
-
Vérifiez que le fichier apparait bien dans les entrées du répertoire racine.
-
Écrivez une petite chaine de caractères dans ce fichier avec la commande write du shell SFS.
Vérifiez qu'un nouveau bloc de données à été alloué, et qu'il contient bien la chaine choisie...
-
Quittez le shell SFS et lancez l'interface fuse :
$ ./test-fuse.sh
Le répertoire TEST doit maintenant contenir le fichier que vous avez créé dans le shell SFS :
-
vous pouvez obtenir son numéro d'inode avec ls -i
-
vous pouvez obtenir sa taille avec ls -l et ls -s
-
vous pouvez visualisez sont contenu avec cat NAME
-
L'appel système statvfs
permet d'obtenir des informations sur un
système de fichiers :
-
taille des blocs
-
taille du système de fichiers
-
nombre de blocs libres
-
...
La fonction sfs_statvfs
du fichier sfs_posix.c contient des appels aux fonction
sfs_count_free_blocks
et sfs_count_free_inodes
qui
doivent être définies dans sfs_low_level.h.
Écrivez ces fonctions.
Remarques: vous aurez probablement besoin des fonctions
-
sfs_block_in_use
pour vérifier dans la bitmap si un numéro de bloc correspond à un bloc utilisé -
sfs_get_inode
Le nombre total de blocs et d'inodes sont définis dans le fichier sfs_parameters.h.
Vous pouvez vérifiez que ces fonctions marchent avec la commande info dans le shell SFS, ou bien avec les commandes df et df -i si vous avez monté un volume SFS avec fuse.
2.3. Amélioration du système de fichiers
Pour la suite du TP, vous allez implanter des fonctionnalités manquantes au système de fichier.
Les fonctionnalités à ajouter sont données par ordre croissant de difficulté mais sont indépendantes les unes des autres.
Au total, les modifications demandées pour les quatre dernières questions sont de l'ordre de 120 lignes ajoutées et quelques lignes supprimées.
Une estimation de la taille des modifications est donnée dans chaque question. Il s'agit uniquement d'une indication pour vous donner un ordre de grandeur...
2.3.1. Liens physiques (facile)
Il s'agit d'ajouter la gestions des liens physiques sur les fichiers.
Ajoutez la gestion des liens physiques dans SFS.
Il faudra pour ceci modifier les fonctions sfs_unlink_inode
et
sfs_link_inode
du fichier sfs_low_level.c.
Taille des modifications : quelques lignes pour chacune des fonctions.
Pour tester, vous pouvez utiliser test-fuse.sh et créer des liens physique avec ln :
$ ln TARGET LINK
Le compteur de liens (visible dans le résultat de ls -l doit passer à 2), et les numéros d'inodes (visible dans le résultat de ls -i) doivent coïncider.
2.3.2. Liens symboliques
Il s'agit maintenant d'ajouter la gestions des liens physiques sur les
fichiers. Les liens symboliques sont représentés par des inodes de type
S_IFLNK
.
Un lien symbolique n'a pas de données réelles, mais il doit contenir un chemin d'accès cible.
Ajoutez la gestion des liens symboliques dans SFS.
Pour ceci, il faut écrire les deux fonctions suivantes :
-
sfs_writelink_inode
qui permet d'initialiser un inode au typeS_IFLNK
et d'inscrire la cible du lien au bon endroit. -
sfs_readlink_inode
qui recupere la cible d'un lien symbolique.
Ces deux fonctions se trouvent dans le fichier sfs_low_level.c.
Taille des modifications : environ une douzaine de lignes pour chacune des fonctions.
Pour tester, vous pouvez utiliser test-fuse.sh et créer des liens physique avec ln -s :
$ ln -s TARGET LINK
La commande ls -l doit afficher la cible des liens symbolique...
2.3.3. Blocs à accès indirect
Actuellement, les blocs de données associes à un inode dans SFS sont tous à
accès direct. Le tableau blocks
contenu dans un inode
contient NB_DIRECT_BLOCKS
cases référençant des blocs de données.
La taille maximale d'un fichier est donc égale a NB_DIRECT_BLOCKS *
BLOCK_SIZE
, soit quelques kibioctets...
Pour augmenter légèrement la taille possible des fichiers, nous allons
ajouter la gestion de blocs indirects. Le tableau blocks
d'un
inode contient une case supplémentaire, actuellement non utilisée. Lorsque
cela est nécessaire, cette case sera utilisée pour référencer un bloc
contenant BLOCK_SIZE / sizeof(int)
blocs de données.
Il n'est pas nécessaire d'ajouter la gestion des blocs indirects pour les répertoires ou les liens symboliques. La limite sur le nombre de fichiers dans un répertoire ou la taille de la cible d'un lien ne sont pas très contraignantes.
Ajoutez la gestion des blocs indirects pour les fichiers dans SFS.
Les fonctions POSIX qui accèdent au contenu du fichier sont read
et write
, ainsi que truncate
.
-
La fonction
sfs_read
(sfs_posix.c) est programmée à partir de la fonctionsfs_read_from_inode
(sfs_low_level.c), elle même programmée à partir desfs_read_from_block
(sfs_low_level.c), qui utilise-
sfs_get_data_block_nb_from_inode
pour chercher les numéros de blocs à lire.
C'est cette dernière que vous devrez modifier...
-
-
La fonction
sfs_write
(sfs_posix.c) est programmée à partir de la fonctionsfs_write_to_inode
(sfs_low_level.c), elle même programmée à partir de :-
sfs_write_to_block
(sfs_low_level.c).
C'est cette dernière que vous devrez modifier...
-
-
La fonction
sfs_truncate
(sfs_posix.c) est programmée à partir de la fonctionsfs_truncate_inode
(sfs_low_level.c), elle même programmée à partir de :-
allocate_datablocks
(sfs_low_level.c), utilisée pour augmenter la taille des données d'un inode, -
deallocate_datablocks
(sfs_low_level.c) utilisée pour diminuer la taille des données d'un inode,
Ce sont ces deux dernières que vous devrez modifier...
-
N'oubliez pas de modifier la fonction do_info
du fichier sfs_shell.c pour mettre à jours la taille maximale
autorisée des fichiers et de documenter vos tests pour mettre en ouvre cette
gestion des blocs indirects.
Taille des modifications :
-
sfs_get_data_block_nb_from_inode
, quelques lignes -
sfs_write_to_block
, une quinzaine de lignes -
allocate_datablocks
, une vingtaine de lignes -
deallocate_datablocks
, une quinzaine de lignes
2.3.4. Fichiers creux
Les systèmes avec inodes permettent de créer des fichiers creux : il
suffit de mettre un numéro de bloc spécial qui n'est pas alloué sur le
support. Les opérations de lecture sur un tel bloc renvoient des
0
, et les opérations d'écriture vont provoquer l'allocation d'un
vrai bloc...
Pour SFS, nous allons utiliser le numéro de bloc ZERO_BLOCK
(constante définie à 0
) pour représenter un bloc de 0 non alloué... (Plus précisément, ce bloc est alloué
mais en "lecture seule".
Ajoutez la gestion des fichiers creux dans SFS.
Comme précédemment, les fonctions POSIX qui accèdent au contenu du fichier
sont read
et write
, ainsi que truncate
.
Comme le bloc ZERO_BLOCK
est un vrai bloc ne contenant que des
0
, il n'est pas nécessaire de modifier les fonctions de lecture.
Il reste donc
-
truncate
, qui accède aux blocs à travers les fonctionsallocate_datablocks
etdeallocate_datablocks
, -
sfs_write_to_block
.
Il faut également modifier la fonction
sfs_count_used_blocks_inode
qui permet de calculer la taille
occupée par les données d'un inode.
N'oubliez pas de documenter vos modifications dans le fichier RAPPORT...
Taille des modifications :
-
sfs_write_to_block
, une dizaine de lignes, -
allocate_datablocks
, une demi douzaine de lignes, -
deallocate_datablocks
, quelques lignes, -
sfs_count_used_blocks_inode
, quelques lignes.
2.4. Pour aller plus loin
Les fonctionnalités suivantes sont en bonus...
-
écrire une fonction
badblocks
qui recherche les blocs défectueux et les associe à l'inodeBADBLOCKS_INODE
(pour modéliser de blocs défectueux, vous pouvez par exemple considérer les blocs dont les premiers octets sont de ad be ef), -
gérer les blocs avec deux indirections dans les inodes, pour augmenter la taille maximale des fichiers,
-
écrire une fonction
checkfs
qui vérifie la cohérence du volume SFS, -
gérer une date de modification.