Ce cours approfondit les mécanismes fondamentaux de la gestion des fichiers sous Linux, en explorant les concepts depuis les descripteurs de fichiers bas niveau jusqu’à l’architecture complète du Virtual File System (VFS). Il serait intéressant de consulter également le cours Partitions et système de fichiers , qui complète celui-ci en apportant un éclairage supplémentaire sur la structuration de l’information au niveau des partitions.

La pointe de l’iceberg

Écrire dans un fichier est une opération fondamentale dans la plupart des langages de programmation, permettant de stocker des données de manière persistante ou de générer des fichiers destinés à d’autres programmes. Selon le langage et le niveau d’abstraction choisi, cette tâche peut se faire directement via des descripteurs de fichiers bas niveau, offrant un contrôle précis et rapide avec le noyau, ou via des flux/objets plus haut niveau qui simplifient la gestion des tampons et des erreurs. La compréhension des mécanismes sous-jacents, comme le concept de File Descriptor en C, est essentielle pour écrire du code robuste et éviter les fuites de ressources ou les comportements inattendus.

En C (version linux):

#include <fcntl.h>    // Pour 'open'
#include <unistd.h>   // Pour 'write'
#include <string.h>   // Pour 'strlen'
#include <stdio.h>    // Pour 'perror'
#include <stdlib.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    const char *en_tete = "P3\n100 100\n255\n";

    // Création du fichier : Écrasement si existe, lecture/écriture, droits rw- pour l'utilisateur
    int fd = open("firstPPM.ppm", O_CREAT | O_TRUNC | O_RDWR, S_IRUSR | S_IWUSR);

    if (fd == -1) {
        perror("Erreur à l'ouverture du fichier");
        return EXIT_FAILURE;
    }

    ssize_t bytes_written = write(fd, en_tete, strlen(en_tete));

    if (bytes_written == -1) {
        perror("Erreur à l'écriture dans le fichier");
        close(fd); // On ferme quand même le fichier
        return EXIT_FAILURE;
    }

    close(fd);
    return EXIT_SUCCESS;
}

Ici, fd signifie File Descriptor : c’est un entier représentant une ressource ouverte par le système (fichier, socket, etc.). Le programmeur doit gérer manuellement l’ouverture, l’écriture et la fermeture. Le système d’exploitation garde la trace de tous les descripteurs de fichiers ouverts par chaque programme. L’entier fd correspond à un index dans une table de mappage interne gérée par le noyau. On peut observer cette table depuis l’utilisateur via /proc/self/fd (self étant le processus courant) :

$ ls -id -l /proc/self/fd
total 0
267056 lrwx------. 1 ovan ovan 64 Jan 22 19:28 0 -> /dev/pts/0
267057 lrwx------. 1 ovan ovan 64 Jan 22 19:28 1 -> /dev/pts/0
267058 lrwx------. 1 ovan ovan 64 Jan 22 19:28 2 -> /dev/pts/0
270772 lr-x------. 1 ovan ovan 64 Jan 22 19:28 3 -> /proc/2645/fd

Vous pouvez compilé le programme précédent. Ajoutez simplement un getc() pour éviter que le processus ne ce termine trop vite.

Ici, ce sont les descripteurs de fichiers ouverts par ls. À chaque exécution, le PID de ls change (c’est une nouvelle instance du programme) et de nouveaux fd sont générés. Les trois premiers id sont des flux standards préexistants, souvent appelés « fichiers virtuels », utilisés pour transmettre des données entre le programme et l’environnement. Les descripteurs de fichier sont les suivants :

  • (0) stdin : le flux d’entrée standard, généralement le clavier ou l’entrée d’un autre programme.
  • (1) stdout : le flux de sortie standard, utilisé pour afficher des informations à l’écran ou les rediriger vers un fichier.
  • (2) stderr : le flux de sortie d’erreurs, séparé de stdout pour permettre de distinguer les messages d’information des messages d’erreur.
  • (3) correspond probablement à l’ouverture du dossier /proc/self/fd, c’est donc en réalité un inode

Dans le tableau précédent, le premier chiffre correspond au numéro d’inode. Par exemple, si Firefox a le PID 6073, alors le répertoire /proc/6073 existe. Depuis ce processus, /proc/self est un lien symbolique vers /proc/6073, ce qui facilite l’accès aux informations du processus courant. Dit autrement c’est un pointeur.[6]

Attention : /proc est un dossier virtuel créé par le noyau. Il ne contient pas de fichiers physiques sur le disque, mais expose des informations sur les processus et le système en temps réel. C’est un pseudo-fs.

