Section courante

A propos

Section administrative du site

L'interface système UNIX

Le système d'exploitation UNIX fournit ses services via un ensemble d'appels système, étant en fait des fonctions au sein du système d'exploitation pouvant être appelées par des programmes utilisateur. Cette page décrit comment utiliser certains des appels système les plus importants des programmes C. Si vous utilisez UNIX, cela devrait être directement utile, car il est parfois nécessaire d'utiliser des appels système pour une efficacité maximale ou pour accéder à une fonctionnalité n'étant pas dans la bibliothèque. Cependant, même si vous utilisez C sur un autre système d'exploitation, vous devriez être en mesure de glaner des informations sur la programmation C en étudiant ces exemples ; bien que les détails varient, un code similaire sera trouvé sur n'importe quel système. Étant donné que la bibliothèque ANSI C est dans de nombreux cas calquée sur les fonctionnalités UNIX, ce code peut également vous aider à comprendre la bibliothèque.

Cette page est divisé en trois parties principales : entrée/sortie, système de fichiers et allocation d'entreposage. Les deux premières parties supposent une familiarité modeste avec les caractéristiques externes des systèmes UNIX.

La page Entrée et sortie portait sur une interface d'entrée/sortie uniforme sur tous les systèmes d'exploitation. Sur tout système particulier, les routines de la bibliothèque standard doivent être écrites en fonction des fonctionnalités fournies par le système hôte. Dans les sections suivantes, nous décrirons les appels système UNIX pour les entrées et les sorties, et montrerons comment des parties de la bibliothèque standard peuvent être implémentées avec eux.

Descripteurs de fichiers

Dans le système d'exploitation UNIX, toutes les entrées et sorties se font par la lecture ou l'écriture de fichiers, car tous les périphériques, même le clavier et l'écran, sont des fichiers dans le système de fichiers. Cela signifie qu'une seule interface homogène gère toutes les communications entre un programme et les périphériques.

Dans le cas le plus général, avant de lire et d'écrire un fichier, vous devez informer le système de votre intention de le faire, un processus appelé ouverture du fichier. Si vous allez écrire sur un fichier, il peut également être nécessaire de le créer ou de supprimer son contenu précédent. Le système vérifie votre droit de le faire (le fichier existe-t-il ? Avez-vous l'autorisation d'y accéder ?) et si tout va bien, renvoie au programme un petit entier non négatif appelé descripteur de fichier. Chaque fois qu'une entrée ou une sortie doit être effectuée sur le fichier, le descripteur de fichier est utilisé à la place du nom pour identifier le fichier. (Un descripteur de fichier est analogue au pointeur de fichier utilisé par la bibliothèque standard ou au descripteur de fichier de MS-DOS.) Toutes les informations sur un fichier ouvert sont conservées par le système ; le programme utilisateur fait référence au fichier uniquement par le descripteur de fichier.

Étant donné que les entrées et sorties impliquant le clavier et l'écran sont si courantes, des dispositions spéciales existent pour rendre cela pratique. Lorsque l'interpréteur de commandes (le «shell») exécute un programme, trois fichiers sont ouverts, avec les descripteurs de fichiers 0, 1 et 2, appelés l'entrée standard, la sortie standard et l'erreur standard. Si un programme lit 0 et écrit 1 et 2, il peut effectuer des entrées et des sorties sans se soucier d'ouvrir des fichiers.

L'utilisateur d'un programme peut rediriger les entrées/sorties vers et depuis des fichiers avec < et > :

prog <infile >outfile

Dans ce cas, l'interpréteur de commande modifie les affectations par défaut des descripteurs de fichiers 0 et 1 aux fichiers nommés. Normalement, le descripteur de fichier 2 reste attaché à l'écran, de sorte que les messages d'erreur peuvent y être placés. Des observations similaires s'appliquent aux entrées ou aux sorties associées à un tube. Dans tous les cas, les affectations de fichiers sont modifiées par l'interpréteur de commande, et non par le programme. Le programme ne sait pas d'où vient son entrée ni où va sa sortie, tant qu'il utilise le fichier 0 pour l'entrée et 1 et 2 pour la sortie.

Entrée/sortie de bas niveau - Lecture et écriture

L'entrée et la sortie utilisent les appels système de lecture et d'écriture, auxquels on accède à partir de programmes C via deux fonctions appelées lecture et écriture. Pour les deux, le premier paramètre est un descripteur de fichier. Le deuxième paramètre est un tableau de caractères dans votre programme où les données doivent aller ou d'où elles doivent provenir. Le troisième paramètre est le nombre d'octets à transférer.

  1. int n_read = read(int fd, char *buf, int n);
  2. int n_written = write(int fd, char *buf, int n);    

Chaque appel renvoie un décompte du nombre d'octets transférés. En lecture, le nombre d'octets renvoyés peut être inférieur au nombre demandé. Une valeur de retour de zéro octet implique la fin du fichier et -1 indique une erreur quelconque. Pour l'écriture, la valeur de retour est le nombre d'octets écrits ; une erreur s'est produite si ce nombre n'est pas égal au nombre demandé.

N'importe quel nombre d'octets peut être lu ou écrit en un seul appel. Les valeurs les plus courantes sont 1, ce qui signifie un caractère à la fois ("sans tampon"), et un nombre comme 1024 ou 4096 correspondant à une taille de bloc physique sur un périphérique. Des tailles plus grandes seront plus efficaces car moins d'appels système seront effectués.

En mettant ces faits ensemble, nous pouvons écrire un programme simple pour copier son entrée vers sa sortie, l'équivalent du programme de copie de fichiers écrit dans la page Les premiers pas. Ce programme copiera n'importe quoi vers n'importe quoi, puisque l'entrée et la sortie peuvent être redirigées vers n'importe quel fichier ou périphérique.

  1. #include "syscalls.h"
  2.  
  3. main() { /* copier l'entrée vers la sortie */
  4.  char buf[BUFSIZ];
  5.  int n;
  6.  while ((n = read(0, buf, BUFSIZ)) > 0) write(1, buf, n);
  7.  return 0;
  8. }

