Previous Up Next

Chapter 2  Les fichiers

Le terme “fichier” en Unix recouvre plusieurs types d’objets:

La représentation d’un fichier contient à la fois les données contenues dans le fichier et des informations sur le fichier (aussi appelées méta-données) telles que son type, les droits d’accès, les dernières dates d’accès, etc.

2.1  Le système de fichiers

En première approximation, le système de fichier est un arbre. La racine est notée '/'. Les arcs sont étiquetés par des noms (de fichiers), formés d’une chaîne de caractères quelconques à l’exception des seuls caractères '\000' et '/', mais il est de bon usage d’éviter également les caractères non imprimables ainsi que les espaces. Les nœuds non terminaux du système de fichiers sont appelés répertoires: il contiennent toujours deux arcs .  et . .  qui désignent respectivement le répertoire lui-même et le répertoire parent. Les autres nœuds sont parfois appelés fichiers, par opposition aux répertoires, mais cela reste ambigu, car on peut aussi désigner par fichier un nœud quelconque. Pour éviter toute ambiguïté, on pourra parler de « fichiers non répertoires ».

Les nœuds du système de fichiers sont désignés par des chemins. Ceux-ci peuvent se référer à l’origine de la hiérarchie et on parlera de chemins absolus, ou à un répertoire (en général le répertoire de travail). Un chemin relatif est une suite de noms de fichiers séparés par le caractère '/'; un chemin absolu est un chemin relatif précédé par le caractère '/' (notez le double usage de ce caractère comme séparateur de chemin et comme le nom de la racine).

La bibliothèque Filename permet de manipuler les chemins de façon portable. Notamment Filename.concat permet de concaténer des chemins sans faire référence au caractère '/', ce qui permettra au code de fonctionner également sur d’autres architectures (par exemple le caractère de séparation des chemins est ’\’ sous Windows). De même, le module Filename donne des noms currentdir et parentdir pour désigner les arcs .  et . . . Les fonctions Filename.basename et Filename.dirname extraient d’un chemin p un préfixe d et un suffixe b tel que les chemins p et d/b désignent le même fichier, d désigne le répertoire dans lequel se trouve le fichier et b le nom du fichier dans ce répertoire. Les opérations définies dans Filename opèrent uniquement sur les chemins indépendemment de leur existence dans la hiérarchie.

En fait, la hiérarchie n’est pas un arbre. D’abord les répertoires conventionnels .  et . .  permettent de s’auto-référencer et de remonter dans la hiérarchie, donc de créer des chemins menant d’un répertoire à lui-même. D’autre part les fichiers non répertoires peuvent avoir plusieurs antécédents. On dit alors qu’il a plusieurs «liens durs». Enfin, il existe aussi des «liens symboliques» qui se prêtent à une double interprétation. Un lien symbolique est un fichier non répertoire dont le contenu est un chemin. On peut donc interpréter un lien symbolique comme un fichier ordinaire et simplement lire son contenu, un lien. Mais on peut aussi suivre le lien symbolique de façon transparente et ne voir que le fichier cible. Cette dernière est la seule interprétation possible lorsque le lien apparaît au milieu d’un chemin: Si s est un lien symbolique dont la valeur est le chemin ℓ, alors le chemin p/s/q désigne le fichier ℓ/q si ℓ est un lien absolu ou le fichier ou p/ℓ/q si ℓ est un lien relatif.


    
  • Liens inverses omis
  • Liens durs
    7 a deux antécédents 2 et 6
  • Liens symboliques
    10 désigne 5
    11 ne désigne aucun nœud
  • Chemins équivalents de 9 à 8?
    . . /usr/lib
    . /. . /usr/lib, etc.
    foo/lib
Figure 2.1: Un petit exemple de hiérarchie de fichiers

La figure 2.1 donne un exemple de hiérarchie de fichiers. Le lien symbolique 11 désigné par le chemin /tmp/bar, dont la valeur est le chemin relatif . . /gnu, ne désigne aucun fichier existant dans la hiérarchie (à cet instant).

En général un parcours récursif de la hiérarchie effectue une lecture arborescente de la hiérarchie:

Si l’on veut suivre les liens symboliques, on est alors ramené à un parcourt de graphe et il faut garder trace des nœuds déjà visités et des nœuds en cours de visite.

Chaque processus a un répertoire de travail. Celui-ci peut être consulté par la commande getcwd et changé par la commande chdir. Il est possible de restreindre la vision de la hiérarchie. L’appel chroot p fait du nœud p, qui doit être un répertoire, la racine de la hiérarchie. Les chemins absolus sont alors interprétés par rapport à la nouvelle racine (et le chemin . .  appliqué à la nouvelle racine reste bien entendu à la racine).

2.2  Noms de fichiers, descripteurs de fichiers

Il y a deux manières d’accéder à un fichier. La première est par son nom, ou chemin d’accès à l’intérieur de la hiérarchie de fichiers. Un fichier peut avoir plusieurs noms différents, du fait des liens durs. Les noms sont représentés par des chaînes de caractères (type string). Voici quelques exemples d’appels système qui opèrent au niveau des noms de fichiers:

unlink fefface le fichier de nom f (comme la commande rm -f f)
link f1   f2crée un lien dur nommé f2 sur le fichier de nom f1 (comme la commande ln f1   f2)
symlink f1   f2crée un lien symbolique nommé f2 sur le fichier de nom f1 (comme la commande ln -s f1   f2)
rename f1   f2renomme en f2 le fichier de nom f1 (comme la commande mv f1   f2).

L’autre manière d’accéder à un fichier est par l’intermédiaire d’un descripteur. Un descripteur représente un pointeur vers un fichier, plus des informations comme la position courante de lecture/écriture dans ce fichier, des permissions sur ce fichier (peut-on lire? peut-on écrire?), et des drapeaux gouvernant le comportement des lectures et des écritures (écritures en ajout ou en écrasement, lectures bloquantes ou non). Les descripteurs sont représentés par des valeurs du type abstrait file_descr.

Les accès à travers un descripteur sont en grande partie indépendants des accès via le nom du fichier. En particulier, lorsqu’on a obtenu un descripteur sur un fichier, le fichier peut être détruit ou renommé, le descripteur pointera toujours sur le fichier d’origine.

Au lancement d’un programme, trois descripteurs ont été préalloués et liés aux variables stdin, stdout et stderr du module Unix:

stdin : file_descrl’entrée standard du processus 
stdout : file_descrla sortie standard du processus 
stderr : file_descrla sortie d’erreur standard du processus

Lorsque le programme est lancé depuis un interpréteur de commandes interactif et sans redirections, les trois descripteurs font référence au terminal. Mais si, par exemple, l’entrée a été redirigée par la notation cmd  < f, alors le descripteur stdin fait référence au fichier de nom f pendant l’exécition de la commande cmd. De même cmd > f (respectivement cmd 2> f) fait en sorte que le descripteur stdout (respectivement stderr) fasse reférence au fichier f pendant l’exécution de la commande cmd.

2.3  Méta-données, types et permissions

Les appels système stat, lstat et fstat retournent les méta-données sur un fichier, c’est-à-dire les informations portant sur le nœud lui-même plutôt que son contenu. Entre autres, ces informations décrivent l’identité du fichier, son type du fichier, les droits d’accès, les dates des derniers d’accès, plus un certain nombre d’informations supplémentaires.

     
   val stat  : string -> stats
   val lstat : string -> stats
   val fstat : file_descr -> stats

Les appels stat et lstat prennent un nom de fichier en argument. L’appel fstat prend en argument un descripteur déjà ouvert et donne les informations sur le fichier qu’il désigne. La différence entre stat et lstat se voit sur les liens symboliques: lstat renvoie les informations sur le lien symbolique lui-même, alors que stat renvoie les informations sur le fichier vers lequel pointe le lien symbolique.


st_dev : intUn identificateur de la partition disque où se trouve le fichier
st_ino : intUn identificateur du fichier à l’intérieur de sa partition. Le couple (st_dev, st_ino) identifie de manière unique un fichier dans le système de fichier.
st_kind : file_kindLe type du fichier. Le type file_kind est un type concret énuméré, de constructeurs:
S_REGfichier normal
S_DIRrépertoire
S_CHRfichier spécial de type caractère
S_BLKfichier spécial de type bloc
S_LNKlien symbolique
S_FIFOtuyau
S_SOCKprise
st_perm : intLes droits d’accès au fichier
st_nlink : intPour un répertoire: le nombre d’entrées dans le répertoire. Pour les autres: le nombre de liens durs sur ce fichier.
st_uid : intLe numéro de l’utilisateur propriétaire du fichier.
st_gid : intLe numéro du groupe propriétaire du fichier.
st_rdev : intL’identificateur du périphérique associé (pour les fichiers spéciaux).
st_size : intLa taille du fichier, en octets.
st_atime : intLa date du dernier accès au contenu du fichier. (En secondes depuis le 1ier janvier 1970, minuit).
st_mtime : intLa date de la dernière modification du contenu du fichier. (Idem.)
st_ctime : intLa date du dernier changement de l’état du fichier: ou bien écriture dans le fichier, ou bien changement des droits d’accès, du propriétaire, du groupe propriétaire, du nombre de liens.
Tableau 2.1: Champs de la structure stats

Le résultat de ces trois appels est un objet enregistrement (record) de type stats décrit dans la table 2.1.

Identification

Un fichier est identifié de façon unique par la paire composé de son numéro de périphérique (typiquement la partition sur laquelle il se trouve) st_dev et de son numéro d’inode st_ino.

Propriétaires

