INF583 - Manipulation du système de fichiers

Retour au cours.

0. Présentation du sujet

Ce premier sujet a pour triple ambition :

Nous étudierons pour cela les manipulations du système de fichiers au niveau de la bibliothèque standard du langage C et au niveau de l'interface POSIX du système d'exploitation.

Attention, les exercices ci-dessous proposent de réaliser une commande de copie de fichiers nommée mon_cp. Il ne faut surtout pas tester cette commande sur les fichiers de votre compte. Il est indispensable de créer des fichiers temporaires qu'on remplira de données aléatoires.

1. Premier exercice : Ma commande cp

Un système d'exploitation fournit aux utilisateurs un certain nombre de commandes permettant d'effectuer des opérations de base. La commande utilisateur cp (1) permet, par exemple, de copier un ou plusieurs fichiers. Lors de ce premier exercice, nous allons écrire notre propre commande cp en langage C.

Manipulation de fichiers avec la libC

Afin d'implémenter cette commande, nous allons utiliser les fonctionnalités proposées par la bibliothèque standard du C (la libC). Cette dernière propose notamment les fonctions suivantes pour accéder aux fichiers : fopen, fread, fwrite, fclose (3). Ces fonctions ne manipulent pas directement les fichiers, mais une représentation abstraite de ceux-ci, des flux.

Le fichier stdio.h doit être inclus afin de disposer de ces fonctions.

Il est important de noter que l'utilisateur doit passer par ces flux pour lire et écrire dans le fichier alors même que la structure de ces flux lui est totalement inconnue. En effet, l'implémentation des flux est à la charge des programmeurs de la libC utilisée et ils ne doivent être manipulés que par les fonctions ci-dessus depuis un programme utilisateur. Ces fonctions représentent l'interface d'utilisation de la bibliothèque. L'utilisation d'interfaces permet de compartimenter les programmes, ce qui les rend plus faciles à débugger et plus modulaires, puisque cela ouvre la possibilité de remplacer une implémentation par une autre sans avoir à modifier tout un programme.

Les flux et les fonctions associées font partie de la bibliothèque standard associée au langage C. Partout où ce langage est utilisable, une implémentation est fournie et fonctionne à l'identique (pour les parties spécifiées par le standard, et sauf bug). Le code utilisant ces fonctions est donc très portable. Historiquement, il existe une autre raison à l'utilisation courante des flux: la "bufferisation", i.e., le stockage temporaire en mémoire de la partie "courante" du fichier, afin d'éviter d'accéder au disque à chaque fois qu'elle est modifiée ou relue, améliorant grandement les performances dans ces cas. Tous les systèmes d'exploitation modernes réalisent eux-mêmes une telle bufferisation, ce qui rend celle-ci inutile de ce point de vue, sauf dans des cas très particuliers. Par contre, la bufferisation présente toujours l'intérêt de diminuer le nombre d'appels systèmes pour transférer la même quantité de données. Les écritures dans un flux peuvent donc être retardée aussi longtemps que le flux n'a pas été fermé. Seules les fonctions fclose, qui ferme le flux, ou fflush permettent d'assurer que les données ont bien été transmises au système d'exploitation. Cela ne signifie pas pour autant que celles-ci seront immédiatement écritres sur le disque.

Ecrivez votre propre commande cp. Utilisez pour cela les fonctions ci-dessus et basez-vous sur le squelette extrait de cette archive. Traitez seulement le cas de la copie d'un fichier régulier ("normal") et ne gérez pas les possibles erreurs.
Pour compiler votre programme, utilisez la commande make avec le fichier Makefile du squelette. Ce fichier sera détaillé lors de la séance.
La correction est disponible en suivant ce lien.

2. Deuxième exercice : Ma bibliothèque de manipulation de fichiers

Dans le premier exercice, nous avons utilisé la libC pour accéder aux fichiers. Mais la libC elle même se base sur les fonctions fournies par le système d'exploitation pour accéder aux fichiers, puisque c'est ce dernier qui, en définitive, réalise les accès disques. Tous les systèmes d'exploitation fournissent ainsi une interface de programmation pour être utilisable. Les systèmes d'exploitation de type UNIX, comme Linux, MacOS X ou *BSD, respectent généralement la norme POSIX, qui définit la liste des fonctions qui doivent être mises à disposition des programmes utilisateurs.

Manipulation de fichiers avec l'interface POSIX