Ils ont rassemblé les prototypes de fonctions pour les appels système dans un fichier appelé syscalls.h afin de pouvoir l'inclure dans les programmes de cette page. Ce nom n'est cependant pas standard.

Le paramètre BUFSIZ est également défini dans syscalls.h ; sa valeur est une taille adaptée au système local. Si la taille du fichier n'est pas un multiple de BUFSIZ, certaines lectures renverront un nombre plus petit d'octets à écrire par écriture ; le prochain appel à read après cela renverra zéro.

Il est instructif de voir comment read et write peuvent être utilisés pour construire des routines de niveau supérieur comme getchar, putchar,... Par exemple, voici une version de getchar effectuant une entrée sans tampon, en lisant l'entrée standard un caractère à la fois.

  1. #include "syscalls.h"
  2.  
  3. /* getchar: entrée de caractère unique non tamponnée */
  4. int getchar(void) {
  5.  char c;
  6.  return (read(0, &c, 1) == 1) ? (unsigned char) c : EOF;
  7. }

c doit être un char, car read nécessite un pointeur de caractère. Le transtypage de c en unsigned char dans l'instruction return élimine tout problème d'extension de signe.

La deuxième version de getchar effectue la saisie par gros morceaux et distribue les caractères un par un.

  1. #include "syscalls.h"
  2.  
  3. /* getchar: version tamponnée simple */
  4. int getchar(void) {
  5.  static char buf[BUFSIZ];
  6.  static char *bufp = buf;
  7.  static int n = 0;
  8.  if (n == 0) { /* le tampon est vide */
  9.   n = read(0, buf, sizeof buf);
  10.   bufp = buf;
  11.  }
  12.  return (--n >= 0) ? (unsigned char) *bufp++ : EOF;
  13. }

Si ces versions de getchar devaient être compilées avec <stdio.h> inclus, il serait nécessaire de #undef le nom getchar au cas où il serait implémenté en tant que macro.

open, creat, close, unlink

Outre l'entrée, la sortie et l'erreur standard par défaut, vous devez ouvrir explicitement les fichiers pour les lire ou les écrire. Il existe deux appels système pour cela, open et creat.

open ressemble plutôt à fopen décrit dans la page Entrée et sortie, sauf qu'au lieu de renvoyer un pointeur de fichier, il renvoie un descripteur de fichier, n'étant qu'un int. open renvoie -1 si une erreur se produit.

  1. #include <fcntl.h>
  2.  
  3. int fd;
  4. int open(char *name, int flags, int perms);
  5.  
  6. fd = open(name, flags, perms);    

Comme pour fopen, le paramètre name est une chaîne de caractères contenant le nom du fichier. Le second paramètre, flags, est un int spécifiant comment le fichier doit être ouvert ; les principales valeurs sont :

Constante Description
O_RDONLY Ouvert en lecture seulement
O_WRONLY Ouvert uniquement pour l'écriture
O_RDWR Ouvert à la lecture et à l'écriture

Ces constantes sont définies dans <fcntl.h> sur les systèmes UNIX System V et dans <sys/file.h> sur les versions Berkeley (BSD).

Pour ouvrir un fichier existant en lecture&nbps;:

  1. fd = open(name, O_RDONLY,0);    

Le paramètre perms est toujours nul pour les utilisations de open que nous allons aborder.

C'est une erreur d'essayer d'ouvrir un fichier n'existant pas. L'appel système creat est fourni pour créer de nouveaux fichiers ou pour réécrire les anciens :

  1. int creat(char *name, int perms);
  2.  
  3. fd = creat(name, perms);    

renvoie un descripteur de fichier s'il a pu créer le fichier, et -1 dans le cas contraire. Si le fichier existe déjà, creat le tronquera à une longueur nulle, supprimant ainsi son contenu précédent ; ce n'est pas une erreur de créer un fichier existant déjà.

Si le fichier n'existe pas encore, creat le crée avec les autorisations spécifiées par le paramètre perms. Dans le système de fichiers UNIX, neuf bits d'informations d'autorisation sont associés à un fichier contrôlant l'accès en lecture, écriture et exécution pour le propriétaire du fichier, pour le groupe du propriétaire et pour tous les autres. Ainsi, un nombre octal à trois chiffres est pratique pour spécifier les autorisations. Par exemple, 0775 spécifie l'autorisation de lecture, d'écriture et d'exécution pour le propriétaire, et l'autorisation de lecture et d'exécution pour le groupe et tous les autres.

Pour illustrer, voici une version simplifiée du programme cp de UNIX, copiant un fichier dans un autre. Notre version copie un seul fichier, elle ne permet pas que le deuxième paramètre soit un répertoire, et elle invente des autorisations au lieu de les copier.

  1. #include <stdio.h>
  2. #include <fcntl.h>
  3. #include "syscalls.h"
  4. #define PERMS 0666 /* RW pour propriétaire, groupe, autres */
  5.  
  6. void error(char *, ...);
  7. /* cp: copier f1 vers f2 */
  8. main(int argc, char *argv[]) {
  9.  int f1, f2, n;
  10.  char buf[BUFSIZ];
  11.  if (argc != 3) error("Syntaxe: cp source dest");
  12.  if ((f1 = open(argv[1], O_RDONLY, 0)) == -1) error("cp: ne peut pas ouvrir %s", argv[1]);
  13.  if ((f2 = creat(argv[2], PERMS)) == -1) error("cp: impossible de créer %s, mode %03o", argv[2], PERMS);
  14.  while ((n = read(f1, buf, BUFSIZ)) > 0) if (write(f2, buf, n) != n) error("cp: erreur d'écriture sur le fichier %s", argv[2]);
  15.  return 0;
  16. }

Ce programme crée le fichier de sortie avec des permissions fixes de 0666. Avec l'appel système stat, nous pouvons déterminer le mode d'un fichier existant et ainsi donner le même mode à la copie.

