1. Introduction

Sous UNIX toutes les Entrées/Sorties sont traitées de la même façon à travers la notion de fichier. C'est l'une des caractéristiques majeures d'UNIX. La notion d'enregistrement au sens classique n'existe pas , sinon elle se limite au caractère, et qu'un fichier est donc vu comme un flot d'octets (stream) sans structure. Cette dernière revenant aux applications utilisateurs, dont en particulier, les fonctions bibliothèques. C'est ainsi, qu'un fichier peut être un terminal, un fichier classique sur disque ou même une ressource particulière gérée en mode First-In-First-Out qu'est un tube.

UNIX justement distingue entre les divers types de fichiers par la sémantique des opérations qui leur sont applicables.

Se référer à un manuel UNIX pour plus de détails. Notons seulement que les fichiers spéciaux sont lus en deux modes:

  1. mode bloc pour les disques et
  2. mode caractère pour les terminaux par exemple.

La distinction réside dans les transferts au niveau système qui, dans le mode bloc, se font par blocs de 512 ou 1024 caractères et transitent donc par les caches système. Dans ce qui suit, on considérera que les fichiers réguliers sauf indication contraire.

En C, il y a deux niveaux de traitement de fichiers:

  • Le niveau des primitives de bases (creat(), open(), close(), read(), write(), lseek() ...) qui sont des appels systèmes qui permettent de lire ou écrire des fichiers par blocs de 1 ou plusieurs caractères.
  • Des fonctions bibliothèques plus abstraites comme fopen(), fread(), fwrite(), printf(), fgetc(), fprintf() ...qui se trouvent dans la bibliothèques Entrées/Sorties Standard stdio.h (écrite à l'origine par D. RITCHIE) et qui sont de plus haut niveau et contiennent des fonctionnalités plus nombreuses. Elles offrent par ailleurs des performances meilleures, les appels systèmes y sont gardés au strict minimum. Certaines sont explicitement programmées et d'autres sont macro-définies. Elles présentent une interface uniforme indépendante du système d'exploitation sous-jacent et donc favorables à l'écriture de programmes portables. (Ces fonctions sont réécrites une fois pour toute pour une machine hôte, en fonction des facilités offertes par cette machine).

Un fichier C est donc un flot d'octets. Et comme tel on peut y écrire tout ce qu'on veut. Cela par opposition à d'autres langages où les fichiers sont typés, i.e. on n'y écrit que des objets d'un même type (un record en général). En Pascal par exemple on a file of <typeObjet>. Ainsi, en C, on peut conserver/retrouver tout objet dans un fichier (objet ne contenant pas de pointeurs toutefois). Il suffit pour cela de fournir l'adresse et la taille de la zone mémoire contenant l'objet. Le transfert se fera de cette zone vers le fichier pour l'opération d'écriture (write) ou, inversement, du fichier vers cette zone pour l'opération de lecture (read). On retrouve ce mécanisme aux deux niveaux de traitement des fichiers.

2. Accès à Un Fichier

Un fichier est accessible à travers un descripteur local à un processus qui permet de le référencer indépendamment de son nom physique. Ce descripteur permet d'accéder à une table qui indique l'ensemble des fichiers ouverts. Plus exactement, chaque processus possède une table de descripteurs, un pour chaque fichier ouvert, selon le schéma:

Points à noter:

  1. L'indice dans la table des descripteurs est en fait le descripteur lui-même (e.g. 0, 1, 2 sont les descripteurs (indices) associés à l'entrée standard stdin, la sortie standard stdout et la sortie erreur standard stderr).

     

  2. Chaque entrée dans cette table contient un pointeur vers la table de fichiers ouverts du système UNIX qui, quand à elle, est composées d'une entrée par fichier disque ouvert. Chacune de ces entrées a la structure FILE (prédéfinie dans <sys/file.h>) dont les principales informations sont:
    • Le nombre total de descripteurs correspondants au fichier disque ouvert, (e.g. cas de plusieurs processus partageant un même fichier hérité)
    • Le mode d'ouverture (lire "r", écrire "w", lire/écrire "rw")
    • Position (dite offset) courante en lecture/écriture, i.e. prochain caractère en E/S
    • Pointeur vers un I_Node (Index_Node) qui est un élément de la table d'allocation des fichiers d'un disque. Cette dernière, est la table (cf. FAT, VTOC, ...) qui se trouve dans les premiers blocs d'un disque logique, et qui décrit l'ensemble des fichiers de ce disque. Elle est en partie chargée en mémoire au demarrage.

Un processus hérite dès sa naissance d'une copie de la table des descripteurs du père. C'est ainsi par exemple que tout programme récupère systématiquement les Entrées/Sorties standard stdin, stdout et stderr du processus shell de lancement et qui ont respectivement pour descripteurs 0, 1 et 2 (constantes symboliques STDIN_FILENOSTDOUT_FILENO et STDERR_FILENO si elles existent).

Un fichier est désigné dans un programme par une variable qu'habituellement on considère comme le nom logique du fichier, par opposition à son nom physique sur disque. En cas de manipulation directe par les primitives de base, cette variable est du type entier et contient le descripteur du fichier et en cas d'usage de la bibliothèque <stdio.h> cette variable est du type FILE*, un pointeur vers toute une structure qui contient beaucoup plus d'informations, notamment le vrai descripteur entier et un tampon cache (buffer) de taille BUFSIZE. En effet, comme ces fonctions sont de haut niveau, elles utilisent une structure plus appropriée. A ce propos, elles ont par exemple leur propre tampon cache d'E/S qui se trouvera donc, comme une donnée, dans l'espace d'adressage du processus (les primitives de base travaillant elles sur le cache système). D'où la présence d'une fonction fflush() et la nécessité de vider le tampon utilisateur dans le vrai fichier (ou le cache system du moins). Dans la suite, on va en faire abstraction, car cela est transparent.

Remarque: On peut vider le cache système, par la primitive sync(), ou la commande UNIX homologue sync.