Un fichier a un propriétaire st_uid et un groupe propriétaire st_gid. L’ensemble des utilisateurs et des groupes d’utilisateurs sur la machine est habituellement décrit dans les fichiers /etc/passwd et /etc/groups. On peut les interroger de façon portable par nom à l’aide des commandes getpwnam et getgrnam ou par numéro à l’aide des commandes getpwuid et getgrgid.

Le nom de l’utilisateur d’un processus en train de tourner et l’ensemble des groupes auxquels il appartient peuvent être récupérés par les commandes getlogin et getgroups.

L’appel chown modifie le propriétaire (deuxième argument) et le groupe propriétaire (troisième argument) d’un fichier (premier argument). Seul le super utilisateur a le droit de changer arbitrairement ces informations. Lorsque le fichier est tenu par un descripteur, on utilisera fchown en passant les descripteur au lieu du nom de fichier.

Droits

Les droits sont codés sous forme de bits dans un entier et le type file_perm est simplement une abréviation pour le type int: Les droits comportent une information en lecture, écriture et exécution pour l’utilisateur, le groupe et les autres, plus des bits spéciaux. Les droits sont donc représentés par un vecteur de bits:

 
 
Special


 
 
User


 
 
Group


 
 
Other


 
 


0oSUGO

où pour chacun des champs user, group et other on indique dans l’ordre les droits en lecture (r), écriture (w) et exécution (x). Les permissions sur un fichier sont l’union des permissions individuelles:

Bit (octal)Notation ls -lDroit
0o100--x------exécution, pour le propriétaire
0o200-w-------écriture, pour le propriétaire
0o400r--------lecture, pour le propriétaire
0o10-----x--- exécution, pour les membres des groupes du propriétaire
0o20----w---- écriture, pour les membres des groupes du propriétaire
0o40---r---- lecture, pour les membres des groupes du propriétaire
0o1--------xexécution, pour les autres utilisateurs
0o2-------w-écriture, pour les autres utilisateurs
0o4------r--lecture, pour les autres utilisateurs
0o1000--------tle bit t sur le groupe (sticky bit)
0o2000-----s---le bit s sur le groupe (set-gid)
0o4000--s------le bit s sur l’utilisateur (set-uid)

Le sens des droits de lecture et d’écrire est évident ainsi que le droit d’exécution pour un fichier. Pour un répertoire, le droit d’exécution signifie le droit de se placer sur le répertoire (faire chdir sur ce répertoire). Le droit de lecture sur un répertoire est nécessaire pour en lister son contenu mais pas pour en lire ses fichiers ou sous-répertoires (mais il faut alors en connaître le nom).

Les bits spéciaux ne prennent de sens qu’en présence du bit x (lorsqu’il sont présents sans le bit x, ils ne donnent pas de droits supplémentaires). C’est pour cela que leur représentation se superpose à celle du bit x et on utilise les lettres S et T au lieu de s et t lorsque le bit x n’est pas simultanément présent. Le bit t permet aux sous-répertoires créés d’hériter des droits du répertoire parent. Pour un répertoire, le bit s permet d’utiliser le uid ou le gid de propriétaire du répertoire plutôt que de l’utilisateur à la création des répertoires. Pour un fichier exécutable, le bit s permet de changer au lancement l’identité effective de l’utilisateur (setuid) ou du groupe (setgid). Le processus conserve également ses identités d’origine, à moins qu’il ait les privilèges du super utilisateur, auquel cas, setuid et setgid changent à la fois son identité effective et son identité d’origine. L’identité effective est celle sous laquelle le processus s’exécute. L’identité d’origine est maintenue pour permettre au processus de reprendre ultérieurement celle-ci comme effective sans avoir besoin de privilèges. Les appels système getuid et getgid retournent les identités d’origine et geteuid et getegid retournent les identités effectives.

Un processus possède également un masque de création de fichiers représenté de la même façon. Comme son nom l’indique, le masque est spécifie des interdictions (droits à masquer): lors de la création d’un fichier tous les bits à 1 dans le masque de création sont mis à zéro dans les droits du fichier créé. Le masque peut être consulté et changé par la fonction

     
   val umask : int -> int

Comme pour de nombreux appels système qui modifient une variable système, l’ancienne valeur de la variable est retournée par la fonction de modification. Pour simplement consulter la valeur, il faut donc la modifier deux fois, une fois avec une valeur arbitraire, puis remettre l’ancienne valeur en place. Par exemple, en faisant:

     
           let m = umask 0 in ignore (umask m); m

Les droits d’accès peuvent être modifiés avec l’appel chmod.

On peut également tester les droits d’accès «dynamiquement» avec l’appel système access

     
   type access_permission = R_OK | W_OK | X_OK | F_OK
     
   val access : string -> access_permission list -> unit

où les accès demandés sont représentés pas le type access_permission dont le sens est immédiat sauf pour F_OK qui signifie seulement que le fichier existe (éventuellement sans que le processus ait les droits correspondants).

Notez que access peut retourner une information plus restrictive que celle calculée à partir de l’information statique retournée par lstat car une hiérarchie de fichiers peut être montrée avec des droits restreints, par exemple en lecture seule. Dans ce cas, access refusera le droit d’écrire alors que l’information contenue dans les méta-données relative au fichier peut l’autoriser. C’est pour cela qu’on parle d’information «dynamique» (ce que le processus peut réellement faire) par opposition à «statique» (ce que le système de fichier indique).

2.4  Opérations sur les répertoires

Seul le noyau écrit dans les répertoires (lorsque des fichiers sont créés). Il est donc interdit d’ouvrir un répertoire en écriture. Dans certaines versions d’Unix on peut ouvrir un répertoire en lecture seule et le lire avec read, mais d’autres versions l’interdise. Cependant, même si c’est possible, il est préférable de ne pas le faire car le format des entrées des répertoires varie suivant les versions d’Unix, et il est souvent complexe. Les fonctions suivantes permettent de lire séquentiellement un répertoire de manière portable:

     
   val opendir   : string -> dir_handle
   val readdir   : dir_handle -> string
   val rewinddir : dir_handle -> unit
   val closedir  : dir_handle -> unit

La fonction opendir renvoie un descripteur de lecture sur un répertoire. La fonction readdir lit la prochaine entrée d’un répertoire (ou déclenche l’exception End_of_file si la fin du répertoire est atteinte). La chaîne renvoyée est un nom de fichier relatif au répertoire lu. La fonction rewinddir repositionne le descripteur au début du répertoire.

Pour créer un répertoire, ou détruire un répertoire vide, on dispose de:

     
   val mkdir : string -> file_perm -> unit
   val rmdir : string -> unit

Le deuxième argument de mkdir encode les droits d’accès donnés au nouveau répertoire. Notez qu’on ne peut détruire qu’un répertoire déjà vide. Pour détruire un répertoire et son contenu, il faut donc d’abord aller récursivement vider le contenu du répertoire puis détruire le répertoire.

