Aller au contenu principal

Appels système d'entrées-sorties

Les appels système d'entrées-sorties, ou entrées-sorties de bas niveau, sont rudimentaires mais polymorphes : ce sont eux qui permettent d'écrire des programmes indépendants des supports physiques sur lesquels se font les entrées-sorties, et de pouvoir facilement changer les supports physiques associés à une entrée-sortie. Les appels système du système de gestion de fichiers sont :

  • open/creat : ouverture/création d'un fichier
  • read/write : lecture/écriture sur un fichier ouvert
  • lseek : déplacement du pointeur de fichier
  • dup, dup2 : duplication d'un descripteur
  • close : fermeture d'un fichier
  • mount : montage d'un système de fichiers
  • mknod : création d'une inode de fichier spécial
  • pipe : création d'un tube
  • fcntl : manipulation des caractéristiques des ouvertures de fichiers

Les appels système sont réalisés par le noyau ; en cas d'erreur, ils retournent en général -1 et placent le code d'erreur dans la variable errno (affichable avec perror ou strerror).

open​

#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

Ouverture du fichier de référence pathname (absolue ou relative au répertoire courant). Le mode d'ouverture flags contient exactement un des trois modes d'accès O_RDONLY, O_WRONLY ou O_RDWR, combiné par un OU bit à bit (|) avec d'éventuelles options :

O_RDONLY   /* lecture seule */
O_WRONLY /* écriture seule */
O_RDWR /* lecture et écriture */
O_NONBLOCK /* entrées-sorties non bloquantes (O_NDELAY en System V) */
O_APPEND /* chaque écriture se fait en fin de fichier */
O_CREAT /* création du fichier s'il n'existe pas */
O_TRUNC /* troncature à la taille 0 */
O_EXCL /* avec O_CREAT : échec si le fichier existe déjà */

Le paramètre mode (permissions) n'a de sens qu'à la création du fichier (O_CREAT) ; il permet de positionner les droits dans le champ mode de l'inode. Les droits effectivement positionnés dépendent de la valeur du masque umask du processus, selon la formule droits = mode & ~umask. La valeur de umask est souvent 022 par défaut. La valeur de retour de open est le numéro de l'entrée de la table des descripteurs du processus qui a été allouée par open (le plus petit numéro libre). Ce numéro est appelé descripteur de l'ouverture. Ce descripteur est utilisé dans les autres appels système pour spécifier l'ouverture de fichier sur laquelle on veut travailler. open retourne -1 en cas d'échec de l'ouverture.

Déroulement interne d'un appel de open​

  1. Le système détermine l'inode du fichier référencé.
  2. Soit l'inode est déjà dans la table des inodes en mémoire, soit il alloue une entrée et recopie l'inode du disque.
  3. Le système vérifie les droits d'accès dans le mode demandé.
  4. Il alloue une entrée dans la table des fichiers ouverts du système, et positionne le curseur de lecture-écriture dans le fichier (offset = 0). Avec O_APPEND, le curseur est replacé en fin de fichier avant chaque écriture.
  5. Le système alloue une entrée dans la table des descripteurs du processus.
  6. Il renvoie au processus le numéro de descripteur, c'est-à-dire le numéro de l'entrée qu'il vient d'allouer dans cette table.

Si l'opération a échoué dans une des étapes, le système renvoie -1.

creat​

Création d'un fichier et ouverture en écriture : int creat(const char *pathname, mode_t mode);, équivalent à open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode). Le système détermine l'inode du catalogue où l'on demande la création du fichier.

  • S'il existe déjà une inode pour le fichier :
    • le noyau lit l'inode en question (allocation dans la table des inodes en mémoire), vérifie que c'est un fichier ordinaire autorisé en écriture pour le propriétaire effectif du processus, sinon échec ;
    • le système libère les blocs de données et réduit la taille du fichier à zéro ; il ne modifie pas les droits qu'avait le fichier antérieurement.
  • S'il n'existait pas d'inode pour le fichier :
    • le système teste les droits en écriture sur le catalogue ;
    • il alloue une nouvelle inode ;
    • il alloue une nouvelle entrée dans la table des inodes en mémoire.