Concernant la manipulation de fichiers, les fonctions qui nous intéressent sont open, read, write et close (2). Elles se distinguent des méthodes de la libC car elles ne manipulent pas des flux, mais des descripteurs de fichiers. Un descripteur de fichier est un entier qui sert à référencer un fichier ouvert par un processus. Il s'obtient par l'utilisation de open pour les fichiers réguliers. Il est valable dès l'ouverture du fichier et jusqu'à sa fermeture. Contrairement aux fonctions de la libC, les accès aux fichiers sont immédiatement pris en compte par le système d'exploitation. Ils seront donc notamment immédiatement visibles depuis d'autres processus.

Compilation d'une bibliothèque

Le but d'une bibliothèque est de permettre à différent programmes de réutiliser les mêmes fonctions. Outre l'uniformité que les bibliothèques apportent, elles permettent généralement d'économiser de la mémoire en ne chargeant qu'une fois le code des différentes fonctions.

Une bibliothèque est constituée de deux parties :

  1. Un fichier .h qui contient le prototype des fonctions disponibles dans la bibliothèque. Ce fichier doit être inclus par le code du programme voulant utiliser cette bibliothèque.
  2. Un fichier .so ou .a qui contient le code compilé de cette bibliothèque.
    • S'il s'agit d'un fichier .a, alors le code de la bibliothèque sera inclus dans le programme lors de l'édition de liens. On parle alors de bibliothèque statique. Une bibliothèque statique est obtenue en archivant des fichiers .o dans un fichier .a avec la commande ar. On peut détailler la compilation d'une bibliothèque de la façon suivante :
      1. Compilation des fichiers .c en fichiers .o : $(CC) $(CFLAGS) file_manip.c
      2. Archivages des fichiers .o dans une bibliothèque .a : $(CC) $(CFLAGS) file_manip.c
    • S'il s'agit d'un .so, le code de la bibliothèque sera chargé lors du démarrage du programme (en réalité, seulement lors de l'utilisation d'une de ces fonctions). On parle alors de bibliothèque dynamique. Une bibliothèque dynamique est obtenue en compilant des fichiers .c avec l'option -fPIC et en les assemblant à l'aide de gcc avec l'option -shared. On peut détailler la compilation d'une bibliothèque de la façon suivante :
      1. Compilation des fichiers .c en fichiers .o avec l'option -fPIC : $(CC) $(CFLAGS) -fPIC file_manip.c
      2. Obtention de la bibliothèque .o dans une bibliothèque .a : $(CC) -shared -o libfilemanip.so file_manip.o

Compilation d'un programme pour utiliser une bibliothèque