ls -la /proc/6073/fd
total 0
dr-x------ 2 ovan ovan 460 Sep 4 11:29 .
dr-xr-xr-x 9 ovan ovan   0 Sep 4 11:29 ..
lr-x------ 1 ovan ovan  64 Sep 8 18:56 0 -> /dev/null
l-wx------ 1 ovan ovan  64 Sep 8 18:56 1 -> /dev/null
lrwx------ 1 ovan ovan  64 Sep 8 18:56 10 -> 'anon_inode:[eventfd]'
lrwx------ 1 ovan ovan  64 Sep 8 18:56 100 -> 'anon_inode:[eventfd]'
lrwx------ 1 ovan ovan  64 Sep 8 18:56 101 -> 'anon_inode:[eventpoll]'
lrwx------ 1 ovan ovan  64 Sep 8 18:56 102 -> 'socket:[31432]'
lrwx------ 1 ovan ovan  64 Sep 8 18:56 103 -> 'socket:[32402]'
l-wx------ 1 ovan ovan  64 Sep 8 18:56 107 -> 'pipe:[32426]'
lrwx------ 1 ovan ovan  64 Sep 8 18:56 114 -> bounce-tracking-protection.sqlite
lrwx------ 1 ovan ovan  64 Sep 8 18:56 115 -> content-prefs.sqlite
lrwx------ 1 ovan ovan  64 Sep 8 18:56 16 -> /dev/nvidiactl
lrwx------ 1 ovan ovan  64 Sep 8 18:56 263 -> '/memfd:pulseaudio'
lrwx------ 1 ovan ovan  64 Sep 8 18:56 319 -> /dev/nvidia-modeset
lrwx------ 1 ovan ovan  64 Sep 8 18:56 45 -> /dev/nvidia0
...

De cette façon, on se rend compte que chaque entier fd correspond à une ressource système unique, et que le noyau maintient la correspondance entre cet entier et l’objet réel (fichier, socket, pipe, etc.). Cela permet également de comprendre pourquoi oublier de fermer un fd peut bloquer des ressources sur le système jusqu’à la libération de cette dernière. Dans cet exemple, on retrouve :

  • les entrées/sorties usuelles
  • des connexions réseaux
  • des communications inter-processus (IPC : pipe)
  • des fichiers
  • le serveur d’affichage via NVIDIA
  • le service audio via PulseAudio

Supposons que le programme dialogue directement avec une imprimante (en réalité, c’est le service CUPS qui gère l’impression, sous linux). Si le programme oublie de fermer la ressource et continue de tourner, il pourrait bloquer l’accès à l’imprimante pour tout autre programme, empêchant ainsi toute impression tant que le processus n’a pas libéré le descripteur de fichier.

En résumé : le File Descriptor est le lien direct entre le programme et le noyau, et observer /proc/[pid]/fd permet de visualiser concrètement toutes les ressources ouvertes par un processus à un instant donné. C’est la table des descripteurs ouverts. Au niveau du système, il existe une table des fichiers ouverts partagée par tous les processus. Chaque entrée de cette table contient des informations sur l’état du fichier (position du curseur, indicateurs d’erreur et de fin de fichier), ainsi qu’un pointeur vers un i-node (c’est-à-dire sa localisation physique ou virtuelle).

flowchart TD subgraph Processus1["Premier Processus"] P1[Programme en cours d'exécution] T1[Table des descripteurs de fichiers] subgraph FDs1["Descripteurs Standards"] STDIN1["stdin (fd 0)"] STDOUT1["stdout (fd 1)"] STDERR1["stderr (fd 2)"] end P1 --> T1 T1 --> STDIN1 T1 --> STDOUT1 T1 --> STDERR1 end subgraph Processus2["Second Processus"] P2[Programme en cours d'exécution] T2[Table des descripteurs de fichiers] subgraph FDs2["Descripteurs Standards"] STDIN2["stdin (fd 0)"] STDOUT2["stdout (fd 1)"] STDERR2["stderr (fd 2)"] end P2 --> T2 T2 --> STDIN2 T2 --> STDOUT2 T2 --> STDERR2 end subgraph Systeme["Niveau Système"] subgraph Inodes["Informations Physiques"] I1["inode 1:<br/>- Position du curseur<br/>- Indicateurs d'erreur<br/>- Pointeur inode"] I2["inode 2:<br/>- Position du curseur<br/>- Indicateurs d'erreur<br/>- Pointeur inode"] I3["inode 3:<br/>- Position du curseur<br/>- Indicateurs d'erreur<br/>- Pointeur inode"] I4["inode 4:<br/>- Position du curseur<br/>- Indicateurs d'erreur<br/>- Pointeur inode"] I5["inode 5:<br/>- Position du curseur<br/>- Indicateurs d'erreur<br/>- Pointeur inode"] I6["inode 6:<br/>- Position du curseur<br/>- Indicateurs d'erreur<br/>- Pointeur inode"] end end STDIN1 --> I1 STDOUT1 --> I2 STDERR1 --> I3 STDIN2 --> I4 STDOUT2 --> I5 STDERR2 --> I6 classDef process fill:#a8d08d,stroke:#507e32,color:#333 classDef desc fill:#95b3d7,stroke:#355e8c,color:#333 classDef sys fill:#e6b8b7,stroke:#953735,color:#333 classDef inode fill:#ffe599,stroke:#b08917,color:#333 class P1,T1,P2,T2 process class STDIN1,STDOUT1,STDERR1,STDIN2,STDOUT2,STDERR2 desc class TF sys class I1,I2,I3,I4,I5,I6 inode