Nous allons commencer par examiner les primitives de base avant de présenter les E/S Standards.

3. Les Primitives de Base

Nous allons donc commencer par voir les primitives fondamentales que UNIX offre pour manipuler des fichiers par programmes, et qui sont listées dans la table suivante:

 

Les Primitives Fondamentales
d'Accès à un Fichier

 

Nom

Description

open()
creat()
close()
read()
write()
lseek()
unlink()
rename()

Ouvre un fichier
Crée un fichier vide
Ferme un fichier
Lit dans un fichier
Ecrit dans un fichier
Se positionne sur un octet
Supprime un fichier
Renomme un fichier


 

Dans un souci de portabilité, un certain nombre de fichiers #include est associé à ces manipulations, et fournissent principalement des types et des consantes symboliques. On notera en particulier:

#include <sys/types.h>   /* types POSIX mode_t off_t size_t... */
#include <sys/stat.h>    /* status des E/S */
#include <fcntl.h>       /* Constantes POSIX O_RDONLY... */

3.1. Création de Fichier: creat()

Avant d'utiliser un fichier il faut d'abord qu'il existe déjà sur le support concerné et/ou qu'une entrée lui soit allouée dans la table des fichiers ouverts.

La fonction creat() --sic--

int creat (nom, pmode)
char *nom, mode_t pmode;

permet de créer un nouveau fichier (ou de l'écraser s'il existe) appelé nom et retourne son descripteur. Sinon la valeur retour est -1.

  • nom est une chaîne qui indique le nom physique du fichier.

     

  • pmod, de type mode_t (macro-défini short), est une protection initiale associée au fichier créé. Si le fichier existe déjà, il garde son ancien mode. Ce mode (c.f. commande UNIX chmod) définit d'une part les droits d'accès usergroup et other et d'autre part les privilèges readwrite et execute.

Un moyen pratique pour spécifier ce mode est une constante octale. 0755 par exemple pour readwrite et execute au propriétaire, read et execute pour le groupe et les autres. c.f. la partie mode "-rwxr-xr-x" dans la commande UNIX "ls -l".

rwx r-x r-x
111 101 101
 7   5   5

Exemple:

int fd;
fd = creat("monFichier",0644);

crée un fichier nouveau, de nom "monFichier" et lui associe la protection "-rw-r--r--", assez standard: lecture pour tous, lecture/écriture pour le propriétaire.

3.2. Ouverture de Fichier: open()

La primitive open() réalise l'opération d'ouverture d'un fichier en allouant une nouvelle entrée dans la table des fichiers ouverts (si elle n'est pas déjà allouée) et retourne son descripteur.

int open(nom, mode_ouverture [,pmode])
char *nom, int mode_ouverture, mode_t pmode;

Les paramètres sont:

  • nom est le nom physique du fichier à ouvrir.

     

  • mode_ouvertur est un entier indiquant le mode d'ouverture. Pour les cas simples, il est égal à 0 pour la lecture seule (read), 1 pour l'écriture seule (write) et 2 pour la lecture et l'écriture, c'est à dire la mise à jour (read/write).

     

  • pmode est la protection initiale, comme pour creat, si l'ouverture est aussi création de fichier (constante O_CREAT dans mode d'ouverture).


On peut utiliser les constantes standards symboliques (dites drapeau ou flag) macro-définies dans <fcntl.h>

O_RDONLY pour 0 donc lecture seule (ReaDONLY),

O_WRONLY pour 1 donc écriture seule (WRiteONLY) et

O_RDWR pour 2 donc lecture et écriture (ReaDWRite).

Par ailleurs, on peut demander une paramétrisation plus poussée pour l'ouverture en faisant une disjonction or bit à bit (opérateur |) d'une de ces trois constantes avec d'autres constantes drapeau de <fcntl.h>. Parmi ces dernières, il y a:

O_TRUNC pour écraser (tronquer) et ouvrir le fichier s'il existe. Sa taille devient nulle.

O_CREAT pour créer s'il n'existe pas et ouvrir le fichier. Dans ce dernier cas le troisième paramètre pmode est nécessaire.

O_APPEND qui permet d'ouvrir pour rallonger le fichier. Le positionnement se fait alors en bout de fichier (offset = taille du fichier) au lieu du début (offset= 0).

On aura remarqué que open() surclasse creat(), qui n'en est qu'un cas particulier. (Exercice: lequel?).