Notez que la fonction error est appelée avec des listes de paramètres variables, un peu comme printf. L'implémentation de error illustre comment utiliser un autre membre de la famille printf. La fonction de bibliothèque standard vprintf est comme printf, sauf que la liste de paramètres variables est remplacée par un seul paramètre ayant été initialisé en appelant la macro va_start. De même, vfprintf et vsprintf correspondent à fprintf et sprintf.

  1. #include <stdio.h>
  2. #include <stdarg.h>
  3.  
  4. /* error: Afficher un message d'erreur et de terminer */
  5. void error(char *fmt, ...) {
  6.  va_list args;
  7.  va_start(args, fmt);
  8.  fprintf(stderr, "error: ");
  9.  vprintf(stderr, fmt, args);
  10.  fprintf(stderr, "\n");
  11.  va_end(args);
  12.  exit(1);
  13. }

Il existe une limite (souvent d'environ 20) au nombre de fichiers qu'un programme peut ouvrir simultanément. Par conséquent, tout programme souhaitant traiter de nombreux fichiers doit être prêt à réutiliser les descripteurs de fichiers. La fonction close(int fd) rompt la connexion entre un descripteur de fichier et un fichier ouvert, et libère le descripteur de fichier pour l'utiliser avec un autre fichier ; elle correspond à fclose dans la bibliothèque standard, sauf qu'il n'y a pas de tampon à vider. La fin d'un programme via la sortie ou le retour du programme principal ferme tous les fichiers ouverts.

La fonction unlink(char *name) supprime le nom du fichier du système de fichiers. Elle correspond à la fonction remove de la bibliothèque standard.

Accès aléatoire - lseek

L'entrée et la sortie sont normalement séquentielles : chaque lecture ou écriture a lieu à une position du fichier juste après la précédente. Cependant, si nécessaire, un fichier peut être lu ou écrit dans n'importe quel ordre arbitraire. L'appel système lseek permet de se déplacer dans un fichier sans lire ni écrire de données :

  1. long lseek(int fd, long offset, int origin);

définit la position actuelle dans le fichier dont le descripteur est fd sur offset, étant pris par rapport à l'emplacement spécifié par origin. Les lectures ou écritures ultérieures commenceront à cette position. origin peut être 0, 1 ou 2 pour spécifier que le déplacement doit être mesuré à partir du début, de la position actuelle ou de la fin du fichier respectivement. Par exemple, pour ajouter à un fichier (la redirection >> dans l'interpréteur de commande UNIX, ou "a" pour fopen), recherchez la fin avant d'écrire :

  1. lseek(fd, 0L, 2);

Pour revenir au début («rewind») :

  1. lseek(fd, 0L, 0);

Notez le paramètre 0L ; il pourrait aussi être écrit comme (long) 0 ou simplement comme 0 si lseek est correctement déclaré.

Avec lseek, il est possible de traiter les fichiers plus ou moins comme des tableaux, au prix d'un accès plus lent. Par exemple, la fonction suivante lit n'importe quel nombre d'octets à partir de n'importe quel emplacement arbitraire dans un fichier. Elle renvoie le nombre lu, ou -1 en cas d'erreur.

  1. #include "syscalls.h"
  2.  
  3. /*get: lire n octets à partir de la position pos */
  4. int get(int fd, long pos, char *buf, int n) {
  5.  if (lseek(fd, pos, 0) >= 0) /* arriver à la position */
  6.   return read(fd, buf, n);
  7.  else
  8.   return -1;
  9. }

La valeur de retour de lseek est un long donnant la nouvelle position dans le fichier, ou -1 si une erreur se produit. La fonction de la bibliothèque standard fseek est similaire à lseek, sauf que le premier argument est un FILE * et que le retour est différent de zéro si une erreur se produit.

Exemple - Une implémentation de fopen et getc

Illustrons comment certaines de ces pièces s'assemblent en montrant une implémentation des routines de la bibliothèque standard fopen et getc.

Rappelons que les fichiers de la bibliothèque standard sont décrits par des pointeurs de fichier plutôt que par des descripteurs de fichier. Un pointeur de fichier est un pointeur vers une structure contenant plusieurs éléments d'information sur le fichier : un pointeur vers un tampon, afin que le fichier puisse être lu en gros morceaux ; un décompte du nombre de caractères restant dans le tampon ; un pointeur vers la position de caractère suivante dans le tampon ; le descripteur de fichier ; et des indicateurs décrivant le mode de lecture/écriture, l'état d'erreur,...

La structure de données décrivant un fichier est contenue dans <stdio.h>, devant être incluse (par #include) dans tout fichier source utilisant des routines de la bibliothèque d'entrée/sortie standard. Elle est également incluse par les fonctions de cette bibliothèque. Dans l'extrait suivant d'un fichier <stdio.h> typique, les noms destinés à être utilisés uniquement par les fonctions de la bibliothèque commencent par un trait de soulignement afin qu'ils soient moins susceptibles d'entrer en conflit avec les noms du programme d'un utilisateur. Cette convention est utilisée par toutes les routines de la bibliothèque standard.

  1. #define NULL 0
  2. #define EOF (-1)
  3. #define BUFSIZ 1024
  4. #define OPEN_MAX 20 /* nombre maximum de fichiers ouverts simultanément */
  5.  
  6. typedef struct _iobuf {
  7.  int cnt;    /* caractères restants */
  8.  char *ptr;  /* position du caractère suivant */
  9.  char *base; /* emplacement du tampon */
  10.  int flag;   /* mode d'accès au fichier */
  11.  int fd;     /* descripteur de fichier */
  12. } FILE;    
  13.  
  14. extern FILE _iob[OPEN_MAX];
  15.  
  16. #define stdin (&_iob[0])
  17. #define stdout (&_iob[1])
  18. #define stderr (&_iob[2])
  19.  
  20. enum _flags {
  21.  _READ = 01,  /* fichier ouvert pour lecture */
  22.  _WRITE = 02, /* fichier ouvert pour écriture */
  23.  _UNBUF = 04, /* le fichier n'est pas mis en mémoire tampon */
  24.  _EOF = 010,  /* EOF s'est produit sur ce fichier */
  25.  _ERR = 020   /* une erreur s'est produite sur ce fichier */
  26. };
  27.  
  28. int _fillbuf(FILE *);
  29. int _flushbuf(int, FILE *);
  30.  
  31. #define feof(p) ((p)->flag & _EOF) != 0)
  32. #define ferror(p) ((p)->flag & _ERR) != 0)
  33. #define fileno(p) ((p)->fd)
  34.  
  35. #define getc(p) (--(p)->cnt >= 0 \
  36.                ? (unsigned char) *(p)->ptr++ : _fillbuf(p))
  37. #define putc(x,p) (--(p)->cnt >= 0 \
  38.                ? *(p)->ptr++ = (x) : _flushbuf((x),p))
  39.  
  40. #define getchar() getc(stdin)
  41. #define putcher(x) putc((x), stdout)