Modes d’ouverture et permissions :

Modes d’ouvertures :
- Lecture seule (O_RDONLY)
- Écriture seule (O_WRONLY)
- Lecture-Écriture (O_RDWR)
- O_APPEND : ajout à la fin du fichier
- O_CLOEXEC : fermeture automatique lors d’un exec
- O_CREAT : création du fichier s’il n’existe pas
- O_DIRECTORY : erreur si le nom n’est pas un répertoire
- O_EXCL : assure la création du fichier (erreur sur fichier existant)
- O_TRUNC : efface le contenu du fichier après ouverture
- O_DIRECT : accès direct sans cache (voir section avancée)

Permissions (format symbolique et octal) :
- S_IRWXU, S_IRUSR, S_IWUSR, S_IXUSR → 0700, 0400, 0200, 0100, → rwx – –, r– – –, -w- – –, –x – –
- S_IRWXG, S_IRGRP, S_IWGRP, S_IXGRP → 0070, 0040, 0020, 0010, → – rwx –, – r– –, – -w- –, – –x –
- S_IRWXO, S_IROTH, S_IWOTH, S_IXOTH → 0007, 0004, 0002, 0001, → – – rwx, – – r–, – – -w-, – – –x

En C (version lib standard):

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    FILE *fp = fopen("firstPPM.ppm", "w"); // ouverture en écriture (création/écrasement)

    if (fp == NULL) {
        perror("Erreur d'ouverture du fichier");
        return EXIT_FAILURE;
    }

    const char *en_tete = "P3\n100 100\n255\n";
    fprintf(fp, "%s", en_tete); // écriture du texte dans le fichier

    fclose(fp); // fermeture explicite du fichier
    return EXIT_SUCCESS;
}

Ici, on utilise un flux FILE* plutôt qu’un descripteur de fichier brut. La bibliothèque standard C ajoute une couche d’abstraction. Un tampon mémoire est utilisé pour optimiser les écritures, les fonctions (fprintf, fscanf, etc.) facilitent la manipulation de données textuelles.

Modes d’ouverture pour fopen :
- r Ouvre le fichier en lecture, curseur positionné au début
- r+ Ouvre le fichier en lecture et écriture, curseur positionné au début
- w Efface ou le crée le fichier, l’ouvre en écriture seule, curseur positionné au début
- w+ Idem mais ouverture en lecture-écriture
- a Ouvre ou crée le fichier en écriture seule, curseur à la fin du fichier.
- a+ Ouvre ou crée le fichier en lecture-écriture, curseur d’écriture à la fin du fichier, curseur de lecture au début.

Exemples supplémentaires en C:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    const char *en_tete = "P3\n100 100\n255\n";

    // Utilisation de fopen/fwrite (écriture binaire)
    FILE *fp = fopen("firstPPM.ppm", "w");
    if (fp == NULL) {
        perror("Erreur d'ouverture");
        return EXIT_FAILURE;
    }

    fwrite(en_tete, sizeof(char), strlen(en_tete), fp);
    fclose(fp);

    return EXIT_SUCCESS;
}

En C : Les flux standards stdin, stdout et stderr sont des variables globales de type FILE* déclarées dans <stdio.h> et correspondent aux descripteurs de fichiers 0, 1 et 2 respectivement.

Architecture du VFS

Le Virtual File System (VFS) est une couche logicielle essentielle du noyau Linux qui fournit une interface unifiée et standardisée pour accéder à différents types de systèmes de fichiers. Cette abstraction permet au noyau de supporter simultanément des systèmes de fichiers très différents (ext4, NFS, procfs, tmpfs, etc.) en exposant une API commune aux programmes de l’espace utilisateur.

