Blog ENI : Toute la veille numérique !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
💥 Les 22 & 23 novembre : Accès 100% GRATUIT
à la Bibliothèque Numérique ENI. Je m'inscris !
  1. Livres et vidéos
  2. Programmation système
  3. Les fichiers
Extrait - Programmation système Maîtrisez les appels système Linux avec le langage C (Nouvelle édition)
Extraits du livre
Programmation système Maîtrisez les appels système Linux avec le langage C (Nouvelle édition)
1 avis
Revenir à la page d'achat du livre

Les fichiers

Principes généraux

Linux a repris les principes de gestion de fichiers des systèmes de type Unix. Grâce à un concept de fichier très simple et très souple, ces systèmes permettent aux applications d’accéder à une unique arborescence composée de répertoires et contenant des fichiers qui peuvent être des fichiers disques, mais aussi des périphériques (imprimantes, terminaux, etc.) ou des canaux de communication.

1. L’interface fichier universelle

Linux présente aux applications une interface fichier universelle. Cela signifie que toutes les entrées-sorties peuvent être effectuées par l’intermédiaire de fichiers, indépendamment de la nature physique de l’objet sous-jacent. Par exemple, un programme peut utiliser l’appel système write() pour écrire une suite d’octets dans un fichier sur disque, vers une imprimante, l’écran d’un terminal, une connexion réseau ou vers l’entrée standard d’un autre programme. Il peut de même utiliser l’appel système read() pour lire une suite d’octets depuis un fichier sur disque, un disque entier, le clavier d’un terminal, une connexion réseau ou la sortie standard d’un autre programme.

Le concept de flot d’octets (bytestream), considérant un fichier comme une suite d’octets non structurée, donne au noyau Linux une grande souplesse pour les opérations d’entrées/sorties, et rend les applications qui l’utilisent indépendantes des caractéristiques physiques des périphériques accédés.

L’interface fichier universelle est possible pour tout élément géré par un contrôleur de périphérique (driver) Linux implémentant la gestion de quatre appels système élémentaires : open(), close(), read(), write().

Bien entendu, chaque périphérique peut avoir des spécificités, en dehors de l’interface commune. L’appel système ioctl() permet de tirer parti de ces spécificités.

2. Système de fichiers

Le noyau Linux gère une arborescence globale, organisée en répertoires qui peuvent contenir des répertoires...

Ouverture/fermeture d’un fichier

Pour pouvoir accéder au contenu d’un fichier, un processus doit disposer d’un descripteur de fichier associé à ce fichier, descripteur obtenu par l’ouverture du fichier.

1. Appel système open()

L’appel système open() permet à un processus de demander au noyau l’ouverture du fichier dont il lui passe le chemin d’accès en argument. Si le fichier n’existe pas, il peut éventuellement être créé, en fonction des paramètres d’ouverture spécifiés lors de l’appel système.

Le noyau traite la demande d’ouverture, en contrôlant les droits d’accès du processus sur le fichier spécifié par rapport au type d’accès demandé (lecture, écriture, avec ou sans création).

Si l’ouverture réussit, le noyau ajoute une nouvelle entrée dans la table des fichiers ouverts du processus, dans celle des descripteurs ouverts et dans celle des fichiers ouverts (sauf si le fichier était déjà ouvert).

L’appel système retourne le descripteur de fichier correspondant.

Syntaxe

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

L’appel système attend au moins deux arguments. Le troisième n’est utilisé que si le fichier doit être créé.

Arguments

path

Chemin d’accès du fichier à ouvrir. S’il s’agit d’un lien symbolique, c’est le fichier cible qui sera ouvert.

oflag

Mode et options d’ouverture.

mode

Permissions en cas de création (en combinaison avec l’umask du processus).

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

>=0

Succès, descripteur associé au fichier ouvert

Si l’ouverture réussit, les normes imposent que le descripteur retourné soit le plus petit disponible.

2. Modes d’ouverture

Il y a trois modes d’ouverture de base : lecture, écriture, lecture/écriture. Chacun est identifié par une valeur entière positive ou nulle, associée à une constante symbolique :

O_RDONLY

Lecture seulement

O_WRONLY

Écriture seulement

O_RDWR

Lecture et écriture

3. Options d’ouverture

Différentes...

Lecture d’un fichier

Pour lire le contenu d’un fichier, on peut utiliser l’appel système read().

Syntaxe

#include <unistd.h> 
ssize_t read(int fd, void *buffer, size_t count); 

