Table des matières Suivant

1  Quelques commandes Unix

Il est proposé de réimplanter quelques commandes Unix courantes. Ces quelques exercices illustrent l’utilisation des primitives POSIX relatives au système de fichiers : autorisations d’accès, parcours d’un répertoire, parcours d’un système de fichiers, entrées/sorties.

Vérifier les droits d’accès... et expliquer

La commande Unix test permet d’effectuer de très nombreux tests. Nous allons nous intéresser ici à la vérification des droits d’accès (lecture : option -r, écriture : option -w, exécution : option -x) à un fichier donné.

La commande test n’affiche rien mais retourne son résultat sous la forme d’une terminaison sur un succès ou un échec. Ce comportement peut par exemple être mis en valeur dans une session telle la suivante :

$ test -r /tmp
$ echo $?
0
$ test -r /tmp && echo OK
OK
$ test -w /etc/passwd
$ echo $?
1
$ test -w /etc/passwd && echo OK
%

Une autre approche possible est de configurer votre shell afin qu’il affiche systématiquement le retour de la dernière commande exécutée. Si vous utilisez bash, vous trouverez dans le dépôt git du cours un modèle de .bashrc, contenant quelques options de configuration que vous pouvez utiliser : voir l’encart.

Dans le cadre de ces exercices, il s’agit de fournir une commande maccess1 qui supporte les options -r, -w, -x (et non toutes les nombreuses options de test), ainsi que l’option supplémentaire -v (verbose) expliquant, en cas d’échec, ce pourquoi l’accès est impossible. Vous consulterez la page de manuel de access() pour savoir quels sont les motifs d’échec possible.

Contraintes POSIX

La norme POSIX fournit un certain nombres de contraintes de taille ou longueur diverses. Ces contraintes définissent des valeurs minimales que doit garantir toute implémentation de la norme. La norme POSIX impose aussi que les valeurs effectivement garanties par l’implémentation soient accessibles aux applications.

Parmi les macro-définitions fournies par les fichiers <limits.h> et <unistd.h> on trouve

NAME_MAX
qui est la longueur maximale d’un nom d’entrée dans le système de fichiers (dont la valeur minimale requise est 14) ;
PATH_MAX
qui est la longueur maximale d’un chemin dans le système de fichiers (dont la valeur minimale requise est 255).

Code source POSIX & Makefile

Afin de spécifier au compilateur que le code source C que l’on désire compiler est conforme à la norme POSIX, on définira la macro _XOPEN_SOURCE avec une valeur supérieure ou égale à 500 (les curieux pourront consulter le contenu du fichier /usr/include/features.h).

Un Makefile comportera donc par exemple :

CC      = gcc
CFLAGS  = -Wall -Werror -ansi -pedantic
CFLAGS += -D_XOPEN_SOURCE=500
CFLAGS += -g

Dépôt git du cours

Afin de vous mettre le pied à l’étrier, vous trouverez dans le dépôt git du cours, en plus du code des exemples vus en cours, un modèle de Makefile et des éléments de configuration bash.

Vous pouvez cloner ce dépôt en tapant :

git clone http://www.fil.univ-lille1.fr/~hym/d/pds.git

À l’avenir, la commande git pull (à lancer depuis le répertoire cloné) vous permettra d’obtenir les mises à jour.


Exercice 1
 (prlimit, information de configuration)   Fournissez une commande prlimit qui affiche la valeur des constantes NAME_MAX et PATH_MAX sur votre système, voir l’encart.

Exercice 2
 (maccess)   Fournissez une commande maccess qui permette de tester les droits d’accès à un fichier (par les options -r, -w et -x) et qui propose une option -v fournissant un des motifs d’erreur possibles.

Les permissions d’accès à un fichier peuvent être vérifiées par l’utilisation de l’appel système access(). La page de manuel :

$ man 2 access

fournit tous les renseignements nécessaires sur l’appel système access.

Vous utiliserez la fonction getopt() pour analyser plus facilement les arguments. Consultez la page de manuel de cette fonction (man 3 getopt) pour avoir toutes les explications nécessaires, en particulier un exemple complet d’utilisation.

On voudra notamment pouvoir tester à la fois les droits de lecture et d’écriture, par exemple. Ce test ne devra malgré tout nécessiter qu’un seul appel système access.

Copie d’une session

Il existe plusieurs façons de garder une trace d’une session.


Exercice 3
 (Trouver toutes les erreurs)   Fournissez un exemple de session shell comportant une suite d’invocations de la commande maccess produisant toutes les erreurs possibles, c’est-à-dire toutes les causes d’échec de la fonction access(). Vous expliquerez pourquoi, s’il y a des erreurs que vous ne pouvez pas produire.

Cette session inclura aussi les commandes permettant de créer les fichiers que vous donnez en argument à maccess.

La meilleure façon de mettre en évidence ces erreurs est de proposer un script shell qui crée chacun des cas, déclenche maccess, et vérifie le code de retour. Ce script sera déclenché pour la cible test de votre Makefile.