Le VFS agit comme une couche d’abstraction entre les appels système (comme open(), read(), write()) et les implémentations spécifiques des systèmes de fichiers. Sans le VFS, chaque programme devrait connaître les détails d’implémentation de chaque système de fichiers, ce qui serait ingérable. Grâce au VFS, un programme peut ouvrir et lire un fichier de la même manière, que ce fichier soit sur une partition ext4 locale, un partage NFS distant, ou même un fichier virtuel dans /proc.

L’architecture en couches du VFS se compose de plusieurs niveaux :

  1. Couche d’appels système : Interface entre l’espace utilisateur et le noyau via des appels comme open(), close(), read(), write()
  2. Couche VFS : Gère les structures de données communes et dispatch les opérations vers les systèmes de fichiers appropriés
  3. Implémentations de systèmes de fichiers : Chaque système de fichiers (ext4, XFS, Btrfs, etc.) implémente l’interface VFS
  4. Couche de bloc : Gère l’accès aux périphériques de stockage

Le VFS repose sur quatre structures de données principales définies dans le noyau Linux :

Super Block

Le superbloc contient les métadonnées sur un système de fichiers monté entier. Il inclut des informations comme :

  • Le type de système de fichiers
  • La taille des blocs
  • L’état du système de fichiers (monté en lecture seule ou lecture-écriture)
  • Un pointeur vers les opérations spécifiques du système de fichiers (s_op)

Chaque système de fichiers monté possède son propre superbloc en mémoire.

Inodes

L’inode (index node) représente un fichier ou un répertoire spécifique et contient toutes ses métadonnées :

  • Type de fichier (fichier régulier, répertoire, lien symbolique, périphérique, etc.)
  • Permissions d’accès (lecture, écriture, exécution pour propriétaire, groupe, autres)
  • Propriétaire (UID et GID)
  • Taille du fichier
  • Horodatages (création, modification, dernier accès)
  • Nombre de liens physiques pointant vers cet inode
  • Pointeurs vers les blocs de données sur le disque

Point important : l’inode ne contient pas le nom du fichier, qui est stocké dans la structure dentry. Un même inode peut avoir plusieurs noms (liens physiques ou hard links).

Chaque inode possède un numéro unique au sein d’un système de fichiers donné. Ce numéro d’inode peut être visualisé avec la commande ls -i.

// Structure simplifiée d'un inode dans le VFS
struct inode {
    umode_t i_mode;           // Type et permissions
    uid_t i_uid;              // Propriétaire
    gid_t i_gid;              // Groupe
    loff_t i_size;            // Taille du fichier
    struct timespec i_atime;  // Dernier accès
    struct timespec i_mtime;  // Dernière modification
    struct timespec i_ctime;  // Dernier changement d'inode
    unsigned long i_ino;      // Numéro d'inode
    nlink_t i_nlink;          // Nombre de liens physiques
    struct super_block *i_sb; // Superbloc parent
    const struct inode_operations *i_op;
    const struct file_operations *i_fop;
    void *i_private;          // Données privées du FS
    // ... autres champs
};

Il est crucial de distinguer deux types d’inodes :

  1. L’inode sur disque : Structure de données persistante qui contient les métadonnées du fichier stockées sur le système de fichiers physique
  2. L’inode en mémoire (struct inode) : Structure du VFS dans le noyau qui représente un fichier actuellement utilisé

Lorsqu’un fichier est accédé, le système de fichiers lit l’inode sur disque et crée une structure struct inode en mémoire dans le format VFS. Les systèmes de fichiers qui n’ont pas d’inodes natifs (comme FAT) doivent générer ces structures en mémoire à partir de leurs propres métadonnées.

Dans le système de fichiers ext4, les inodes sont organisés de manière particulière :

  • Le système de fichiers est divisé en groupes de blocs (block groups)
  • Chaque groupe de blocs contient une table d’inodes qui est un tableau linéaire de structures ext4_inode
  • Le nombre d’inodes par groupe est défini à la création du système de fichiers
  • Le numéro d’un inode permet de calculer son emplacement …

L’inode 0 n’existe jamais, la numérotation commence à 1. Cette organisation permet un accès rapide aux métadonnées des fichiers.

Pointeurs de données

Traditionnellement (ext2/ext3), les inodes utilisaient un système de pointeurs directs et indirects pour localiser les blocs de données :

  • 12 pointeurs directs : pointent directement vers les blocs de données
  • 1 pointeur indirect simple : pointe vers un bloc contenant des pointeurs de données
  • 1 pointeur indirect double : pointe vers un bloc de pointeurs qui pointent eux-mêmes vers des blocs de pointeurs de données
  • 1 pointeur indirect triple : ajoute encore un niveau d’indirection