La macro getc décrémente normalement le compte, avance le pointeur et renvoie le caractère. (Rappelez-vous qu'un long #define est continué par une barre oblique inverse.) Cependant, si le compte devient négatif, getc appelle la fonction _fillbuf pour réapprovisionner le tampon, réinitialiser le contenu de la structure et renvoyer un caractère. Les caractères sont renvoyés non signés, ce qui garantit que tous les caractères seront positifs.

Bien que nous n'allons pas discuter des détails, ils ont inclus la définition de putc pour montrer qu'elle fonctionne de la même manière que getc, en appelant une fonction _flushbuf lorsque son tampon est plein. Nous avons également inclus des macros pour accéder à l'état d'erreur et de fin de fichier et au descripteur de fichier.

La fonction fopen peut maintenant être écrite. La majeure partie de fopen consiste à ouvrir le fichier et à le positionner au bon endroit, et à définir les bits d'indicateur pour indiquer l'état approprié. fopen n'alloue aucun espace tampon ; cela est fait par _fillbuf lors de la première lecture du fichier.

  1. #include <fcntl.h>
  2. #include "syscalls.h"
  3. #define PERMS 0666 /* RW pour propriétaire, groupe, autres */
  4.  
  5. FILE *fopen(char *name, char *mode) {
  6.  int fd;
  7.  FILE *fp;
  8.  if (*mode != 'r' && *mode != 'w' && *mode != 'a') return NULL;
  9.  for (fp = _iob; fp < _iob + OPEN_MAX; fp++) if ((fp->flag & (_READ | _WRITE)) == 0) break; /* trouvé un emplacement libre */
  10.  if (fp >= _iob + OPEN_MAX) return NULL; /* pas de fentes de libres */
  11.  if (*mode == 'w') fd = creat(name, PERMS);
  12.  else if (*mode == 'a') {
  13.   if ((fd = open(name, O_WRONLY, 0)) == -1)
  14.   fd = creat(name, PERMS);
  15.   lseek(fd, 0L, 2);
  16.  } 
  17.   else
  18.  fd = open(name, O_RDONLY, 0);
  19.  if (fd == -1) /* impossible d'accéder au nom */
  20.   return NULL;
  21.  fp->fd = fd;
  22.  fp->cnt = 0;
  23.  fp->base = NULL;
  24.  fp->flag = (*mode == 'r') ? _READ : _WRITE;
  25.  return fp;
  26. }

Cette version de fopen ne gère pas toutes les possibilités de mode d'accès de la norme, bien que les ajouter ne nécessiterait pas beaucoup de code. En particulier, notre fopen ne reconnaît pas le «b» signalant l'accès binaire, car cela n'a pas de sens sur les systèmes UNIX, ni le «+» permettant à la fois la lecture et l'écriture.

Le premier appel à getc pour un fichier particulier trouve un compte de zéro, ce qui force un appel à _fillbuf. Si _fillbuf trouve que le fichier n'est pas ouvert en lecture, il renvoie immédiatement EOF. Sinon, il essaie d'allouer un tampon (si la lecture doit être mise en mémoire tampon).

Une fois le tampon établi, _fillbuf appelle read pour le remplir, définit le compte et les pointeurs, et renvoie le caractère au début du tampon. Les appels suivants à _fillbuf trouveront un tampon alloué.

  1. #include "syscalls.h"
  2.  
  3. /* _fillbuf: allouer et remplir le tampon d'entrée */
  4. int _fillbuf(FILE *fp) {
  5.  int bufsize;
  6.  if ((fp->flag&(_READ|_EOF_ERR)) != _READ) return EOF;
  7.  bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ;
  8.  if (fp->base == NULL) /* pas encore de tampon */
  9.   if ((fp->base = (char *) malloc(bufsize)) == NULL)
  10.    return EOF; /* impossible d'obtenir le tampon */
  11.  fp->ptr = fp->base;
  12.  fp->cnt = read(fp->fd, fp->ptr, bufsize);
  13.  if (--fp->cnt < 0) {
  14.   if (fp->cnt == -1)
  15.    fp->flag |= _EOF;
  16.   else
  17.    fp->flag |= _ERR;
  18.   fp->cnt = 0;
  19.   return EOF;
  20.  }
  21.  return (unsigned char) *fp->ptr++;
  22. }

La seule chose restant à régler est de savoir comment tout cela va démarrer. Le tableau _iob doit être défini et initialisé pour stdin, stdout et stderr :

  1. FILE _iob[OPEN_MAX] = { /* stdin, stdout, stderr */
  2.  { 0, (char *) 0, (char *) 0, _READ, 0 },
  3.  { 0, (char *) 0, (char *) 0, _WRITE, 1 },
  4.  { 0, (char *) 0, (char *) 0, _WRITE, | _UNBUF, 2 }
  5. };    

L'initialisation de la partie indicateur de la structure montre que stdin doit être lu, stdout doit être écrit et stderr doit être écrit sans tampon.

Exemple - Listing des répertoires

Un autre type d'interaction avec le système de fichiers est parfois nécessaire : déterminer les informations sur un fichier, et non ce qu'il contient. Un programme de listage de répertoires tel que la commande ls de UNIX en est un exemple : il affiche les noms des fichiers d'un répertoire et, éventuellement, d'autres informations, telles que les tailles, les autorisations,... La commande DIR du MS-DOS est analogue.

Puisqu'un répertoire UNIX n'est qu'un fichier, ls n'a besoin que de le lire pour récupérer les noms de fichiers. Mais il est nécessaire d'utiliser un appel système pour accéder à d'autres informations sur un fichier, telles que sa taille. Sur d'autres systèmes, un appel système peut être nécessaire même pour accéder aux noms de fichiers ; c'est le cas sur MS-DOS par exemple. Ce que nous voulons, c'est fournir un accès aux informations d'une manière relativement indépendante du système, même si l'implémentation peut être très dépendante du système. Nous allons illustrer cela en écrivant un programme appelé fsize. fsize est une forme spéciale de ls affichant les tailles de tous les fichiers nommés dans sa liste de paramètres de ligne de commande. Si l'un des fichiers est un répertoire, fsize s'applique de manière récursive à ce répertoire. S'il n'y a aucun paramètre, il traite le répertoire courant.

Commençons par un bref aperçu de la structure du système de fichiers UNIX. Un répertoire est un fichier contenant une liste de noms de fichiers et une indication de leur emplacement. L'«emplacement» est un index dans une autre table appelée «liste d'inodes». L'inode d'un fichier est l'endroit où toutes les informations sur le fichier, à l'exception de son nom, sont conservées. Une entrée de répertoire se compose généralement de deux éléments seulement, le nom de fichier et un numéro d'inode.

Malheureusement, le format et le contenu précis d'un répertoire ne sont pas les mêmes sur toutes les versions du système. Nous allons donc diviser la tâche en deux parties pour essayer d'isoler les parties non portables. Le niveau externe définit une structure appelée Dirent et trois routines opendir, readdir et closedir pour fournir un accès indépendant du système au nom et au numéro d'inode dans une entrée de répertoire. Nous écrirons fsize avec cette interface. Nous montrerons ensuite comment les mettre en oeuvre sur des systèmes utilisant la même structure de répertoire que la version 7 et System V UNIX.

La structure Dirent contient le numéro d'inode et le nom. La longueur maximale d'un composant de nom de fichier est NAME_MAX, étant une valeur dépendante du système. opendir renvoie un pointeur vers une structure appelée DIR, analogue à FILE, étant utilisée par readdir et closedir. Ces informations sont collectées dans un fichier appelé dirent.h.

  1. #define NAME_MAX 14 /* composante du nom de fichier le plus long ; dépendant du système */
  2.  
  3. typedef struct {        /* entrée de répertoire portable */
  4.  long ino;              /* nombre inode */
  5.  char name[NAME_MAX+1]; /* name + terminateur '\0'  */
  6. } Dirent;
  7.  
  8. typedef struct { /* DIR minimal : pas de mise en mémoire tampon,... */
  9.  int fd;         /* Descripteur de fichier pour le répertoire */
  10.  Dirent d;       /* l'entrée du répertoire */
  11. } DIR;
  12.  
  13. DIR *opendir(char *dirname);
  14. Dirent *readdir(DIR *dfd);
  15. void closedir(DIR *dfd);    

L'appel système stat prend un nom de fichier et renvoie toutes les informations de l'inode pour ce fichier, ou -1 en cas d'erreur. C'est-à-dire :

  1. char *name;
  2. struct stat stbuf;
  3. int stat(char *, struct stat *);
  4.  
  5. stat(name, &stbuf);    

remplit la structure stbuf avec les informations d'inode pour le nom de fichier. La structure décrivant la valeur renvoyée par stat se trouve dans <sys/stat.h> et ressemble généralement à ceci :

  1. struct stat { /* informations d'inode renvoyées par stat */
  2.  dev_t st_dev;    /* périphérique d'inode */
  3.  ino_t st_ino;    /* numéro d'inode */
  4.  short st_mode;   /* bits de mode */
  5.  short st_nlink;  /* nombre de liens vers le fichier */
  6.  short st_uid;    /* identifiant utilisateur du propriétaire */
  7.  short st_gid;    /* identifiant du groupe de propriétaires */
  8.  dev_t st_rdev;   /* pour les fichiers spéciaux */
  9.  off_t st_size;   /* taille du fichier en caractères */
  10.  time_t st_atime; /* heure du dernier accès */
  11.  time_t st_mtime; /* heure de la dernière modification */
  12.  time_t st_ctime; /* temps créé à l'origine */
  13. };    

La plupart de ces valeurs sont expliquées par les champs de commentaires. Les types comme dev_t et ino_t sont définis dans <sys/types.h>, devant également être inclus.

L'entrée st_mode contient un ensemble d'indicateurs décrivant le fichier. Les définitions d'indicateurs sont également incluses dans <sys/types.h> ; nous n'avons besoin que de la partie traitant du type de fichier :

  1. #define S_IFMT 0160000 /* type de fichier: */
  2. #define S_IFDIR 0040000 /* répertoire */
  3. #define S_IFCHR 0020000 /* caractère spécial */
  4. #define S_IFBLK 0060000 /* bloc spécial */
  5. #define S_IFREG 0010000 /* régulier */
  6. /* ... */    

Nous sommes maintenant prêts à écrire le programme fsize. Si le mode obtenu à partir de stat indique qu'un fichier n'est pas un répertoire, alors la taille est disponible et peut être affichée directement. Si le nom est un répertoire, cependant, nous devons traiter ce répertoire un fichier à la fois ; il peut à son tour contenir des sous-répertoires, le processus est donc récursif.

La routine principale traite des arguments de ligne de commande ; elle transmet chaque paramètre à la fonction fsize.

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include "syscalls.h"
  4. #include <fcntl.h> /* drapeaux pour la lecture et l'écriture */
  5. #include <sys/types.h> /* typedefs */
  6. #include <sys/stat.h> /* structure renvoyée par stat */
  7. #include "dirent.h"
  8.  
  9. void fsize(char *)
  10.  
  11. /* afficher le nom du fichier */
  12. main(int argc, char **argv) {
  13.  if (argc == 1) /* par défaut : répertoire courant */
  14.   fsize(".");
  15.  else
  16.   while (--argc > 0) fsize(*++argv);
  17.  return 0;
  18. }

La fonction fsize affiche la taille du fichier. Si le fichier est un répertoire, fsize appelle d'abord dirwalk pour gérer tous les fichiers qu'il contient. Notez comment les noms d'indicateurs S_IFMT et S_IFDIR sont utilisés pour décider si le fichier est un répertoire. La mise en parenthèses est importante, car la priorité de & est inférieure à celle de ==.

  1. int stat(char *, struct stat *);
  2. void dirwalk(char *, void (*fcn)(char *));
  3.  
  4. /* fsize: afficher le nom du fichier "nom" */
  5. void fsize(char *name) {
  6.  struct stat stbuf;
  7.  if (stat(name, &stbuf) == -1) {
  8.   fprintf(stderr, "fsize: impossible d'accéder %s\n", name);
  9.   return;
  10.  }
  11.  if ((stbuf.st_mode & S_IFMT) == S_IFDIR) dirwalk(name, fsize);
  12.  printf("%8ld %s\n", stbuf.st_size, name);
  13. }

La fonction dirwalk est une routine générale appliquant une fonction à chaque fichier d'un répertoire. Elle ouvre le répertoire, parcourt les fichiers qu'il contient, appelle la fonction sur chacun d'eux, puis ferme le répertoire et revient. Comme fsize appelle dirwalk sur chaque répertoire, les deux fonctions s'appellent mutuellement de manière récursive.

  1. #define MAX_PATH 1024
  2.  
  3. /* dirwalk: appliquer fcn à tous les fichiers du répertoire */
  4. void dirwalk(char *dir, void (*fcn)(char *)) {
  5.  char name[MAX_PATH];
  6.  Dirent *dp;
  7.  DIR *dfd;
  8.  if ((dfd = opendir(dir)) == NULL) {
  9.   fprintf(stderr, "dirwalk: ne peut pas ouvrir %s\n", dir);
  10.   return;
  11.  }
  12.  while ((dp = readdir(dfd)) != NULL) {
  13.   if (strcmp(dp->name, ".") == 0 || strcmp(dp->name, "..")) continue; /* sauter soi-même et le parent */
  14.   if (strlen(dir)+strlen(dp->name)+2 > sizeof(name)) fprintf(stderr, "dirwalk: nom %s %s trop long\n", dir, dp->name);
  15.   else {
  16.     sprintf(name, "%s/%s", dir, dp->name);
  17.     (*fcn)(name);
  18.   }
  19.  }
  20.  closedir(dfd);
  21. }

Chaque appel à readdir renvoie un pointeur vers les informations du fichier suivant, ou NULL lorsqu'il n'y a plus de fichiers. Chaque répertoire contient toujours des entrées pour lui-même, appelées ".", et son parent, ".." ; celles-ci doivent être ignorées, sinon le programme bouclera indéfiniment.

Jusqu'à ce dernier niveau, le code est indépendant de la façon dont les répertoires sont formatés. L'étape suivante consiste à présenter des versions minimales de opendir, readdir et closedir pour un système spécifique. Les routines suivantes sont destinées aux systèmes UNIX version 7 et System V ; elles utilisent les informations de répertoire dans l'entête <sys/dir.h>, ressemblant à ceci :

  1. #ifndef DIRSIZ
  2. #define DIRSIZ 14
  3. #endif
  4. struct direct { /* entrée de répertoire */
  5.  ino_t d_ino;         /* numéro d'inode */
  6.  char d_name[DIRSIZ]; /* le nom long n'a pas '\0' */
  7. };

Certaines versions du système autorisent des noms beaucoup plus longs et ont une structure de répertoire plus compliquée.

Le type ino_t est un typedef décrivant l'index dans la liste des inodes. Il se trouve qu'il s'agit d'un type short non signé sur les systèmes que nous utilisons régulièrement, mais ce n'est pas le genre d'informations à intégrer dans un programme ; il peut être différent sur un autre système, donc le typedef est meilleur. Un ensemble complet de types «système» se trouve dans <sys/types.h>.

opendir ouvre le répertoire, vérifie que le fichier est un répertoire (cette fois par l'appel système fstat, étant comme stat sauf qu'il s'applique à un descripteur de fichier), alloue une structure de répertoire et enregistre les informations :

  1. int fstat(int fd, struct stat *);
  2. /* opendir: ouvrir un répertoire pour les appels readdir */
  3. DIR *opendir(char *dirname) {
  4.  int fd;
  5.  struct stat stbuf;    
  6.  DIR *dp;
  7.  
  8.  if ((fd = open(dirname, O_RDONLY, 0)) == -1 || fstat(fd, &stbuf) == -1 || (stbuf.st_mode & S_IFMT) != S_IFDIR || (dp = (DIR *) malloc(sizeof(DIR))) == NULL) return NULL;
  9.  dp->fd = fd;
  10.  return dp;
  11. }

closedir ferme le fichier du répertoire et libère l'espace :

  1. /* closedir: fermer le répertoire ouvert par opendir */
  2. void closedir(DIR *dp) {
  3.  if (dp) {
  4.   close(dp->fd);
  5.   free(dp);
  6.  }
  7. }

Enfin, readdir utilise read pour lire chaque entrée du répertoire. Si un emplacement du répertoire n'est pas actuellement utilisé (parce qu'un fichier a été supprimé), le numéro d'inode est zéro et cette position est ignorée. Sinon, le numéro d'inode et le nom sont placés dans une structure statique et un pointeur vers celle-ci est renvoyé à l'utilisateur. Chaque appel écrase les informations du précédent.

  1. #include <sys/dir.h> /* structure du répertoire local */
  2.  
  3. /* readdir: lire les entrées du répertoire en séquence */
  4. Dirent *readdir(DIR *dp) {
  5.  struct direct dirbuf; /* structure du répertoire local */
  6.  static Dirent d; /* return: structure portative */
  7.  while (read(dp->fd, (char *) &dirbuf, sizeof(dirbuf))== sizeof(dirbuf)) {
  8.   if (dirbuf.d_ino == 0) continue; /* emplacement non utilisé */
  9.   d.ino = dirbuf.d_ino;
  10.   strncpy(d.name, dirbuf.d_name, DIRSIZ);
  11.   d.name[DIRSIZ] = '\0'; /* assurer la résiliation */
  12.   return &d;
  13.  }
  14.  return NULL;
  15. }

Bien que le programme fsize soit assez spécialisé, il illustre quelques idées importantes. Tout d'abord, de nombreux programmes ne sont pas des «programmes système» ; ils utilisent simplement des informations maintenues par le système d'exploitation. Pour de tels programmes, il est crucial que la représentation des informations n'apparaisse que dans des en-têtes standards et que les programmes incluent ces en-têtes au lieu d'intégrer les déclarations dans eux-mêmes. La deuxième observation est qu'avec soin, il est possible de créer une interface vers des objets dépendants du système qui soit elle-même relativement indépendante du système. Les fonctions de la bibliothèque standard en sont de bons exemples.

Exemple - Un allocateur d'entreposage

Dans la page Pointeurs et tableaux, nous avons présenté un allocateur d'entreposage orienté pile et limité à plusieurs variables. La version que nous allons maintenant écrire n'est pas restreinte. Les appels à malloc et free peuvent se produire dans n'importe quel ordre ; malloc fait appel au système d'exploitation pour obtenir plus de mémoire si nécessaire. Ces routines illustrent certaines des considérations impliquées dans l'écriture de code dépendant de la machine d'une manière relativement indépendante de la machine, et montrent également une application réelle des structures, des unions et du typedef.

Au lieu d'allouer à partir d'un tableau de taille fixe compilé, malloc demandera de l'espace au système d'exploitation selon les besoins. Étant donné que d'autres activités du programme peuvent également demander de l'espace sans appeler cet allocateur, l'espace géré par malloc peut ne pas être contigu. Ainsi, son espace d'entreposage libre est conservé sous la forme d'une liste de blocs libres. Chaque bloc contient une taille, un pointeur vers le bloc suivant et l'espace lui-même. Les blocs sont conservés dans l'ordre croissant d'adresse d'entreposage, et le dernier bloc (adresse la plus élevée) pointe vers le premier.

Lorsqu'une demande est formulée, la liste libre est analysée jusqu'à ce qu'un bloc suffisamment grand soit trouvé. Cet algorithme est appelé «premier ajustement», par opposition à «meilleur ajustement», recherchant le plus petit bloc satisfaisant la demande. Si le bloc a exactement la taille demandée, il est dissocié de la liste et renvoyé à l'utilisateur. Si le bloc est trop gros, il est divisé et la quantité appropriée est renvoyée à l'utilisateur tandis que le résidu reste sur la liste libre. Si aucun bloc suffisamment grand n'est trouvé, un autre gros bloc est obtenu par le système d'exploitation et lié à la liste libre.

La libération entraîne également une recherche dans la liste libre, pour trouver l'emplacement approprié pour insérer le bloc en cours de libération. Si le bloc en cours de libération est adjacent à un bloc libre de chaque côté, il est fusionné avec lui en un seul bloc plus grand, de sorte que l'entreposage ne soit pas trop fragmenté. Déterminer la contiguïté est facile car la liste libre est maintenue dans l'ordre d'adresse décroissante.

Un problème, auquel nous avons fait allusion à la page Pointeurs et tableaux, est de s'assurer que l'entreposage renvoyé par malloc est correctement aligné pour les objets y étant entreposés. Bien que les machines varient, pour chaque machine il existe un type le plus restrictif : si le type le plus restrictif peut être entreposé à une adresse particulière, tous les autres types peuvent l'être également. Sur certaines machines, le type le plus restrictif est un double ; sur d'autres, int ou long suffisent.

Un bloc libre contient un pointeur vers le bloc suivant de la chaîne de caractères, un enregistrement de la taille du bloc, puis l'espace libre lui-même ; les informations de contrôle au début sont appelées «entête». Pour simplifier l'alignement, tous les blocs sont des multiples de la taille de l'entête, et l'entête est correctement aligné. Ceci est réalisé par une union contenant la structure d'entête souhaitée et une instance du type d'alignement le plus restrictif, que nous avons arbitrairement transformé en long :

  1. typedef long Align; /* pour l'alignement sur la limite longue */
  2. union header { /* entête de bloc */
  3.  struct {
  4.   union header *ptr; /* bloc suivant s'il est sur la liste libre */
  5.   unsigned size;     /* taille de ce bloc */
  6.  } s;
  7.  Align x; /* alignement forcé des blocs */
  8. };
  9. typedef union header Header;

Le champ Align n'est jamais utilisé ; il force simplement chaque entête à être aligné sur une limite du pire des cas.

Dans malloc, la taille demandée en caractères est arrondie au nombre approprié d'unités de la taille de l'entête ; le bloc étant alloué contient une unité supplémentaire, pour l'entête lui-même, et c'est la valeur enregistrée dans le champ size de l'entête. Le pointeur renvoyé par malloc pointe vers l'espace libre, pas vers l'entête lui-même. L'utilisateur peut faire ce qu'il veut avec l'espace demandé, mais si quelque chose est écrit en dehors de l'espace alloué, la liste est susceptible d'être brouillée.

Le champ size est nécessaire car les blocs contrôlés par malloc n'ont pas besoin d'être contigus - il n'est pas possible de calculer des tailles par arithmétique de pointeur.

La variable base est utilisée pour commencer. Si freep est NULL, comme c'est le cas lors du premier appel de malloc, alors une liste libre dégénérée est créée ; elle contient un bloc de taille zéro et pointe vers elle-même. Dans tous les cas, la liste libre est alors recherchée. La recherche d'un bloc libre de taille adéquate commence au point (freep) où le dernier bloc a été trouvé ; cette stratégie permet de garder la liste homogène. Si un bloc trop gros est trouvé, la fin est renvoyée à l'utilisateur ; de cette façon, l'en-tête de l'original n'a besoin que d'être ajusté en taille. Dans tous les cas, le pointeur renvoyé à l'utilisateur pointe vers l'espace libre dans le bloc, commençant une unité au-delà de l'entête :

  1. static Header base;          /* liste vide pour commencer */
  2. static Header *freep = NULL; /* début de la liste libre */
  3.  
  4. /* malloc: allocateur d'entreposage à usage général */
  5. void *malloc(unsigned nbytes) {
  6.  Header *p, *prevp;
  7.  Header *moreroce(unsigned);
  8.  unsigned nunits;
  9.  nunits = (nbytes+sizeof(Header)-1)/sizeof(header) + 1;
  10.  if ((prevp = freep) == NULL) { /* pas encore de liste libre */
  11.   base.s.ptr = freeptr = prevptr = &base;
  12.   base.s.size = 0;
  13.  }    
  14.  for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) {
  15.   if (p->s.size >= nunits) { /* assez grand */
  16.    if (p->s.size == nunits) /* exactement */
  17.     prevp->s.ptr = p->s.ptr;
  18.    else {                   /* allouer la fin de la queue */
  19.     p->s.size -= nunits;
  20.     p += p->s.size;
  21.     p->s.size = nunits;
  22.    }
  23.    freep = prevp;
  24.    return (void *)(p+1);
  25.   }
  26.   if (p == freep) /* enroulé autour de la liste libre */
  27.   if ((p = morecore(nunits)) == NULL)
  28.   return NULL; /* il n'en reste plus */
  29.  }
  30. }