Voir l’encart sur la manière de réaliser une copie d’une session qui vous permettra de rendre une trace du déclenchement de make test.

Retrouver une commande

La commande Unix which recherche les commandes dans les chemins de recherche de l’utilisateur.

La variable d’environnement $PATH contient une liste de répertoires séparés par des « : ». Lors de l’invocation d’une commande depuis le shell, un fichier exécutable du nom de la commande est recherchée dans l’ordre dans ces répertoires. C’est ce fichier qui est invoqué pour l’exécution de la commande. Si aucun fichier ne peut être trouvé, le shell le signale par un message.

La commande which affiche le chemin du fichier qui serait trouvé par le shell lors de l’invocation de la commande :

$ echo $PATH
/bin:/usr/local/bin:/usr/X11R6/bin:/home/alice/bin
$ which ls
/bin/ls
$ echo $?
0
$ which foo
foo: Command not found.
$ echo $?
1

Exercice 4
 (Liste des répertoires de recherche)   Dans un premier temps, proposez une fonction filldirs() qui remplit un tableau dirs contenant la liste des noms des répertoires de $PATH. Ce tableau sera terminé par un pointeur NULL.

Exercice 5
 (Une fonction which())   Écrivez maintenant une fonction which() qui affiche le chemin absolu correspondant à la commande dont le nom est passé en paramètre ou un message d’erreur si la commande ne peut être trouvée dans les répertoires de recherche de $PATH. Cette fonction retourne une valeur booléenne indiquant un succès ou un échec de la recherche.

On pourra utiliser l’appel système

#include <unistd.h>
int access(const char *path, int mode);

qui vérifie les permissions fournies sous la forme d’un « ou » des valeurs R_OK (read, lecture), W_OK (write, écriture), X_OK (execution, exécution), F_OK (existence).


Exercice 6
 (La commande which)  

Terminez par écrire votre propre version de la commande which qui se termine par un succès si et seulement si toutes les commandes données en paramètre ont été trouvées dans les répertoires de recherche désignés par $PATH.

Afficher le répertoire courant

La commande pwd (print working directory) affiche le chemin absolu du répertoire de travail courant. Nous allons proposer une implantation de cette commande qui exploite la structuration des répertoires et les liens . et .. pour construire ce chemin absolu.

Pour un processus donné, le système ne conserve que le numéro d’inœud courant, à partir duquel il lui est possible d’accéder au répertoire courant. La navigation à partir de ce répertoire est utilisé pour tous les chemins relatifs, en particulier ceux vers des répertoires parents, tels .. ou ../.., etc.

Le système conserve aussi l’inœud du répertoire racine / qui lui permet d’accéder à tous les chemins absolus.

Il s’agit maintenant de proposer une implantation de la commande pwd ne reposant pas sur la bibliothèque C. Le principe en est le suivant :

Informations sur une entrée

Deux valeurs peuvent être utilisées pour rechercher des informations sur une entrée dans un système de fichiers :

Reportez-vous au manuel pour les détails des champs que ces structures contiennent et des macros définies pour tester les valeurs de ces champs (en particulier les macros testant le type d’un fichier (ordinaire, répertoire, etc.)).


Exercice 7
 (Racine du système de fichiers)   Donnez le code d’une fonction qui détermine si un chemin donné en paramètre correspond à la racine du système de fichiers.

Exercice 8
 (Mon nom dans le répertoire parent)   Fournissez une fonction print_name_in_parent() qui affiche sur la sortie standard le nom du lien d’un nœud donné dans son répertoire père.

Exercice 9
 (Construction du chemin absolu)   L’affichage du chemin absolu d’un répertoire pouvant être produit par l’affichage du chemin absolu du répertoire père suivi du nom du répertoire courant dans le répertoire père, proposez une définition de la fonction récursive print_node_dirname() qui affiche le chemin absolu du répertoire passé en paramètre.

Exercice 10
 (Fonction pwd())   Terminez par une fonction pwd() qui affiche le chemin absolu du répertoire courant.

Parcours d’une hiérarchie

La commande du (disk usage) rapporte la taille disque utilisée par un répertoire et l’ensemble de ses fichiers (y compris ses sous-répertoires).

Deux tailles peuvent être prises en compte pour un fichier :

Les champs st_size et st_blocks d’une structure de type struct stat retournée par l’appel système stat() contiennent respectivement la taille apparente et la taille réelle d’un fichier ; vous trouverez plus de détails dans le manuel de stat.

Comme pour toutes les commandes réalisant un parcours d’une hiérarchie, il faut préciser le traitement réalisé sur les liens symboliques rencontrés : doivent-ils être suivis ou non ?

La commande du comporte donc deux options

-L
indiquant de suivre les liens symboliques ; ce qui n’est pas le cas par défaut ;
-b
indiquant de rapporter les tailles apparentes ; ce sont les tailles réelles qui sont rapportées sinon.