Arguments

fd

Descripteur du fichier, ouvert en lecture ou en lecture/écriture

buffer

Adresse de la zone de stockage des octets lus

count

Nombre maximum d’octets à lire

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

0

Fin de fichier

>0

Nombre d’octets effectivement lus

Description

L’appel système stocke à l’adresse fournie les octets lus dans le fichier associé au descripteur, à concurrence de count octets.

La lecture se fait à l’emplacement courant dans le fichier associé au descripteur. En effet, le noyau gère la position courante dans le fichier pour chaque descripteur, dans la table des descripteurs de fichiers. Il déplace automatiquement cette position après chaque lecture.

Une demande de lecture en fin de fichier retourne zéro octet.

Le nombre d’octets effectivement lus n’est pas forcément égal au nombre demandé, pour différentes raisons, par exemple :

La fin du fichier a été atteinte avant le nombre d’octets demandé.

Le fichier n’est pas un fichier disque et il est géré en mode ligne, chaque lecture s’arrête...

Écriture d’un fichier

L’appel système write() permet d’écrire dans un fichier ouvert, à partir de la position courante.

Syntaxe

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

Arguments

fd

Descripteur du fichier

buffer

Adresse de la zone de stockage des octets à écrire

count

Nombre maximum d’octets à écrire

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

>=0

Nombre d’octets effectivement écrits

Description

L’appel retourne le nombre d’octets effectivement pris en compte pour l’écriture. Ce nombre n’est pas forcément égal au nombre demandé, pour différentes raisons, par exemple :

  • Le système de fichiers peut être saturé.

  • Le quota d’espace disque attribué à l’utilisateur associé au processus a pu être atteint.

  • La taille maximale d’un fichier a pu être atteinte.

Le noyau positionne automatiquement l’emplacement courant du descripteur utilisé après le dernier octet de l’écriture qui vient d’être effectuée.

Attention, le retour avec succès de l’appel n’implique pas que les données sont effectivement écrites physiquement sur le disque. En effet, par défaut, pour certains types de fichiers (notamment les fichiers ordinaires), le noyau utilise des mécanismes de mise en tampon des entrées/sorties, afin d’optimiser le nombre d’accès aux disques.

Exemple

Ce programme, ecritfic.c, copie un fichier dans un autre.

S’il n’y a qu’un seul argument, le programme copie le fichier vers la sortie standard. Si le fichier destinataire n’existe pas, il est créé avec comme permissions 666 - la valeur d’umask.

Syntaxe d’exécution :

ecritfic FicOrig [FicDest] 

#include...

Déplacement dans un fichier

L’appel système lseek() permet de modifier l’emplacement courant associé à un descripteur de fichier.

On peut spécifier la position à atteindre en partant de la position courante, du début ou de la fin du fichier.

Syntaxe

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

Arguments

fd

Descripteur du fichier

offset

Longueur en octets du déplacement (positif, nul ou négatif)

whence

Point de départ du déplacement

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

>= 0

Nouvelle position en octets par rapport au début du fichier

Description

Le point de départ du déplacement peut être spécifié en utilisant l’une de ces trois constantes symboliques :

SEEK_SET à partir du début du fichier. Le nombre d’octets de déplacement doit être positif. Un déplacement nul place la position courante au premier octet du fichier.

SEEK_CUR à partir de la position courante. Le nombre d’octets de déplacement peut être positif ou négatif.

SEEK_END à partir de la fin du fichier. Le nombre d’octets de déplacement est généralement négatif, mais peut être positif. Un déplacement nul place la position courante après le dernier octet du fichier.

Le déplacement ne peut pas amener la position courante avant le début du fichier. 

Le déplacement demandé peut conduire à dépasser la fin du fichier, en « sautant » des octets, c’est-à-dire en fixant la position courante n octets après la fin actuelle (n > 0). Dans ce cas, une écriture à cette position ne provoquera pas d’erreur, le résultat de l’opération sera un fichier à « trous »...

Gestion des fichiers ouverts

Une fois un fichier ouvert, associé à un descripteur de fichier, on peut gérer certaines de ses caractéristiques par l’appel système fcntl(). Les fonctionnalités de l’appel dépendent du type de fichier ouvert. Certaines caractéristiques ne sont pas modifiables.

Syntaxe

#include <fcntl.h> 
int fcntl(int fd, int cmd, ... [arg]); 

Arguments

fd

Descripteur du fichier

cmd

Opération à effectuer

arg