La fonction morecore obtient de la mémoire du système d'exploitation. Les détails de la manière dont elle le fait varient d'un système à l'autre. Comme demander de la mémoire au système est une opération relativement coûteuse, nous ne voulons pas faire cela à chaque appel à malloc, donc morecore demande au moins des unités NALLOC ; ce bloc plus grand sera découpé selon les besoins. Après avoir défini le champ size, morecore insère la mémoire supplémentaire dans l'arène en appelant free.

L'appel système sbrk(n) de UNIX renvoie un pointeur vers n octets supplémentaires d'entreposage. sbrk renvoie -1 s'il n'y avait pas d'espace, même si NULL aurait pu être une meilleure conception. Le -1 doit être converti en char * afin de pouvoir être comparé à la valeur de retour. Encore une fois, les conversions rendent la fonction relativement insensible aux détails de la représentation des pointeurs sur différentes machines. Il existe cependant une hypothèse selon laquelle les pointeurs vers différents blocs renvoyés par sbrk peuvent être comparés de manière significative. Cela n'est pas garanti par la norme, qui autorise les comparaisons de pointeurs uniquement dans un tableau. Ainsi, cette version de malloc n'est portable qu'entre les machines pour lesquelles la comparaison générale de pointeurs est significative :

  1. #define NALLOC 1024 /* nombre minimum d'unités à demander */
  2.  
  3. /* morecore: demander au système plus de mémoire */
  4. static Header *morecore(unsigned nu) {
  5.  char *cp, *sbrk(int);
  6.  Header *up;
  7.  if (nu < NALLOC) nu = NALLOC;
  8.  cp = sbrk(nu * sizeof(Header));
  9.  if (cp == (char *) -1) return NULL; /* pas d'espace du tout */
  10.  up = (Header *) cp;
  11.  up->s.size = nu;
  12.  free((void *)(up+1));
  13.  return freep;    
  14. }