Avec ext4, ce système a été remplacé par les extents. Un extent décrit une plage continue de blocs physiques, ce qui est beaucoup plus efficace pour les fichiers volumineux. Au lieu d’avoir besoin de milliers de pointeurs pour un gros fichier, quelques extents peuvent suffire. Un seul extent peut décrire jusqu’à 128 MiB de données contiguës avec des blocs de 4 KiB.

DEntry

La dentry (directory entry) représente un composant de chemin et fait le lien entre un nom de fichier et son inode. Elle contient :

  • Le nom du fichier ou du répertoire
  • Un pointeur vers l’inode correspondant
  • Un pointeur vers la dentry parent
  • Une liste des dentries enfants (pour les répertoires)

Le VFS maintient un cache de dentries (dcache) en mémoire pour accélérer la résolution des chemins. Les dentries ne sont jamais sauvegardées sur le disque, elles existent uniquement en mémoire.

Par exemple, pour le chemin /home/user/document.txt, le VFS crée une chaîne de dentries : une pour /, une pour home, une pour user, et une pour document.txt.

File

La structure file représente une instance d’un fichier ouvert par un processus. Elle contient :

  • Un pointeur vers la dentry associée
  • Un pointeur vers l’inode
  • La position courante dans le fichier (offset)
  • Les flags d’ouverture (lecture, écriture, append, etc.)
  • Les opérations disponibles sur ce fichier (f_op)

Plusieurs structures file peuvent pointer vers le même inode si le fichier est ouvert plusieurs fois. Chaque processus qui ouvre un fichier obtient sa propre structure file avec son propre offset.

Gestion des répertoires

Un répertoire sous Linux est un type spécial de fichier qui contient une liste d’associations entre noms de fichiers et numéros d’inodes. Chaque entrée de répertoire (dentry sur disque, à ne pas confondre avec struct dentry en mémoire) comprend :

  • Un nom de fichier
  • Un numéro d’inode
  • Optionnellement, le type de fichier (pour optimiser les performances)

Pour parcourir un répertoire en C, on utilise les fonctions de la famille opendir()/readdir() :

#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    DIR *dirp;
    struct dirent *entry;

    dirp = opendir(".");
    if (dirp == NULL) {
        perror("opendir");
        return EXIT_FAILURE;
    }

    while ((entry = readdir(dirp)) != NULL) {
        printf("%s (inode: %lu)\n", entry->d_name, entry->d_ino);
    }

    closedir(dirp);
    return EXIT_SUCCESS;
}

La structure dirent retournée par readdir() contient :

  • d_ino : numéro d’inode
  • d_name : nom du fichier
  • d_type : type de fichier (DT_REG, DT_DIR, DT_LNK, etc.)

Attention : readdir() n’est pas thread-safe. Pour un environnement multithread, utilisez readdir_r().

Chaque répertoire contient deux entrées spéciales :

  • . (point) : référence au répertoire lui-même
  • .. (double point) : référence au répertoire parent

Ces entrées sont créées automatiquement par le système de fichiers et comptent comme des liens physiques. C’est pourquoi un répertoire vide a un compte de liens (i_links_count) de 2 : un depuis son parent et un depuis . lui-même.

Liens physiques et symboliques

Un lien physique est simplement une entrée de répertoire supplémentaire pointant vers un inode existant. Créer un lien physique incrémente le compteur i_links_count de l’inode.

Caractéristiques des liens physiques :

  • Tous les liens physiques vers un fichier sont équivalents ; il n’y a pas de “lien original”
  • Les modifications du fichier via n’importe quel lien sont visibles par tous les autres
  • Le fichier n’est réellement supprimé que lorsque le compteur de liens atteint zéro
  • Les liens physiques doivent être sur le même système de fichiers (car ils partagent un numéro d’inode)
  • On ne peut pas créer de lien physique vers un répertoire (pour éviter les cycles)
# Créer un lien physique
ln fichier_original lien_physique

# Vérifier les inodes
ls -li fichier_original lien_physique
# Les deux auront le même numéro d'inode

A contrario il existe aussi des lien symbolique. C’est un fichier spécial qui contient le chemin vers un autre fichier. C’est comme un raccourci ou une redirection.

Caractéristiques des liens symboliques :

  • Le lien symbolique a son propre inode, distinct du fichier cible
  • Il peut pointer vers un fichier sur un système de fichiers différent
  • Il peut pointer vers un répertoire
  • Si le fichier cible est déplacé ou supprimé, le lien symbolique devient “cassé” (dangling)
  • La résolution des liens symboliques est transparente pour la plupart des opérations
  • Les permissions du lien symbolique lui-même ne sont généralement pas utilisées