En cas de succès, open() retourne l'entier descripteur alloué au fichier, et en cas d'échec la valeur -1 (fichier inexistant, droits existants incompatibles avec mode d'ouverture, trop de fichiers ouverts etc...), la variable globale errno indique l'erreur produite. Les valeurs possibles avec leur signification sont dans <errno.h> et on peut utiliser la fonction perror(), déjà rencontrée, pour ce besoin.

Le descripteur retourné est celui qui va servir à lire ou écrire dans le fichier. Par ailleurs, le fichier est positionné au début, offset 0 en l'absence de O_APPEND.

Exemples:

1.    Tester si un fichier existe et l'ouvrir

char *argv;
int f1;
if ((f1 = open(argv[1], 1)) == -1)
     printf("Je ne peux ouvrir %s", argv[1]);

2.    Ouvrir "monFichier" en lecture seule

open("monFichier", O_RDONLY);

3.    Créer ou écraser s'il existe "tonfichier" et l'ouvrir en écriture seule.

open("tonFichier", O_WRONLY|O_CREAT|O_TRUNC, 0644 );

Cette dernière instruction est donc équivalente à

creat("tonFichier",0644)

(réponse à l'exercice proposé juste avant).

3.3. Fermeture de fichier: close()

Pour fermer un fichier, on appelle la fonction:

int close(fd) int fd;

qui ferme le fichier de descripteur fd, en libérant ce descripteur. Auparavant, le tampon cache d'E/S est vidé s'il est encore rempli en mode écriture. On peut désirer forcer le vidage prématuré de ce tampon par la primitive sync().

3.4. Lecture de Fichier: read()

La fonction :

int read(fd, buf, nb_octets)
int fd; char *buf; int nb_octets;   /* ou size_t nb_octets */

lit nb_octets de données dans le fichier de descripteur fd et les place dans la zone pointée par buf. Cette zone doit donc être au moins de taille nb_octets comme par exemple:

#define TAILLE 100
char zone[TAILLE];

ou

#define TAILLE 100
char * zone;
zone = (char *)malloc(TAILLE);

Ainsi

read (fd, zone, 50) ou read(fd, zone, 1);

lit 50 (resp. 1) octets dans le fichier désigné par fd et les met dans zone. La valeur de retour de read() est le nombre effectif d'octets transférés, ou -1, si erreur (fichier non ouverts, ou pas ouvert en lecture lors d'un premier read()).

La fin de fichier est détectée par read() en comparant l'offset du fichier, i.e. position caractère courant, avec sa taille. Il n'y a en effet aucun indicateur ou caractère spécial dans le fichier qui indique sa fin. Donc, read() retourne 0, aucun caractère lu, si fin de fichier. Faire attention ici à ne pas confondre avec la valeur retour -1 alias EOF de <stdio.h>, qui n'est que la convention retour, en cas de fin de fichier, lors de l'usage de certaines fonctions (scanf(), getchar(), ...) de cette bibliothèque.

Exemples:

1.    L'exemple suivant lit caractère par caractère sur le fichier Standard d'Entrée et renvoi la valeur ascii correspondante. C'est une façon de réaliser la fonction getchar(). A la rencontre de fin de fichier (read renvoie 0), mais getchar renvoi -1 alias EOF (justement).

/* Auteur KERNIGHAN & RITCHIE */
#include <stdio.h>
#define CHARMASK 0377 /* pour rendre un caractère positif */
getchar()
{
     char c;
     return( (read(0, &c, 1) > 0)? c & CHARMASK : EOF);
}

read lit un caractère sur stdin (descripteur 0) et le met dans cc est ensuite rendu positif (bit de gauche à 0) par le masque CHARMASK.

2.    L'exemple suivant est une version de getchar() bufferisée:

Lit en mémoire jusqu'à 1024 (read a 1024 comme paramètre) caractères en avance, et les renvoie un à un.

___________________________________
mazoughou@magoe.net 80>cat getchar.c
#define CHARMASK 0377
#define TAILLEBUF 1024
getchar()
{
        static char buffer[TAILLEBUF];
        static char *car_courant = buffer;
        static int nb_car_lu = 0;
        if (nb_car_lu == 0){     /* buffer est vide */
                nb_car_lu = read(0, buffer, TAILLEBUF);
                car_courant = buffer;
        }
        return ((--nb_car_lu >= 0) ? *car_courant++ & CHARMASK : EOF);
}
___________________________________

1024 caractères sont lus sur stdin et mis dans le tableau déclaré buffer, variable staticcar_courantstatic aussi, pointe toujours vers le caractère à envoyer lors du prochain appel à getchar(). Il est incrémenté (++) en conséquence. nb_car_lustatic aussi, initialisé au nombre de caractères lus sur stdin, joue le rôle du compteur à decrémenter (--) à chaque appel de getchar(). Atteignant 0, il signifie qu'il faut remplir buffer par lecture sur stdin. En cas de fin de fichier, read met 0 dans nb_car_lu, et EOF est retourné.

Exécution:

___________________________________
mazoughou@magoe.net 81>cc -o getchar getchar.c
mazoughou@magoe.net 82>cat source
la sensibilité aux valeurs initiales..
mazoughou@magoe.net 83>getchar <source >cible
mazoughou@magoe.net 84>cat cible
la sensibilité aux valeurs initiales.. 
___________________________________

3.5. Ecriture sur Fichier: write()

La fonction:

int write (fd, buf, nb_octets)
int fd; char *buf; int nb_octets;

écrit sur le fichier de descripteur fd, les nb_octets octets trouvés dans la zone pointée par buf. Là aussi, la fonction renvoie le nombre de caractères effectivement écrits, ou -1 si erreur (descripteur inexistant, fichier non ouvert en écriture, ...). Si l'entier retourné est inférieur à nb_octets, c'est qu'il y a eu erreur aussi (disque plein par exemple).

Exemple: avec les mêmes déclarations que précédemment

write (fd, zone, TAILLE);

écrit TAILLE octets trouvés dans zone, dans le fichier désigné par fd.

write (fd, zone, 1);

écrit 1 octet trouvé dans zone, dans le fichier désigné par fd.

L'écriture se fait à la position courant du fichier.

3.6. Exemple de Copie d'un Fichier par read() et write()

Cet exemple classique va illustrer la copie d'un fichier existant sur un autre à créer ou écraser s'il existe. C'est

grosso-modo la commande cp de unix. Il est adapté de KERNIGHAN & RITCHIE.

mazoughou@magoe.net 33>cat copie.c
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define TAILLE_BUF 1024
# define PMODE 0644 /* -rw--r--r */
#define NULL 0
void perreur(char*, char*);
main(argc,argv)
int argc;
char *argv[];
{
        int fd_source,fd_cible;
        int nb_car_lu;
        char buf[TAILLE_BUF];
        /* test si parametres correctes */
        if (argc !=3)
                perreur("usage: copie f1 f2 ",NULL);
        /* test ouverture fichier source */
        if (( fd_source = open(argv[1], O_RDONLY))== -1)
                perreur("Ne peux ouvrir ",argv[1]);
        /* test ouverture en création/écrasement fichier cible 
            usage de open équivalent a creat */
        if (( fd_cible = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, PMODE)) == -1)
                perreur("Ne peux creer ",argv[2]);
        /* On copie donc */
        while (( nb_car_lu = read(fd_source, buf, TAILLE_BUF)) >0) 
              if ( write(fd_cible, buf, nb_car_lu) != nb_car_lu)
                   perreur("Erreur Ecriture en cour de copie",NULL);
        exit(0);
}
void perreur(s1, s2)
char *s1,*s2;
{
      fprintf(stderr,"%s %s\n",s1,s2);
      exit(1);
}                                         