Pour qu'un programme puisse utiliser les fonctions d'une bibliothèque, il faut spécifier l'option -lfilemanip lors de l'édition de liens (cette option impose un nom précis pour la bibliothèque : ici cette dernière doit s'appeler libfilemanip.a ou libfilemanip.so). Il faut également donner, si nécessaire, l'emplacement de cette bibliothèque grâce à l'option: -Lchemin. Enfin, il faut spécifier si on souhaite utiliser la version statique (-static) ou dynamique (-dynamic). Tout ceci peut se résumer par les 2 lignes suivantes :

  1. Compilation statique : $(CC) mon_cp.o -lfilemanip -L/chemin -static -o mon_cp_static
  2. Compilation dynamique  $(CC) mon_cp.o -lfilemanip -L/chemin. -dynamic -o mon_cp_shared

Éxecution d'un tel programme

Lors de l'exécution d'un programme créé en version dynamique (utilisation de bibliothèques partagées), l'éditeur de liens dynamique est lancé et recherche les bibliothèques nécessaires dans les répertoires habituels d'un système UNIX (/lib, /usr/lib et /usr/local/lib). La bibliothèque que vous avez créée n'étant pas installée dans un de ces répertoires, il vous faudra spécifier où elle se trouve, soit lors de la compilation du programme, en utilisant l'option -rpath, soit lors de l'exécution en utilisant la variable d'environnement LD_LIBRARY_PATH (consulter la page de manuel de ld.so).

Nous vous proposons dans cet exercice de remplacer les appels à la libC de votre commande mon_cp par des appels à votre propre bibliothèque de manipulation de fichiers. Cette bibliothèque à l'image de la libC utilisera les primtives POSIX proposées par le système d'exploitation.

  1. Ecriture de la bibliothèque : récupérez l'archive contenant le squelette de bibliothèque en vous servant des fonctions open, read, write et close (2).
  2. Compilation de la bibliothèque : le Makefile du premier exercice a été modifé afin de choisir de compiler votre bibliothèque de manière statique ou dynamique (vous pouvez explorer les modifications correspondantes sur les options de compilation et d'édition de liens). (voir encadré)
  3. Test de la bibliothèque :
    1. modifiez le code source de votre programme mon_cp pour que ce dernier utilise les primitives de votre bibliothèque.
    2. testez !
La correction est disponible en suivant ce lien.

3. Troisième exercice : La gestion des erreurs

Notre commande mon_cp peut facilement échouer. Par exemple, il est possible de lui fournir un nom de fichier source incorrect ou un répertoire comme fichier de destination. Par ailleurs, d'autres types d'erreurs peuvent survenir dans le code de notre bibliothèque, notamment lors de l'allocation mémoire des buffers. Dans de tels cas, l'erreur est automatiquement détectée dans la bibliothèque par notre fonction fopen et plus précisément lors de l'appel des fonctions open ou malloc. La libC utilise la variable errno afin de spécifier l'erreur qui s'est produite.

Afin de rendre votre commande mon_cp plus facile à utiliser, affichez les erreurs éventuelles en modifiant file_manip.c et/ou mon_cp.c. Vous utiliserez pour cela le contenu de la fonction perror(3) ou la variable errno (3) et la fonction strerror (3).

Pour l'instant, nous détectons les erreurs uniquement au niveau des appels système. Mais une mauvaise utilisation de votre bibliothèque peut également entraîner des erreurs qui ne peuvent/doivent être vues par les appels systèmes.

Étendez votre bibliothèque pour que cette dernière renvoie les erreurs pouvant survenir à son niveau. Vous devez pour cela utliser errno comme le ferait la libC. Dans notre cas, il n'existe qu'un seul type d'erreur possible, il vous suffit de lire attentivement le paragraphe ERRORS des pages de manuel de fopen et fclose pour le connaitre. Vous aurez besoin d'inclure les fichiers à entête sys/types.h et errno.h.

Certaines erreurs pouvant survenir dans la bibliothèque ne doivent pas être propagées au programme principal. Par exemple, l'appel système read peut être interrompu par le système d'exploitation (signal), alors que l'interface de fread ne prévoit pas que ce cas se produise. Afin que notre bibliothèque respecte correctement l'interface, il est nécessaire de prendre en compte ce cas.

Étendez votre bibliothèque pour que cette dernière gère automatiquement le cas où les appels systèmes read et write ont été interrompus par le système d'exploitation. Ce problème a fait l'objet d'un transparent de cours.
La correction est disponible en suivant ce lien.

4. Quatrième exercice : Copie dans un répertoire

Notre commande mon_cp se contente pour l'instant de copier un simple fichier dans un nouveau fichier. Ainsi le premier paramètre de notre application est le nom du fichier à copier et le deuxième le nom du fichier après la copie. Nous voulons désormais que le deuxième paramètre puisse être soit le nom du fichier après la copie, soit le nom du répertoire dans lequel la copie doit avoir lieu (auquel cas le nom du fichier original est conservé).
Notre commande mon_cp doit donc déterminer le type du deuxième argument. L'utilisation d'un simple paramètre est exclu afin de respecter le fonctionnement de la commande cp originale et la présence ou non d'un / à la fin du deuxième argument n'est en rien significative. Nous devons donc nous servir de l'appel system stat (2).

Fonctionnement de la fonction stat

La fonction stat permet de connaitre les informations disponibles sur un fichier. Les informations sont communiquées à travers une structure de données de type stat qui doit être alloué par l'appelant. Les principaux champs de la structure sont :

struct stat {
    dev_t         st_dev;      /* Périphérique                  */
    ino_t         st_ino;      /* Numéro i-noeud                */
    mode_t        st_mode;     /* Droits                        */
    nlink_t       st_nlink;    /* Nb liens matériels            */
    uid_t         st_uid;      /* UID propriétaire              */
    gid_t         st_gid;      /* GID propriétaire              */
    dev_t         st_rdev;     /* Type périphérique             */
    off_t         st_size;     /* Taille totale en octets       */
    blksize_t     st_blksize;  /* Taille de bloc pour E/S       */
    blkcnt_t      st_blocks;   /* Nombre de blocs alloués       */
    time_t        st_atime;    /* Heure dernier accès           */
    time_t        st_mtime;    /* Heure dernière modification   */
    time_t        st_ctime;    /* Heure dernier changement état */
};
Nous n'avons besoin de nous intéresser qu'aux informations de st_mode. Pour vous en convaincre, affichez les droits d'un fichier et d'un répertoire avec la commande unix ls -l, que constatez vous ?

En vous aidant de la page de manuel de stat, modifiez votre programme pour qu'il gère de facon transparente la copie dans un répertoire et qu'il vérifie que le fichier source soit un fichier régulier.

Si le fichier destination se révèle être un lien « dur » sur le fichier source, alors notre programme mon_cp lorsqu'il ouvre le fichier destination efface notre fichier source.

Toujours à l'aide de stat, vérifiez que les fichiers sources et destinations ne sont pas un lien « dur » de l'un sur l'autre. Il vous suffit pour cela de comparer leur numéro d'inoeud et l'identifiant de leur périphérique de stockage.
La correction est disponible en suivant ce lien.

5. Cinquième exercice : Copie à partir d'un répertoire

Nous voulons désormais copier tous les fichiers d'un répertoire dans un autre répertoire. Nous décidons pour cela de modifier notre commande mon_cp en vérifiant la présence de l'option -r et le type du premier paramètre. Si ce dernier se révèle être un répertoire, il faut alors copier l'intégralité des fichiers de ce répertoire dans le répertoire désigné par le deuxième paramètre. On gère uniquement le cas où ce répertoire d'arrivée existe. Il existe pour cela les fonctions opendir, readdir, closedir de la libC.

En vous aidant de la page de manuel, modifiez votre programme pour qu'il gère de facon transparente la copie dans un répertoire. Contentez vous de ne copier que les fichiers réguliers (ignorez les sous-répertoires).
Ajoutez la gestion des sous-répertoires en vous aidant de la fonction mkdir(2)

.
La correction est disponible en suivant ce lien.

6. Sixième exercice: les fichiers non réguliers (ou: un lecteur de musique rudimentaire)

Attention, cet exercice fonctionne en salle de TD à condition de désactiver complètement le serveur de son dans le Control Center de KDE, onglet Sound and Multimedia. Sinon le périphérique /dev/dsp est réservé pour l'usage exclusif de ce serveur de son.
Si vous voulez travailler sur votre propre PC, il est indispensable que votre noyau soit compilé avec le support pour OSS, l'ancien système de son de Linux. Pour cela, la commande sudo modprobe snd-pcm-oss doit réussir sans erreur. Si cette commande ne fonctionne pas, consulter votre chargé de TD.

Comme nous venons de le voir, tous les fichiers ne sont pas des fichiers réguliers ("normaux") et certains représentent même des périphériques matériels. Nous allons étudier maintenant l'intéraction avec ces périphériques matériels. Comme pour les fichiers normaux, il est possible de les ouvrir en lecture et en écriture, mais ces 2 opérations ne servent qu'à envoyer ou à récupérer des données sur les périphériques. Il faut utiliser un autre appel système pour pouvoir les configurer.

ioctl autorise cette configuration en prenant comme premier paramètre un descripteur de fichier correspondant au périphérique qu'on souhaite manipuler. Le deuxième paramètre correspond quant à lui au type de configuration qu'on souhaite effectuer. Enfin, tous les paramètres restant servent à effectuer cette configuration.

Pour étudier ce mécanisme, nous allons nous intéresser à la configuration de votre carte son dans le but de lire des fichiers .au sans compression. Ce type de fichier est décrit en détails sur cette page.

Completez le squelette de lecteur dans l'archive ex6.tar afin d'afficher les informations contenues dans le header du fichier : nombre magique, décalage des données, taille des données, encodage, fréquence d'échantillonnage, canaux... Vous pouvez utiliser les fichiers suivants : 1 et 2. Attention, les données sont stockées en BigEndian (Grand Boutiste).

Afin de pouvoir jouer les données dans le fichier, il vous faut configurer votre carte son correctement. Le fichier représentant votre carte son se nomme /dev/dsp.

En utilisant la documentation suivante, configurez votre carte son pour qu'elle puisse jouer le fichier que vous venez d'ouvrir à la question précédente.

Musique !
La correction est disponible en suivant ce lien.