Argument optionnel, dépendant de l’opération à effectuer

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

!= -1

Succès, valeur variable selon l’opération effectuée

Description

L’appel système permet de spécifier différentes opérations sur un fichier ouvert, soit pour obtenir des informations, soit pour modifier certaines d’entre elles. Les opérations possibles sont, entre autres :

F_GETFL

Obtenir les options du fichier ouvert

F_SETFL

Modifier les options du fichier ouvert

D’autres opérations sont possibles, nous les étudierons ultérieurement.

1. Obtenir les options d’ouverture

L’opération F_GETFL permet d’obtenir les options d’ouverture du fichier associé au descripteur, par exemple :

int flags, accessMode; 
flags = fcntl(fd, F_GETFL); 

Dans ce cas, l’appel système fcntl() retourne un entier, résultant de la combinaison des options d’ouverture. On peut comparer cette valeur avec différentes valeurs d’options, via les constantes symboliques.

Pour connaître le mode d’ouverture (lecture, écriture, lecture/écriture), il faut d’abord combiner la valeur retournée, par un ET binaire, avec la constante symbolique O_ACCMODE, pour isoler les valeurs correspondantes, puis tester avec les constantes symboliques O_WRONLY, O_RDWR ou O_RDONLY.

a. Exemple

Ce programme, infostdio.c, affiche les options d’ouverture des descripteurs de fichier associés aux entrées/sorties standard :