read​

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • fd : entrée de la table des descripteurs correspondant au fichier dans lequel doit être effectuée la lecture (fourni par open)
  • count : nombre d'octets à lire dans le fichier
  • buf : un tampon alloué par l'utilisateur, d'au moins count octets. Les octets lus sont placés dans ce tampon
  • valeur de retour : nombre d'octets effectivement lus, ou -1 en cas d'échec de l'appel système. La fin de fichier est signalée par une valeur de retour égale à 0. Un nombre d'octets lus inférieur à count ne signifie pas nécessairement la fin du fichier (tube, terminal, socket, interruption par un signal).

Déroulement :

  1. Vérification du descripteur, accès aux tables système
  2. Vérification des droits (mode d'ouverture adéquat)
  3. Grâce à l'inode, le système obtient les adresses des blocs contenant les données à lire. Le système effectue la lecture de ces blocs (via le buffer cache)
  4. Le système recopie les données du buffer cache vers le tampon de l'utilisateur
  5. Le curseur dans le fichier est mis à jour dans l'entrée de la table des fichiers ouverts
  6. Le système renvoie le nombre d'octets effectivement lus

write​

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

Même déroulement que read, mais avec une allocation éventuelle de blocs disque dans le cas d'un ajout au-delà de la fin du fichier. Dans le cas où l'appel concerne un périphérique en mode caractère, le système active la fonction write (réciproquement read pour une lecture) du pilote du périphérique, qui utilise directement l'adresse du tampon utilisateur.

Remarquons ici encore le polymorphisme de ces deux appels système, qui permet de lire et d'écrire sur une grande variété de périphériques en utilisant une seule syntaxe. Le code C utilisant l'appel système marchera donc indifféremment sur tous les types de périphériques qui sont définis dans le système de fichiers. Par exemple, il existe deux périphériques « logiques », /dev/null et /dev/zero. Le premier est toujours vide en lecture (fin de fichier immédiate) et les écritures n'ont aucun effet (il est donc possible d'y déverser n'importe quoi). Le second fournit en lecture une suite infinie d'octets nuls ; les écritures y sont acceptées et ignorées.

lseek​

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

lseek permet de déplacer le curseur de fichier, stocké dans la table des fichiers ouverts du système. offset est un déplacement en octets, fd est le descripteur et whence une des trois constantes SEEK_SET, SEEK_CUR ou SEEK_END (anciennement L_SET, L_INCR et L_XTND sous BSD).

  • SEEK_SET : la nouvelle position est offset
  • SEEK_CUR : la position courante est incrémentée de offset
  • SEEK_END : la nouvelle position est la taille du fichier plus offset

Une position au-delà de la fin du fichier est autorisée : la taille du fichier n'est pas modifiée par lseek, mais une écriture ultérieure à cette position crée un « trou » (lu comme des octets nuls). Une position résultante négative provoque une erreur (EINVAL). lseek n'est pas applicable aux tubes, FIFO et sockets (ESPIPE).

La valeur de retour de lseek est la nouvelle position du curseur dans le fichier (mesurée depuis le début), ou -1 si l'appel a échoué.

dup et dup2​

Les appels dup et dup2 permettent de dupliquer des entrées de la table des descripteurs du processus.

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

Déroulement de dup :

  1. vérification que oldfd est le numéro d'une entrée utilisée ;
  2. recopie, dans la première entrée libre de la table des descripteurs, de l'entrée correspondant à oldfd ;
  3. incrémentation du compteur de descripteurs de l'entrée associée à oldfd dans la table des fichiers ouverts ;
  4. renvoi de l'indice, dans la table des descripteurs, de l'entrée nouvellement allouée.

Les deux descripteurs partagent alors la même entrée de la table des fichiers ouverts, donc le même curseur et le même mode d'ouverture. dup2 réalise la même opération mais en imposant le numéro du nouveau descripteur newfd : si newfd était ouvert, il est d'abord fermé (de façon atomique). C'est le mécanisme utilisé par le shell pour les redirections, par exemple dup2(fd, STDOUT_FILENO).

close​

Fermeture d'un fichier : int close(int fd);

  1. si fd n'est pas un descripteur valide, retour de -1 ;
  2. l'entrée d'indice fd de la table des descripteurs est libérée ;
  3. le compteur de l'entrée de la table des fichiers ouverts associée à fd est décrémenté ; lorsqu'il atteint zéro, cette entrée est libérée.