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/
         ├─ Makefile
         ├─ copy.c
         ├─ fcopy.c
         ├─ README
         ├─ ...
         ├─ ...
         └─ sfs/
             ├─ RAPPORT
             ├─ Makefile
             ├─ ...
             └─ ...

Attention :

Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note de TP !

Liens utiles

1. Programmation et fichiers

Préliminaires (facultatif)

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...)

Le langage C offre deux interfaces pour les fichiers :

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.

  1. É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".

  2. Comparez les temps d'exécution 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.)

    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...

  3. Modifiez la taille des buffers (et leur types) dans fcopy avec la fonction setvbuf et comparez les temps d'exécution.

  4. Décrivez vos tests et les résultats dans le fichier README.

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. Blocs

  1. 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.

  2. 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 fichiers ext2 !

    Vous pouvez remplacer ext2 par d'autre systèmes de fichiers comme ext3, ext4, fat...

  3. La commande df -T affiche la liste des systèmes de fichiers montés, ainsi que leur type.

  4. Pour les systèmes de fichiers ext[234], la commande dump2fs -h /dev/... permet d'afficher les paramètres du système. (Attention, il faut les droits administrateurs pour accéder au périphérique /dev/...)

Chaque partition d'un support de stockage utilise un système de fichiers. Sous Linux, le système de fichiers 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 :

Faites (et décrivez dans le fichier README) 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...)

1.2. Taille des répertoires