# Créer un lien symbolique
ln -s /chemin/vers/cible lien_symbolique

# Vérifier
ls -l lien_symbolique
# Affichera: lien_symbolique -> /chemin/vers/cible

Métadonnées

Pour obtenir les métadonnées d’un fichier, Linux fournit la famille stat() :

#include <sys/stat.h>
#include <stdio.h>

int main(void) {
    struct stat sb;

    if (stat("fichier.txt", &sb) == -1) {
        perror("stat");
        return 1;
    }

    printf("Inode: %lu\n", sb.st_ino);
    printf("Taille: %ld octets\n", sb.st_size);
    printf("Blocs: %ld\n", sb.st_blocks);
    printf("Liens: %lu\n", sb.st_nlink);
    printf("UID: %u, GID: %u\n", sb.st_uid, sb.st_gid);
    printf("Permissions: %o\n", sb.st_mode & 07777);

    return 0;
}

Les trois variantes principales :

  • stat(pathname, buf) : obtient les infos d’un fichier par son chemin
  • fstat(fd, buf) : obtient les infos d’un fichier ouvert via son descripteur
  • lstat(pathname, buf) : comme stat() mais ne suit pas les liens symboliques

Toutes ces fonctions retournes une copie d’une structure systeme qui contient les infomations souhaitees :

struct stat {
    dev_t     st_dev;     // ID du périphérique contenant le fichier
    ino_t     st_ino;     // Numéro d'inode
    mode_t    st_mode;    // Type et permissions du fichier
    nlink_t   st_nlink;   // Nombre de liens physiques
    uid_t     st_uid;     // UID du propriétaire
    gid_t     st_gid;     // GID du groupe
    dev_t     st_rdev;    // ID du périphérique (si fichier spécial)
    off_t     st_size;    // Taille totale en octets
    blksize_t st_blksize; // Taille de bloc pour I/O optimal
    blkcnt_t  st_blocks;  // Nombre de blocs alloués (512 octets)
    struct timespec st_atim;  // Dernier accès
    struct timespec st_mtim;  // Dernière modification
    struct timespec st_ctim;  // Dernier changement de statut
};

Le champ st_mode encode à la fois le type de fichier et ses permissions :

// Macros pour tester le type de fichier
S_ISREG(mode)   // Fichier régulier ?
S_ISDIR(mode)   // Répertoire ?
S_ISLNK(mode)   // Lien symbolique ?
S_ISBLK(mode)   // Périphérique bloc ?
S_ISCHR(mode)   // Périphérique caractère ?
S_ISFIFO(mode)  // FIFO/pipe ?
S_ISSOCK(mode)  // Socket ?

// Extraction des permissions
mode & S_IRWXU  // Permissions du propriétaire (rwx)
mode & S_IRWXG  // Permissions du groupe (rwx)
mode & S_IRWXO  // Permissions des autres (rwx)

Systèmes de fichiers virtuels

Le système /proc

Le répertoire /proc est un système de fichiers virtuel (procfs) qui expose les informations du noyau et des processus sous forme de fichiers. Il ne contient aucune donnée réelle sur disque ; tout est généré dynamiquement par le noyau.

Structure de /proc :

/proc/
├── [pid]/          # Un répertoire par processus
│   ├── cmdline     # Ligne de commande du processus
│   ├── cwd         # Lien vers le répertoire de travail
│   ├── environ     # Variables d'environnement
│   ├── exe         # Lien vers l'exécutable
│   ├── fd/         # Descripteurs de fichiers ouverts
│   ├── maps        # Régions mémoire mappées
│   ├── stat        # Statistiques du processus
│   └── status      # Statut détaillé (lisible)
├── cpuinfo         # Informations sur les processeurs
├── meminfo         # Informations sur la mémoire
├── mounts          # Systèmes de fichiers montés
├── net/            # Statistiques réseau
└── sys/            # Paramètres kernel modifiables

Exemple d’utilisation pour monitorer un processus :

# Voir les fichiers ouverts par Firefox (PID 1234)
ls -l /proc/1234/fd

# Lire les stats CPU
cat /proc/cpuinfo

# Modifier un paramètre kernel
echo 1 > /proc/sys/net/ipv4/ip_forward

Le système /sys (sysfs)

Le répertoire /sys expose la vue du noyau sur le matériel et les sous-systèmes. Il est organisé de manière hiérarchique selon les bus, les classes de périphériques et les pilotes.

Structure typique :