#include <fcntl.h> 
#include <unistd.h> 
#include <errno.h> 
#include <stdio.h> 
// Ce programme affiche le mode d'ouverture 
// de chacune des trois entrées/sorties standard. 
int main(void) 
{ ...

Duplication de descripteurs

Nous avons vu que deux descripteurs de fichiers d’un processus pouvaient correspondre à un même descripteur de fichier ouvert pour le système. Dans ce cas, ils partagent les mêmes attributs d’ouverture et la même position courante dans le fichier.

Ce mécanisme est utilisé par le shell pour permettre de rediriger les deux sorties standards (sortie standard et sortie d’erreur standard) vers un même fichier, avec cette syntaxe par exemple :

./sauve.sh >fic.log 2>&1 

ou celle-ci :

./sauve.sh 2>fic.log >&2 

Ouvrir deux fois le même fichier, par la ligne de commande ./sauve.sh >fic.log 2>fic.log, ne produirait pas le même résultat, car chaque ouverture serait associée à un descripteur différent, chacun avec ses attributs d’ouverture et sa position courante. Les écritures se mélangeraient et s’écraseraient les unes les autres.

1. Appel système dup()

Pour dupliquer un descripteur de fichier ouvert, on peut utiliser l’appel système dup().

Syntaxe

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

Arguments

oldfd

Descripteur du fichier déjà ouvert à dupliquer.

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

>= 0

Nouveau descripteur, duplication de oldfd

Description

Cet appel système duplique le descripteur reçu en argument, en utilisant le plus petit descripteur de fichier disponible pour le processus.

Exemple

Ce programme, testdup.c, redirige la sortie d’erreur standard sur la sortie standard. Il écrit ensuite sur la sortie d’erreur standard et l’écriture se fait dans le fichier associé à la sortie standard.

#include <unistd.h> 
#include <errno.h> 
#include <stdio.h> 
// Ce programme duplique la sortie standard sur la sortie 
d'erreur standard. 
// Il écrit ensuite...

Gestion des répertoires et des liens

Un répertoire constitue un nœud de l’arborescence d’un système de fichiers. Il peut contenir des fichiers et des répertoires. Il s’agit d’un type de fichier spécifique (identifié par la valeur 4 pour les quatre premiers bits du champ st_mode de l’inode), stockant une table dans laquelle chaque nom d’objet qu’il contient est associé à un numéro d’inode du système de fichier. Ce nom permet au noyau de trouver les informations sur le fichier correspondant (via l’inode), c’est la raison pour laquelle on l’appelle un lien (link).

Un répertoire ne peut être ouvert qu’en lecture. Pour le créer, modifier ses caractéristiques, le supprimer ou pour parcourir son contenu, il faut utiliser des appels système spécifiques.

1. Notion de lien physique

Comme nous l’avons vu, un répertoire contient une table avec une entrée pour chacun des objets qu’il contient. Chaque entrée associe un nom (un lien physique) et un inode (par son numéro). Le nom est utilisable avec certains appels système, pour accéder au fichier physique associé (open(), stat(), etc.). Mais ce nom ne sert au noyau que pour faire le lien avec l’inode correspondant. Le contrôle d’accès puis l’accès aux données éventuelles du fichier se font en allant lire les attributs du fichier (et les adresses des blocs de données) dans la table des inodes (ou en lui allouant une nouvelle entrée en cas de création).

Ce mécanisme a des conséquences :

  • Plusieurs liens peuvent pointer sur le même inode.

  • Tant qu’un fichier a au moins un lien physique associé, il n’est pas supprimé par le noyau. L’appel système de suppression, unlink(), supprime le lien, pas directement le fichier physique.

  • Comme chaque système de fichiers possède sa propre table d’inode, on ne peut pas faire un lien physique vers un fichier d’un autre système de fichiers.

2. Répertoires et liens physiques

Tout fichier de type répertoire possède plusieurs liens physiques :

  • Son nom dans le répertoire qui le contient.

  • Le lien . dans sa propre table de contenu.

  • Le lien .. dans chacun...

Lire les attributs d’un fichier

Les appels système de la famille stat() permettent de lire les informations stockées dans l’inode d’un fichier.

1. Appels système stat(), lstat(), fstat()

Syntaxe

#include <sys/stat.h> 
int stat(const char *pathname, struct stat *statbuf); 
int lstat(const char *pathname, struct stat *statbuf); 
int fstat(int fd, struct stat *statbuf); 

Arguments

pathname

Chemin d’accès du fichier

fd

Descripteur du fichier ouvert

statbuf

Adresse d’une structure de type stat

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

0

Succès

Description

Ces trois appels système retournent les attributs du fichier dans une structure de type stat. stat() traite un chemin d’accès, fstat()un fichier ouvert. lstat() permet d’obtenir les informations d’un lien symbolique et non pas celles de son fichier cible, si le fichier indiqué est un lien symbolique.

Les appels stat() et lstat() n’accèdent pas au fichier lui-même, ils n’ont donc pas besoin d’avoir de droits d’accès particuliers sur celui-ci. Par contre, ils doivent pouvoir accéder en lecture au répertoire qui le contient et en accès sur les répertoires à traverser pour l’atteindre.

fstat(), utilisant un fichier déjà ouvert, ne peut pas échouer, sauf si le descripteur est invalide.

2. Appel système fstatat()

Cet appel système, plus récent, peut remplacer les trois précédents.

Syntaxe

#include <fcntl.h> 
#include <sys/stat.h> 
int fstatat(int dirfd, const char *pathname, struct stat 
*statbuf, int flags); 

Arguments

dirfd

Descripteur de répertoire, de fichier ou entier définissant une option

pathname

Chemin d’accès du fichier

statbuf

Adresse d’une structure de type stat

flags

Options

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

0

Succès

Description

Cet appel système très général permet de traiter différents cas de figure, selon la valeur des arguments passés.

Si le chemin d’accès indiqué dans pathname est relatif, il sera recherché par rapport au répertoire associé au handle de répertoire dirfd.

Si le chemin...

Gestion du contrôle d’accès

Unix dispose d’un mécanisme de contrôle d’accès aux ressources gérées par le système, s’appuyant sur les notions de compte utilisateur et de groupe d’utilisateurs. Bien que très simple, il suffit à la plupart des besoins courants, a été repris par tous les systèmes de type Unix, et a été inclus dans les normes POSIX. 

Linux a repris ce modèle, mais propose également des mécanismes plus récents et plus riches de fonctionnalités, parmi lesquels la gestion des ACL (Access Control List) POSIX, plus récente.

Pour un contrôle d’accès plus général, une meilleure granularité et pour s’affranchir des limites conceptuelles des autres modèles de sécurité, Linux implémente aussi des modèles spécifiques de gestion du contrôle d’accès, via la gestion des capacités (capabilities) et Linux SE (Security Enhanced).

Ces éléments ne seront pas traités dans le cadre de cet ouvrage.

1. Propriétaire et groupe d’un fichier

Un fichier Linux est toujours associé à un compte utilisateur et à un groupe d’utilisateur. Ces deux éléments permettent de définir trois catégories d’utilisateurs par rapport au fichier : son propriétaire, les utilisateurs membres du groupe, et ceux qui ne sont ni propriétaires ni membres du groupe. Un compte utilisateur ne peut faire partie que d’une catégorie, déterminée dans l’ordre :

  • Si le compte est propriétaire du fichier, il appartient à la catégorie propriétaire (user).

  • Sinon

  • Si le compte utilisateur fait partie du groupe associé au fichier, il appartient à la catégorie groupe (group).

  • Sinon il appartient à la catégorie autres (other).

La détermination de l’utilisateur propriétaire du fichier et du groupe associé au fichier se fait à la création du fichier. Des appels système permettent de les modifier ensuite.

Le propriétaire d’un fichier peut modifier ses permissions d’accès (y compris les droits spéciaux), changer son groupe associé...

Verrouillage d’un fichier

Par défaut, les fichiers des systèmes de type Unix peuvent être accédés simultanément par différents processus, y compris en écriture. Un processus peut difficilement déterminer si un fichier est en cours d’utilisation par un autre processus, il est même possible de supprimer un fichier qu’un processus a ouvert au préalable : ce dernier continuera à accéder normalement au fichier, en lecture comme en écriture, et le fichier ne sera effectivement supprimé que lorsque tous les processus l’auront fermé. Ces caractéristiques sont liées au concept même de fichier d’Unix, l’interface fichier universelle, et c’est ce qui permet de gérer les périphériques comme s’il s’agissait de fichiers.

Cependant, il peut être essentiel pour une application de garantir un accès exclusif sur tout ou partie d’un fichier. Un mécanisme de verrouillage a donc été implémenté sur la plupart des systèmes de type Unix, dont Linux.

Il existe plusieurs mécanismes distincts de verrouillage des fichiers, gérés par des appels système différents. L’appel système flock(), limité (verrou sur l’intégralité du fichier, pas de verrouillage impératif) et en voie d’obsolescence, ne sera pas traité ici.

1. Types de verrous

a. Verrouillage consultatif ou impératif

Un processus peut demander à verrouiller un fichier, entièrement ou en partie, par l’appel système fcntl(). Cependant, indépendamment du processus, l’effet de ce verrouillage peut être consultatif (advisory) ou impératif (mandatory).

Verrou consultatif (advisory)

Par défaut, un verrou est consultatif. Cela signifie qu’il ne s’impose pas aux processus. C’est au développeur de vérifier la présence éventuelle d’un verrou sur le fichier (ou la partie du fichier) qu’il veut lire ou écrire, via l’appel système fcntl(). Si le fichier est verrouillé, le processus doit se mettre en attente de son déverrouillage par le processus propriétaire du verrou.

Mais si un processus accède au fichier...

Fichiers gérés en mémoire (mapping)

Le mécanisme de projection en mémoire (memory mapping) d’un fichier ouvert permet à un ou plusieurs processus de gérer son contenu (complet ou seulement certaines zones) directement en mémoire, sans avoir besoin d’utiliser les appels système vus dans ce chapitre (read(), write(), etc.).

Le fichier projeté en mémoire peut être en usage exclusif ou partagé pour le processus à l’origine du mapping.

C’est cette technique qui est utilisée pour partager les programmes exécutables chargés en mémoire (bibliothèques dynamiques).

1. Projection en mémoire : mmap()

L’appel système mmap() permet de projeter en mémoire tout ou partie d’un fichier.

Syntaxe

#include <sys/mman.h> 
void *mmap(void *addr, size_t length, int prot, int flags, int 
fd, off_t offset); 

Arguments

addr

NULL ou adresse mémoire où projeter le fichier

length

Nombre d’octets à projeter

prot

Type de protection d’accès de la zone mémoire

flags

Type de projection : MAP_PRIVATE ou MAP_SHARED

fd

Descripteur du fichier à projeter

offset

Position de départ en octets du mapping dans le fichier

Valeur retournée

-1

Erreur, code erreur positionné dans la variable errno

!= -1

Succès, adresse de début de la projection

Description

En général, l’argument addr est NULL. Dans ce cas, le noyau choisit une adresse adéquate, retournée par l’appel système. Si le programme spécifie une adresse dans l’argument addr, le noyau essaye de l’utiliser, mais peut la modifier pour l’adapter à un alignement correct. La véritable adresse de chargement sera celle retournée par l’appel système.

Le type de protection mémoire est spécifié dans l’argument prot et peut prendre les valeurs suivantes :

  • PROT_NONE

Aucun accès n’est autorisé. Ce type de protection sert à éviter des débordements de mémoire intempestifs de la part du processus. Ce n’est pas une valeur adaptée pour des projections de fichier.

  • Une combinaison de ces trois droits d’accès :

PROT_READ : accès autorisé en lecture

PROT_WRITE...