Une possible implantation peut définir deux variables globales pour mémoriser l’état de ces options :

static int opt_follow_links = 0;
static int opt_apparent_size = 0;

Dans un premier temps on suppose que l’option opt_follow_links est définie à faux.

Validation de votre du

Votre du devra être validé rigoureusement par une série de tests. Vous devrez ainsi créer des fichiers et des répertoires, de tailles différentes, avec ou sans liens symboliques, etc. et vérifier que votre du donne les mêmes résultats que la commande standard.

Pour vous comparer au du standard dans les salles du FIL, vous utiliserez, suivant les cas de tests, les options :

Ainsi, en notant mdu votre du :

Référez-vous au manuel pour en savoir plus sur du.


Exercice 11
 (Taille d’un fichier)   Proposez une implantation d’une fonction récursive
int du_file(const char *pathname);
qui retourne la taille occupée par le fichier désigné et ses éventuels sous-répertoires.

Comme dans un premier temps on ne suit pas les liens symboliques, la taille d’un lien symbolique est la taille occupée par le lien, et non par le fichier visé.

Votre implantation devra faire en particulier attention à filtrer les entrées des répertoires : il faut ignorer . et .. afin d’éviter une boucle infinie.


Exercice 12
 (Suivre les liens symboliques)   Modifiez l’implantation précédente pour suivre les liens symboliques.

Exercice 13
 (Comptage multiple ?)   Un nœud qui serait référencé plusieurs fois dans une hiérarchie dont on veut afficher la taille serait comptabilisé plusieurs fois. En quoi est-ce gênant ? Comment s’en affranchir ?

Afficher un fichier à l’envers

L’objectif de cette section est de définir une commande retourne permettant d’afficher le contenu d’un fichier à l’envers. Ainsi si un fichier fic contient exactement trois octets « abc », retourne fic affichera « cba ».


Exercice 14
 (Version simpliste)   Proposez une implantation de retourne utilisant lseek (ou pread) pour aller lire le fichier source octet par octet en commençant par le dernier.

Exercice 15
 (Version gourmande)   Proposez une implantation de retourne qui charge le fichier source complètement en mémoire avant de l’afficher.
Listez les avantages et inconvénients de cette approche.

Une autre approche est de n’avoir à chaque instant qu’une partie du fichier en mémoire, une partie qui tient dans un tampon mémoire de taille fixée TAILLE_TAMPON.


Exercice 16
 (Version économe)   Proposez une implantation de retourne qui:

Pensez au cas où la taille du fichier n’est pas un multiple de TAILLE_TAMPON: que faut-il faire?
En quoi cet algorithme est-il plus économe?

Afficher la fin d’un fichier

La commande Unix tail affiche les dernières lignes des fichiers désignés par les paramètres de la ligne de commande. L’option -n N (où N est un entier) indique d’afficher les N dernières lignes. Par défaut, les 10 dernières lignes sont produites.


Exercice 17
 (Version simpliste de tail)   Une version simpliste de la commande détermine le nombre de lignes du fichier puis parcourt le fichier pour débuter l’affichage n lignes avant la fin.

Malgré l’inconvénient majeur de cette approche, proposez une telle implantation.

Une solution plus efficace consiste à lire le fichier en partant de la fin. On commence en lisant les TAILLE_TAMPON derniers octets du fichier, c’est-à-dire les TAILLE_TAMPON octets à la position (par rapport au début du fichier) longueur_fichier - TAILLE_TAMPON, on y compte le nombre de retours à la ligne. S’il est supérieur à n, on peut afficher les n dernières lignes. Sinon, on lit récursivement le tampon précédent, c’est-à-dire les TAILLE_TAMPON octets à partir de la position longueur_fichier - 2*TAILLE_TAMPON, on y compte les retours à ligne. Si n n’est toujours pas atteint, on continue à lire récursivement le tampon précédent, c’est-à-dire les TAILLE_TAMPON octets à partir de la position longueur_fichier - 3*TAILLE_TAMPON, etc.


Exercice 18
 (Version efficace de tail)   Écrivez une fonction
tail_efficace(const char *path, int ntail);
qui affiche les ntail dernières lignes du fichier désigné.

Vous définirez les fonctions intermédiaires qui vous sembleront pertinentes (en particulier la fonction récursive esquissée ci-dessus).


Exercice 19
 (Version utile de tail)   Les versions précédentes ne fonctionnent pas si le fichier ne supporte pas les appels à lseek. Citez des cas où cela arrive. Proposez une solution (que vous n’implanterez pas) pour remédier à ce problème.

Validation de votre tail

Votre tail devra être validé rigoureusement par une série de tests et en comparant vos résultats avec ceux du tail standard. Vous penserez à tester en particulier :

Si votre tail ne supporte pas tous les types de fichiers, vous penserez à montrer malgré tout ce cas par un test.

Référez-vous au manuel pour en savoir plus sur tail.


Table des matières Suivant