/sys/
├── block/          # Périphériques bloc (disques)
├── bus/            # Bus système (PCI, USB, etc.)
├── class/          # Classes de périphériques (net, input, etc.)
├── devices/        # Hiérarchie complète des périphériques
├── firmware/       # Interfaces firmware
├── kernel/         # Paramètres kernel
├── module/         # Modules kernel chargés
└── power/          # Gestion de l'énergie

Exemple : modifier la luminosité de l’écran

# Lire la luminosité actuelle
cat /sys/class/backlight/intel_backlight/brightness

# Modifier la luminosité
echo 500 > /sys/class/backlight/intel_backlight/brightness

Le système /dev

Le répertoire /dev contient des fichiers spéciaux représentant les périphériques matériels. Il existe deux types de périphériques :

  1. Périphériques caractères : accès séquentiel, octet par octet (terminaux, souris)
  2. Périphériques blocs : accès par blocs, avec cache possible (disques)

Exemples courants :

/dev/
├── sda, sdb        # Disques SCSI/SATA
├── nvme0n1         # Disque NVMe
├── tty0, tty1      # Terminaux virtuels
├── null            # Périphérique null (poubelle)
├── zero            # Source infinie de zéros
├── random, urandom # Générateurs aléatoires
└── input/          # Périphériques d'entrée

Performance et opérations avancées

Buffering et performance des I/O

Le page cache

Le page cache (ou cache de pages) est l’un des mécanismes les plus importants pour les performances I/O sous Linux. Il utilise la mémoire RAM non allouée pour mettre en cache les données des fichiers.

Fonctionnement :

  1. En lecture : Lorsqu’un fichier est lu, les données sont copiées du disque vers le page cache, puis du page cache vers l’application. Si les mêmes données sont relues, elles sont servies directement depuis le cache, évitant un accès disque.

  2. En écriture : Les données écrites sont d’abord placées dans le page cache et marquées comme “sales” (dirty). Le noyau les écrit périodiquement sur le disque en arrière-plan via les threads flusher.

Le page cache est dynamique : il grandit pour utiliser la mémoire libre et se réduit automatiquement sous pression mémoire.

Buffer cache vs page cache

Historiquement, Linux avait deux caches séparés :

  • Buffer cache : cache de blocs de périphériques
  • Page cache : cache de pages de fichiers

Depuis le noyau 2.4, ces deux caches ont été unifiés. Aujourd’hui, le terme “buffer” désigne principalement les métadonnées des blocs (descripteurs de blocs) tandis que le “cache” fait référence au contenu réel des fichiers.

On peut observer l’utilisation du cache avec la commande free :

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           15Gi       3.2Gi       8.1Gi       234Mi       4.2Gi        11Gi

La colonne buff/cache montre la mémoire utilisée pour le cache de fichiers.

Forcer la synchronisation

Par défaut, les écritures sont bufferisées, ce qui améliore les performances mais introduit un risque : en cas de crash, les données non encore écrites sur disque sont perdues.

La commande sync force l’écriture de tous les buffers sales vers le disque :

sync  # Écrit tous les buffers sales

L’appel système fsync(fd) force l’écriture sur disque de toutes les données d’un fichier spécifique, incluant ses métadonnées :

#include <unistd.h>

int fd = open("fichier.txt", O_WRONLY);
write(fd, data, size);
fsync(fd);  // Garantit que les données sont sur disque
close(fd);

Attention : fsync() peut être très coûteux en performance car il peut forcer l’écriture de tout le cache dirty du système, pas seulement du fichier en question, selon le système de fichiers.

fdatasync(fd) est similaire à fsync() mais ne synchronise pas certaines métadonnées non critiques (comme les timestamps), ce qui peut être plus rapide:

fdatasync(fd);  // Plus rapide que fsync

On peut également demander au noyau d’écrire directement sans bufferisation en utilisant le flag O_SYNC lors de l’ouverture :

int fd = open("fichier.txt", O_WRONLY | O_SYNC);
write(fd, data, size);  // Écrit directement, bloque jusqu'à completion

Avec O_SYNC, chaque write() bloque jusqu’à ce que les données soient physiquement sur le disque. C’est plus sûr mais beaucoup plus lent.

I/O direct

Direct I/O (O_DIRECT)

Par défaut, toutes les I/O passent par le page cache. Dans certains cas (bases de données, applications de streaming), on préfère gérer le cache au niveau applicatif. Le flag O_DIRECT permet de contourner le page cache du noyau.

#include <fcntl.h>
int fd = open("database.db", O_RDWR | O_DIRECT);

