Les fichiers
Sous Unix, le terme fichier désigne un grand nombre de type de ressources:
- Fichier régulier: programmes, données.
- Répertoire: contenant des associations de noms à des fichiers (les fichiers que le répertoire contient).
- Lien symbolique: contient le chemin d'un autre fichier.
- Fichier spécial: associé à des ressources (disque, port série, ...) (mode bloc ou caractère).
- Tube ou socket: mécanisme de communication interprocessus.
L'option -l de la commande ls permet d'afficher le détails des informations des fichier. Par exemple:
$ ls -lLa première lettre de la sortie désigne le type de fichier parmi:
-rw-r--r-- 1 root daemon 0 May 27 14:47 passwd
drwxrwxr-x 2 root daemon 68 May 27 14:46 cron
srw-rw-rw- 1 root daemon 0 May 27 14:46 udevd
lrwxr-xr-x 1 root wheel 26 Feb 1 02:23 weekly -> cron/weekly/500.weekly
$ ls -l /dev
crw-rw---- 1 root root 15, 0 May 28 11:34 tap0
brw-r----- 1 root root 14, 0 May 27 14:46 disk0
- -: fichier régulier
- d: répertoire
- l: lien symbolique
- c, b: fichier spécial en mode caractère/bloc
- p: tube
- s: socket.
À chaque fichier est associée sur le disque une structure, le i-node, qui contient les caractéristiques du fichier: propriétaire, groupe propriétaire, droits, accès aux blocs de données, nombre de références, etc. Un i-node est chargé en mémoire dans une table du noyau si le fichier est en cours d'utilisation. L'option -i de la commande ls affiche les numéros d'i-node.
Accéder aux fichiers en C
Il existe deux façon d'accéder aux fichiers, et deux groupes de fonctions distinct pour les manipuler.
La façon la plus courante de manipuler les fichiers est d'utiliser les fonctions de haut niveau fournies dans la bibliothèque standard de C. Ces fonctions sont disponibles sur tout les systèmes d'exploitation et prennent en charge un certains nombre fonctionnalités tel que les tampons. Toutes ces fonctions manipulent un pointeur vers un objet FILE pour accéder un fichier. L'objet FILE* utilisé dans les programmes est appelé "flux de fichier".
L'autre façon de manipuler les fichiers est d'utiliser les fonctions de bas niveau fournies par Unix. Ces fonctions ne sont portable qu'entre Unix, et sont (un peu) plus compliquées à utiliser. Toutes ces fonctions manipulent un entier qui sert à identifier et à accéder au fichier. L'entier représentant un fichier est aussi appelé "descripteur de fichier".
Accès de bas niveau au fichier
Comme dit précédemment, au sein d'un processus les fichiers utilisés sont identifiés par un entier baptisé "descripteur de fichier". Il est associé à un fichier par les appels système d'ouverture, création, etc. Il est ensuite utilisé dans les appels système pour effectuer des opérations sur le fichier.
Le descripteur de fichier est un indice dans une table de descripteurs associée au processus. Cette table contient un pointeur vers un objet fichier présent dans une table des fichiers ouverts du noyau. Un objet fichier contient lui-même un lien vers l'i-node en mémoire du fichier, il contient aussi la position courante dans le fichier.
La table des descripteurs est propre à un processus (sauf si partagée via clone() ou vfork()). La table des objets fichier par contre est commune à tous les processus. Plusieurs descripteur (éventuellement dans des processus différents) peuvent pointer vers un même objet fichier.
Les descripteurs 0, 1, 2 correspondent respectivement à l'entrée standard, la sortie standard et la sortie d'erreur standard. On peut aussi (et c'est conseillé) utiliser les constantes symboliques STDIN_FILENO, STDOUT_FILENO et STDERR_FILENO. Différentier la sortie standard de la sortie d'erreur permet de réagir différemment lorsqu'un programme génère une erreur, comme par exemple sauver l'ensemble des erreurs dans un fichier.
Ouverture de fichier
L'ouverture d'un fichier s'effectue par la fonction open(), celle-ci est à retour covariant dans l'espace de nom de la procédure (bon d'accord, ça veut rien dire, mais ça fait classe):
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
Où pathname est le chemin du fichier à ouvrir.
flags indique le type d'accès souhaité, il s'agit d'un "ou binaire" ( | ) entre l'une des 3 valeurs
- O_RDONLY: lecture seule
- O_WRONLY: écriture seule
- O_RDWR: lecture/écriture
et une combinaison (ou binaire) des valeurs
- O_APPEND: positionnement à la fin
- O_TRUNC: effacement du fichier
- O_CREAT: création du fichier
- O_EXCL: avec O_CREAT, provoque une erreur si le fichier existe déjà.
Pour la création (O_CREAT), il faut indiquer les droits du fichier via le paramètre mode. Le mode est indiqué sous la forme d'un "ou binaire" entre des constantes qui s'écrivent:
S_I<op><utilisateur>
- où <op> est soit R (lecture), W (écriture), X (exécution).
- <utilisateur> est soit USR (le propriétaire), GRP (le groupe propriétaire), OTH (les autres utilisateurs).
Par exemple S_IWGRP correspond au droit d'accès en écriture pour les membres du groupe propriétaire du fichier.
La fonction renvoie un descripteur de fichier en cas de succès, et -1 en cas d'échec. Pour rappel, le descripteur de fichier est un numéro dans la table des descripteurs de fichier du processus.
Création d'un fichier
Plutôt que de créer un fichier avec open(), on peut le créer explicitement avec la fonction creat():
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
Cette fonction correspond à l'appel à open en utilisant les drapeaux O_CREAT | O_WRONLY | O_TRUNC. Tout comme open(), creat() renvoie "-1" en cas d'échec.
Fermer un fichier
Comme d'habitude, quand vous ouvrez quelque chose, faut le fermer. En l'occurence, fermer le fichier permet de supprimer son entrée de la tables des descripteurs de fichier et ainsi en libérer les ressources.
La fonction close() permet de fermer un fichier:
#include <unistd.h>
int close(int fd);
Lecture et écriture dans un fichier
Il n'y a que une fonction pour lire dans un fichier: read(), et une fonction pour y écrire: write():
#include <unistd.h>
ssize_t read(int fd, void*buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
Toutes deux prennent comme paramètres un descripteur de fichier (fd), l'adresse d'une zone où seront lus/écrits les octets (buf), le nombre de caractères à lire/écrire (count). Ces fonctions retournent le nombre d'octets réellement lus/écrits ou -1 en cas d'erreur.
Se déplacer dans un fichier
Le positionnement à un endroit particulier d'un fichier est réalisé par la fonction
#include<unistd.h>Cette fonction a comme paramètres un descripteur de fichier (filedes), un déplacement à effectuer (offset), une indication de l'origine du déplacement (whence) parmi
off_t lseek(int fildes, off_t offset,int whence);
- SEEK_SET: depuis le début du fichier
- SEEK_CUR: depuis la position actuelle
- SEEK_END: depuis la fin du fichier.
Les déplacements peuvent être négatifs, sauf bien sûr avec SEEK_SET.
Obtenir les caractéristiques d'un fichiers
Pour obtenir les caractéristiques d'un fichier (taille, date de création, etc), on peut utiliser les fonctions
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *file_name,struct stat *buf);
int fstat(int filedes, struct stat *buf);
stat() prend le chemin d'un fichier comme premier paramètre tandis que fstat() prend un descripteur.
Les deux fonctions remplissent une structure struct stat avec les caractéristiques du fichier. Cette structure contient entre autres les champs:
- st_uid et st_gid: qui donnent le propriétaire et le groupe du fichier
- st_atime, st_mtime: date du dernier accès et date de la dernière modification du contenu
- st_mode: type et droit d'accès au fichier (à décoder avec des macros).
Cloner un descripteur
Les fonctions
#include<unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);
permettent de "dupliquer" un descripteur, c'est à dire d'ouvrir un nouveau descripteur qui correspond au même objet fichier qu'un fichier existant. La fonction dup retourne comme nouveau descripteur le plus petit indice libre dans la table des descripteurs. La fonction dup2 fait correspondre le descripteur newfd au même fichier que celui de oldfd. Si newfd était déjà utilisé, il est fermé (close).
Ces opérations sont surtout utilisées pour rediriger les entrée/sortie/erreur standards ainsi que les tubes. Pour rediriger la sortie standard vers un
fichier déjà ouvert avec pour descripteur desc, on peut effectuer:
close(STDOUT_FILENO); //On libère le descripteur 1
dup(desc); //Le descripteur 1 devient "synonyme" de desc
Attention, l'entrée standard ne dois pas être "libre" sinon c'est elle (descripteur 0) qui est redirigée vers le fichier correspondant à desc.
Avec dup2(), il suffisait d'écrire
dup2(desc, STDOUT_FILENO);
Pour rediriger l'erreur standard vers la sortie standard, il suffit d'écrire
dup2(STDOUT_FILENO, STDERR_FILENO);
Écrire le cache sur le disque
L'appel système write() ne provoque pas directement l'écriture dans le fichier sur disque. Le noyau utilise des tampons pour retarder le plus possible l'écriture sur disque qui est un mécanisme lent. On peut toutefois forcer la synchronisation des fichiers avec les tampons par les
appels système
#include<unistd.h>
int fsync(int fd);
void sync(void);
L'appel système fsync() effectue l'action pour un descripteur de fichier en particulier. sync() effectue l'action pour tous les fichiers ouverts du système.
La bibliothèque d'entrée-sortie standard
Après avoir vu les fonctions de bas niveau pour la manipulation de fichier, voyons la bibliothèque de haut niveau.
Cette bibliothèque est une "couche logicielle" au dessus des appels système et des descripteurs vus précédemment. Les avantages de cette bibliothèque par rapport aux appels système directs sont:
- La portabilité
- Optimisation des appels système sous-jacents par l'utilisation de tampons (buffers)
- Grand nombre de fonctions et d'options
Avec les fonctions de la bibliothèque d'E/S standard, les fichiers sont représentés par un objet du type "opaque" FILE (qui contiendra le descripteur du fichier). On utilise des pointeurs vers ces objets plutôt que les objets eux-mêmes. On parle souvent de flux plutôt que de fichier quand on utilise cette bibliothèque.
Les objets FILE (en fait FILE *) correspondant aux flux standard sont stdin, stdout, stderr.
Manipulation de FILE
L'ouverture d'un flux s'effectue par l'une des fonctions
#include <stdio.h>
FILE *fopen (const char *path, const char *mode);
FILE *fdopen (int fildes,constchar *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);
fopen() retourne un pointeur vers l'objet FILE correspondant au fichier. Le mode est une chaîne de caractère parmi les suivante:
| mode | description |
|---|---|
| "r" | lecture seule (le fichier doit exister) |
| "r+" |
lecture et écriture (le fichier doit exister) |
| "w" |
écriture (crée le fichier si inexistant, l'écrase si il existe) |
| "w+" |
lecture et écriture (crée le fichier si inexistant, l'écrase si il existe) |
| "a" |
écriture en fin de fichier (ajout) |
| "a+" |
ajout et lecture |
int fileno (FILE *stream);
retourne le descripteur associé au flux stream.
La fonction fdopen() permet d'ouvrir un flux à partir d'un descripteur déjà ouvert. Le mode doit être compatible avec le mode du descripteur.
La fonction freopen() permet d'associer un nouveau fichier à un flux déjà ouvert. Cette fonction sert principalement à effectuer des redirections.
Pare exemple:
freopen("erreurs", "a", stderr);
//redirection de l'erreur vers le fichier "erreurs".
Ces fonctions retournent NULL en cas d'erreur.
La fermeture d'un flux s'effectue par
#include <stdio.h>
int fclose (FILE *stream);
Écriture dans un fichier
Il existe une série de fonctions permettant d'écrire dans un flux.
L'écriture d'un caractère ou d'une chaîne peut être réalisée par l'une des fonctions suivantes:
#include<stdio.h>
int fputc(intc, FILE *stream);
int putc(intc, FILE *stream);
int putchar(intc);
int fputs(constchar *s, FILE *stream);
int puts(constchar * s);
Les fonctions qui n'ont pas de paramètre de type FILE* écrivent sur stdout. putc() et putchar() peuvent être implémentées sous forme de macro et sont donc parfois plus performantes mais attention de ne pas utiliser des paramètres ayant des effets de bord car ils peuvent être évalués plusieurs fois.
Les fonctions qui écrivent un caractère le retournent comme valeur si elles réussissent et retournent EOF en cas d'erreur. Les fonctions qui écrivent des chaînes retournent une valeur non négative si elles réussissent et EOF en cas d'échec.
La fonction
#include <stdio.h>
size_t fwrite (const void *ptr, size_t size, size_t nmemb, FILE *stream);
écrit dans le flux stream, le tableau pointé par le pointeur ptr, size est la taille (en octets) d'un élément du tableau et nmemb est le nombre
d'éléments à écrire. La fonction retourne le nombre d'éléments écrits.
Les fonctions
#include <stdio.h>
int printf (const char *format, ...);
int fprintf (FILE *stream, const char *format, ...);
permettent d'écrire dans un flux des données de différents types. printf() écrit dans stdout.
Dans la chaîne format, chaque caractère % indique une donnée à écrire. Le caractère qui suit % indique généralement le type de la donnée et le format sous lequel l'écrire. P.e: 'd' un entier à écrire sous forme décimale, 'x' un entier non signé à écrire sous forme hexadécimale, 'f' un double flottant en virgule fixe, 'e' un double flottant en notation avec exposant, 's' une chaîne de caractères ...
Les paramètres qui suivent la chaîne de format sont les données (variables, valeur littérale, ...).
La fonction parcourt la chaîne de format et pour chaque indication de donnée (%) écrit selon le format spécifié le paramètre suivant dans la liste des paramètres.
Gestion du tampon
Les fonctions de la bibliothèque d'E/S standard utilisent un tampon et chaque écriture dans un flux ne provoque pas forcément un écriture dans le
fichier sous-jacent (appel à write()).
Les écritures réelles n'ont lieu que lorsque le tampon est plein (c'est le cas par défaut pour les fichiers réguliers) ou lorsqu'on écrit un caractère
de retour à la ligne '\n' (c'est le cas sur la sortie standard).
Parfois il n'y a pas de tampon du tout (c'est le cas de la sortie d'erreur standard, stderr).
On peut forcer à tout moment l'écriture du tampon par la fonction
#include<stdio.h>
int fflush(FILE *flux);
Cette opération a parfois lieu automatiquement par exemple quand on effectue une lecture dans un flux.
La configuration du tampon d'un flux est réalisée par la fonction
#include <stdio.h>
int setvbuf (FILE *stream, char *buf, int mode, size_t size);
stream est le flux concerné, buf est un pointeur vers un nouveau tampon à utiliser (si NULL, la fonction l'allouera toute seule ce qui est conseillé), size est la taille du tampon et mode indique à quel moment le tampon est écrit. Les valeurs possibles de mode sont:
- _IONBF pas de tampon, écriture directe
- _IOLBF écriture du tampon à chaque écriture d'un retour à la ligne
- _IOFBF écriture du tamponquand il est plein.
La fonction setvbuf() ne peut être appelée qu'après l'ouverture du flux et avant toute opération sur celui-ci.
Lecture d'un fichier
La lecture d'un caractère où d'une chaîne dans un fichier s'effectue par l'une des fonctions suivantes:
#include <stdio.h>
int fgetc (FILE *stream);
int getc (FILE * stream);
int getchar (void);
char * fgets (char * s, int size, FILE * stream);
char * gets (char * s);
qui sont le pendant des fonctions d'écriture.
Attention toutefois à la fonction gets() qui ne permet pas de préciser la taille du tableau de caractères comme le fait fgets(), ce qui peut engendrer un débordement du tableau.
Ces fonctions retournent le caractère lu ou EOF en cas d'échec.
La fonction
#include <stdio.h>
int ungetc (int c, FILE *stream);
permet de replacer un caractère dans tampon de lecture du flux, ce qui peut être très pratique pour implémenter un lexeur par exemple. Il est conseillé de ne faire qu'un seul appel. Ce caractère sera le 1er caractère de la prochaine lecture.
La fonction
#include <stdio.h>
size_t fread (void *ptr, size_t size, size_t nmemb, FILE *stream);
est le pendant de la fonction fwrite(). Les objets lus sont stockés à l'adresse indiquée par ptr. La fonction retourne le nombre d'objets lus.
Les fonctions
#include <stdio.h>
int scanf(constchar * format, ...);
int fscanf(FILE * stream,const char * format, ...);
sont le pendant de printf() et fprintf().
Les paramètres qui suivent la chaîne de format doivent être des pointeurs vers des objets où seront stockées les valeurs lues (contrairement à printf où on donnait directement l'objet au lieu d'un pointeur).
Comme avec fprintf(), les données à lire dans le flux sont introduites par le caractère %. Les caractères qui ne sont pas précédés par % doivent apparaître "tel quel" dans le flux sauf les "blancs" (espace, tabulation, retour à la ligne) qui correspondent à un nombre quelconque de blancs.
La lecture d'une chaîne de caractères s'arrête au premier caractère blanc.
La fonction retourne le nombre de données lues ou EOF en cas d'erreur.
Il existe d'autres spécifications de format via % qui rendent les fonctions fprintf() et fscanf() très pratiques pour créer ou décomposer un flux de données au point que ces deux fonctions existent avec comme paramètre une chaîne de caractères plutôt
qu'un flux:
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
int sscanf (const char * str, const char * format, ...);
Les fonctions sprintf() et snprintf() écrivent les données dans la chaîne str plutôt que dans un flux. La fonction snprintf() est moins "dangereuse" à utiliser que sprintf() dans la mesure où elle permet de spécifier la taille de la chaîne afin que la fonction ne la
dépasse pas.
La fonction sscanf() lit les données à partir de la chaîne plutôt que d'un flux.
Traitement des erreurs
La plupart des fonctions vues retournent EOF en cas d'erreur. Pour tester si l'erreur provient du fait que la fin du fichier est atteinte, on peut utiliser la fonction
int feof (FILE *stream);
Inversément, on pourra utiliser la fonction pour vérifier si il y a une erreur:
int ferror (FILE *stream);
Ces deux fonctions retournent un valeur nulle pour indiquer que la fin de fichier n'est pas atteinte (pour feof()) ou qu'il n'y a pas d'erreur (ferror()) et une valeur non nulle sinon.
Manipulation de fichier
Certaines fonctions de nécessitent pas d'ouvrir le fichier dans l'espace programme, ce sont les fonctions de manipulation de fichier.
Gérer les liens (lien-dur ou hard-link)
Il est possible de créer plusieurs lien vers un même fichier, ce sont différents nom pour accéder aux mêmes données (donc à la même inode), c'est ce qu'on appelle un hard-link. Un fichier n'est supprimé que quand il n'existe plus aucun lien vers lui, il n'existe donc pas de fonction de suppression de fichier, il suffit d'en supprimer tout les liens.
Les hard-links ne peuvent être fait que dans la même partition que le fichier.
La commande ln permet de créer des liens dur entre les fichiers.
$ ls -i #On a un seul fichier
1537068 Ikipou
$ ln Ikipou IkipouBis #On crée le hardlink de nom "IkipouBis"
$ ls -i #On a maintenant deux liens vers la même inode (même numéro)
1537068 Ikipou 1537068 IkipouBis
$ # Il faut supprimer les deux liens pour supprimer effectivement le fichier
La gestion des liens peut se faire avec les fonctions link() et unlink():
#include <unistd.h>
int link (const char *oldpath,const char *newpath);
int unlink(constchar *pathname);
permettent respectivement de créer/supprimer un lien (dur) sur un fichier. La suppression d'un lien correspond à la suppression de l'entrée dans le répertoire où se trouve ce lien.
La suppression effective d'un fichier sur disque n'a lieu que lorsqu'il n'y a plus de lien sur lui (y compris les liens dus au fait que le fichier est ouvert dans un processus en cours d'exécution).
Gérer les liens symboliques
La fonction
#include <unistd.h>
int symlink(constchar *cible, const char *nom);
permet la création d'un lien symbolique. Le paramètre nom est le chemin du lien et cible est le fichier "pointé" par le lien.
La fonction
#include <unistd.h>
int readlink(const char *path, char *buf, size_t bufsize);
Permet de récupérer dans le tableau de caractères buf le chemin du fichier pointé par le lien symbolique dont le chemin path. bufsize est la taille
du tableau passée pour éviter que la fonction ne "déborde" du tableau. La fonction retourne le nombre de caractères écrits dans le tableau.
Manipulation de répertoire
Le type DIR est le type utilisé pour représenter un répertoire. Il n'est pas nécessaire de connaître son implémentation qui peut varier d'un système à un autre pour pouvoir l'utiliser.
Ouverture d'un dossier
L'ouverture d'un répertoire pour pouvoir consulter son contenu s'effectue par la fonction
#include<dirent.h>
DIR *opendir(constchar *name);
où name est le chemin du répertoire. Un pointeur NULL est retourné en cas d'échec.
Lecture du contenu
La fonction
#include <dirent.h>
struct dirent *readdir (DIR * dir);
retourne à chaque appel une structure dirent possedant un champ d_name qui est une chaîne de caractères contenant le nom du fichier suivant du répertoire dir. Attention chaque appel utilise la même structure dirent, un appel écrase donc le nom du fichier précédent.
La fonction:
#include <dirent.h>
int rewinddir(DIR *dir)
permet de se déplacer sur la première entrée du répertoire (de rebobinner).
Fermeture d'un dossier
La fonction
#include <dirent.h>
int closedir (DIR *dir);
permet de "fermer" le répertoire en libérant les ressources qu'il utilisait.
Créer et supprimer des dossiers
Les fonctions
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
int mkdir(constchar *pathname, mode_t mode);
int rmdir(constchar *pathname);
permettent de créer et supprimer un répertoire. Le paramètre mode a la même signification que pour les fichiers.

