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
- Le système détermine l'inode du fichier référencé.
- 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.
- Le système vérifie les droits d'accès dans le mode demandé.
- 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. - Le système alloue une entrée dans la table des descripteurs du processus.
- 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
countoctets. 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 à
countne signifie pas nécessairement la fin du fichier (tube, terminal, socket, interruption par un signal).
Déroulement :
- Vérification du descripteur, accès aux tables système
- Vérification des droits (mode d'ouverture adéquat)
- 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)
- Le système recopie les données du buffer cache vers le tampon de l'utilisateur
- Le curseur dans le fichier est mis à jour dans l'entrée de la table des fichiers ouverts
- 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 estoffsetSEEK_CUR: la position courante est incrémentée deoffsetSEEK_END: la nouvelle position est la taille du fichier plusoffset
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 :
- vérification que
oldfdest le numéro d'une entrée utilisée ; - recopie, dans la première entrée libre de la table des descripteurs, de
l'entrée correspondant à
oldfd; - incrémentation du compteur de descripteurs de l'entrée associée à
oldfddans la table des fichiers ouverts ; - 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);
- si
fdn'est pas un descripteur valide, retour de -1 ; - l'entrée d'indice
fdde la table des descripteurs est libérée ; - le compteur de l'entrée de la table des fichiers ouverts associée à
fdest décrémenté ; lorsqu'il atteint zéro, cette entrée est libérée.