Dans ce TD, nous allons utiliser les fonctions open, close, read, write, et lseek pour manipuler les fichiers
directement en utilisant les appels système. Il ne faut donc pas utiliser les fonctions plus haut niveau telles que
fopen, fgetc, fputc, printf, etc.
Si nécessaire, vous pouvez vous référer aux transparents du second cours.
-
Cherchez à l'aide de la commande
manles bibliothèques à inclure pour utiliser les appels systèmeopen,close,read,writeetlseek.Remarque : Pour indiquer que l'on veut la documentation des appels système, il faut utiliser l'option
2dansman(par exemple «man 2 read»). -
Écrivez le programme
readfile.cqui ouvre le fichiermessage.txtdu répertoire courant et affiche son contenu à l'écran.Indication : Pour détecter que l'on arrive à la fin du fichier il faut regarder le résultat renvoyé par
read, qui correspond au nombre de caractères lus.Rappel : les descripteurs de fichier 0, 1 et 2 correspondent respectivement à l'entrée standard, la sortie standard et la sortie d'erreur. Vous pouvez donc par exemple utiliser la fonction
writeen lui passant le descripteur 1 pour écrire du texte à l'écran. -
Écrivez le programme
copy.cqui copie le contenu du fichiermessage.txtdans un fichiercopie.txt.Indication : Il faut ouvrir le premier fichier en lecture et le second en écriture (en le créant si nécessaire) et copier le contenu du premier dans le second.
Attention : Si le fichier de destination n'existe pas, il faut le créer en lui donnant des permissions. Ces permissions sont données à la fonction
openen quatrième argument. Vous pouvez utiliser la notation en octal comme pour la fonctionchmod(par exemple 0644) ou utiliser des flags, dont vous trouverez le détail dans la page de manuel deopen.
On veut maintenant écrire une fonction int lireligne(int fd, char *s, int size) pour lire une ligne d'un
fichier :
- la fonction lit des caractères dans le fichier correspondant au descripteur
fdjusqu'à lire un retour à la ligne (\n) ou avoir lusizecaractères ; - elle place les octets lus dans le tableau
spassé en argument (on suppose que le tableauspeut contenirsizecaractères) ; - le résultat renvoyé par la fonction est le nombre de caractères effectivement lus dans le fichier.
-
En utilisant les appels système, écrivez la fonction
lirelignedans le fichierlireligne.cen lui faisant lire les caractères un par un afin de pouvoir s'arrêter facilement si elle rencontre un retour à la ligne.Indication : Pour lire les caractères un par un, vous pouvez déclarer un tableau d'un caractère
char c[1]ou bien déclarer un caractèrechar cet passer l'adresse du caractère&cà la fonctionread. -
Vérifiez le bon fonctionnement de
lireligneen lui faisant lire des lignes du fichiermessage.txt. Essayez de lire à la fois des lignes plus courtes et des lignes plus longues que la valeur indiquée.
Chaque appel à l'une des commandes open, close, read, write et lseek provoque un appel système, qui est
très coûteux en nombre d'opérations effectuées puisqu'il faut sauvegarder l'état du processus, donner le contrôle au
système pour effectuer l'action puis recharger l'état du processus pour reprendre le fonctionnement.
Il est donc préférable de réduire autant que possible le nombre d'appels systèmes effectués. En particulier, il est
très inefficace de lire le contenu d'un fichier en faisant un appel à read pour chaque caractère. Pour lire un
fichier, on créé donc un tableau pouvant contenir des caractères (un tampon ou buffer) et on lit les octets en
remplissant le tampon à chaque lecture (tant qu'on n'est pas au bout du fichier).
Si la taille du tampon est trop petite, on va effectuer beaucoup d'appels système. Si elle est trop grande, on va utiliser de la mémoire inutilement. La taille optimale est difficile à déterminer. Les documentations Unix conseillent d'utiliser un tampon de 4096 octets.
Dans le cas de la fonction lireligne, on peut utiliser le tableau passé en argument comme tampon, et donc lire
size caractères à la fois.
Si les caractères lus contiennent un retour à la ligne, la fonction renvoie le nombre de caractères lus jusqu'au
retour à la ligne. Le problème est que pour remplir le tampon, la fonction read a avancé le curseur de lecture
dans le fichier. Si l'on trouve un retour à la ligne dans les octets lus, il faut donc replacer le curseur juste
après ce retour à la ligne pour ne pas perturber les lectures suivantes dans le fichier (qui reprendront donc au
début de la ligne suivante). On utilise pour cela la fonction lseek.
-
Modifiez la fonction
lirelignepour qu'elle lise les caractères du fichier par blocs desizecaractères à la fois.Indication : La fonction doit donc utiliser
readpour remplir le tableaus, puis parcourir le tableau afin de localiser le premier retour à la ligne. Quand un retour à la ligne est trouvé, il faut remettre le curseur de lecture du fichier au bon emplacement. La fonction doit également renvoyer le nombre d'octets lus jusqu'au retour à la ligne.
Jusqu'ici, tous les programmes que nous avons écrits avaient une fonction main ne prenant aucun argument :
int main(). Il est toutefois possible de lui passer deux arguments, un entier et un tableau de chaînes de
caractères : int main(int argc, char **argv)1. Dans ce cas, si l'exécutable est
appelé depuis un terminal avec des arguments, l'entier argc correspond au nombre d'arguments, et le tableau argv
contient tous les arguments sous forme de chaînes de caractères.
Le nom de l'exécutable qui a été appelé est toujours considéré comme le premier argument (donc il y a toujours au
moins un argument). Si par exemple on a créé un exécutable prog et qu'on l'exécute avec la commande
$ ./prog -l totoalors argc vaut 3 et le tableau argv contient les chaînes de caractères « prog », « -l » et « toto » aux
indices 0, 1 et 2 respectivement.
-
Modifiez les programmes
readfile.cetcopy.cpour qu'ils prennent en paramètres les noms des fichiers sur lesquels ils travaillent (le premier attend un nom de fichier, le second en attend deux). Faites en sorte que le programme s'interrompe en affichant un message d'erreur si le nombre d'arguments reçus ne correspond pas au nombre attendu.Indication : Pour interrompre l'exécution d'un programme, vous pouvez utiliser la commande «
exit(1);» de la bibliothèque<stdlib.h>.
Lorsqu'une erreur se produit pendant l'exécution d'un appel système, la fonction appelée renvoie en général le
résultat -1. Pour connaître la nature de l'erreur qui s'est produite, il faut regarder la valeur de la variable
errno.
- Regardez rapidement la page de documentation correspondant à
errno. Quelle bibliothèque faut-il ajouter à votre programme si vous voulez utiliser cette variable ?
Lorsqu'une erreur se produit, la fonction qui a généré l'erreur donne une valeur à la variable errno puis
renvoie un code d'erreur (dans le cas des appels systèmes c'est -1). La documentation des fonctions utilisant
ce mécanisme donne en général la liste des erreurs pouvant se produire.
- Regardez la documentation de l'appel système
openet cherchez la section donnant la liste des erreurs.
Chaque erreur correspond à un nom, qui est en réalité un mot clé associé à un entier (les valeurs associées à chaque mot-clé sont définies dans les bibliothèques mais il n'est pas nécessaire de les connaître puisqu'on doit justement utiliser les mots-clés).
On trouve par exemple dans la documentation de open (éventuellement en français sur votre machine) :
ENOENT O_CREAT is not set and the named file does not
exist. Or, a directory component in pathname does
not exist or is a dangling symbolic link.
qui indique que lorsqu'on essaie d'ouvrir un fichier qui n'existe pas sans donner l'option O_CREAT (qui force la
création des fichiers inexistants), l'erreur ENOENT est placée dans la variable errno et la fonction open
renvoie -1.
Pour connaître l'erreur qui s'est produite, on peut alors tester l'égalité de errno et de chacune des erreurs
possibles : « if (errno == ENOENT) ».
-
Modifiez le programme
readfile.cpour qu'il affiche un message d'erreur spécifique lorsque le fichier à ouvrir n'existe pas.Indication : Il faut tester si le résultat de
openest -1, et si c'est le cas, regarder sierrnoest égal àENOENT.
Cette façon de faire est cependant très pénible à réaliser (dans la question précédente, on ne tient compte que d'une
seule erreur possible, mais il faudrait toutes les faire). Fort heureusement, il existe une fonction permettant de
décrire automatiquement l'erreur qui s'est produite : perror.
La fonction perror prend un argument qui est une chaîne de caractères et affiche cette chaîne de caractères suivie
d'une description en langage courant de l'erreur qui se trouve dans la variable errno. Ainsi, si l'on appelle
« perror(argv[1]); », le programme affichera
$ ./a.out toto.txt
toto.txt: No such file or directorysi le fichier toto.txt n'existe pas.
-
Modifiez à nouveau le programme
readfile.cpour qu'en cas d'échec d'ouverture du fichier il affiche une description de l'erreur produite et s'interrompe. -
Modifiez tous les programmes écrits dans ce TD pour gérer les éventuelles erreurs après les appels systèmes.
De manière générale, il est très important de toujours vérifier qu'il n'y a pas eu d'erreur après un appel à open.
Pour les autres il est toujours conseillé de surveiller les erreurs éventuelles mais elles se produisent moins
fréquemment.
La fonction stat correspond à un appel système permettant d'obtenir des informations concernant l'inode d'un
fichier dont le nom est passé en paramètre. Son prototype est :
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
Les informations sur le fichier sont placées dans la structure buf passée en argument. Les champs de la structure
sont décrits dans la page de manuel.
Par exemple, pour obtenir le numéro du propriétaire du fichier blop.txt, on peut utiliser les instructions
suivantes :
struct stat buf;
uid_t proprietaire;
if (stat("blop.txt", &buf) == 0) {
proprietaire = buf.st_uid;
} else {
perror("blop.txt");
}-
Écrivez le programme
bigfich.cqui prend en argument une liste de noms de fichiers et affiche le nom du plus grand d'entre eux.Le nombre de noms de fichiers passé en argument doit être au moins 1 mais peut être arbitrairement grand. Affichez un message d'erreur si aucun fichier n'est passé en argument, ainsi qu'un message d'erreur pour chaque nom de fichier pour lequel on ne peut pas obtenir d'informations à l'aide de la fonction
stat.
Le but de cet exercice est de réécrire la fonction getc (que nous nommerons mygetc) de deux façons différentes
afin de comparer les performances des deux versions lorsque l'on effectue un grand nombre d'appels à mygetc.
La spécification de mygetc est la suivante : int mygetc(int fd) retourne le prochain caractère du fichier désigné
par le descripteur fd sous forme d'entier (c'est à dire retourne son code ASCII), ou retourne -1 en cas de fin du
fichier ou de problème à la lecture.
- Écrivez la fonction
mygetcdu fichiermygetc.cde manière naïve en utilisant l'instructionreadpour lire un caractère.
On veut maintenant réécrire cette fonction pour qu'elle fasse moins d'appels à read si on l'utilise plusieurs fois
de suite.
-
Définissez une constante
BUF_SIZE(valant par exemple 5), puis écrivez la fonctionmygetc2ayant la même spécification quemygetcde telle sorte qu'elle n'appelle la fonctionreadqu'une fois si on appelleBUF_SIZEfois de suite la fonctionmygetc2.Indications :
- La première fois, l'appel à
readdoit lireBUF_SIZEoctets et les placer dans un tableau qui ne doit pas être perdu entre deux appels àmygetc2. Vous pouvez utiliser le mot-cléstaticlors de la déclaration d'une variable locale dans une fonction. Cherchez la documentation en ligne. - Il faut également utiliser des variables permettant de savoir quels caractères de ce buffer ont déjà été lus,
afin de pouvoir adopter la stratégie suivante :
- si il n'y a plus de nouveau caractère à lire dans le buffer, refaire un
readpour le remplir ; - sinon, retourner simplement le prochain caractère de votre buffer sans faire appel à
read.
- si il n'y a plus de nouveau caractère à lire dans le buffer, refaire un
- La première fois, l'appel à
-
Comparez l'efficacité de
mygetcetmygetc2de la façon suivante :- Créez un fichier texte de plusieurs Mo.
- Dans la fonction
main, ouvrez ce fichier, et lisez-le caractère par caractère en utilisantmygetc. - Chronométrez le temps d'exécution, puis recommencez en remplaçant
mygetcparmygetc2. Faites varier la valeur de la constanteBUF_SIZEen utilisant les valeurs 5, 10 puis 1024. Que constatez vous ?
Notes :
1. On peut également utiliser les déclarations int main(int argc, char argv[][]) ou même
int main(int argc, char *argv[]) qui sont équivalentes. ↩