Programmation:C:Entrees:Sorties
Sommaire
Introduction
Le monde UNIX obéit à la règle Tout est fichier. Cette règle s'applique aussi bien à l'environnement utilisateur où la plus part des périphériques ou certains cannaux de communication apparaissent comme des pseudo-fichiers, mais aussi dans l'environnement de programmation, puisque le système fournit une interface simple et standard pour toutes les sortes d'entrée/sortie.
Dans ce cours nous allons nous limiter aux opérations de base qui permettent de réaliser des entrées sorties sur toutes sortes de ressources.
L'interface du système utilise un type unique pour désigner ces ressources: les file descriptors. Ce type est un simple entier correspondant au numéro la case du tableau gérant les ressources de chaque processus dans le noyeau.
Les File descriptors et les appels systèmes read(2) et write(2)
Les deux appels systèmes que nous utiliseronts sont read(2) et write(2). Les deux prennent en paramètre un file descriptor comme cible. Voici leurs prototypes:
#include <unistd.h> int read (int fd, void *buf, size_t nbytes); int write (int fd, void *buf, size_t nbytes);
- read(fd, buf, n) lit (au plus) n octets dans la ressource attachée au file descriptor fd et les écrit à l'adresse buf. read renvoie le nombre de caractères lus.
- write(fd, buf, n) écrit (au plus) n de buf vers la ressource attachée au file descriptor fd. write renvoie le nombre de caractères écrits.
Si la lecture ou l'écriture échoue l'appel système renvoie -1 et fixe la variable globale errno avec un code d'erreur correspondant. Les erreurs les plus courantes sont les suivantes:
- EBADF: le paramètre fd n'est pas un file descriptor valide.
- EFAULT: le paramètre buf pointe en dehors de l'espace alloué.
- EIO: une erreur s'est produit lors de l'accès au file system.
- Il existe de nombreux autres codes d'erreur tous listés dans les pages de manuel.
Pour read(2), il existe deux codes d'erreurs particuliers qui ne correspondent pas à une erreur bloquante:
- EAGAIN: le file descriptor est marqué comme non-bloquant est aucune données n'est disponible pour l'instant.
- EINTR: la lecture sur une ressource lente a été interrompue par l'arrivée d'un signal avant que des données ne soient disponibles.
Dans ces deux cas, il faut recommencer la lecture.
Les appels à read(2) et write(2) sont bloquants: tant que l'opération n'a pu être effectuée le programme est en sommeil et ne reprendra la main que lorsque l'opération pourra être terminée. En particulier pour la lecture sur une ressource lente (pas un fichier) bloquera jusqu'à l'arrivée de nouvelles données. On notera que bien que les opérations sont effectuées atomiquement (sans entrelacement avec d'autres opérations), le système répondra au plus vite même si le nombre d'octets lus (ou écrits dans certains cas) est inférieur au nombre d'octets demandés.
La fin de fichier sera indiqué par read(2) par une valeur de retour de 0, c'est le seul cas fiable de détection de fin de fichier. Une lecture dans un fichier ressemble donc en générale à l'exemple suivant:
/* N est une constante correspondant a la taille a lire */ int pos=0, r; char buf[N]; while ( pos < N && (r = read(fd,buf + pos, N - pos)) ) { if (r == -1) { if ( errno == EINTR ) continue; /* traitement d'erreur */ } pos += r; }
Pour finir, voici l'exemple classique: cat. La commande cat(1) affiche le fichier passé en paramètre sur la sortie standard. Cet exemple n'inclue pas la gestion des options habituelles (affichages des caractères spéciaux, numérotation des lignes ... ) Par contre, nous incluront l'affichage de l'entrée standard (- en paramètre.) On ajoute également le support de fichiers multiples en paramètre.
/* cat.c: A simple cat */ /* full support of POSIX and X/Open for glibc (linux) based system */ #define _XOPEN_SOURCE 500 #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #define STEP 1024 void mycat(int fd) { char buf[STEP]; int r; while( (r = read(fd, buf, STEP)) ) { if (r == -1) { if ( errno == EINTR || errno == EAGAIN ) continue; perror("cat"); exit(3); } write(STDOUT_FILENO,buf,r); } } char *makemsg(char f[]) { char *msg; size_t s; s = 5 + strlen(f); msg = malloc(s+1); strncpy(msg,"cat: ",6); strncat(msg,f,s-5); return msg; } void process(char f[]) { int fd = STDIN_FILENO; /* treat all files begining by '-' as standard input */ if (f[0] != '-') { char *msg; /* build error message here to avoid errno modifications */ msg = makemsg(f); if ( (fd = open(f,O_RDONLY)) == -1) { perror(msg); exit(2); } free(msg); } mycat(fd); } int main(int argc, char *argv[]) { int i; if (argc < 2) { write(STDERR_FILENO,"cat: missing parameters.\n",25); exit(1); } for (i=1; i<argc; ++i) process(argv[i]); return 0; }
Ouverture et fermeture de fichiers
Pour ouvrir et fermer les fichiers nous disposons de deux appels système (deux versions pour open(2)):
#include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, int mode);
#include <unistd.h> int close(int fd);
- open(path, flags, mode): ouvre le fichier désigné par path avec les options désignées par flags. Si l'ouverture ce fait en mode création, mode est utilisé en combinaison avec le umask(2) pour déterminer les permissions du fichier créé.
- close(fd): ferme une ressource précédement associée au file descriptor fd. Attention, la ressource n'est effectivement fermée que lorsque tous les file descriptors sur cette ressource auront été fermés (voir entre autre dup(2) et dup2(2).)
Les flags d'open(2) permettent de contrôler le mode d'ouverture des fichiers:
- O_RDONLY: lecture seule
- O_WRONLY: écriture seule
- O_RDWR: lecture et écriture
En complément, ces flags permettent de contrôler certains aspects des modes d'ouverture, on retiendra les valeurs suivantes:
- O_APPEND: les écritures ont lieu en fin de fichier.
- O_CREAT: si le fichier n'existe pas, il sera créé. C'est le seul cas où le troisième paramètre est utilisé.
- O_TRUNC: en écriture sur un fichier régulier, le fichier est tronqué à zéro (destruction du contenu existant.)
On combine ces différents flags grace l'opérateur de ou arithmétique (ou bit à bit.) Par exemple, pour ouvrir le fichier foo en lecture et écriture qui sera créé s'il n'existe pas et tronqué sinon, avec les permissions standard, on utilisera la commande:
int fd; fd = open("foo", O_RDWR|O_CREAT|O_TRUC, 0666);
On notera que les permissions doivent être données en octal (base 8) et seront combiné avec le umask(2) de la manière suivante: mode & ~umask
Déplacements
Les opérations de lecture et d'écriture dans un file descriptor s'appuie sur l'existance (lorsque cela a du sens) d'un index de position dans la ressource. Cet index est mis à jour à chaque opération (si vous lisez 5 caractères, il avancera de 5 octets ... ) L'index est unique pour la ressource: il est le même pour les opérations de lecture et d'écriture mais aussi pour tous les file descriptors pointant sur la même ressource.
Il peut être pratique de se déplacer explicitement, pour ça nous disposons de l'appel système lseek(2):
off_t lseek(int fildes, off_t offset, int whence);
- fildes: le file descriptor sur lequel le déplacement doit avoir lieu.
- offset: le décalage
- Le type off_t est système dépendant, mais dans tous les cas, il s'agit d'un type entier signé (int 64bits sur un FreeBSD actuel.)
- whence: le type de décalage. Ce paramètre doit prendre sa valeur parmis:
- SEEK_SET: l'index est positionné sur offset (décalage par rapport au début du fichier.)
- SEEK_CUR: l'index est décalé de offset octets (décalage par rapport à la position courrante.)
- SEEK_END: le nouvel index est la somme de la position de la fin de fichier et d'offset. Si la nouvelle position dépasse la fin de fichier, le trou entre la fin du fichier et la nouvelle position est rempli de caractères '\0'.
Il existe deux autres valeurs possibles pour whence, SEEK_HOLE et SEEK_DATA qui permettent de se déplacer de trou en trou (un trou est un bloc consécutif de caractères '\0') malheureusement le marquage des trous dans les fichiers est dépendant du système de fichiers, il peut ne pas être supporté, mais, pire encore, même lorsque le marquage est supporté tous les trous ne sont pas forcément marqués comme tel (et par conséquent ne seront pas utilisés pour les déplacements, voir lseek(2).)
Enfin, si l'opération réussit, lseek(2) renvoit la position en octet depuis le début du fichier. Si l'opération échoue, lseek(2) renvoit -1 et positionne errno sur le code d'erreur correspondant:
- EBADF: mauvais file descriptor
- EINVAL: whence ne correspond pas à une valeur supportée
- ENXIO: plus de trous ou de région de données pour les modes SEEK_HOLE et SEEK_DATA
- EOVERFLOW: le résultat de l'opération ne pourra pas être représenté avec le type off_t
- ESPIPE: le file descriptor pointe sur un tube (pipe(2)), une socket ou un tube nomé (mkfifo(2))
Redirections
Pour l'instant, nous ne disposons que de file descriptors ouvert avec open(2) (en plus des trois toujours ouverts.) Avant de voir d'autres possibilités (section suivante: les tubes), nous allons voir qu'il est possible de rediriger un file descriptor sur un autre. Pour ça nous avons deux appels système à notre disposition:
int dup(int oldfd); int dup2(int oldfd, int newfd);
- dup(oldfd): si oldfd est valide, dup(2) sélectionne le plus petit file descriptor disponible, lui associe la même ressource que oldfd et le renvoie.
- dup2(oldfd, newfd): dans ce cas la valeur du nouveau file descriptor est fourni (newfd) et si celui-ci est différent de oldfd (i.e. oldfd != newfd) et qu'il est déjà associé à une ressource, alors il sera libérer (comme avec un close(newfd)) avant d'être associé à la même ressource que oldfd.
Il est donc possible de rediriger certains file descriptors comme l'entrée ou la sortie standard:
int fd; fd = open("someFile",O_WRONLY|O_CREAT|O_TRUNC,0666); dup2(fd,STDOUT_FILENO); /* following operations on standard output file will be done on the file "someFile". */
Il est également possible de sauvegarder un pointeur sur une ressource avant une redirection pour pouvoir restaurer l'association plus tard:
int fd, save; save = dup(STDOUT_FILENO); fd = open("someFile",O_WRONLY|O_CREAT|O_TRUNC,0666); dup2(fd,STDOUT_FILENO); /* some operations ... */ dup2(save, STDOUT_FILENO); close(fd);
Les opérations dup et surtout dup2 sont utilisés par votre shell pour, justement, effectuer les redirections avec les opérateurs <, >, << et >>.
Tubes
Les tubes fournissent un mécanisme de communication unidirectionnel entre processus. Un tube est une file FIFO: on écrit d'un côté et on lit de l'autre dans l'ordre d'arrivée des données d'origine.
La lecture est destructrice: les octets lus dans le tube avec read(2) sont supprimés du tube (et ne sont donc plus disponibles pour les autres.)
La communication est unidirectionnel: il n'y a pas différence entre le(s) processus écrivain(s) et le(s) processus lecteur(s). Par conséquent, si on lit dans un tube après y avoir écrit, on lira ce qu'on a écrit (en fonction de l'ordre d'arrivée des données et des lectures.) Par conséquent, il faut identifier clairement les rôles des participants: certains sont lecteurs et d'autres sont rédacteurs. Si l'on veut une communication bi-directionnel, il faut deux tubes.
L'appel système pour créer un tube est pipe(2):
#include <unistd.h> int pipe(int fd[]);
- fd est un tableau d'entier d'au moins deux cases. C'est cases contiendront au retour de l'appel chaqu'une un file descriptor respectivement sur l'entrée et la sortie du tube.
- fd[0]: est la sortie (partie en lecture) du tube.
- fd[1]: est l'entrée (partie en écriture) du tube.
En cas de succès, pipe(2) renvoie 0 et -1 sinon. Dans certaines implantations (c'est par exemple le cas de FreeBSD), les deux file descriptors sont en lecture et écriture mais ce comportement n'est pas standard.
La gestion de la fermeture des extrémités du tube est très importante pour avoir un comportement cohérent. En effet, la lecture sur un tube ne renverra la fin de fichier uniquement lorsque toutes extrémités en écriture seront fermées. De plus, si toutes les extrémités en lecture sont fermées, une écriture sur le tube provoquera la réception du signal SIGPIPE (qui non rattrapé provoquera la fin du programme.)
Les tubes sont normalement utilisés avec fork(2) pour assurer la communication entre deux processus et avec le dup2(2) et le couple fork(2)/exec*(3) pour rediriger la sortie d'un programme dans l'entrée d'un autre. Voici un exemple (pour les explications sur fork(2) voir le cours sur la gestion des processus dans Programmation:C:Systeme) où l'on transmet un message entre deux processus à l'aide d'un tube. Après séparation (fork(2)) l'un des processus attend les données envoyées par l'autre sur le tube et les affiche.
/* tube.c : exemple avec les tube */ /* full support of POSIX and X/Open for glibc (linux) based system */ #define _XOPEN_SOURCE 500 #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/stat.h> char *text = NULL; #define STEPFATHER 8 void father(int fd) { size_t r, pos=0, remain; remain = strlen(text); while (remain) { r = write(fd,text+pos, STEPFATHER<remain ? STEPFATHER : remain); pos += r; remain -= r; } } #define STEPSON 4 void son(int fd) { char buf[STEPSON]; int r; while ( (r = read(fd, buf, STEPSON)) ) { if (r == -1) { if (errno == EINTR) continue; perror("tube"); exit(3); } write(STDOUT_FILENO, buf, r); } } void build_text(char f[]) { int fd, r, pos=0; struct stat s; if ( stat(f,&s) == -1) { perror("tube"); exit(3); } if ( (fd = open(f, O_RDONLY)) == -1 ) { perror("tube"); exit(3); } text = malloc(s.st_size); while (pos<s.st_size && (r = read(fd, text+pos, s.st_size - pos)) ) { if (r == -1) { if (errno == EINTR || errno == EAGAIN) continue; perror("tube"); exit(3); } pos += r; } close(fd); } int main(int argc, char *argv[]) { int t[2]; char *txt = "text.txt"; if (argc>1) txt = argv[1]; build_text(txt); pipe(t); if (fork()) { close(t[0]); father(t[1]); close(t[1]); wait(NULL); } else { close(t[1]); son(t[0]); } return 0; }
Entrées/Sorties avec buffer: stdio.h
Les appels système sont couteux (changement de contexte, probabilité suplémentaire de passer en idle ... ), on essaie donc en générale de minimiser leur usage. C'est (entre autre) pourquoi, nous avons systématiquement prévu de ne pas réaliser les opérations octet par octet.
Mais une gestion correcte des entrées/sorties pour minimiser les appels système, n'est pas toujours simple. C'est pourquoi la libc propose une solution toute faite d'entrée/sortie avec buffer.
Le principe est simple: le type FILE* cache un file descriptor et un buffer et les différentes opérations vont d'abord agir sur le buffer (si possible) et en fonction de la situation le disque et le buffer seront re-synchronisés.
Toutes les fonctions avec buffer se trouvent dans stdio.h, parmis les plus usuelles on trouve: printf(3), scanf(3), fgets(3) ...
Attention, les opérations sont asynchrones: une écriture sur la sortie standard (par exemple) ne sortira pas forcément au moment de l'opération mais au moment de la synchronisation du buffer.
Mapping de fichier en mémoire (mmap(2))
- voire le TP sur les entrée/sorties : Entrées/Sorties en C (pdf)
Cours | Partie |
---|---|
Cours de Programmation EPITA/spé | Programmation:C |