Contraintes du Direct I/O :

  • Les transferts doivent être alignés sur des multiples de 512 octets (ou 4096 pour certains disques)
  • L’adresse du buffer en mémoire doit aussi être alignée (512 ou 4096 octets)
  • La position dans le fichier (offset) doit être alignée

Exemple correct :

#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int fd = open("file.dat", O_RDWR | O_DIRECT);
void *buffer;

// Aligner le buffer sur 4096 octets
posix_memalign(&buffer, 4096, 4096);

// Lire 4096 octets à l'offset 0
pread(fd, buffer, 4096, 0);

free(buffer);
close(fd);

Avantages du Direct I/O :

  • Évite la copie des données entre le page cache et l’espace utilisateur
  • Permet à l’application de gérer son propre cache plus efficacement
  • Réduit la pression sur la mémoire système

Inconvénients :

  • Plus complexe à utiliser (contraintes d’alignement)
  • Peut être plus lent pour de petits accès aléatoires
  • Ne garantit pas que les données sont sur le support stable (peut rester dans le cache du disque)

Memory-mapped I/O (mmap)

mmap() permet de mapper un fichier directement dans l’espace d’adressage d’un processus. Le fichier devient accessible comme un simple tableau en mémoire :

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

int fd = open("fichier.dat", O_RDWR);
struct stat sb;
fstat(fd, &sb);

void *addr = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);

if (addr == MAP_FAILED) {
    perror("mmap");
    exit(1);
}

// Accéder au fichier comme un tableau
char *data = (char *)addr;
data[0] = 'A';  // Modifie le fichier

// Libérer le mapping
munmap(addr, sb.st_size);
close(fd);

Paramètres de mmap :

  • addr : adresse souhaitée (NULL = laisse le noyau choisir)
  • length : taille de la région à mapper
  • prot : protection mémoire (PROT_READ, PROT_WRITE, PROT_EXEC)
  • flags : type de mapping (MAP_SHARED, MAP_PRIVATE)
  • fd : descripteur de fichier
  • offset : offset dans le fichier (doit être aligné sur la taille de page)

Types de mapping :

  • MAP_SHARED : les modifications sont visibles par tous les processus et écrites dans le fichier
  • MAP_PRIVATE : les modifications sont privées au processus (copy-on-write)

Avantages de mmap :

  • Simplifie le code (pas besoin de read/write)
  • Le noyau gère automatiquement le chargement des pages (demand paging)
  • Partage efficace de mémoire entre processus
  • Idéal pour les accès aléatoires dans de gros fichiers

Inconvénients :

  • La taille des mappings peut être limitée (particulièrement en 32 bits)
  • Peut fragmenter l’espace d’adressage virtuel
  • Mélanger mmap et Direct I/O sur le même fichier peut causer des corruptions

Gestion des erreurs avec errno

Tous les appels système retournent -1 en cas d’erreur et positionnent la variable globale errno avec un code d’erreur spécifique.

Codes d’erreur courants

Code Nom Signification
1 EPERM Opération non permise (droits insuffisants)
2 ENOENT Fichier ou répertoire inexistant
5 EIO Erreur d’entrée/sortie matérielle
9 EBADF Descripteur de fichier invalide
11 EAGAIN / EWOULDBLOCK Ressource temporairement indisponible
12 ENOMEM Mémoire insuffisante
13 EACCES Permission refusée
14 EFAULT Adresse mémoire invalide
17 EEXIST Le fichier existe déjà
20 ENOTDIR Ce n’est pas un répertoire
21 EISDIR C’est un répertoire
22 EINVAL Argument invalide
23 ENFILE Trop de fichiers ouverts dans le système
24 EMFILE Trop de fichiers ouverts par ce processus
28 ENOSPC Plus d’espace disponible sur le périphérique
30 EROFS Système de fichiers en lecture seule

Utilisation correcte d’errno

#include <errno.h>
#include <string.h>
#include <stdio.h>

int fd = open("fichier.txt", O_RDONLY);
if (fd == -1) {
    // errno est maintenant positionné
    fprintf(stderr, "Erreur lors de l'ouverture: %s\n", strerror(errno));

    // Ou plus simplement avec perror
    perror("open");

    // Traitement spécifique selon l'erreur
    if (errno == ENOENT) {
        fprintf(stderr, "Le fichier n'existe pas\n");
    } else if (errno == EACCES) {
        fprintf(stderr, "Permission refusée\n");
    }

    return -1;
}

Bonnes pratiques :

  • Toujours vérifier le code de retour avant d’examiner errno
  • errno n’est valide qu’après un appel ayant échoué
  • Utiliser perror() ou strerror() pour obtenir un message lisible
  • Sauvegarder errno si vous appelez d’autres fonctions avant de le traiter