Voici la trace d'exécution de ce programme:

_____________________________________________ 
mazoughou@magoe.net 7>cc -o copie copie.c
mazoughou@magoe.net 8>cat source
Ce phénomène, appelé effet papillon par E. LORENTZ, 
le battement des ailes d'un papillon en Méditerranée 
peut être à l'origine d'une tempête en Atlantique... 
mazoughou@magoe.net 9>cat cible
cible: No such file or directory

mazoughou@magoe.net 10>copie source cible
mazoughou@magoe.net 11>cat cible
Ce phénomène, appelé effet papillon par E. LORENTZ,
le battement des ailes d'un papillon en Méditerranée 
peut être à l'origine d'une tempête en Atlantique...
mazoughou@magoe.net 12>copie /usr/source
usage: copie f1 f2 (null)
mazoughou@magoe.net 13>copie /usr/source cible 
Ne peux ouvrir /usr/source
_____________________________________________ 

Ce n'est pas tout, il s'agit surtout de savoir que le nombre de caractères à lire d'un seul coup est important. Voici deux cas d'exécution, avec 1024 octets pour TAILLEBUF et ensuite 1 octet (cas quand même limite).

Avec #define TAILLE_BUF 1024 on a:

_____________________________________________ 
mazoughou@magoe.net 15>cc -o copie copie.c
mazoughou@magoe.net 16>ls -l bigSource
-rw-r--r-- 1 tounsi 307264 Apr 9 16:38 bigSource 
mazoughou@magoe.net 17>/bin/time copie bigSource bigCible
     1.4 real          0.0 user           0.4 sys
_____________________________________________ 

Avec #define TAILLE_BUF 1 on a:

_____________________________________________ 
mazoughou@magoe.net 20>cc -o copie0 copie0.c
mazoughou@magoe.net 21>/bin/time copie0 bigSource bigCible
     109.0 real        5.8 user            99.4 sys 
______________________________________________

