IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Programmation avancée sous Linux


précédentsommairesuivant

12. E/S de Bas Niveau

Les programmeurs C sous GNU/Linux ont deux jeux de fonctions d'entrées/sorties à leur disposition. La bibliothèque C standard fournit des fonctions d'E/S: printf, fopen, etc(La bibliothèque C++ stndard fournit les flux d'E/S (iostreams) qui proposent des fonctionnalités similaires. La bibliothèque C standard est également disponible avec le langage C++.). Le noyau Linux fournit un autre ensemble d'opérations d'E/S qui opèrent à un niveau inférieur à celui des fonctions de la bibliothèque C.

Comme ce livre est destiné à des gens connaissant déjà le langage C, nous supposerons que vous avez déjà rencontré et savez comment utiliser les fonctions d'E/S de la bibliothèque C.

Il y a souvent de bonnes raisons d'utiliser les fonctions d'E/S de bas niveau de Linux. Elles sont pour la plupart des appels systèmes au noyau(Consultez le Chapitre appelssysteme, Chapitre appelssysteme pour des explications concernant la différence entre un appel système et un appel de fonction traditionnelle.) et fournissent un accès direct aux possibilités sous-jacentes offertes par le système aux applications. En fait, les fonctions d'E/S de la bibliothèque C standard sont implantées par dessus les appels systèmes d'E/S de bas niveau de Linux. L'utilisation de cette dernière est généralement la façon la plus efficace d'effectuer des opérations d'entrée/sortie -- et est parfois également plus pratique.

Tout au long de ce livre, nous supposons que vous êtes familier avec les appels décrits dans cette annexe. Vous êtes peut être déjà familiers avec eux car ils sont très proches de ceux fournis avec d'autres système d'exploitation de type unique (ainsi qu'avec la plateforme Win32). Si vous n'êtes pas coutumier de ces appels, cependant, continuez votre lecture; le reste du livre n'en sera que plus simple à comprendre si vous commencez par prendre connaissance de ce chapitre.

12-1. Lire et Écrire des Données

La première fonction d'E/S que vous avez rencontré lorsque vous avez commencé à apprendre le langage C était certainement printf. Elle formate une chaîne de texte puis l'affiche sur la sortie standard. La version générique, fprintf, peut afficher le texte sur un autre flux que la sortie standard. Un flus est représenté par un pointeur FILE*. Vous obtenez un tel pointeur en ouvrant un fichier avec fopen. Lorsque vous en avez fini, vous pouvez le fermer avec fclose. En plus de fprintf, vous pouvez utiliser d'autres fonctions comme fputc, fputs ou fwrite pour écrire des données dans un flux, ou fscanf, fgetc, fgets ou fread pour lire des données.

Avec les opérations d'E/S de bas niveau de Linux, vous utilisez descripteur de fichier au lieu d'un pointeur FILE*. Un descripteur de fichier est un entier qui fait référence à une instance donnée d'un fichier ouvert au sein d'un processus. Il peut être ouvert en lecture, en écriture ou en lecture/écriture. Un descripteur de fichier ne fait pas forcément référence à un fichier ouvert; il peut représenter une connexion vers un composant d'un autre système qui est capable d'envoyer ou de recevoir des données. Par exemple, une connexion vers un dispositif matériel est représentée par un descripteur de fichier (voir Chapitre peripheriques, Chapitre preripheriques), tout comme l'est un socket ouvert (voir Chapitre IPC, Chapitre IPC, Section sockets, Section sockets) ou l'extrémité d'un tube (voir Section tubes, Section tubes).

Incluez les fichiers d'en-tête <fcntl.h>, <sys/types.h>, <sys/stat.h> et <unistd.h> si vous utilisez l'une des fonctions d'E/S de bas niveau décrites ici.

12-1-1. Ouvrir un Fichier

Pour ouvrir un fichier et obtenir un descripteur de fichier pouvant y accéder, utilisez l'appel open. Il prend le chemin du fichier à ouvrir sous forme d'une chaîne de caractères et des indicateurs spécifiant comment il doit l'être. Vous pouvez utiliser open pour créer un nouveau fichier; pour cela, passez un troisième argument décrivant les droits d'accès à appliquer au nouveau fichier.

Si le second argument est O_RDONLY, le fichier est ouvert en lecture seul; un erreur sera signalée si vous essayez d'y écrire. De même, O_WRONLY ouvre le descripteur de fichier en écriture seule. Passer O_RDWR crée un descripteur de fichier pouvant être utilisé à la fois en lecture et en écriture. Notez que tous les fichiers ne peuvent pas être ouverts dans les trois modes. Par exemple, les permissions d'un fichier peuvent interdire à un processus de l'ouvrir en lecture ou en écriture; un fichier sur un périphérique en lecture seule, comme un lecteur CD-ROM ne peut pas être ouvert en lecture.

Vous pouvez passer des options supplémentaires en utilisant un OU binaire de ces valeurs avec d'autres indicateurs. Voici les valeurs les plus courantes:

  • Passez O_TRUNC pour tronquer le fichier ouvert, s'il existait auparavant. Les données écrites remplaceront le contenu du fichier;
  • Passez O_APPEND pour ajouter les données au contenu d'un fichier existant. Elles sont écrites à la fin du fichier;
  • Passez O_CREAT pour créer un nouveau fichier. Si le nom de fichier que vous passez à open correspond à un fichier inexistant, un nouveau fichier sera créé, si tant est que le répertoire le contenant existe et que le processus a les permissions nécessaires pour créer des fichiers dans ce répertoire. Si le fichier existe déjà, il est ouvert;
  • Passez O_EXCL et O_CREATE pour forcer la création d'un nouveau fichier. Si le fichier existe déjà, l'appel à open échouera.

Si vous appelez open en lui passant O_CREATE, fournissez un troisième argument indiquand les permissions applicable au nouveau fichier. Consultez le Chapitre securite, Chapitre securite, Section permissionsfs, Section permissionsfs, pour une description des bits de permission et de la façon de les utiliser.

Par exemple, le programme du Listing createfile crée un nouveau fichier avec le nom de fichier indiqué sur la ligne de commande. Il utilise l'indicateur O_EXCL avec open afin de signaler une erreur si le fichier existe déjà. Le nouveau fichier dispose des autorisations en lecture/écriture pour l'utilisateur et le groupe propriétaireset est en lecture seule pour les autres utilisateurs (si votre umask est positionné à une valeur différente de zéro, les permissions effectives pourraient être plus restrictives).

Lorsque vous créez un nouveau fichier avec open, certains des bits de permissions peuvent être désacrivés. Cela survient lorsque votre umask est différent de zéro. L'umask d'un processus spécifie les bits de permissions qui sont masqués lors de n'importe quelle création de fichier. Les permissions effectives sont obtenues en appliquant un ET binaire entre les permissions que vous passez à open et le complément à un du umask. Pour changer la valeur de votre umask à partir d'un shell, utilisez la commande umask et indiquez la valeur numérique du masque en notation octale. Pour changer le umask d'un processus en cours d'exécution, utilisez l'appel umask en lui passant la valeur du masque à utiliser pour les appels suivants. Par exemple, cette ligne: umask (S_IRWXO S_IWGRP); dans un programme ou l'invocation de cette commande: % umask 027 indiquent que les permissions en écriture pour le groupe et toutes les permissions pour les autres seront toujours masquée pour les créations de fichiers.

Create a New File create-file.c
Sélectionnez
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main (int argc, char* argv[])
{
  /* Chemin vers le nouveau fichier. */
  char* path = argv[1];
  /* Permissions du nouveau fichier. */
  mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |  S_IROTH;
  /* Crée le fichier. */
  int fd = open (path, O_WRONLY | O_EXCL | O_CREAT, mode);
  if (fd == -1) {
    /* Une erreur est survenue, affiche un message et quitte.  */
    perror ("open");
    return 1;
  }
  return 0;
}

Voici le programme en action:

 
Sélectionnez
% ./create-file testfile
% ls -l testfile
-rw-rw-r--    1 samuel   users 0 Feb 1 22:47 testfile
% ./create-file testfile
open: Le fichier existe

Notez que le fichier fait zéro octet car le programme n'y a écrit aucune donnée.

12-1-2. Fermer un fichier

Lorsque vous en avez fini avec un descripteur de fichier, fermez-le avec close. Dans certains cas, comme dans le cas du programme du Listing createfile il n'est pas nécessaire d'appeler close explicitement car Linux ferme tous les descripteurs de fichiers lorsqu'un processus se termine (c'est-à-dire à la fin du programme). Bien sûr, une fois que vous avez fermé un descripteur de fichier, vous ne pouvez plus l'utiliser.

La fermeture d'un descripteur de fichier peut déclencher des actions spécifiques de la part de Linux, selon la nature du descripteur de fichier. Par exemple, lorsque vous fermez un descripteur correspondant à un socket réseau, Linux ferme la connexion entre les deux ordinateurs communicant via le socket.

Linux limite le nombre de descripteurs de fichiers qu'un processus peut maintenir ouverts en même temps. Les descripteurs de fichiers ouverts utilisent des ressources noyau, il est donc conseillé de fermer les descripteurs de fichiers dès que vous avez terminer de les utiliser. La limite classique est de 1024 descripteurs par processus. Vous pouvez l'ajuster avec l'appel système setrlimit; consultez la Section getsetrlimit, Section getsetrlimit, pour plus d'informations.

12-1-3. Écrire des données

Écrire des données dans un fichier se fait par le biais de l'appel write. Passz lui un descripteur de fichier, un pointeur vers un tampon de données et le nombre d'octets à écrire. Les données écrites n'ont pas besoin d'être une chaine de caractères; write copie des octets quelconques depuis le tampon vers le descripteur de fichier.

Le programme du Listing timestamp écrit l'heure courante à la fin du fichier passé sur la ligne de commande. Si le fichier n'existe pas, il est créé. Ce programme utilise également les fonctions time, localtime et asctime pour obtenir et formater l'heure courante; consultez leurs pages de manuel respectives pour plus d'informations.

Ajoute l'Heure Courante à un Fichier timestamp.c
Sélectionnez
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
/* Renvoie une chaine représentant l'heure courante. */
char* get_timestamp ()
{
  time_t now = time (NULL);
  return asctime (localtime (&amp;now));
}
int main (int argc, char* argv[])
{
  /* Fichier auquel ajouter l'horodatage. */
  char* filename = argv[1];
  /* Récupère l'heure courante. */
  char* timestamp = get_timestamp ();
  /* Ouvre le fichier en écriture. S'il existe, ajoute les données
     à la fin, sinon, un nouveau fichier est créé. */
  int fd = open (filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
  /* Calcule la longueur de la chaîne d'horodatage. */
  size_t length = strlen (timestamp);
  /* L'écrit dans le fichier. */
  write (fd, timestamp, length);
  /* Fini. */
  close (fd);
  return 0;
}

Voici comment fonctionne ce programme:

 
Sélectionnez
% ./timestamp tsfile
% cat tsfile
Thu Feb 1 23:25:20 2001
% ./timestamp tsfile
% cat tsfile
Thu Feb 1 23:25:20 2001
Thu Feb 1 23:25:47 2001

Notez que la première fois que nous invoquons timestamp, il crée le fichier tsfile, alors que la seconde fois, les données sont ajoutées à la fin.

L'appel write renvoie le nombre d'octets effectivement écrits ou -1 si une erreur survient. Pour certains types de descripteurs de fichiers, le nombre d'octets écrits peut être inférieur au nombre d'octets demandés. Dans ce cas, c'est à vous d'appeler write encore une fois pour écrire le reste des données. La fonction du Listing writeall montre une façon de le faire. Notez que pour certaines applications, vous pourriez avoir à effectuer des contrôles supplémentaires avant la reprise de l'écriture. Par exemple, si vous utilisez un socket réseau, il faudrait ajouter le code permettant de détecter si la connexion a été fermée pendant l'écriture et, si c'est le cas, prendre les mesures adéquates.

Écrit tout le Contenu d'un Tampon write-all.c
Sélectionnez
/* Écrit l'intégralité des COUNT octets de BUFFER vers le descripteur
   de fichier FD. Renvoie -1 en cas d'erreur ou le nombre d'octets
   écrits. */
ssize_t write_all (int fd, const void* buffer, size_t count)
{
  size_t left_to_write = count;
  while (left_to_write > 0) {
    size_t written = write (fd, buffer, count);
    if (written == -1)
      /* Une erreur est survenue. Terminé. */
      return -1;
    else
      /* Mémorise le nombre d'octets restant à écrire. */
      left_to_write -= written;
  }
  /* Nous ne devons pas avoir écrit plus de COUNT octets ! */
  assert (left_to_write == 0);
  /* Le nombre d'octets écrits est exactement COUNT. */
  return count;
}

12-1-4. Lecture de Données

L'appel permettant de lire des données est read. Tout comme write, il prend en arguments un descripteur de fichier, un pointeur vers un tampon et un nombre d'octets. Ce dernier indique le nombre d'octets à lire à partir du descripteur. L'appel à read renvoie -1 en cas d'erreur ou le nombre d'octets effectivement lus. Il peut être inférieur au nombre d'octets demandé, par exemple, s'il ne reste pas suffisamment d'octets à lire dans le fichier.

Une fois que vous aurez lu ce livre, nous sommes convaincus que vous déciderez d'écrire tous vos programmes pour GNU/Linux. Cependant, il pourrait vous arriver d'avoir à lire des fichiers texte générés par des programmes DOS ou Windows. Il est important d'anticiper une différence majeure entre les deux plateformes dans la façon de structurer les fichiers texte. Dans les fichiers texte GNU/Linux, chaque ligne est séparée de la suivante par un caractère de nouvelle ligne. Ce dernier est représenté par la constante de caractère 'n' dont le code ASCII est 10. Sou Windows, par contre, les lignes sont séparées par une séquence de deux caractères: un retour chariot (le caractère 'r', dont le code ASCII est 13), suivi d'un caractère de nouvelle ligne. Certains éditeurs de texte GNU/Linux affichent un ^M à la fin de chaque ligne lorsqu'ils affichent le contenu d'un fichier texte Windows -- il s'agit du caractère de retour chariot. Emacs affiche les fichiers texte Windows correctement mais les signale en affichant (DOS) dans la barre de mode en bas du tampon. Certains éditeurs Windows, comme Notepad, affichent tout le texte des fichiers GNU/Linux sur une seule ligne car ils ne trouvent pas le caractère de retour chariot à la fin de chaque ligne. D'autres programmes, que ce soit sous GNU/Linux ou Windows, peuvent signaler des erreurs étranges lorsque les fichiers qui leur sont fournis en entrée ne sont pas au bon format. Si votre programme lit des fichiers texte générés par des programmes Windows, vous voudrez probablement remplacer la séquence 'rn' par un seul caractère de nouvelle ligne. De même, si votre programme écrit des fichiers texte qui doivent être lus par des programmes Windows, remplacez le caractère de nouvelle ligne par la séquence 'rn'. Vous devrez le faire, que vous utilisiez les appels d'E/S de bas niveau présentés dans cette annexe ou les fonctions de la bibliothèque C standard.

Le Listing hexdump présente un programme utilisant l'appel read. Il affiche une image hexadécimale du contenu du fichier passé sur la ligne de commande. Chaque ligne affiche le déplacement dans le fichier et les 16 octets suivants.

Affiche l'Image Hexadécimale d'un Fichier hexdump.c
Sélectionnez
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main (int argc, char* argv[])
{
  unsigned char buffer[16];
  size_t offset = 0;
  size_t bytes_read;
  int i;
  /* Ouvre le fichier en lecture. */
  int fd = open (argv[1], O_RDONLY);
  /* Lit le fichier morceau par morceau, jusqu'à ce que la lecture
     soit "trop courte", c'est-à-dire que l'on lise moins que ce que
     l'on a demandé, ce qui indique que la fin du fichier est atteinte. */
  do {
    /* Lis la "ligne" suivante. */
    bytes_read = read (fd, buffer, sizeof (buffer));
    /* Affiche le déplacement dans le fichier, suivi des octets correspondants. */
    printf ("0x x : ", offset);
    for (i = 0; i < bytes_read; ++i)
       printf (" x ", buffer[i]);
    printf ("\n");
    /* Conserve notre position dans le fichier. */
    offset += bytes_read;
  }
  while (bytes_read == sizeof (buffer));
  /* Terminé.   */
  close (fd);
  return 0;
}

Voici hexdump en action. Il affiche l'image de son propre exécutable:

 
Sélectionnez
% ./hexdump hexdump
0x000000 : 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
0x000010 : 02 00 03 00 01 00 00 00 c0 83 04 08 34 00 00 00
0x000020 : e8 23 00 00 00 00 00 00 34 00 20 00 06 00 28 00
0x000030 : 1d 00 1a 00 06 00 00 00 34 00 00 00 34 80 04 08
...

La sortie peut être différente chez vous, selon le compilateur utilisé pour construire hexdump est les options de compilation.

12-1-5. Se Déplacer dans un Fichier

Un descripteur de fichier connait sa position dans le fichier. Lorsque vous y écrivez ou que vous y lisez, sa position est modifiée selon le nombre d'octets lus ou écrits. Parfois, cependant, vous pouvez avoir besoin de vous déplacer dans un fichier sans lire ou écrire de données. Par exemple, vous pouvez vouloir écrire au milieu d'un fichier sans en modifier le début, ou vous pouvez avoir besoin de retourner au début d'un fichier et de le relire sans avoir à le réouvrir.

L'appel lseek vous permet de modifier votre position dans un fichier. Passez lui le descripteur deux fichier et deux autres arguments indiquand la nouvelle position.

  • Si le troisième argument est SEEK_SET, lseek interprète le second argument comme une position, en octets, depusi le début du fichier;
  • Si le troisième argument est SEEK_CUR, lseek interprète le second argument comme un déplacement, positif ou négatif, depuis la position courante;
  • Si le troisième argument est SEEK_END, lseek interprète le second argument comme un déplacement à partir de la fin du fichier. Une valeur positive indique une position au-delà de la fin du fichier.

L'appel à lseek renvoie la nouvelle position, sous forme d'un déplacement par rapport au début du fichier.Le type de ce déplacement est off_t. Si une erreur survient, lseek renvoie -1. Vous ne pouvez pas utiliser lseek avec certains types de descripteurs de fichiers comme les sockets.

Si vous voulez obtenir votre position dans un fichier sans la modifier, indiquez un déplacement de 0 par rapport à votre position actuelle -- par exemple:

 
Sélectionnez
off_t position = lseek (file_descriptor, 0, SEEK_CUR);

Linux autorise l'utilisation de lseek pour spécifier une position au-delà de la fin du fichier. Normalement, si un descripteur de fichier est positionné à la fin d'un fichier et que vous y écrivez, Linux augmente automatiquement la taille du fichier pour faire de la place pour les nouvelles données. Si vous indiquez une position au-delà de la fin du fichier puis que vous y écrivez, Linux commence par agrandir le fichier de la taille du "trou" que vous avez créé en appelant lseek puis écrit à la fin de celui-ci. Ce trou n'occupe toutefois pas de place sur le disque; Linux note simplement sa taille. Si vous essayez de lire le fichier, il apparait comme s'il était rempli d'octets nuls.

En utilisant cette propriété de lseek, il est possible de créer des fichier extrèmement grands qui n'occupent pratiquement aucun espace disque. Le programme lseek-huge du Listing lseekhuge le fait. Il prend en arguments de ligne de commande un nom de fichier et sa taille, en mégaoctets. Le programme ouvre un nouveau fichier, se place après la fin de celui-ci en utilisant lseek puis écrit un octet à 0 avant de fermer le fichier.

Créer de Gros Fichiers avec lseek lseek-huge.c
Sélectionnez
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main (int argc, char* argv[])
{
  int zero = 0;
  const int megabyte = 1024 * 1024;
  char* filename = argv[1];
  size_t length = (size_t) atoi (argv[2]) * megabyte;
  /* Ouvre un nouveau fichier. */
  int fd = open (filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
  /* Se place un octet avant la fin désirée du fichier. */
  lseek (fd, length - 1, SEEK_SET);
  /* Écrit un octet nul. */
  write (fd, &amp;zero, 1);
  /* Terminé. */
  close (fd);
  return 0;
}

Voici comment créer un fichier d'un gigaoctet (1024 Mo) en utilisant lseek-huge. Notez l'espace libre avant et après l'opération.

 
Sélectionnez
% df -h .
Filesystem            Tail. Occ. Disp. %Occ. Monté sur
/dev/hda5             2.9G  2.1G  655M  76% /
% ./lseek-huge grosfichier 1024
% ls -l grosfichier
-rw-r-----    1 samuel   samuel  1073741824 Feb 5 16:29 grosfichier
% df -h .
Filesystem            Tail. Occ. Disp. %Occ. Monté sur
/dev/hda5             2.9G  2.1G  655M  76% /

Aucun espace disque significatif n'est utilisé en dépit de la taille conséquente de grosfichier. Si nous ouvrons grosfichier et que nous y lisons des connées, il apparait comme étant rempli de 1 Go d'octets nuls. Par exemple, nous pouvons inspecter son contenu avec le programme hexdump du Listing hexdump.

 
Sélectionnez
% ./hexdump grosfichier | head -10
0x000000 : 00 00 00 00  00 00 00 00 00 00 00 00 00 00 00 00
0x000010 : 00 00 00 00  00 00 00 00 00 00 00 00 00 00 00 00
0x000020 : 00 00 00 00  00 00 00 00 00 00 00 00 00 00 00 00
0x000030 : 00 00 00 00  00 00 00 00 00 00 00 00 00 00 00 00
0x000040 : 00 00 00 00  00 00 00 00 00 00 00 00 00 00 00 00
0x000050 : 00 00 00 00  00 00 00 00 00 00 00 00 00 00 00 00
...

Si vous exécutez cette commande vous-même, vous devrez probabelement la tuer avec Ctrl+C plutôt de la regarder affichier 2<sup>30</sup> octets nuls.

Notez que ces trous magiques dans les fichiers sont une fonctionalité spéciale du système de fichier ext2 qui est traditionnelement utilisé pour les disques GNU/Linux. Si vous utilisez lseek-huge pour créer un fichier sur un autre type de système de fichiers, comme fat ou vfat qui sont utilisés sur certaines partitions DOS et Windows, vous constaterez que le fichier occupe effectivement autant d'espace que sa taille.

Linux n'autorise pas le positionnement avant le début du fichier avec lseek.

12-2. stat

En utilisant open et read, vous pouvez extraire le contenu d'un fichier. Mais qu'en est-il des autres informations? Par exemple, la commande ls -l affiche des informations comme la taille, la date de dernière modification, les permissions ou le propriétaire pour les fichiers du répertoire courant.

L'appel stat récupère ces informations pour un fichier. Appelez stat en lui passant le chemin du fichier sur lequel vous voulez des informations et un pointeur sur une variable de type struct stat. Si l'appel à stat se déroule correctement, il revoie 0 et renseigne les champs de la structure avec des informations sur le fichier; sinon, il renvoie -1.

Voici les champs les plus intéressant d'une struct stat:

  • st_mode contient les permissions du fichier. Ces permissions sont décrites dans la Section permissionsfs, Section permissionsfs;
  • En plus des permissions clasiques, le champ st_mode encode le type de fichier dans les bits de poids fort. Consultez les explication ci-dessous pour savoir comment le décoder;
  • st_uid et st_gid contiennent les identifiants de l'utilisateur et du groupe auxquels le fichier appartient, respectivement. Les identifiants de groupe et d'utilisateur sont détaillés dans la Section utilisateursgroupes, Section utilisateursgroupes;
  • st_size contient la taille du fichier, en octets;
  • st_atime contient la date de dernier accès au fichier (en lecture ou écriture);
  • st_mtime contient la date de dernière modification du fichier.

Un certain nombre de macros analysent la valeur du champ st_mode pour déterminer le type de fichier sur lequel stat a été invoqué. Une macro vaut vrai si le fichier est de ce type:

$_ISBLK (mode) périphérique bloc
$_ISCHR (mode) périphérique caractère
$_ISDIR (mode) répertoire
$_ISFIFO (mode) fifo (canal nommé)
$_ISLNK (mode) lien symbolique
$_ISREG (mode) fichier classique
$_ISSOCK (mode) socket

Le champ st_dev contient les numéros de périphérique majeur et mineur du dispositif matériel sur lequel le fichier se situe. Les numéros de périphériques sont traités dans le Chapitre peripheriques. Le numéro majeur de périphérique est décalé à gauche de 8 bits, tandis que le numéro mineur occupe les 8 bits de poids faible. Le champ st_ino contient le numéro d'inode du fichier. Cela situe le fichier sur le système de fichiers.

Si vous appelez stat sur un lien symbolique, stat suit le lien et vous renvoie les informations sur le fichier sur lequel il pointe, pas sur le lien symbolique. Cela signifie que S_ISLNK ne sera jamais vrai pour une valeur renvoyée par stat. Utilisez la fonction lstat si vous ne voulez pas suivre les liens symboliques; cette fonction renvoie les informations sur le lien et non pas sur le fichier cible. Si vous appelez lstat sur un fichier qui n'est pas un lien symbolique, elle a le même comportement que stat. Appeler stat sur un lien invalide (un lien qui pointe vers un fichier inexistant ou inaccessible) provoque une erreur, alors que l'appel de lstat sur le même fichier n'a aucune incidence.

Si vous disposez déjà d'un fichier ouvert en lecture/écriture, appelez fstat au lieu de stat. Il prend un descripteur de fichier en premier argument à la place du chemin vers le fichier.

Le Listing readfile présente une fonction qui alloue un tampon suffisamment long pour recevoir le contenu du fichier et charge les données à l'intérieur. Elle utilise fstat pour déterminer la taille du tampon à allouer et vérifier que le fichier est bien un fichier classique.

Charge un Fichier dans un Tampon read-file.c
Sélectionnez
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/* Charge le contenu de FILENAME dans un tampon nouvellement alloué. La
   taille du tampon est stockée dans *LENGTH. Renvoie le tampon, qui devra
   être libéré par l'appelant. Renvoie NULL si FILENAME ne correspond pas
   à un fichier régulier. */
char* read_file (const char* filename, size_t* length)
{
  int fd;
  struct stat file_info;
  char* buffer;
  /* Ouvre le fichier. */
  fd = open (filename, O_RDONLY);
  /* Récupère les informations sur le fichier. */
  fstat (fd, &amp;file_info);
  *length = file_info.st_size;
  /* S'assure que le fichier est un fichier ordinaire.  */
  if (!S_ISREG (file_info.st_mode)) {
    /* Ce n'en est pas un, abandonne. */
    close (fd);
    return NULL;
  }
  /* Alloue un tampon suffisamment grand pour recevoir le contenu du fichier. */
  buffer = (char*) malloc (*length);
  /* Charge le fichier dans le tampon. */
  read (fd, buffer, *length);
  /* Terminé. */
  close (fd);
  return buffer;
}

12-3. Écriture et Lecture Vectorielles

L'appel write prend en arguments un pointeur vers le début d'un tampon de données et la taille de ce tampon. Il écrit le contenu d'une région contigüe de mémoire vers un descripteur de fichier. Cependant, un programme a souvent besoin d'écrir plusieurs éléments de données, chacun se trouvant à un endroit différent. Pour utiliser write, un tel programme devrait soit copier tous les objets dans une région contigüe, ce qui gaspillerait des cycles CPU et de la mémoire, soit effectuer de multiples appels à write.

Pour certaines applications, appeler write plusieurs fois peut être inefficace ou peu souhaitable. Par exemple, lors de l'écriture vers un socket réseau, deux appels à write peut provoquer l'envoi de deux paquets sur le réseau, alors que les mêmes données auraient pu être envoyées en une fois si un seul appel à write avait été possible.

L'appel writev vous permet d'envoyer le contenu de plusieurs zones mémoire non-contigües vers un descripteur de fichier en une seul opération. C'est ce que l'on appelle l'écriture vectorielle. La contrepartie de l'utilisation de writev est que vous devez créer une structure de données indiquand le début et la taille de chaque région mémoire. Cette structure est un tableau d'éléments struct iovec. Chaque élément correspond à un emplacement mémoire à écrire; les champs iov_base et iov_len correspondent respectivement à l'adresse du début de la région et à sa taille. Si vous connaissez à l'avance le nombre de zones mémoire à écrire, vous pouvez vous contenter de déclarer un tableau de struct iovec; si le nombre de régions peut varier, vous devez allouer le tableau dynamiquement.

Appelez writev en lui passant le descripteur de fichier vers lequel envoyer les données, le tableau de struct iovec et le nombre d'éléments contenus dans ce tableau. La valeur de retour correspond au nombre d'octets écrits.

Le programme du Listing writeargs écrit ses arguments de ligne de commande dans un fichier en utilisant un unique appel à writev. Le premier argument est le nom du fichier dans lequel écrire les arguments qui suivent. Le programme alloue un tableau de struct iovec qui fait le double du nombre d'arguments à écrire -- pour chaque argument, le texte de l'argument proprement dit est écrit, suivit d'un caractère de nouvelle ligne. Comme nous ne savons pas à l'avance le nombre d'arguments, le tableau est créé en utilisant malloc.

Écrit la Liste d'Arguments dans un Fichier avec writev write-args.c
Sélectionnez
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
int main (int argc, char* argv[])
{
  int fd;
  struct iovec* vec;
  struct iovec* vec_next;
  int i;
  /* Nous aurons besoin d'un "tampon" contenant un caractère de nouvelle ligne.
     Nous utilisons une variable char normale pour cela. */
  char newline = '\n';
  /* Le premier argument est le nom du fichier de sortie. */
  char* filename = argv[1];
  /* Ignore les deux premiers éléments de la liste d'arguments. L'élément
     à l'indice 0 est le nom du programme et celui à l'indice 1 est
     le nom du fichier de sortie. */
  argc -= 2;
  argv += 2;

  /* Alloue un tableau d'éléments iovec. Nous aurons besoin de deux élements pour
     chaque argument, un pour le texte proprement dit et un pour
     la nouvelle ligne. */
  vec = (struct iovec*) malloc (2 * argc * sizeof (struct iovec));

  /* Boucle sur la liste d'arguments afin de construire les éléments iovec. */
  vec_next = vec;
  for (i = 0; i < argc; ++i) {
    /* Le premier élément est le texte de l'argument. */
    vec_next->iov_base = argv[i];
    vec_next->iov_len = strlen (argv[i]);
    ++vec_next;
    /* Le second élement est un caractère de nouvelle ligne. Il est possible
       de faire pointer plusieurs éléments du tableau de struct iovec vers
       la même région mémoire. */
    vec_next->iov_base = &amp;newline;
    vec_next->iov_len = 1;
    ++vec_next;
  }

  /* Écrit les arguments dans le fichier. */
  fd = open (filename, O_WRONLY | O_CREAT);
  writev (fd, vec, 2 * argc);
  close (fd);

  free (vec);
  return 0;
}

Voici un exemple d'exécution de write-args.

 
Sélectionnez
% ./write-args fichiersortie "premier arg" "deuxième arg" "troisème arg"
% cat outputfile
premier arg
deuxième arg
troisième arg

Linux propose une fonction équivalente pour la lecture, readv qui permet de charger des données dans des zones mémoire non-contigües en une seule fois. Comme pour writev, un tableau d'éléments struct iovec indique les zones mémoire dans lesquelles charger les données à partir du descripteur de fichier.

12-4. Lien avec les Functions d'E/S Standards du C

Nous avons évoqué le fait que les fonctions d'E/S standards du C sont implémentées comme une surcouche de ces fonctions d'E/S de bas niveau. Parfois, cependant, il peut être pratique d'utiliser les fonctions de la bibliothèque standard avec des descripteurs de fichiers ou les fonctions de bas niveau sur un flux FILE*. GNU/Linux autorise ces deux pratiques.

Si vous avez ouvert un fichier en utilisant fopen, vous pouvez obtenir le descripteur de fichier sous-jacent par le biais de la fonction fileno. Elle prend un paramètre FILE* et renvoie le descripteur de fichier. Par exemple, pour ouvrir un fichier avec l'appel standard fopen mais y écrire avec writev, vous pourriez utiliser une séquencence telle que:

 
Sélectionnez
FILE* flux = fopen (nomfichier, "w");
int descripteur = fileno (flux);
writev (descripteur, tableau, taille_tableau);

Notez que flux et descripteur correspondent tous deux au même fichier. Si vous appelez fclose comme ceci, vous ne pourrez plus écrire dans descripteur:

 
Sélectionnez
fclose (flux);

De même, si vous appelez close, vous ne pourrez plus écrire dans flux:

 
Sélectionnez
close (descripteur);

Pour effectuer l'opération inverse, obtenir un flux à partir d'un descripteur, utilisez la fonction fdopen. Elle produit un pointeur FILE* correspondant à un descripteur de fichier. La fonction fdopen prend en paramètres un descripteur de fichier et une chaîne correspondant au mode dans lequel le flux doit être ouvert. La syntaxe de l'argument de mode est la même que pour le second argument de fopen et il doit être compatbile avec le descripteur de fichier. Par exemple, passez la chaine de mode r pour un descripteur de fichier en lecture ou w pour un descripteur de fichier en écriture. Comme pour fileno, le flux et le descripteur de fichier font référence au même fichier, ainsi, si vous en fermez l'un des deux, vous ne pouvez plus utiliser l'autre.

12-5. Autres Opérations sur les Fichiers

Un petit nombre d'opérations supplémentaires sur les fichiers et répertoires peut s'avérer utile:

  • getcwd renvoie le répertoire de travail courant. Il prend deux argument, un tampon de char et sa longueur. Il copie le chemin du répertoire de travail courant dans le tampon;
  • chdir change le répertoire de travail courant pour qu'il corresponde au chemin passé en paramètre;
  • mkdir crée un nouveau répertoire. Son premier argument est le chemin de celui-ci, le second les permissions à y appliquer. L'interprétation des permissions est la même que celle du troisième argument de open, elles sont affectées par l'umask du processus;
  • rmdir supprime le répertoire dont le chemin est passé en paramètre;
  • unlink supprime le fichier dont le chemin est passé en paramètre. Cet appel peut également être utilisé pour supprimer d'autres objets du système de fichiers, comme les canaux nommés (référez-vous à la Section FIFO, Section FIFO) ou les périphériques (voir le Chapitre peripheriques); En fait, unlink ne supprime pas forcément le contenu du fichier. Comme son nom l'indique, il rompt le lien entre le fichier et le répertoire qui le contient. Le fichier n'apparait plus dans le listing du répertoire mais si un processus détient un descripteur de fichier ouvert sur ce fichier, le contenu n'est pas effacé du disque. Cela n'arrive que lorsque qu'aucun processus ne détient de descripteur de fichier ouvert. Ainsi, si un priocessus ouvre un fichier pour y écrire ou y lire et qu'un second processus supprime le fichier avec unlink et crée un nouveau fichier avec le même nom, le premier processus "voit" l'ancien contenu du fichier et non pas le nouveau (à moins qu'il ne ferme le fichier et le ré-ouvre);
  • rename renomme ou déplace un fichier. Le premier argument correspond au chemin courant vers le fichier, le second au nouveau chemin. Si les deux chemins sont dans des répertoires différents, rename déplace le fichier, si tant est que le nouveau chemin est sur le même système de fichiers que l'ancien. Vous pouvez utiliser rename pour déplacer des répertoires ou d'autres objets du système de fichiers.

12-6. Lire le Contenu d'un Répertoire

GNU/Linux dispose de fonctions pour lire le contenu des répertoires. Bien qu'elles ne soient pas directement liées aux fonctions de bas niveau décrites dans cet appendice, nous les présentons car elles sont souvent utiles.

Pour lire le contenu d'un répertoire, les étapes suivantes sont nécessaires:

- Appelez opendir en lui passant le chemin du répertoire que vous souhaitez explorer. opendir renvoie un descripteur DIR*, dont vous aurez besoin pour accéder au contenu du répertoire. Si une erreur survient, l'appel renvoie NULL;

- Appelez readdir en lui passant le descripteur DIR* que vous a renvoyé opendir. À chaque appel, readdir renvoie un pointeur vers une instance de struct dirent correspondant à l'entrée suivante dans le répertoire. Lorsque vous atteignez la fin du contenu du répertoire, readdir renvoie NULL. La struct dirent que vous optenez via readdir dispose d'un champ d_name qui contient le nom de l'entrée.

- Appelez closedir en lui passant le descripteur DIR* à la fin du parcours.

Incluez <sys/types.h> et <dirent.h> si vous utilisez ces fonctions dans votre programme.

Notez que si vous avez besoin de trier les entrées dans un ordre particulier, c'est à vous de le faire.

Le programme du Listing listdir affiche le contenu d'un répertoire. Celui-ci peut être spécifié sur la ligne de commande mais, si ce n'est pas le cas, le répertoire courant est utilisé. Pour chaque entrée, son type et son chemin est affiché. La fonction get_file_type utilise lstat pour déterminer le type d'une entrée.

Affiche le Contenu d'un Répertoire listdir.c
Sélectionnez
#include <assert.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/* Renvoie une chaine qui décrit le type du fichier PATH. */
const char* get_file_type (const char* path)
{
  struct stat st;
  lstat (path, &amp;st);
  if (S_ISLNK (st.st_mode))
    return "lien symbolique";
  else if (S_ISDIR (st.st_mode))
    return "répertoire";
  else if (S_ISCHR (st.st_mode))
    return "périphérique caractère";
  else if (S_ISBLK (st.st_mode))
    return "périphérique bloc";
  else if (S_ISFIFO (st.st_mode))
    return "fifo";
  else if (S_ISSOCK (st.st_mode))
    return "socket";
  else if (S_ISREG (st.st_mode))
    return "fichier ordinaire";
  else
    /* Impossible. Toute entrée doit être de l'un des type ci-dessus. */
    assert (0);
}
int main (int argc, char* argv[])
{
  char* dir_path;
  DIR* dir;
  struct dirent* entry;
  char entry_path[PATH_MAX + 1];
  size_t path_len;
  if (argc >= 2)
    /* Utilise le répertoire spécifié sur la ligne de commande s'il y a lieu. */
    dir_path = argv[1];
  else
    /* Sinon, utilise le répertoire courant. */
    dir_path = ".";
  /* Copie le chemin du répertoire dans entry_path. */
  strncpy (entry_path, dir_path, sizeof (entry_path));
  path_len = strlen (dir_path);
  /* Si le répertoire ne se termine pas par un slash, l'ajoute. */
  if (entry_path[path_len - 1] != '/') {
    entry_path[path_len] = '/';
    entry_path[path_len + 1] = '\0';
    ++path_len;
  }
  /* Démarre l'affichage du contenu du répertoire. */
  dir = opendir (dir_path);
  /* Boucle sur les entrées du répertoire. */
  while ((entry = readdir (dir)) != NULL) {
    const char* type;
    /* Construit le chemin complet en concaténant le chemin du répertoire
       et le nom de l'entrée. */
    strncpy (entry_path + path_len, entry->d_name,
             sizeof (entry_path) - path_len);
    /* Détermine le type de l'entrée. */
    type = get_file_type (entry_path);
    /* Affiche le type et l'emplacement de l'entrée. */
    printf ("%-18s: %s\n", type, entry_path);
  }
  /* Fini. */
  closedir (dir);
  return 0;
}

Voici les premières lignes affichées lors de l'affichage du contenu de /dev (elles peuvent être différentes chez vous):

 
Sélectionnez
% ./listdir /dev
directory        : /dev/.
directory        : /dev/..
socket           : /dev/log
character device : /dev/null
regular file     : /dev/MAKEDEV
fifo             : /dev/initctl
character device : /dev/agpgart
...

Pour vérifier les résultats, vous pouvez utiliser la commande ls sur le même répertoire. Passez l'indicateur -U pour demander à ls de ne pas trier les entrées et passez l'indicateur -a pour inclure le répertoire courant (.) et le répertoire parent (..).

 
Sélectionnez
% ls -lUa /dev
total 124
drwxr-xr-x    7 root root   36864 Feb 1 15:14 .
drwxr-xr-x   22 root root    4096 Oct 11 16:39 ..
srw-rw-rw-    1 root root       0 Dec 18 01:31 log
crw-rw-rw-    1 root root  1,   3 May 5 1998 null
-rwxr-xr-x    1 root root   26689 Mar 2 2000 MAKEDEV
prw-------    1 root root       0 Dec 11 18:37 initctl
crw-rw-r--    1 root root 10, 175 Feb 3 2000 agpgart
...

Le premier caractère de chaque ligne affichée par ls indique le type de l'entrée.


précédentsommairesuivant

Copyright © 2007 Mark Mitchell, Jeffrey Oldham et Alex Samuel. Ce document ne peut être distribué que dans le respect des termes et conditions définies par l?Open Poublication License, v1.0 ou ultérieure (la dernière version est disponible sur http://www.opencontent.org/openpub/).