free lui-même est la dernière chose. Il parcourt la liste des blocs libres, en commençant par freep, à la recherche de l'endroit où insérer le bloc libre. C'est soit entre deux blocs existants, soit à la fin de la liste. Dans tous les cas, si le bloc libéré est adjacent à l'un des voisins, les blocs adjacents sont combinés. Les seuls problèmes sont de garder les pointeurs pointant vers les bons éléments et les tailles correctes.

  1. /* free: mettre le bloc ap dans la liste libre */
  2. void free(void *ap) {
  3.  Header *bp, *p;
  4.  bp = (Header *)ap - 1; /* pointer vers l'entête du bloc */
  5.  for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr) if (p >= p->s.ptr && (bp > p || bp < p->s.ptr)) break; /* bloc libéré au début ou à la fin de l'arène */
  6.  if (bp + bp->size == p->s.ptr) { /* joindre au numéro supérieur */
  7.   bp->s.size += p->s.ptr->s.size;
  8.   bp->s.ptr = p->s.ptr->s.ptr;
  9.  }
  10.   else
  11.  bp->s.ptr = p->s.ptr;
  12.  if (p + p->size == bp) { /* rejoindre pour réduire le nombre */
  13.   p->s.size += bp->s.size;
  14.   p->s.ptr = bp->s.ptr;
  15.  } 
  16.   else
  17.  p->s.ptr = bp;
  18.  freep = p;
  19. }

Bien que l'allocation d'entreposage soit intrinsèquement dépendante de la machine, le code ci-dessus illustre comment les dépendances de la machine peuvent être contrôlées et limitées à une très petite partie du programme. L'utilisation de typedef et d'union gère l'alignement (à condition que sbrk fournisse un pointeur approprié). Les conversions de pointeurs permettent de rendre explicites les conversions de pointeurs et de gérer même une interface système mal conçue. Même si les détails ici concernent l'allocation d'entreposage, l'approche générale est également applicable à d'autres situations.



Dernière mise à jour : Jeudi, le 28 novembre 2024