C'est bien sûr le temps système qui souffre ici (d'où l'intérêt de la bibliothèque d'E/S qui utilise systématiquement une zone buffer de taille BUFSIZE=1024 évitant ainsi ce type de désagrément à d'éventuels programmeurs). Noter aussi que ces exécutions sont faites sur station individuelle (il n'y a que mes programmes) et les fichiers sont locaux ( d'où le changement de station ) donc pas de pénalités réseau. Celles-ci d'ailleurs sont très légères car le taux de rapidité est pratiquement le même (0,4 contre 0,3 en temps système) comme le montre le cas sur ma station habituelle connectée en réseau (très local) avec fichiers non locaux accessibles via NFS.

_________________________________________________
mazoughou@magoe.net 41>/bin/time copie bigSource bigCible
        3.4 real         0.0 user         0.2 sys

avec #define TAILLE_BUF 1

mazoughou@magoe.net 43>/bin/time copie0 bigSource bigCible
        74.2 real        1.6 user         69.2 sys
__________________________________________________

Si les temps sont approximatifs, l'ordre de grandeur y est. La meilleur taille est en principe un multiple du facteur de blocage disque normal d'un système.

Exercice: Essayer le même programme pour plusieurs autres tailles du buffer (e.g. 1023, 1025, 4096, 512 etc...)

3.7. Accès Direct: lseek()

Cette fonction permet de changer la position (caractère) courante pour lire / écrire dans un fichier. Elle permet ainsi de lire / écrire dans un endroit arbitraire dans le fichier.

#include <unistd.h>
#include <sys/types.h>
off_t lseek(fd, offset, origine)
int fd; off_t offset, origine;

Cette fonction force la position courante dans le fichier de descripteur fd, à prendre une nouvelle valeur qui est un déplacement de offset positions par rapport à la valeur indiquée par origine. La prochaine lecture / écriture se fera donc à la nouvelle position. L'origine est:

- soit 0 (ou constante symbolique SEEK_SET) pour le début de fichier,

- soit 1 (ou constante symbolique SEEK_CUR) pour la position courante,

- soit 2 (ou constante symbolique SEEK_END) pour la fin du fichier(sa taille).

et permet donc de se positionner par rapport au début du fichier, à la position actuelle ou à la fin du fichier . Se positionner en fin permet de rajouter dans le fichier. On utilisera de préférence les constantes symboliques qui sont définies dans <stdio.h>.

La valeur retournée par lseek() est cette nouvelle position comptée par rapport au début du fichier. Si la valeur -1 est retournée, errno indique le type d'erreur produite (voir fichier include <sys/errno.h>). La position courante reste alors inchangée.

Exemple: lecture/écriture de 4 caractères dans un fichier à partir d'une position donnée.

tounsi@gnaoui 63>cat lseek.c
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
main()
{
        int file;
        off_t pos;
        char t[10];
        file = open("toto", O_RDWR | O_CREAT, 0644);
        write(file, "abcdefghijklmnopqrstuvwxyz",26);
        /* fichier est en position 26 a ce niveau
           on le positionnne au 11e octet */

        pos = lseek(file, (off_t) 10, SEEK_SET);
        read(file, t, 4);
        printf("a la position:%d je lis %s\n",(off_t)pos, t);
        /* position courante = 10+4 */

        write(file, "OPQR",4);
        pos = lseek(file, (off_t) -8, SEEK_CUR);
        read(file, t, 10);
        printf("a la position:%d je lis %s\n",(off_t)pos, t);
}
__________________________________
mazoughou@magoe.net 64>cc lseek.c
mazoughou@magoe.net 65>a.out
a la position:10 je lis klmn
a la position:10 je lis klmnOPQRst
__________________________________

Exercice: Modifier ce programme de façon à ce que le premier appel printf() imprime:

a la position:10 je lis klmnopqrst

Remarques:

Les types size_tpos_toff_t sont des typedef de int se trouvant dans #include<sys/types.h> comme déjà signalé, pour une conformité POSIX. le type mode_t du même include est un typedef unsigned short. On peut utiliser directement int par exemple si on ne met pas ce include dans son programme.

Pour connaître la taille d'un fichier (cf. filepos() de TURBO PASCAL), il suffit d'appeler lseek() avec un déplacement de 0 par rapport à SEEK_END.

taille = lseek(file, (off_t)0, SEEK_END);

On ne peut donc pas connaître directement la valeur taille d'un fichier, sans déplacer la position courante vers la fin du fichier. Effet de bord à ne pas oublier pour les habitué(e)s de Pascal. De même, pour connaître la position actuelle (cf. filepos() de Turbo Pascal), il suffit d'appeler lseek() avec un déplacement de 0 par rapport à SEEK_CUR.

pos = lseek(file, (off_t)0, SEEK_CUR);

L'écriture en fin de fichier augmente sa taille, mais en milieu ne l'augment pas. (Exercice: le vérifier).

Si on va au delà de la fin d'un fichier pour faire des opérations E/S, le résultat est un peu spécial. Dans le cas de read, la taille du fichier reste la même et read renvoie 0, i.e. fin de fichier bien sûr (position de lecture supérieure à la taille du fichier). Mais un write augmente la taille du fichier d'autant de positions que la distance à laquelle il écrit. Les cases intermédiaires sont alors remplies par le caractère null "\0". Si on essaye de lire alors dans ces trous, ça marche: read envoie effectivement le nombre de caractères lus, i.e. ce n'est pas une fin de fichier, mais la zone de lecture reste vide comme si on n'avait rien lu (pourquoi à votre avis?). La séquence suivante illustre un fichier avec des trous.

file = open("toto", O_RDWR | O_CREAT | O_TRUNC, 0644); 
write(file, "abcdefghijklmnopqrstuvwxyz",(size_t)26);

pos = lseek(file, (off_t)0, SEEK_END);
printf("TAILLE =%d\n",pos);

lseek(file, (off_t)10000, SEEK_SET);
write(file,"FIN",3);
pos = lseek(file, (off_t)0, SEEK_END);
printf("apres write TAILLE =%d\n",pos);

Voici le résultat obtenu:

_________________________________
mazoughou@magoe.net 149>ls -l toto
toto: No such file or directory
mazoughou@magoe.net 150>a.out

TAILLE =26                                         <--- taille est 26
apres write TAILLE =10003                          <--- taille augmentée
mazoughou@magoe.net 151>ls -l toto
-rw-r--r--  1 tounsi    10003 Mar 22 13:11 toto    <--- toto fait dix blocs

mazoughou@magoe.net 152>cat toto 
abcdefghijklmnopqrstuvwxyzFIN                  <--- les trous sont sautés
mazoughou@magoe.net 153>od -c toto                   <-- Octal Dump
0000000    a   b   c   d   e   f   g   h   i   j   k   l   m   n   o   p
0000020    q   r   s   t   u   v   w   x   y   z  \0  \0  \0  \0  \0  \0
0000040   \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0023420    F   I   N
0023423
_________________________________

Noter aussi ce qu'a fait la commande cat toto: elle n'affiche pas les trous. Mais FIN en bout du fichier, a mis un petit délai pour sortir, le temps de... «lire les trous» (cat -t les afficherait). Il en est de même pour od, sauf pour les tous premiers trous.

3.8. Suppression d'un Fichier: unlink()

En terme de système, on parle de lien (link), entrée dans un répertoire qui référence un fichier. cf. commande UNIX ln.

#include <unistd.h>
int unlink(nom)
char *nom;

Cette primitive supprime le fichier (ou lien) en paramètre. Ce dernier est utilisé par son vrai nom sur disque plutôt que par son descripteur. Ce nom en outre ne peut être celui d'un répertoire (utiliser rmdir()). Le fichier n'est supprimé, physiquement donc, que s'il ne possède aucun autre lien. C'est à dire, plus précisément, si dans le répertoire contenant le fichier, ce nom est le dernier lien existant sur ce fichier. En outre, il ne faut pas qu'un processus possède encore un descripteur sur ce fichier (le fichier a été fermé).

La fonction retourne 0 en cas de succès. Sinon -1 et la variable errno est positionnée sur la valeur indiquant la cause d'échec (Il y en a beaucoup, voir fichier include <sys/errno.h>).

3.9. Renommage de Fichier: rename()

La primitive

rename (anc_nom, nouv_nom)
char *anc_nom, nouv_nom;

renomme le fichier physique appelé anc_nom par nouv_nom (remplace un lien existant par un nouveau). Si ce dernier nom existe, son fichier est perdu (supprimé d'abord). rename retourne 0 en cas de succès et -1 sinon. errno indique alors l'erreur s'étant produite. Comme la fonction précédente, le fichier est désigné par son nom physique et non par un descripteur.

4. La Bibliothèque Standard d'Entrées / Sorties

Comme mentionné au § 1, c'est la bibliothèque des fonctions d'entrées/sorties de haut niveau et donc plus portables. Ce sont les fonctions habituellement utilisées. Elles font référence si nécessaire aux primitives de base juste étudiées.

Le fichier include <stdio.h>, en contient les prototypes.

extern size_t   fread( void *__ptr, size_t __size,
                       size_t __nitems, FILE *__stream );    
extern size_t   fwrite( const void *__ptr, size_t __size,
                       size_t __nitems, FILE *__stream );
...
extern int      scanf( const char *__format, ... ); 
extern int      printf( const char *__format, ... ); 
...
extern int      getc( FILE *__stream );
extern int      getchar( void );  
...
#define getchar()       getc(stdin)

Il contient aussi les constantes NULL (pointeur de valeur 0), EOF (valeur -1, indication de fin de fichier ), BUFSIZE (valeur 1024 en général, taille du tampon cache utilisateur) et _NFILE (valeur 64, nombre maximum de fichiers ouverts).

#define BUFSIZ 1024
#define _NFILE 64
#define NULL 0
#define EOF (-1)

Le type structure FILE y est aussi défini. C'est par l'intermédiaire d'un objet de ce type, un descripteur de haut niveau et plus complexe, qu'on manipule un fichier. Un programme doit déclarer une variable FILE* (pointeur vers cette structure) pour manipuler un fichier et la transmettre en totalité aux opérations d'E/S c'est tout. En général, il n'a jamais besoin d'en manipuler les différents champs. En effet c'est un de descripteur abstrait manipulable justement par ces opérations, non moins abstraites, d'E/S. Savoir néanmoins qu'on y trouve, entre autre, le champs descripteur entier du fichier et (un pointeur vers) le tampon cache de taille BUFSIZ associé à chaque fichier ouvert. Ce qui implique que les E/S faites avec les fonctions bibliothèques se font automatiquement en bloc de 1024 octets comme déjà signalé.

La structure FILE

extern struct _iobuf {
int _cnt;
unsigned char *_ptr; /* ptr vers buffer */
unsigned char *_base;
int _bufsiz;
short _flag;
short _file; /* Le descripteur entier */
} _iob[3];

#define FILE struct _iobuf

_iob[3] est un tableau d'objets de type FILE. (La constante 3 est en fait macro-défine par _N_STATIC_IOBS)

A ce propos stdinstdout et stderr sont des variables FILE* prédéfinies. Voici d'ailleurs la partie de stdio.h qui les définit

#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])

Dans la présentation qui suit le fichier include stdio.h ne sera pas répété pour chaque spécification. Comme auparavant, le profil donné est celui du man de ULTRIX 4.3, conforme POSIX. Nous appellerons flot une variable FILE* et la considérerons comme fichier logique en paramètre.

4.1. Fonctions Utilitaires Générales

Ce sont des fonctions macro-définies dans stdio.h.

#include <stdio.h>
int ferror(flot)
FILE* flot
void clearerr(flot)
FILE* flot
int feof(flot)
FILE* flot;
int fileno(flot)
FILE* flot;

La fonction ferror() retourne un entier non nul (vrai) ou nul (faux), selon qu'une erreur s'est ou pas produite dans le déroulement d'une opération de lecture/écriture sur le fichier en paramètre.

La fonction clearerr() doit être appelée (pour reinitialiser l'erreur) avant d'autres appels à ferror().

La fonction feof() retourne un entier non nul (vrai) si fin de fichier est atteinte pour son paramètre, autrement un entier nuli (faux).

Quand à la fonction fileno(), elle retourne le descripteur entier de son paramètre flot.

Exemple:

printf("%d %d %d\n", fileno(stdin), fileno(stdout), fileno(stderr));

imprime

0 1 2

Ces fonctions ne font qu'utiliser les informations (champs _flag_file) disponibles dans la structure FILE.

4.2. Ouverture de Fichier: fopen(), freopen() et fdopen()

L'ouverture d'un fichier au niveau de la bibliothèque standard a essentiellement pour but de créer un fichier logique (objet du programme) en l'assignant à un fichier physique réél. Cela se fait par l'instanciation d'une structure

FILE (un flot donc), rendue au programme qui y accède via un pointeur. L'ouverture au niveau système (primitive open()), réalisée de toute façon pour créer une entrée dans la table des fichiers, se fera de façon transparente si ce n'est déjà fait. Dans la suite on dira fichier flot par opposition à fichier physique.

Trois fonctions, ayant des rôles sensiblement différents, sont alors proposées:

#include <stdio.h>
FILE *fopen (nom, mode_ouverture)
char *nom, *mode_ouverture;
FILE *freopen (nom, mode_ouverture, flot)
char *nom, *mode_ouverture;
FILE *flot;
FILE *fdopen (desc, mode_ouverture)
int desc;
char *mode_ouverture;

Elle retournent toutes l'adresse du flot ouvert.

fopen() ouvre le fichier physique de nom donné selon le mode d'ouverture donné et lui associe un flot. En cas de réussite elle retourne un pointeur vers ce fichier flot qui sera utilisé dans le programme pour accéder au fichier disque. Le résultat est NULL sinon (cas d'erreur, fichier inexistant par exemple).

Exemple:

FILE* f;
f = fopen ("monfichier, "r");

Ouvre le fichier disque monfichier en lecture ("r") et l'associe (l'assigne) à f.

freopen() redirige le fichier flot fourni en paramètre vers le nom de fichier physique donné et retourne le nouveau fichier flot (même adresse que l'ancien) ou NULL si erreur. C'est une réassignation de fichier logique tout simplement.

freopen("monFichier", "r", f);

réassigne f à monFichierfreopen() est prévue surtout pour rediriger l'entrée standard et la sortie standard assignées systématiquement au clavier et à l'écran.

Exemple: On teste si on peut ouvrir un fichier redirection de shell

if ( freopen("source", "r", stdin) != NULL) ...

Si oui source devient fichier stdin ouvert en lecture.

Pour réassigner plus tard stdin au clavier, on peut faire

freopen("/dev/tty05", "r", stdin);

en utilisant le nom, tty05 par exemple, du fichier clavier concerné (obtenu par la commande UNIX tty).

Enfin, fdopen() associe un descripteur connu, son premier paramètre à un fichier flot qu'elle retourne. Elle convertit donc un descripteur entier en un flot. Ce descripteur est celui retourné par une primitive open() ou creat() précédente, ou récupéré par une opération qui ne peut fournir que le descripteur entier d'un fichier comme pipe(2). Justement, en faisant ainsi, fdopen() permet de manipuler les tubes (pipe) par les fonctions de la bibliothèque standard.

Les modes d'ouverture sont:

"r" ouverture d'un fichier texte en lecture
"w" ouverture d'un fichier texte en écriture
"a" ouverture d'un fichier texte en écriture à la fin
"rb" ouverture d'un fichier binaire en lecture
"wb" ouverture d'un fichier binaire en écriture
"ab" ouverture d'un fichier binaire en écriture à la fin
"r+" ouverture d'un fichier texte en lecture/écriture
"w+" ouverture d'un fichier texte en lecture/écriture
"a+" ouverture d'un fichier texte en lecture/écriture à la fin
"r+b" ouverture d'un fichier binaire en lecture/écriture
"w+b" ouverture d'un fichier binaire en lecture/écriture
"a+b" ouverture d'un fichier binaire en lecture/écriture à la fin

 

4.3. Vidage et Fermeture de Fichier: fflush() et fclose()

int fclose(flot)
FILE *flot;

int fflush(flot)
FILE *flot;

La fonction fclose() vide le buffer du fichier flot donné et ferme le fichier au niveau système. fclose() est appelée systématiquement à l'appel de exit(). Retourne 0 en cas de réussite sinon -1, alias EOF.

La fonction fflush(), vide le buffer en cas d'écriture mais ne ferme pas le fichier flot donné. Appelée avec le paramètre NULL, elle vide tous les flots en écriture dans le programme. Le vidage est fait dans les caches systèmes que le noyau vide périodiquement. (La primitive sync() peut forcer ce dernier à les vider sur disque). fflush() retourne 0 en cas de réussite, i.e. la dernière opération sur le flot était une écriture. Autrement le résultat est non défini.

4.4. Lecture et Ecriture d'un Fichier: fread() et fwrite()

Voici maintenant les opérations, read() et write(), de lecture ou écriture dans un fichier flot. Ce sont deux routines très utiles permettant des E/S sur des fichiers binaires (e.g. fichiers UNIX données) par transfert d'objets C quelconques. Y compris des objets textes.

#include <stdio.h>
size_t fread(ptr, taille, nb_elem, flot)
void * ptr;
size_t taille, nb_elem;
FILE *flot;
size_t fwrite(ptr, taille, nb_elem, flot)
void * ptr;
size_t taille, nb_elem;
FILE *flot;

Ces opérations correspondent à la lecture et l'écriture d'une séquence (tableau) d'un ou plusieurs objets. Typiquement un seul objet. Si ce n'est pas des caractères (accédés en général par séquences de plusieurs). Un objet lu par fread()doit avoir été écrit par fwrite() pour être cohérent: l'écriture reflète l'image interne en mémoire d'un objet. Si on a écrit 123 en trois caractères on ne peut pas lire un entier 123 et inversement; voir plutôt fscanf() (Par contre, on retrouvera la chaîne "123").

fread()

L'opération fread() lit, dans un tableau (ou bloc mémoire) d'adresse ptr, une suite de nb_elem objets, chacun de taille égale au paramètre taille (habituellement sizeof (*ptr)). On récupère donc ce qu'on a lu, par l'intermédiaire du pointeur ptr. Le fichier à lire est flot (dernier paramètre contrairement aux primitives read() et write()).

fread() retourne le nombre effectif d'objets lus, 0 signifiant par conséquent fin de fichier (ou erreur rencontrée). Se rappeler que fin de fichier n'est pas -1 (EOF) pour read() ni pour fread(). Si un objet à lire est incomplet, la valeur retour est non définie.

Exemples:

#define TAILLE_BUF 100
int i;

fread (&i, sizeof(int), 1, f)

Lit un entier i dans le fichier f précédemment ouvert.

int nb_element_lu;
char buf[TAILLE_BUF];

nb_element_lu = fread (buf, 1, TAILLE_BUF, g)

Lit le tableau de caractères buf à partir du fichier g, chaque caractère étant de longueur 1 octet. buf est un tableau C de taille TAILLE_BUF ici. (ne pas confondre ce tableau avec le tampon cache utilisateur de taille BUFSIZ = 1024 celui-ci, et qui se trouve, rappelons-le, dans la structure FILE du fichier).

fwrite()

L'opération fwrite(), inverse de fread(), écrit dans le fichier flot nb_elem objets (au plus) de taille taille et commençant à l'adresse ptr. Ces objets sont écrits en bout de fichier. Le nombre d'objets effectivement écrits est retourné.

int i;
fwrite (&i, sizeof(int), 1, f)

Ecrit l'entier i dans le fichier f et

int nb_element_lu;
char buf[TAILLE_BUF];
fwrite (buf, 1, TAILLE_BUF, g)

Ecrit le tableau buf dans le fichier g.

4.5. Exemple de Copie d'un Fichier par fread() et fwrite():

Nous allons maintenant examiner un programme de Copie d'un Fichier par fread() et fwrite(). Programme à Comparer avec l'exemple copie.c utilisant les primitives de base (§ 3.6).

#include <stdio.h>
#define TAILLE_BUF 1024
void perreur(char*, char*);
main(argc,argv)
int argc;
char *argv[];
{
      FILE *flot_source, *flot_cible;
      int nb_car_lu;
      char buf[TAILLE_BUF];
      /* test du nombre de parametres */
      if (argc !=3)
              perreur("usage: copie f1 f2 ",NULL);
      /* test ouverture fichier source */
      if ( (flot_source = fopen(argv[1], "r")) == NULL)
              perreur("Ne peux ouvrir ",argv[1]);
      /* test ouverture en création/écrasement fichier cible
          usage de open équivalent a creat */
      if ( (flot_cible = fopen(argv[2], "w")) == NULL)
              perreur("Ne peux creer ",argv[2]);
      /* Je copie donc */
      while (( nb_car_lu = fread(buf, sizeof(char), TAILLE_BUF, flot_source))> 0)
            if ( fwrite(buf, sizeof(char), nb_car_lu, flot_cible) != nb_car_lu)
                  perreur("Erreur Ecriture en cour de copie",NULL);
      exit(0);
}                      
__________________________________________________
mazoughou@magoe.gn 58> ls -l bigSource
-rw-r--r-- 1 tounsi 307264 Apr 9 16:38 bigSource
mazoughou@magoe.gn 59> ls -l bigCible
bigCible: No such file or directory 

mazoughou@magoe.gn 60> copie bigSource bigCible 
mazoughou@magoe.gn 61> ls -l bigCible
-rw-r--r-- 1 tounsi 307264 Apr 9 18:41 bigCible 
___________________________________________________

Voici les chiffres avec fread/fwrite bloc par bloc de 1024 (a), ensuite caractère par caractère (b), i.e. TAILLE_BUF respectivement égale 1024 et 1.

(a) mazoughou@magoe.gn 26> cc -o fcopie fcopie.c
    mazoughou@magoe.gn 27> /bin/time fcopie bigSource bigCible
             1.5 real         0.0 user          0.3 sys
(b) mazoughou@magoe.gn 31>cc -o fcopie0 fcopie0.c
    mazoughou@magoe.gn 32>/bin/time fcopie0 bigSource bigCible
             4.4 real         4.0 user          0.2 sys

et où on voit que le temps système n'est pas affecté. (Exercice: Commenter ces résultats).

4.6. Autres Fonctions: Lecture/Ecriture Typés ou Formatés

Nous avons déjà vu les fonctions d'E/S putc()/getc(), putchar()/getchar(), gets()/puts() et scanf()/printf() qui opèrent sur les fichiers E/S standard. Voici les fonctions homologues sur un fichier flot quelconque.

a) Lecture/Ecriture d'un Caractère

#include <stdio.h>
int getc(flot)
FILE *flot;
int fgetc(flot)
FILE *flot;
int putc(c, flot)
char c;
FILE *flot;
int fputc(c, flot)
FILE *flot;
int getchar();
int puchar(c)
char c;

Les fonctions getc() et putc() ne sont pas des fonctions originales, mais des macros.

  • getc() et fgetc() retournent, sous forme d'entier, le prochain caractère en entrée dans le fichier flot indiqué (caractère considéré entier non signé). La valeur EOF-1 est retournée en fin de fichier ou en cas d'erreur (Voir la valeur de errno le cas échéant).

     

  • putc() et fputc() écrivent (ajoutent) le caractère c dans le fichier flot indiqué en sortie. Elles retournent la valeur du caractère ainsi écrit, sinon EOF (-1) en cas d'erreur.

On peut préférer utiliser les fonctions macro-définies getc() et putc(), auxquelles il ne correspond pas toujours un vrai appel de fonction, et donc un peu plus performantes.

Quand à getchar() et putchar() ce sont respectivement getc(stdin) et putc(cstdout) qui lisent ou écrivent sur les flots standard d'entrée/sortie.

b) Lecture/Ecriture de Chaînes

Ces fonctions sont utiles pour les chaînes. On lit ou écrit des chaînes comme des lignes entières.

#include <stdio.h>
char *gets(s)
char *s;
char *fgets(s, taille, flot)
char *s;
int taille;
FILE *flot;

int puts(s)
char *s;
int fputs(s, flot)
char *s;
FILE *flot;
  • gets() lit des caractères donnés en entrée dans le fichier entrée standard stdin, et les transmet dans s. La chaîne lue est une ligne terminée par le caractère retour chariot \n, remplacé dans s par le caractère null \0. Avec gets() il faut s'assurer que s est suffisamment large pour accepter la ligne entrée dont la taille n'est pas spécifiée.

     

  • fgets() lit (taille - 1) caractères donnés en entrée dans le fichier flot indiqué, et les transmet dans s. Le caractère null \0 est systématiquement rajouté dans s(ce qui explique le terme -1). Si le caractère \n est rencontré en entrée, la lecture s'arrête là et \n fera partie de s. Ce qui différencie fgets() de gets().

gets() et fgets() retournent aussi cette chaîne lue. Sinon la valeur NULL 0 si erreur (fichier non ouvert en lecture) ou fin de fichier rencontré. (Attention: le caractère null \0 rencontré en entrée peut faire perdre le reste de la chaîne lue).

  • puts() écrit la chaîne s terminée par le caractère null \0 (non copié) dans le flot standard de sortie stdout. Le caractère \n est rajouté en fin d'écriture. (cf writeln de Pascal)

     

  • fputs() fait pareil mais dans le fichier flot indiqué et ne rajoute pas le caractère \n.

Les deux fonctions retournent la valeur EOF -1 si erreur (e.g. fichier non ouvert en écriture).

Exercices :

  1. Que serait fgetchar() et fputchar() si elles existaient?
  2. Ecrire un programme qui lit une chaîne de type "2.718" ou "+031.4e-1" etc... et rend le réel float associé (2.718 et 3.14 ici) . Ne pas utiliser les E/S avec format bien sûr.

c) Lecture/Ecriture Formatée: scanf() printf() ...