Par exemple, on peut écrire une fonction d’intérêt général dans le module Misc qui itère sur les entrées d’un répertoire.

     
   let iter_dir f dirname =
     let d = opendir dirname in
     try while true do f (readdir ddone
     with End_of_file -> closedir d

2.5  Exemple complet: recherche dans la hiérarchie

La commande Unix find permet de rechercher récursivement des fichiers dans la hiérarchie selon certains critères (nom, type et droits du fichier) etc. Nous nous proposons ici de réaliser d’une part une fonction de bibliothèque Findlib.find permettant d’effectuer de telles recherches et une commande find fournissant une version restreinte de la commande Unix find n’implantant que les options -follow et -maxdepth.

Nous imposons l’interface suivante pour la bibliothèque Findlib:

     
   val find :
     (Unix.error * string * string -> unit) ->
     (string -> Unix.stats -> bool) -> bool -> int -> string list ->
     unit

L’appel de fonction "find" handler action follow depth roots parcourt la hiérarchie de fichiers à partir des racines indiquées dans la liste roots (absolues ou relatives au répertoire courant au moment de l’appel) jusqu’à une profondeur maximale depth en suivant les liens symboliques si le drapeau follow est vrai. Les chemins trouvés sous une racine r incluent r comme préfixe. Chaque chemin trouvé p est passé à la fonction action. En fait, action reçoit également les informations Unix.stat p si le drapeau follow est vrai ou Unix.lstat p sinon. La fonction action retourne un booléen indiquant également dans le cas d’un répertoire s’il faut poursuivre la recherche en profondeur (true) ou l’interrompre (false).

La fonction handler sert au traitement des erreurs de parcours, nécessairement de type Unix_error: les arguments de l’exception sont alors passés à la fonction handler et le parcours continue. En cas d’interruption, l’exception est remontée à la fonction appelante. Lorsqu’une exception est levée par les fonctions action ou handler, elle arrête le parcours de façon abrupte et est remontée immédiatement à l’appelant.

Pour remonter une exception Unix_error sans qu’elle puisse être attrapée comme une erreur de parcours, nous la cachons sous une autre exception.

     
   exception Hidden of exn
   let hide_exn f x = try f x with exn -> raise (Hidden exn);;
   let reveal_exn f x = try f x with Hidden exn -> raise exn;;

Voici le code de la fonction de parcours.

     
   open Unix;;
   let find on_error on_path follow depth roots =
     let rec find_rec depth visiting filename =
       try
         let infos = (if follow then stat else lstatfilename in
         let continue = hide_exn (on_path filenameinfos in
         let id = infos.st_devinfos.st_ino in
         if infos.st_kind = S_DIR && depth > 0 && continue &&
           (not follow || not (List.mem id visiting))
         then
           let process_child child =
             if (child <> Filename.current_dir_name &&
                 child <> Filename.parent_dir_namethen
               let child_name = Filename.concat filename child in
               let visiting =
                 if follow then id :: visiting else visiting in
               find_rec (depth-1) visiting child_name in
           Misc.iter_dir process_child filename
       with Unix_error (ebc) -> hide_exn on_error (ebcin
     reveal_exn (List.iter (find_rec depth [])) roots;;

Les répertoires sont identifiés par la paire id (ligne 21) constituée de leur numéro de périphérique et de leur numéro d’inode. La liste visiting contient l’ensemble des répertoires en train d’être visités. En fait cette information n’est utile que si l’on suit les liens symboliques (ligne 19).

On peut maintenant en déduire facilement la commande find.

     
   let find () =
     let follow = ref false in
     let maxdepth = ref max_int in
     let roots = ref [] in
     let usage_string  =
       ("Usage: " ^ Sys.argv.(0) ^ " [files...] [options...]"in
     let opt_list =  [
       "-maxdepth"Arg.Int ((:=) maxdepth), "max depth search";
       "-follow"Arg.Set follow"follow symbolic links";
     ] in
     Arg.parse opt_list (fun f -> roots := f :: !rootsusage_string;
     let action p infos = print_endline ptrue in
     let errors = ref false in
     let on_error (ebc) =
       errors := trueprerr_endline (c ^ ": " ^ Unix.error_message ein
     Findlib.find on_error action !follow !maxdepth
       (if !roots = [] then [ Filename.current_dir_name ]
        else List.rev !roots);
     if !errors then exit 1;;
   
   Unix.handle_unix_error find ();;

L’essentiel du code est constitué par l’analyse de la ligne de commande, pour laquelle nous utilisons la bibliothèque Arg.

Bien que la commande find implantée ci-dessus soit assez restreinte, la fonction de bibliothèque Findlib.find est quant à elle très générale, comme le montre l’exercice suivant.

Exercice 1   Utiliser la bibliothèque Findlib pour écrire un programme find_but_CVS équivalent à la commande Unix find . -type d -name CVS -prune -o -print qui imprime récursivement les fichiers à partir du répertoire courant mais sans voir (ni imprimer, ni visiter) les répertoires de nom CVS.
(Voir le corrigé)
Exercice 2   La fonction getcwd n’est pas un appel système mais définie en bibliothèque. Donner une implémentation «primitive» de getcwd. Décrire le principe de l’algorithme.
(Voir le corrigé)
Puis écrire l’algorithme (on évitera de répéter plusieurs fois le même appel système).

2.6  Ouverture d’un fichier

La primitive openfile permet d’obtenir un descripteur sur un fichier d’un certain nom (l’appel système correspond est open, mais open est un mot clé en OCaml).

     
   val openfile : string -> open_flag list -> file_perm -> file_descr

Le premier argument est le nom du fichier à ouvrir. Le deuxième argument est une liste de drapeaux pris dans le type énuméré open_flag, et décrivant dans quel mode le fichier doit être ouvert, et que faire s’il n’existe pas. Le troisième argument de type file_perm indique avec quels droits d’accès créer le fichier, le cas échéant. Le résultat est un descripteur de fichier pointant vers le fichier indiqué. La position de lecture/écriture est initialement fixée au début du fichier.

La liste des modes d’ouverture (deuxième argument) doit contenir exactement un des trois drapeaux suivants:

O_RDONLYouverture en lecture seule 
O_WRONLYouverture en lecture seule 
O_RDWRouverture en lecture et en écriture

Ces drapeaux conditionnent la possibilité de faire par la suite des opérations de lecture ou d’écriture à travers le descripteur. L’appel openfile échoue si on demande à ouvrir en écriture un fichier sur lequel le processus n’a pas le droit d’écrire, ou si on demande à ouvrir en lecture un fichier que le processus n’a pas le droit de lire. C’est pourquoi il ne faut pas ouvrir systématiquement en mode O_RDWR.

La liste des modes d’ouverture peut contenir en plus un ou plusieurs des drapeaux parmi les suivants:

O_APPENDouverture en ajout 
O_CREATcréer le fichier s’il n’existe pas
O_TRUNCtronquer le fichier à zéro s’il existe déjà
O_EXCLéchouer si le fichier existe déjà
O_NONBLOCKouverture en mode non bloquant 
 
O_NOCTTYne pas fonctionner en mode terminal de contrôle
O_SYNCeffectuer les écritures en mode synchronisé
O_DSYNCeffectuer les écritures de données en mode synchronisé
O_RSYNCeffectuer les lectures en mode synchronisé

Le premier groupe indique le comportement à suivre selon que le fichier existe ou non.

Si O_APPEND est fourni, le pointeur de lecture/écriture sera positionné à la fin du fichier avant chaque écriture. En conséquence, toutes les écritures s’ajouteront à la fin du fichier. Au contraire, sans O_APPEND, les écritures se font à la position courante (initialement, le début du fichier).

Si O_TRUNC est fourni, le fichier est tronqué au moment de l’ouverture: la longueur du fichier est ramenée à zéro, et les octets contenus dans le fichier sont perdus. Les écritures repartent donc d’un fichier vide. Au contraire, sans O_TRUNC, les écritures se font par dessus les octets déjà présents, ou à la suite.

Si O_CREAT est fourni, le fichier est créé s’il n’existe pas déjà. Le fichier est créé avec une taille nulle, et avec pour droits d’accès les droits indiqués par le troisième argument, modifiés par le masque de création du processus. (Le masque de création est consultable et modifiable par la commande umask, et par l’appel système de même nom).

Exemple:

la plupart des programmes prennent 0o666 comme troisième argument de openfile, c’est-à-dire rw-rw-rw- en notation symbolique. Avec le masque de création standard de 0o022, le fichier est donc créé avec les droits rw-r--r--. Avec un masque plus confiant de 0o002, le fichier est créé avec les droits rw-rw-r--.

Si O_EXCL est fourni, openfile échoue si le fichier existe déjà. Ce drapeau, employé en conjonction avec O_CREAT, permet d’utiliser des fichiers comme verrous (locks).1 Un processus qui veut prendre le verrou appelle openfile sur le fichier avec les modes O_EXCL et O_CREAT. Si le fichier existe déjà, cela signifie qu’un autre processus détient le verrou. Dans ce cas, openfile déclenche une erreur, et il faut attendre un peu, puis réessayer. Si le fichier n’existe pas, openfile retourne sans erreur et le fichier est créé, empêchant les autres processus de prendre le verrou. Pour libérer le verrou, le processus qui le détient fait unlink dessus. La création d’un fichier est une opération atomique: si deux processus essayent de créer un même fichier en parallèle avec les options O_EXCL et O_CREAT, au plus un seul des deux seulement peut réussir. Évidemment cette méthode n’est pas très satisfaisante car d’une part le processus qui n’a pas le verrou doit être en attente active, d’autre part un processus qui se termine anormalement peux laisser le verrou bloqué.

Exemple:

pour se préparer à lire un fichier:

     
   openfile filename [O_RDONLY] 0

Le troisième argument peut être quelconque, puisque O_CREAT n’est pas spécifié. On prend conventionnellement 0. Pour écrire un fichier à partir de rien, sans se préoccuper de ce qu’il contenait éventuellement:

     
   openfile filename [O_WRONLYO_TRUNCO_CREAT] 0o666

Si le fichier qu’on ouvre va contenir du code exécutable (cas des fichiers créés par ld), ou un script de commandes, on ajoute les droits d’exécution dans le troisième argument:

     
   openfile filename [O_WRONLYO_TRUNCO_CREAT] 0o777

Si le fichier qu’on ouvre est confidentiel, comme par exemple les fichiers “boîte aux lettres” dans lesquels mail stocke les messages lus, on le crée en restreignant la lecture et l’écriture au propriétaire uniquement:

     
   openfile filename [O_WRONLYO_TRUNCO_CREAT] 0o600

Pour se préparer à ajouter des données à la fin d’un fichier existant, et le créer vide s’il n’existe pas:

     
   openfile filename [O_WRONLYO_APPENDO_CREAT] 0o666


Le drapeau O_NONBLOCK assure que si le support est un tuyau nommé ou un fichier spécial, alors l’ouverture du fichier ainsi que les lectures et écritures ultérieur se feront en mode non bloquant.


Le drapeau O_NOCTYY assure que si le support est un terminal de contrôle (clavier, fenêtre, etc.), alors celui-ci ne devient pas le terminal de contrôle du processus appelant.


Le dernier groupe de drapeaux indique comment synchroniser les opérations de lectures et écritures. Par défaut, ces opérations ne sont pas synchronisées.

Si O_DSYNC est fourni, les données sont écrites de façon synchronisée de telle façon que la commande est bloquante et ne retourne que lorsque toutes les écritures auront été effectuées physiquement sur le support (disque en général).

Si O_SYNC est fourni, ce sont à la fois les données et les informations sur le fichier qui sont synchronisées.

Si O_RSYNC est fourni en présence de O_DSYNC les lectures des données sont également synchronisées: il est assuré que toutes les écritures en cours (demandées mais pas nécessairement enregistrées) sur ce fichier seront effectivement écrites sur le support avant la prochaine lecture. Si O_RSYNC est fourni en présence de O_SYNC cela s’applique également aux informations sur le fichier.

2.7  Lecture et écriture

Les appels systèmes read et write permettent de lire et d’écrire les octets d’un fichier. Pour des raisons historiques, l’appel système write est relevé en OCaml sous le nom single_write:

     
   val read  : file_descr -> string -> int -> int -> int
   val single_write : file_descr -> string -> int -> int -> int

Les deux appels read et single_write ont la même interface. Le premier argument est le descripteur sur lequel la lecture ou l’écriture doit avoir lieu. Le deuxième argument est une chaîne de caractères contenant les octets à écrire (cas de single_write), ou dans laquelle vont être stockés les octets lus (cas de read). Le troisième argument est la position, dans la chaîne de caractères, du premier octet à écrire ou à lire. Le quatrième argument est le nombre d’octets à lire ou à écrire. Le troisième argument et le quatrième argument désignent donc une sous-chaîne de la chaîne passée en deuxième argument. (Cette sous-chaîne ne doit pas déborder de la chaîne d’origine; read et single_write ne vérifient pas ce fait.)


L’entier renvoyé par read ou single_write est le nombre d’octets réellement lus ou écrits.

Les lectures et les écritures ont lieu à partir de la position courante de lecture/écriture. (Si le fichier a été ouvert en mode O_APPEND, cette position est placée à la fin du fichier avant toute écriture.) Cette position est avancée du nombre d’octets lus ou écrits.

Dans le cas d’une écriture, le nombre d’octets effectivement écrits est normalement le nombre d’octets demandés, mais il y a plusieurs exceptions à ce comportement: (i) dans le cas où il n’est pas possible d’écrire les octets (si le disque est plein, par exemple); (ii) lorsqu’on écrit sur un descripteur de fichiers qui référence un tuyau ou une prise placé dans le mode entrées/sorties non bloquantes, les écritures peuvent être partielles; enfin, (iii) OCaml qui fait une copie supplémentaire dans un tampon auxiliaire et écrit celui-ci limite la taille du tampon auxiliaire à une valeur maximale (qui est en général la taille utilisée par le système pour ses propres tampons) ceci pour éviter d’allouer de trop gros tampons; si le le nombre d’octets à écrire est supérieure à cette limite, alors l’écriture sera forcément partielle même si le système aurait assez de ressource pour effectuer une écriture totale.

Pour contourner le problème de la limite des tampons, OCaml fournit également une fonction write qui répète plusieurs écritures tant qu’il n’y a pas eu d’erreur d’écriture. Cependant, en cas d’erreur, la fonction retourne l’erreur et ne permet pas de savoir le nombre d’octets effectivement écrits. On utilisera donc plutôt la fonction single_write que write parce qu’elle préserve l’atomicité (on sait exactement ce qui a été écrit) et est donc plus fidèle à l’appel système d’Unix (voir également l’implémentation de single_write décrite dans le chapitre suivant 5.7).

Nous verrons dans le chapitre suivant que lorsqu’on écrit sur un descripteur de fichier qui référence un tuyau ou une prise qui est placé dans le mode entrées/sorties bloquantes et que l’appel est interrompu par un signal, l’appel single_write retourne une erreur EINTR.

Exemple:

supposant fd lié à un descripteur ouvert en écriture,

     
   write fd "Hello world!" 3 7

écrit les caractères “"lo worl"” dans le fichier correspondant, et renvoie 7.

Dans le cas d’une lecture, il se peut que le nombre d’octets effectivement lus soit strictement inférieur au nombre d’octets demandés. Premier cas: lorsque la fin du fichier est proche, c’est-à-dire lorsque le nombre d’octets entre la position courante et la fin du fichier est inférieur au nombre d’octets requis. En particulier, lorsque la position courante est sur la fin du fichier, read renvoie zéro. Cette convention “zéro égal fin de fichier” s’applique aussi aux lectures depuis des fichiers spéciaux ou des dispositifs de communication. Par exemple, read sur le terminal renvoie zéro si on frappe ctrl-D en début de ligne.

Deuxième cas où le nombre d’octets lus peut être inférieur au nombre d’octets demandés: lorsqu’on lit depuis un fichier spécial tel qu’un terminal, ou depuis un dispositif de communication comme un tuyau ou une prise. Par exemple, lorsqu’on lit depuis le terminal, read bloque jusqu’à ce qu’une ligne entière soit disponible. Si la longueur de la ligne dépasse le nombre d’octets requis, read retourne le nombre d’octets requis. Sinon, read retourne immédiatement avec la ligne lue, sans forcer la lecture d’autres lignes pour atteindre le nombre d’octets requis. (C’est le comportement par défaut du terminal; on peut aussi mettre le terminal dans un mode de lecture caractère par caractère au lieu de ligne à ligne. Voir section 2.13 ou page ?? pour avoir tous les détails.)

Exemple:

l’expression suivante lit au plus 100 caractères depuis l’entrée standard, et renvoie la chaîne des caractères lus.

     
   let buffer = String.create 100 in
   let n = read stdin buffer 0 100 in
     String.sub buffer 0 n

Exemple:

la fonction really_read ci-dessous a la même interface que read, mais fait plusieurs tentatives de lecture si nécessaire pour essayer de lire le nombre d’octets requis. Si, ce faisant, elle rencontre une fin de fichier, elle déclenche l’exception End_of_file.

     
   let rec really_read fd buffer start length =
     if length <= 0 then () else
       match read fd buffer start length with
         0 -> raise End_of_file
       | r -> really_read fd buffer (start + r) (length - r);;

2.8  Fermeture d’un descripteur

L’appel système close ferme le descripteur passé en argument.

     
   val close : file_descr -> unit

Une fois qu’un descripteur a été fermé, toute tentative de lire, d’écrire, ou de faire quoi que ce soit avec ce descripteur échoue. Il est recommandé de fermer les descripteurs dès qu’ils ne sont plus utilisés. Ce n’est pas obligatoire; en particulier, contrairement à ce qui se passe avec la bibliothèque standard Pervasives, il n’est pas nécessaire de fermer les descripteurs pour être certain que les écritures en attente ont été effectuées: les écritures faites avec write sont immédiatement transmises au noyau. D’un autre côté, le nombre de descripteurs qu’un processus peut allouer est limité par le noyau (plusieurs centaines à quelques milliers). Faire close sur un descripteur inutile permet de le désallouer, et donc d’éviter de tomber à court de descripteurs.

2.9  Exemple complet: copie de fichiers

On va programmer une commande file_copy, à deux arguments f1 et f2, qui recopie dans le fichier de nom f2 les octets contenus dans le fichier de nom f1.

     
   open Unix;;
   
   let buffer_size = 8192;;
   let buffer = String.create buffer_size;;
   
   let file_copy input_name output_name =
     let fd_in = openfile input_name [O_RDONLY] 0 in
     let fd_out = openfile output_name [O_WRONLYO_CREATO_TRUNC] 0o666 in
     let rec copy_loop () =
       match read fd_in buffer 0 buffer_size with
         0 -> ()
       | r -> ignore (write fd_out buffer 0 r); copy_loop () in
     copy_loop ();
     close fd_in;
     close fd_out;;
     
   
   let copy () =
     if Array.length Sys.argv = 3 then begin
       file_copy Sys.argv.(1) Sys.argv.(2);
       exit 0
     end else begin
       prerr_endline
         ("Usage: " ^Sys.argv.(0)^ " <input_file> <output_file>");
       exit 1
     end;;
   
   handle_unix_error copy ();;

L’essentiel du travail est fait par la fonction file_copy des lignes 6–15. On commence par ouvrir un descripteur en lecture seule sur le fichier d’entrée (ligne 7), et un descripteur en écriture seule sur le fichier de sortie (ligne 8). Le fichier de sortie est tronqué s’il existe déjà (option O_TRUNC), et créé s’il n’existe pas (option O_CREAT), avec les droits rw-rw-rw- modifiés par le masque de création. (Ceci n’est pas satisfaisant: si on copie un fichier exécutable, on voudrait que la copie soit également exécutable. On verra plus loin comment attribuer à la copie les mêmes droits d’accès qu’à l’original.) Dans les lignes 9–13, on effectue la copie par blocs de buffer_size caractères. On demande à lire buffer_size caractères (ligne 10). Si read renvoie zéro, c’est qu’on a atteint la fin du fichier d’entrée, et la copie est terminée (ligne 11). Sinon (ligne 12), on écrit les r octets qu’on vient de lire sur le fichier de destination, et on recommence. Finalement, on ferme les deux descripteurs. Le programme principal (lignes 17–24) vérifie que la commande a reçu deux arguments, et les passe à la fonction file_copy.

Toute erreur pendant la copie, comme par exemple l’impossibilité d’ouvrir le fichier d’entrée, parce qu’il n’existe pas ou parce qu’il n’est pas permis de le lire, ou encore l’échec d’une écriture par manque de place sur le disque, se traduit par une exception Unix_error qui se propage jusqu’au niveau le plus externe du programme, où elle est interceptée et affichée par handle_unix_error.

Exercice 3   Ajouter une option -a au programme, telle que file_copy -a f1 f2 ajoute le contenu de f1 à la fin de f2 si f2 existe déjà.
(Voir le corrigé)

2.10  Coût des appels système. Les tampons.

Dans l’exemple file_copy, les lectures se font par blocs de 8192 octets. Pourquoi pas octet par octet? ou mégaoctet par mégaoctet? Pour des raisons d’efficacité. La figure 2.2 montre la vitesse de copie, en octets par seconde, du programme file_copy, quand on fait varier la taille des blocs (la variable buffer_size) de 1 octet a 8 mégaoctets, en doublant à chaque fois.

Pour de petites tailles de blocs, la vitesse de copie est à peu près proportionnelle à la taille des blocs. Cependant, la quantité de données transférées est la même quelle que soit la taille des blocs. L’essentiel du temps ne passe donc pas dans le transfert de données proprement dit, mais dans la gestion de la boucle copy_loop, et dans les appels read et write. En mesurant plus finement, on voit que ce sont les appels read et write qui prennent l’essentiel du temps. On en conclut donc qu’un appel système, même lorsqu’il n’a pas grand chose à faire (read d’un caractère), prend un temps minimum d’environ 4 micro-secondes (sur la machine employée pour faire le test—un Pentium 4 à 2.8 GHz), disons 1 à 10 micro-secondes. Pour des blocs d’entrée/sortie de petite taille, c’est ce temps d’appel système qui prédomine.

Pour des blocs plus gros, entre 4K et 1M, la vitesse est constante et maximale. Ici, le temps lié aux appels systèmes et à la boucle de copie est petit devant le temps de transfert des données. D’autre part la taille du tampon devient supérieur à la tailles des caches utilisés par le système. Et le temps passé par le système à gérer le transfert devient prépondérant sur le coût d’un appel système2

Enfin, pour de très gros blocs (8M et plus), la vitesse passe légèrement au-dessous du maximum. Entre en jeu ici le temps nécessaire pour allouer le bloc et lui attribuer des pages de mémoire réelles au fur et à mesure qu’il se remplit.


Figure 2.2: Vitesse de copie en fonction de la taille des blocs

Moralité: un appel système, même s’il fait très peu de travail, coûte cher — beaucoup plus cher qu’un appel de fonction normale: en gros, de 2 à 20 micro-secondes par appel système, suivant les architectures. Il est donc important d’éviter de faire des appels système trop fréquents. En particulier, les opérations de lecture et d’écriture doivent se faire par blocs de taille suffisante, et non caractère par caractère.

Dans des exemples comme file_copy, il n’est pas difficile de faire les entrées/sorties par gros blocs. En revanche, d’autres types de programmes s’écrivent naturellement avec des entrées caractère par caractère (exemples: lecture d’une ligne depuis un fichier, analyse lexicale), et des sorties de quelques caractères à la fois (exemple: affichage d’un nombre). Pour répondre aux besoins de ces programmes, la plupart des systèmes fournissent des bibliothèques d’entrées-sorties, qui intercalent une couche de logiciel supplémentaire entre l’application et le système d’exploitation. Par exemple, en OCaml, on dispose du module Pervasives de la bibliothèque standard, qui fournit deux types abstraits in_channel et out_channel, analogues aux descripteurs de fichiers, et des opérations sur ces types, comme input_char, input_line, output_char, ou output_string. Cette couche supplémentaire utilise des tampons (buffers) pour transformer des suites de lectures ou d’écritures caractère par caractère en une lecture ou une écriture d’un bloc. On obtient donc de bien meilleures performances pour les programmes qui procèdent caractère par caractère. De plus, cette couche supplémentaire permet une plus grande portabilité des programmes: il suffit d’adapter cette bibliothèque aux appels système fournis par un autre système d’exploitation, et tous les programmes qui utilisent la bibliothèque sont immédiatement portables vers cet autre système d’exploitation.

2.11  Exemple complet: une petite bibliothèque d’entrées-sorties

Pour illustrer les techniques de lecture/écriture par tampon, voici une implémentation simple d’un fragment de la bibliothèque Pervasives de OCaml. L’interface est la suivante:

     
   type in_channel
   exception End_of_file
   val open_in : string -> in_channel
   val input_char : in_channel -> char
   val close_in : in_channel -> unit
   type out_channel
   val open_out : string -> out_channel
   val output_char : out_channel -> char -> unit
   val close_out : out_channel -> unit

Commençons par la partie “lecture”. Le type abstrait in_channel est implémenté comme suit:

     
   open Unix;;
   
   type in_channel =
     { in_bufferstring;
       in_fdfile_descr;
       mutable in_posint;
       mutable in_endint };;
   exception End_of_file

La chaîne de caractères du champ in_buffer est le tampon proprement dit. Le champ in_fd est un descripteur de fichier (Unix), ouvert sur le fichier en cours de lecture. Le champ in_pos est la position courante de lecture dans le tampon. Le champ in_end est le nombre de caractères valides dans le tampon.

Les champs in_pos et in_end vont être modifiés en place à l’occasion des opérations de lecture; on les déclare donc mutable.

     
   let buffer_size = 8192;;
   let open_in filename =
     { in_buffer = String.create buffer_size;
       in_fd = openfile filename [O_RDONLY] 0;
       in_pos = 0;
       in_end = 0 };;

À l’ouverture d’un fichier en lecture, on crée le tampon avec une taille raisonnable (suffisamment grande pour ne pas faire d’appels système trop souvent; suffisamment petite pour ne pas gâcher de mémoire), et on initialise le champ in_fd par un descripteur de fichier Unix ouvert en lecture seule sur le fichier en question. Le tampon est initialement vide (il ne contient aucun caractère du fichier); le champ in_end est donc initialisé à zéro.

     
   let input_char chan =
     if chan.in_pos < chan.in_end then begin
       let c =  chan.in_buffer.[chan.in_posin
         chan.in_pos <- chan.in_pos + 1;
         c
     end else begin
       match read chan.in_fd chan.in_buffer 0 buffer_size
       with 0 -> raise End_of_file
          | r -> chan.in_end <- r;
                 chan.in_pos <- 1;
                 chan.in_buffer.[0]
     end;;

Pour lire un caractère depuis un in_channel, de deux choses l’une. Ou bien il reste au moins un caractère dans le tampon; c’est-à-dire, le champ in_pos est strictement inférieur au champ in_end. Alors on renvoie le prochain caractère du tampon, celui à la position in_pos, et on incrémente in_pos. Ou bien le tampon est vide. On fait alors un appel système read pour remplir le tampon. Si read retourne zéro, c’est que la fin du fichier a été atteinte; on déclenche alors l’exception End_of_file. Sinon, on place le nombre de caractères lus dans le champ in_end. (On peut avoir obtenu moins de caractères que demandé, et donc le tampon peut être partiellement rempli.) Et on renvoie le premier des caractères lus.

     
   let close_in chan =
     close chan.in_fd;;

La fermeture d’un in_channel se réduit à la fermeture du descripteur Unix sous-jacent.


La partie “écriture” est très proche de la partie “lecture”. La seule dissymétrie est que le tampon contient maintenant des écritures en retard, et non plus des lectures en avance.

     
   type out_channel =
     { out_bufferstring;
       out_fdfile_descr;
       mutable out_posint };;
   
   let open_out filename =
     { out_buffer = String.create 8192;
       out_fd = openfile filename [O_WRONLYO_TRUNCO_CREAT] 0o666;
       out_pos = 0 };;
   
   let output_char chan c =
     if chan.out_pos < String.length chan.out_buffer then begin
       chan.out_buffer.[chan.out_pos] <- c;
       chan.out_pos <- chan.out_pos + 1
     end else begin
       ignore (write chan.out_fd chan.out_buffer 0 chan.out_pos);
       chan.out_buffer.[0] <- c;
       chan.out_pos <- 1
     end;;
   
   let close_out chan =
     ignore (write chan.out_fd chan.out_buffer 0 chan.out_pos);
     close chan.out_fd;;

Pour écrire un caractère sur un out_channel, ou bien le tampon n’est pas plein, et on se contente de stocker le caractère dans le tampon à la position out_pos, et d’avancer out_pos; ou bien le tampon est plein, et dans ce cas on le vide dans le fichier par un appel write, puis on stocke le caractère à écrire au début du tampon.

Quand on ferme un out_channel, il ne faut pas oublier de vider le contenu du tampon (les caractères entre les positions 0 incluse et out_pos exclue) dans le fichier. Autrement, les écritures effectuées depuis la dernière vidange seraient perdues.

Exercice 4   Implémenter une fonction
     
   val output_string out_channel -> string -> unit
qui se comporte comme une série de output_char sur chaque caractère de la chaîne, mais est plus efficace.
(Voir le corrigé)

2.12  Positionnement

L’appel système lseek permet de changer la position courante de lecture et d’écriture.

     
   val lseek : file_descr -> int -> seek_command -> int

Le premier argument est le descripteur qu’on veut positionner. Le deuxième argument est la position désirée. Il est interprété différemment suivant la valeur du troisième argument, qui indique le type de positionnement désiré:

SEEK_SETPositionnement absolu. Le deuxième argument est le numéro du caractère où se placer. Le premier caractère d’un fichier est à la position zéro.
SEEK_CURPositionnement relatif à la position courante. Le deuxième argument est un déplacement par rapport à la position courante. Il peut être négatif aussi bien que positif.
SEEK_ENDPositionnement relatif à la fin du fichier. Le deuxième argument est un déplacement par rapport à la fin du fichier. Il peut être négatif aussi bien que positif.

L’entier renvoyé par lseek est la position absolue du pointeur de lecture/écriture (après que le positionnement a été effectué).

Une erreur se déclenche si la position absolue demandée est négative. En revanche, la position demandée peut très bien être située après la fin du fichier. Juste après un tel positionnement, un read renvoie zéro (fin de fichier atteinte); un write étend le fichier par des zéros jusqu’à la position demandée, puis écrit les données fournies.

Exemple:

pour se placer sur le millième caractère d’un fichier:

     
   lseek fd 1000 SEEK_SET

Pour reculer d’un caractère:

     
   lseek fd (-1) SEEK_CUR

Pour connaître la taille d’un fichier:

     
   let file_size = lseek fd 0 SEEK_END in ...

Pour les descripteurs ouverts en mode O_APPEND, le pointeur de lecture/écriture est automatiquement placé à la fin du fichier avant chaque écriture. L’appel lseek ne sert donc à rien pour écrire sur un tel descripteur; en revanche, il est bien pris en compte pour la lecture.

Le comportement de lseek est indéterminé sur certains types de fichiers pour lesquels l’accès direct est absurde: les dispositifs de communication (tuyaux, prises), mais aussi la plupart des fichiers spéciaux (périphériques), comme par exemple le terminal. Dans la plupart des implémentations d’Unix, un lseek sur de tels fichiers est simplement ignoré: le pointeur de lecture/écriture est positionné, mais les opérations de lecture et d’écriture l’ignorent. Sur certaines implémentations, lseek sur un tuyau ou sur une prise déclenche une erreur.

Exercice 5   La commande tail affiche les N dernières lignes d’un fichier. Comment l’implémenter efficacement si le fichier en question est un fichier normal? Comment faire face aux autres types de fichiers? Comment ajouter l’option -f? (cf. man tail).
(Voir le corrigé)

2.13  Opérations spécifiques à certains types de fichiers

En Unix, la communication passe par des descripteurs de fichiers que ceux-ci soient matérialisés (fichiers, périphériques) ou volatiles (communication entre processus par des tuyaux ou des prises). Cela permet de donner une interface uniforme à la communication de données, indépendante du média. Bien sûr, l’implémentation des opérations dépend quant à elle du média. L’uniformité trouve ses limites dans la nécessité de donner accès à toutes les opérations offertes par le média. Les opérations générales (ouverture, écriture, lecture, etc.) restent uniformes sur la plupart des descripteurs mais certaines opérations ne fonctionnent que sur certains types de fichiers. En revanche, pour certains types de fichiers dits spéciaux, qui permettent de traiter la communication avec les périphériques, même les opérations générales peuvent avoir un comportement ad-hoc défini par le type et les paramètres du périphérique.

Fichiers normaux

On peut raccourcir un fichier ordinaire par les appels suivants:

     
   val truncate  : string -> int -> unit
   val ftruncate : file_descr -> int -> unit

Le premier argument désigne le fichier à tronquer (par son nom, ou via un descripteur ouvert sur ce fichier). Le deuxième argument est la taille désirée. Toutes les données situées à partir de cette position sont perdues.

Liens symboliques

La plupart des opérations sur fichiers “suivent” les liens symboliques: c’est-à-dire, elles s’appliquent au fichier vers lequel pointe le lien symbolique, et non pas au lien symbolique lui-même. Exemples: openfile, stat, truncate, opendir. On dispose de deux opérations sur les liens symboliques:

     
   val symlink  : string -> string -> unit
   val readlink : string -> string

L’appel symlink   f1   f2 créé le fichier f2 comme étant un lien symbolique vers f1. (Comme la commande ln -s   f1   f2.) L’appel readlink renvoie le contenu d’un lien symbolique, c’est-à-dire le nom du fichier vers lequel il pointe.

Fichiers spéciaux

Les fichiers spéciaux peuvent être de type caractère ou de type block. Les premiers sont des flux de caractères: on ne peut lire ou écrire les caractères que dans l’ordre. Ce sont typiquement les terminaux, les périphériques sons, imprimantes, etc. Les seconds, typiquement les disques, ont un support rémanent ou temporisé: on peut lire les caractères par blocs, voir à une certaine distance donnée sous forme absolue ou relative par rapport à la position courante. Parmi les fichiers spéciaux, on peut distinguer:

/dev/null
C’est le trou noir qui avale tout ce qu’on met dedans et dont il ne sort rien. Très utile pour ignorer les résultats d’un processus: on redirige sa sortie vers /dev/null (voir le chapitre 5).
/dev/tty*
Ce sont les terminaux de contrôle.
/dev/pty*
Ce sont les pseudo-terminaux de contrôle: ils ne sont pas de vrais terminaux mais les simulent (ils répondent à la même interface).
/dev/hd*
Ce sont les disques.
/proc
Sous Linux, permet de lire et d’écrire certains paramètres du système en les organisant comme un système de fichiers.

Les fichiers spéciaux ont des comportements assez variables en réponse aux appels système généraux sur fichiers. La plupart des fichiers spéciaux (terminaux, lecteurs de bandes, disques, …) obéissent à read et write de la manière évidente (mais parfois avec des restrictions sur le nombre d’octets écrits ou lus). Beaucoup de fichiers spéciaux ignorent lseek.

En plus des appels systèmes généraux, les fichiers spéciaux qui correspondent à des périphériques doivent pouvoir être paramétrés ou commandés dynamiquement. Exemples de telles possibilités: pour un dérouleur de bande, le rembobinage ou l’avance rapide; pour un terminal, le choix du mode d’édition de ligne, des caractères spéciaux, des paramètres de la liaison série (vitesse, parité, etc). Ces opérations sont réalisées en Unix par l’appel système ioctl qui regroupe tous les cas particuliers. Cependant, cet appel système n’est pas relevé en OCaml... parce qu’il est mal défini et ne peut pas être traité de façon uniforme.

Terminaux de contrôle

Les terminaux (ou pseudo-terminaux) de contrôle sont un cas particulier de fichiers spéciaux de type caractère pour lequel OCaml donne accès à la configuration. L’appel tcgetattr prend en argument un descripteur de fichier ouvert sur le fichier spécial en question et retourne une structure de type terminal_io qui décrit le statut du terminal représenté par ce fichier selon la norme POSIX (Voir page ?? pour une description complète).

     
   val tcgetattr : file_descr -> terminal_io
     
   type terminal_io =
     { c_ignbrk : boolc_brk_int : bool; ...;  c_vstop : char }

Cette structure peut être modifiée puis passée à la fonction tcsetattr pour changer les attributs du périphérique.

     
   val tcsetattr : file_descr -> setattr_when -> terminal_io -> unit

Le premier argument est le descripteur de fichier désignant le périphérique. Le dernier argument est une structure de type tcgetattr décrivant les paramètres du périphérique tels qu’on veut les établir. Le second argument est un drapeau du type énuméré setattr_when indiquant le moment à partir duquel la modification doit prendre effet: immédiatement (TCSANOW), après avoir transmis toutes les données écrites (TCSADRAIN) ou après avoir lu toutes les données reçues (TCAFLUSH). Le choix TCSADRAIN est recommandé pour modifier les paramètres d’écriture et TCSAFLUSH pour modifier les paramètres de lecture. Exemple:

Pendant la lecture d’un mot de passe, il faut retirer l’écho des caractères tapés par l’utilisateur si le flux d’entrée standard est connecté à un terminal ou pseudo-terminal.

     
   let read_passwd message =
     match
       try
         let default = tcgetattr stdin in
         let silent =
           { default with
             c_echo = false;
             c_echoe = false;
             c_echok = false;
             c_echonl = false;
           } in
         Some (defaultsilent)
       with _ -> None
     with
     | None -> input_line Pervasives.stdin
     | Some (defaultsilent) ->
         print_string message;
         flush Pervasives.stdout;
         tcsetattr stdin TCSANOW silent;
         try
           let s = input_line Pervasives.stdin in
           tcsetattr stdin TCSANOW defaults
         with x ->
           tcsetattr stdin TCSANOW defaultraise x;;

La fonction read_passwd commence par récupérer la valeur par défaut des paramètres du terminal associé à stdin et construire une version modifiée dans laquelle les caractères n’ont plus d’écho. En cas d’échec, c’est que le flux d’entrée n’est pas un terminal de contrôle, on se contente de lire une ligne. Sinon, on affiche un message, on change le terminal, on lit la réponse et on remet le terminal dans son état normal. Il faut faire attention à bien remettre le terminal dans son état normal également lorsque la lecture a échoué.

Il arrive qu’une application ait besoin d’en lancer une autre en liant son flux d’entrée à un terminal (ou pseudo terminal) de contrôle. Le système OCaml ne fournit pas d’aide pour cela3: il faut manuellement rechercher parmi l’ensemble des pseudo-terminaux (en général, ce sont des fichiers de nom de la forme /dev/tty[a-z][a-f0-9]) et trouver un de ces fichiers qui ne soit pas déjà ouvert, pour l’ouvrir puis lancer l’application avec ce fichier en flux d’entrée.

Quatre autres fonctions permettent de contrôler le flux (vider les données en attente, attendre la fin de la transmission, relancer la communication).

     
   val tcsendbreak : file_descr -> int -> unit

La fonction tcsendbreak envoie une interruption au périphérique. Son deuxième argument est la durée de l’interruption (0 étant interprété comme la valeur par défaut pour le périphérique).

     
   val tcdrain : file_descr -> unit

La fonction tcdrain attend que toutes les données écrites aient été transmises.

     
   val tcflush : file_descr -> flush_queue -> unit

Selon la valeur du drapeau passé en second argument, la fonction tcflush abandonne les données écrites pas encore transmises (TCIFLUSH), ou les données reçues mais pas encore lues (TCOFLUSH) ou les deux (TCIOFLUSH).

     
   val tcflow : file_descr -> flow_action -> unit

Selon la valeur du drapeau passé en second argument, la fonction tcflow suspend l’émission (TCOOFF), redémarre l’émission (TCOON), envoie un caractère de contrôle STOP ou START pour demander que la transmission soit suspendue (TCIOFF) ou relancée (TCION).

     
   val setsid : unit -> int

La fonction setsid place le processus dans une nouvelle session et le détache de son terminal de contrôle.

2.14  Verrous sur des fichiers

Deux processus peuvent modifier un même fichier en parallèle au risque que certaines écritures en écrasent d’autres. Dans certains cas, l’ouverture en mode O_APPEND permet de s’en sortir, par exemple, pour un fichier de log où on se contente d’écrire des informations toujours à la fin du fichier. Mais ce mécanisme ne résout pas le cas plus général où les écritures sont à des positions a priori arbitraires, par exemple, lorsqu’un fichier représente une base de données . Il faut alors que les différents processus utilisant ce fichier collaborent ensemble pour ne pas se marcher sur les pieds. Un verrouillage de tout le fichier est toujours possible en créant un fichier verrou auxiliaire (voir page ??). L’appel système lockf permet une synchronisation plus fine qui en ne verrouillant qu’une partie du fichier.

2.15  Exemple complet: copie récursive de fichiers

On va étendre la commande file_copy pour copier, en plus des fichiers normaux, les liens symboliques et les répertoires. Pour les répertoires, on copie récursivement leur contenu.

On commence par récupérer la fonction file_copy de l’exemple du même nom pour copier les fichiers normaux (page ??).

     
   open Unix
     
   ...
     
   let file_copy input_name output_name =
     
   ...

La fonction set_infos ci-dessous modifie le propriétaire, les droits d’accès et les dates de dernier accès/dernière modification d’un fichier. Son but est de préserver ces informations pendant la copie.

     
   let set_infos filename infos =
     utimes filename infos.st_atime infos.st_mtime;
     chmod filename infos.st_perm;
     try
       chown filename infos.st_uid infos.st_gid
     with Unix_error(EPERM,_,_) ->
       ()

L’appel système utime modifie les dates d’accès et de modification. On utilise chmod et chown pour rétablir les droits d’accès et le propriétaire. Pour les utilisateurs normaux, il y a un certain nombres de cas où chown va échouer avec une erreur “permission denied”. On rattrape donc cette erreur là et on l’ignore.

Voici la fonction récursive principale.

     
   let rec copy_rec source dest =
     let infos = lstat source in
     match infos.st_kind with
       S_REG ->
         file_copy source dest;
         set_infos dest infos
     | S_LNK ->
         let link = readlink source in
         symlink link dest
     | S_DIR ->
         mkdir dest 0o200;
         Misc.iter_dir
           (fun file ->
             if file <> Filename.current_dir_name
                 && file <> Filename.parent_dir_name
             then
               copy_rec
                 (Filename.concat source file)
                 (Filename.concat dest file))
           source;
         set_infos dest infos
     | _ ->
         prerr_endline ("Can't cope with special file " ^ source)

On commence par lire les informations du fichier source. Si c’est un fichier normal, on copie son contenu avec file_copy, puis ses informations avec set_infos. Si c’est un lien symbolique, on lit ce vers quoi il pointe, et on crée un lien qui pointe vers la même chose. Si c’est un répertoire, on crée un répertoire comme destination, puis on lit les entrées du répertoire source (en ignorant les entrées du répertoire vers lui-même Filename.current_dir_name et vers son parent Filename.parent_dir_name, qu’il ne faut certainement pas copier), et on appelle récursivement copy pour chaque entrée. Les autres types de fichiers sont ignorés, avec un message d’avertissement.

Le programme principal est sans surprise:

     
   let copyrec () =
     if Array.length Sys.argv <> 3 then begin
       prerr_endline ("Usage: " ^Sys.argv.(0)^ " <source> <destination>");
       exit 2
     end else begin
       copy_rec Sys.argv.(1) Sys.argv.(2);
       exit 0
     end
   ;;
   handle_unix_error copyrec ();;
Exercice 6   Copier intelligemment les liens durs. Tel que présenté ci-dessus, copyrec duplique N fois un même fichier qui apparaît sous N noms différents dans la hiérarchie de fichiers à copier. Essayer de détecter cette situation, de ne copier qu’une fois le fichier, et de faire des liens durs dans la hiérarchie de destination.
(Voir le corrigé)

2.16  Exemple: Tape ARchive

Le format tar (pour tape archive) permet de représenter un ensemble de fichiers en un seul fichier. (Entre autre il permet de stocker toute une hiérarchie de fichiers sur une bande.) C’est donc d’une certaine façon un mini système de fichiers.

Dans cette section nous décrivons un ensemble de fonctions qui permettent de lire et d’écrire des archives au format tar. La première partie, décrite complètement, consiste à écrire une commande readtar telle que readtar a affiche la liste des fichiers contenus dans l’archive a et readtar a f affiche le contenu du fichier f contenu dans l’archive a. Nous proposons en exercice l’extraction de tous les fichiers contenus dans une archive, ainsi que la fabrication d’une archive à partir d’un ensemble de fichiers.

Description du format

Une archive tar est une suite d’enregistrements, chaque enregistrement représentant un fichier. Un enregistrement est composé d’un entête qui code les informations sur le fichier (son nom, son type, sa taille, son propriétaire etc.) et du contenu du fichier. L’entête est représenté sur un bloc (512 octets) comme indiqué dans le tableau 2.2.


OffsetLength1Codage2  Nom  Description
0  100  chaîne  name  Nom du fichier
100  8  octal  perm  Mode du fichier
108  8  octal  uid  ID de l’utilisateur
116  8  octal  gid  ID du groupe de l’utilisateur
124  12  octal  size  Taille du fichier3
136  12  octal  mtime  Date de la dernière modification
148  8  octal  checksum  Checksum de l’entête
156  1  caractère  kind  Type de fichier
157  100  octal  link  Lien
257  8  chaîne  magic  Signature ("ustar\032\032\0")
265  32  chaîne  user  Nom de l’utilisateur
297  32  chaîne  group  Nom du groupe de l’utilisateur
329  8  octal  major  Identificateur majeur du périphérique
337  8  octal  minor  Identificateur mineur du périphérique
345  167       Padding
1 en octets.
2
tous les champs sont codés sur des chaînes de caractères et
terminés par le caractère nul ’\000’, sauf les champs kind (Type de fichier) et
le champ size (Taille du fichier) (’\000’ optionnel).
  
Tableau 2.2: Représentation de l’entête

Le contenu est représenté à la suite de l’entête sur un nombre entier de blocs. Les enregistrements sont représentés les uns à la suite des autres. Le fichier est éventuellement complété par des blocs vides pour atteindre au moins 20 blocs.

Comme les archives sont aussi conçues pour être écrites sur des supports fragiles et relues plusieurs années après, l’entête comporte un champ checksum qui permet de détecter les archives dont l’entête est endommagé (ou d’utiliser comme une archive un fichier qui n’en serait pas une.) Sa valeur est la somme des codes des caractères de l’entête (pendant ce calcul, on prend comme hypothèse que le le champ checksum, qui n’est pas encore connu est composé de blancs et terminé par le caractère nul).

Le champ kind représente le type des fichiers sur un octet. Les valeurs significatives sont les caractères indiqués dans le tableau ci-dessous4:

’\0’ ou ’0’’1’’2’’3’’4’’5’’6’’7’
REGLINKLNKCHRBLKDIRFIFOCONT

La plupart des cas correspondent au type st_link des types de fichier Unix. Le cas LINK représente des liens durs: ceux-ci ont le même nœud (inode) mais accessible par deux chemins différents; dans ce cas, le lien doit obligatoirement mener à un autre fichier déjà défini dans l’archive. Le cas CONT représente un fichier ordinaire, mais qui est représenté par une zone mémoire contigüe (c’est une particularité de certains systèmes de fichiers, on pourra donc le traiter comme un fichier ordinaire). Le champ link représente le lien lorsque kind vaut LNK ou LINK. Les champs major et minor représentent les numéros majeur et mineur du périphérique dans le cas où le champ kind vaut CHR ou BLK. Ces trois champs sont inutilisés dans les autres cas.

La valeur du champ kind est naturellement représentée par un type concret et l’entête par un enregistrement:

     
   open Sys
   open Unix
   
   type kind =
     | REG
     | LNK of string
     | LINK of string
     | CHR of int * int
     | BLK of int * int
     | DIR
     | FIFO
     | CONT
     
   type header = {
       name : string;
       perm : int;
       uid : int;
       gid : int;
       size : int;
       mtime : int;
       kind : kind;
       user : string;
       group : string
    }
Lecture d’un entête

La lecture d’un entête n’est pas très intéressante, mais elle est incontournable.

     
   exception Error of string * string
   let error err mes = raise (Error (errmes));;
   let handle_error f s =
     try f s with
     | Error (errmes) ->
         Printf.eprintf "Error: %s: %s" err mes;
         exit 2
   
   let substring s offset len =
     let max_length = min (offset + len + 1) (String.length sin
     let rec real_length j =
       if j < max_length && s.[j] <> '\000' then real_length (succ j)
       else j - offset in
     String.sub s offset (real_length offset);;
   
   let integer_of_octal nbytes s offset =
     let i = int_of_string ("0o" ^ substring s offset nbytesin
     if i < 0 then error "Corrupted archive" "integer too large" else i;;
   
   let kind s i =
     match s.[iwith
       '\000' | '0' -> REG
     | '1' -> LINK (substring s (succ i) 99)
     | '2' -> LNK (substring s (succ i) 99)
     | '3' -> CHR (integer_of_octal 8 s 329, integer_of_octal 8 s 329)
     | '4' -> BLK (integer_of_octal 8 s 329, integer_of_octal 8 s 337)
     | '5' -> DIR | '6' -> FIFO | '7' -> CONT
     | _ -> error "Corrupted archive" "kind"
   
   let header_of_string s =
     { name = substring s 0 99;
       perm = integer_of_octal 8 s 100;
       uid = integer_of_octal 8 s 108;
       gid = integer_of_octal 8 s 116;
       size = integer_of_octal 12 s 124;
       mtime = integer_of_octal 12 s 136;
       kind = kind s 156;
       user = substring s 265 32;
       group = substring s 297 32;
     }
   
   let block_size = 512;;
   let total_size size =
     block_size + ((block_size -1 + size) / block_size) * block_size;;

La fin de l’archive s’arrête soit sur une fin de fichier là ou devrait commencer un nouvel enregistrement, soit sur un bloc complet mais vide. Pour lire l’entête, nous devons donc essayer de lire un bloc, qui doit être vide ou complet. Nous réutilisons la fonction really_read définie plus haut pour lire un bloc complet. La fin de fichier ne doit pas être rencontrée en dehors de la lecture de l’entête.

     
   let buffer_size = block_size;;
   let buffer = String.create buffer_size;;
   
   let end_of_file_error() =
     error "Corrupted archive" "unexpected end of file"
   let without_end_of_file f x =
     try f x with End_of_file -> end_of_file_error()
   
   let read_header fd =
     let len = read fd buffer 0 buffer_size in
     if len = 0 ||  buffer.[0] = '\000' then None
     else begin
       if len < buffer_size then
         without_end_of_file (really_read fd buffer len) (buffer_size - len);
       Some (header_of_string buffer)
     end;;
Lecture d’une archive

Pour effectuer une quelconque opération dans une archive, il est nécessaire de lire l’ensemble des enregistrements dans l’ordre au moins jusqu’à trouver celui qui correspond à l’opération à effectuer. Par défaut, il suffit de lire l’entête de chaque enregistrement, sans avoir à en lire le contenu. Souvent, il suffira de lire le contenu de l’enregistrement recherché ou de lire le contenu après coup d’un enregistrement le précédent. Pour cela, il faut garder pour chaque enregistrement une information sur sa position dans l’archive, en plus de son entête. Nous utilisons le type suivant pour les enregistrements:

     
   type record = { header : headeroffset : intdescr : file_descr };;

Nous allons maintenant écrire un itérateur général qui lit les enregistrements (sans leur contenu) et les accumulent. Toutefois, pour être général, nous restons abstrait par rapport à la fonction d’accumulation f (qui peut aussi bien ajouter les enregistrements à ceux déjà lus, les imprimer, les jeter, etc.)

     
   let fold f initial fd  =
     let rec fold_aux offset accu =
       ignore (without_end_of_file (lseek fd offsetSEEK_SET);
       match without_end_of_file read_header fd with
         Some h ->
           let r =
             { header = hoffset = offset + block_sizedescr = fd } in
           fold_aux (offset + total_size h.size) (f r accu)
       | None -> accu in
     fold_aux 0 initial;;

Une étape de fold_aux commence à une position offset avec un résultat partiel accu. Elle consiste à se placer à la position offset, qui doit être le début d’un enregistrement, lire l’entête, construire l’enregistrement r puis recommencer à la fin de l’enregistrement avec le nouveau résultat f r accu (moins partiel). On s’arrête lorsque l’entête est vide, ce qui signifie qu’on est arrivé à la fin de l’archive.

Affichage de la liste des enregistrements d’une archive

Il suffit simplement d’afficher l’ensemble des enregistrements, au fur et à mesure, sans avoir à les conserver:

     
   let list tarfile =
     let fd = openfile tarfile [ O_RDONLY ] 0o0 in
     let add r () = print_string r.header.nameprint_newline() in
     fold add () fd;
     close fd
Affichage du contenu d’un fichier dans une archive

La commande readtar  a  f doit rechercher le fichier de nom f dans l’archive et l’afficher si c’est un fichier régulier. De plus un chemin f de l’archive qui est un lien dur et désigne un chemin g de l’archive est suivi et le contenu de g est affiché: en effet, bien que f et g soient représentés différemment dans l’archive finale (l’un est un lien dur vers l’autre) ils désignaient exactement le même fichier à sa création. Le fait que g soit un lien vers f ou l’inverse dépend uniquement de l’ordre dans lequel les fichiers ont été parcourus à la création de l’archive. Pour l’instant nous ne suivons pas les liens symboliques.

L’essentiel de la résolution des liens durs est effectué par les deux fonctions suivantes, définies récursivement.

     
   let rec find_regular r list =
     match r.header.kind with
     | REG | CONT -> r
     | LINK name -> find_file name list
     | _ -> error r.header.name "Not a regular file"
   and find_file name list =
     match list with
       r :: rest ->
         if r.header.name = name then find_regular r rest
         else find_file name rest
     | [] -> error name "Link not found (corrupted archive)";;

La fonction find_regular trouve le fichier régulier correspondant à un enregistrement (son premier) argument. Si celui-ci est un fichier régulier, c’est gagné. Si c’est un fichier spécial (ou un lien symbolique), c’est perdu. Il reste le cas d’un lien dur: la fonction recherche ce lien dans la liste des enregistrements de l’archive (deuxième argument) en appelant la fonction find_file.

Un fois l’enregistrement trouvé il n’y a plus qu’à afficher son contenu. Cette opération ressemble fortement à la fonction file_copy, une fois le descripteur positionné au début du fichier dans l’archive.

     
   let copy_file file output =
     ignore (lseek file.descr file.offset SEEK_SET);
     let rec copy_loop len =
       if len > 0 then
         match read file.descr buffer 0 (min buffer_size lenwith
           0 -> end_of_file_error()
         | r -> ignore (write output buffer 0 r); copy_loop (len-rin
     copy_loop file.header.size

Il ne reste plus qu’à combiner les trois précédents.

     
   exception Done
   let find_and_copy tarfile filename =
     let fd = openfile tarfile [ O_RDONLY ] 0o0 in
     let found_or_collect r accu =
       if r.header.name = filename then begin
         copy_file (find_regular r accustdout;
         raise Done
       end else r :: accu in
     try
        ignore (fold found_or_collect [] fd);
        error "File not found" filename
     with
     | Done -> close fd

On lit les enregistrements de l’archive (sans lire leur contenu) jusqu’à rencontrer un enregistrement du nom recherché. On appelle la fonction find_regular pour rechercher dans la liste des enregistrements lus celui qui contient vraiment le fichier. Cette seconde recherche, en arrière, doit toujours réussir si l’archive est bien formée. Par contre la première recherche, en avant, va échouer si le fichier n’est pas dans l’archive. Nous avons pris soin de distinguer les erreurs dues à une archive corrompue ou à une recherche infructueuse.

Et voici la fonction principale qui réalise la commande readtar:

     
   let readtar () =
     let nargs = Array.length Sys.argv in
     if nargs = 2 then list Sys.argv.(1)
     else if nargs = 3 then find_and_copy Sys.argv.(1) Sys.argv.(2)
     else
       prerr_endline ("Usage: " ^Sys.argv.(0)^ " <tarfile> [ <source> ]");;
   
   handle_unix_error (handle_error readtar) ();;
Exercice 7   Étendre la commande readtar pour qu’elle suive les liens symboliques, c’est-à-dire pour qu’elle affiche le contenu du fichier si l’archive avait été au préalable extraite et si ce fichier correspond à un fichier de l’archive.

Derrière l’apparence triviale de cette généralisation se cachent quelques difficultés, car les liens symboliques sont des chemins quelconques qui peuvent ne pas correspondre exactement à des chemins de l’archive ou carrément pointer en dehors de l’archive (ils peuvent contenir . . ). De plus, les liens symboliques peuvent désigner des répertoires (ce qui est interdit pour les liens durs).

(Voir le corrigé)
Exercice 8   Écrire une commande untar telle que untar a extrait et crée tous les fichiers de l’archive a (sauf les fichiers spéciaux) en rétablissant si possible les informations sur les fichiers (propriétaires, droits d’accès) indiqués dans l’archive.

L’arborescence de l’archive ne doit contenir que des chemins relatifs, sans jamais pointer vers un répertoire parent (donc sans pourvoir pointer en dehors de l’archive), mais on devra le détecter et refuser de créer des fichiers ailleurs que dans un sous-répertoire du répertoire courant. L’arborescence est reconstruite à la position où l’on se trouve à l’appel de la commande untar. Les répertoires non mentionnés explicitement dans l’archive qui n’existent pas sont créés avec les droits par défaut de l’utilisateur qui lance la commande.

(Voir le corrigé)
Exercice 9 (Projet)   Écrire une commande tar telle que tar -xvf a f1 f2 … construise l’archive a à partir de la liste des fichiers f1, f2, etc. et de leurs sous-répertoires.
(Voir le corrigé)

1
Ce n’est pas possible si le fichier verrou réside sur une partition NFS, car NFS n’implémente pas correctement l’option O_CREAT de open.
2
En fait, OCaml limite la tailles des données transférées à 16K (dans la version courante) en répétant plusieurs appels système write pour effectuer le transfert complet—voir la discussion la section 5.7. Mais cette limite est au delà de la taille des caches du système et n’est pas observable.
3
La bibliothèque de Cash [3] fournit de telles fonctions.
4
Ce champ peut également prendre d’autres valeurs pour coder des cas pathologiques, par exemple lorsque la valeur d’un champ déborde la place réservée dans l’entête, ou dans des extensions de la commande tar.

Previous Up Next