À FAIRE SUR UNE PARTITION EXT2, EXT3 OU EXT4, OU DANS /TMP/ (MACHINE DE L'UNIVERSITÉ)

NE FONCTIONNE PAS DANS LES SOUS-RÉPERTOIRES DE ~/HomeUSMB (SYSTÈME DE FICHIER CIFS)

  1. Quelle est la taille des données d'un répertoire ext2/3/4 ? (commande ls -lhd <DIR>)

  2. Quelle est la taille occupée sur le disque par répertoire ext2/3/4 ? (commande ls -shd <DIR>)

  3. Est-ce que la taille des fichiers contenus dans le répertoire impacte la taille occupée sur le disque par le répertoire ?

1.3. Taille des liens symboliques

À FAIRE SUR UNE PARTITION EXT2, EXT3 OU EXT4, OU DANS /TMP/ (MACHINE DE L'UNIVERSITÉ)

NE FONCTIONNE PAS DANS LES SOUS-RÉPERTOIRES DE ~/homeUSMB (SYSTÈME DE FICHIER CIFS)

La commande suivante permet de créer un lien symbolique appelé LIEN pointant vers le chemin CHEMIN

$ ln -s <CHEMIN> <LIEN>
  1. Qu'elle est la taille occupée sur le disque par un lien symbolique dont le chemin cible (argument CHEMIN) est "court" (quelques caractères) ?

    Qu'elle est la taille occupée sur le disque par un lien symbolique dont le chemin cible (argument CHEMIN) est "long" (plusieurs dizaines de caractères) ?

    Attention : je parle bien de la taille des chemins et non pas de la taille des fichiers.

  2. À quoi correspond la taille des données (affichée par ls -l) pour un lien symbolique ?

  3. À votre avis, comment est stockée la cible d'un lien symbolique ?

1.4. 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 :

  1. ouvrir un fichier en écriture

  2. écrire quelques octets au début (facultatif)

  3. se déplacer pour dépasser la fin du fichier (fonction lseek)

  4. écrire quelques octets au nouvel emplacement

  5. fermer le fichier

En C :

    int size = 1<<20;  // 1 Mio
    char* start = "start\n";
    char* end = "\nend";
    int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    write(fd, start, strlen(start));
    lseek(fd, size - strlen(end), SEEK_SET);
    write(fd, end, strlen(end));
    close(fd);
  1. É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>
  2. Testez votre programme sur plusieurs partitions pour voir celles qui autorisent de tels fichiers.

    Décrivez comment vous procédez.

  3. Facultatif : 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 simplifiée d'inode. Les volumes 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 install libfuse-dev rlwrap

devrait suffire. (rlwrap est un utilitaire pour gérer un historique de commandes dans une application textuelle...)

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.h

contenant les définitions de types et macros globales

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.

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 fonction write...

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

Vous ne devez normalement modifier que le fichier sfs_low_level.c.

2.1.2. Inodes

SFS est basé sur le type d'inodes suivant (fichier sfs_volume.h):

  typedef struct inode {
      int type;                         // inode type: S_IFREG, S_IFDIR, S_IFLNK
      int size;                         // size of the corresponding file in bytes
      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 ("regular file"), S_IFDIR ("directory"), S_IFLNK ("symbolic link") et NB_DIRECT_BLOCKS sont définies dans le fichiers sfs_volume.h.

Chaque inode est représenté par son numéro (type int), avec les constantes suivantes :

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 renvoient 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 lancer des commandes sur les systèmes de fichiers SFS :

$ ./sfs_shell <FILE>

lance ce shell et monte automatiquement la "partition" SFS contenue dans le fichier FILE. (Il faut auparavant avoir créé la partition SFS dans le fichier FILE comme expliqué plus bas.)

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 :

Le shell SFS ne gère ni les touches "flèches" ni l'historique des commandes. Pour cela, utilisez 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, (cf fichier sfs_posix.[ch]) etc. et il est possible de manipuler un système de fichiers "virtuel" (comme SFS) comme un système de fichiers réel. Par exemple, la commande cp sur un système monté avec libfuse utilisera automatiquement les fonctions fopen , fread et fwrite correspondantes...

La commande sfs_fuse écrit 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...

Pour faciliter les tests, l'archive contient un script test-fuse.sh qui fait les choses suivantes :

  1. compile SFS

  2. créé un volume SFS dans le fichier TEST.sfs (avec la commande sfs_shell)

  3. monte le volume TEST.sfs dans le répertoire TEST (avec la commande sfs_fuse)

  4. démonte le répertoire TEST lorsque que le shell se termine (avec la commande fusermount -u ...)

Si vous voulez faire ces opérations à la main (déconseillé),

2.2. Un premier exercice

  1. Copiez le code sur une partition "standard" :

    • si vous êtes sur votre portable, n'importe quelle partition devrait fonctionner

    • si vous êtes sur une machine de l'université, un sous-répertoire direct de votre répertoire personnel, ou bien /tmp/ devrait fonctionner. (Votre répertoire ~/homeUSMB/ est stocké sur une partition réseau de type CIFS, qui pose des problèmes de synchronisation pour SFS.)

  2. Compilez le code fourni et lancez le shell SFS :

    $ rlwrap ./sfs_shell
  3. Créez un système de fichiers dans le fichier TEST.sfs

    (NULL)% mkfs TEST.sfs
  4. Regarder le contenu de l'inode 2 (la racine)

    (TEST.sfs)% show_inode 2

    et regarder le contenu du premier bloc de données associé :

    (TEST.sfs)% dump_hexa <N>
    (TEST.sfs)% dump_entries <N>

    Dans les commandes précédentes, <N> doit être le numéro 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).

  5. 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.

  6. Ajoutez cet inode dans le repertoire / avec la commande

    (TEST.sfs)% add_entry 2 <NAME> <N>

    N'oubliez pas de choisir un nom pour votre fichier (deuxième argument) et d'utiliser le numéro d'inode approprié (troisième argument).

  7. Vérifiez que le fichier apparait bien dans les entrées du répertoire racine.

  8. É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...

  9. 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 -lh et ls -sh

    • vous pouvez visualisez sont contenu avec cat <FILENAME>

L'appel système statvfs permet d'obtenir des informations sur un système de fichiers :

La fonction sfs_statvfs du fichier sfs_posix.c contient des appels aux fonction

Écrivez ces fonctions (fichier sfs_low_level.c).

Remarques: vous aurez probablement besoin des fonctions et données suivantes :

Vous pouvez vérifiez que ces fonctions marchent avec la commande info dans le shell SFS, ou bien avec df et df -i si vous avez monté un volume SFS avec fuse. (La commande df utilise l'appel système statvfs...)

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 fichiers.

Les fonctionnalités à ajouter sont données par ordre croissant de difficulté mais sont indépendantes les unes des autres.

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 symboliques 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 doit contenir un chemin d'accès cible.

Pour ajoutez la gestion des liens symboliques dans SFS, il faut compléter les deux fonctions (dans le fichier sfs_low_level.c)

Dans le code fournit, ces deux fonctions récupèrent l'inode demandé et renvoient une erreur s'il ne s'agit pas d'un lien symbolique. Elle ne stockent, ni ne renvoie la cible du lien symbolique.

  1. Dans un premier temps, stockez la cible du lien symbolique directement dans le tableau nd->blocks de l'inode en utilisant strncpy. Vous pourrez stocker la taille de la chaine cible dans le champs size de l'inode.

  2. La stratégie précédente ne fonctionne que si la cible est suffisamment petite pour tenir sur les quelques dizaines d'octets du tableau nd->nb_blocks. Si la cible est trop grande, utilisez la fonction sfs_write_to_block pour stocker la cible dans un nouveau bloc de données associé à l'inode. Vous pourrez également stocker la taille de la chaine cible dans le champs size de l'inode.

Taille des modifications (total) : environ une dizaine de lignes très similaires pour chacune des fonctions.

Pour tester, vous pouvez utiliser test-fuse.sh et créer des liens symboliques avec ln -s :

$ ln -s <TARGET> <LINK>

La commande ls -l doit afficher la cible des liens symbolique...

Vous pourrez également faire la question 4 pour comparer avec ce qui est fait sur votre système de fichiers favori...

2.3.3. Blocs à accès indirect

Actuellement, les blocs de données associés à un inode dans SFS sont tous à accès direct. Le tableau blocks d'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.

  1. La fonction sfs_read (sfs_posix.c) est programmée à partir de la fonction sfs_read_from_inode (sfs_low_level.c), elle même programmée à partir de sfs_read_from_block (sfs_low_level.c), qui utilise

    • sfs_get_data_block_nb_from_inode pour chercher les numéros de blocs à lire.

  2. La fonction sfs_write (sfs_posix.c) est programmée à partir de la fonction sfs_write_to_inode (sfs_low_level.c), elle même programmée à partir de :

    • sfs_write_to_block (sfs_low_level.c).

Taille des modifications :

N'oubliez pas de documenter (dans le fichier RAPPORT) vos tests pour mettre en ouvre cette gestion des blocs indirects.

Vous pouvez, si vous le souhaitez, modifier la fonction do_info du fichier sfs_shell.c pour mettre à jours la taille maximale autorisée des fichiers.

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 sfs_truncate_inode, qui accède aux blocs à travers les fonctions

Pour l'appel système write, il faudra modifier la fonction

C'est notamment cette fonction qui, lors de l'écriture dans un blocs de zéros, le remplacera par un bloc standard, cherché avec la fonction sfs_find_free_block.

Il faut également modifier la fonction

qui permet de calculer la taille occupée par les données d'un inode.

Taille des modifications :

N'oubliez pas de documenter vos modifications dans le fichier RAPPORT...

Pour faciliter la correction, copiez la version de sfs_write_to_block dans votre fichier RAPPORT (pour la question 10) avant de la modifier pour cette question !

2.4. Pour aller plus loin

  1. écrire une fonction badblocks qui recherche les blocs défectueux et les associe à l'inode BADBLOCKS_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),

  2. gérer les blocs avec deux indirections dans les inodes, pour augmenter la taille maximale des fichiers,

  3. écrire une fonction checkfs qui vérifie la cohérence du volume SFS,

  4. gérer une date de modification.