Nous avons déjà parlé de ces fonctions, du moins pour celles qui opèrent sur les E/S standard. Les voici placées dans le contexte général des fichiers et pour mémoire.

int fscanf( flot, format [, adresse ] ... )
FILE *flot;
char *format;
int fprintf ( flot, format [, expression ] ... )
FILE *flot;
char *format;
int scanf( format [, adresse ] ... )
char *format;

int printf ( format [, expression ] ... )
char *format;
int sscanf( s, format [, adresse ] ... )
char *s, *format;
int sprintf ( s, format [, expression ] ... )
char *s, *format;

Ces fonctions permettent des lectures écritures d'objets avec

  • en cas de lecture, conversion du format caractères imprimables vers une représentation interne (cf. execice 2 ci-dessus)
  • en cas d'écriture, conversion d'une représentation interne vers un format caractères imprimables.

fscanf() --et son homologue scanf() lisant sur le flot entrée standard-- lit des données en entrée dans le fichier flot indiqué en paramètre, les interprète selon le format indiqué et les affecte un à un aux objets dont la liste des adresses est fournie dans le reste des paramètres.

fprintf() --et son homologue printf() écrivant sur le fichier standard de sortie-- écrit sur le fichier flot sortie indiqué et selon le format indiqué, les valeurs des variables ou expression dont la liste est fournie en reste des paramètre. Avec fscanf() et fprintf() on peut par exemple lire et écrire des fichiers d'enregistrements au sens classique, mais stockés sous forme imprimable,


2017-10-15 16:12:22 / mazoughou@magoe.gn

0 commentaires

Votre impression compte aussi