Sous les systèmes d'exploitation de type Unix, la gestion des entrées/sorties (E/S) repose sur une abstraction fondamentlae où "tout est fichier". Comprendre comment le noyau Linux gère ces opérations nécessite d'explorer la relation entre les processus, les descripteurs de fichiers et les appels système.
Abstraction des Fichiers et Appels Système
Un fichier n'est pas seulement une collection de données sur un disque ; il est défini par son contenu et ses métadonnées (attributs). Toute interaction avec un fichier implique une manipulation de ces deux aspects. Cependant, un fichier sur le disque ne peut pas être directement manipulé par un processus utilisateur. Il doit d'abord être chargé en mémoire via une opération d'ouverture.
Les langages de haut niveau comme le C, le C++ ou Java fournissent leurs propres bibliothèques d'E/S (par exemple, fopen, fread en C). Ces fonctions sont des envelopeps (wrappers) autour des appels système du noyau. Puisque le noyau est l'unique entité autorisée à interagir avec le matériel, toutes les opérations de fichiers transitent inévitablement par des appels système tels que open, read, write et close. Maîtriser ces appels système permet de comprendre le comportement sous-jacent, indépendament du langage de programmation utilisé.
Descripteurs de Fichiers et Table d'Allocation
Lorsqu'un processus ouvre un fichier, le noyau crée une structure de données interne (généralement struct file) pour représenter ce fichier ouvert. Pour permettre au processus d'y accéder, le noyau retourne un entier non négatif appelé descripteur de fichier (fd).
Le descripteur de fichier n'est rien d'autre qu'un index dans la table des descripteurs de fichiers du processus (un tableau de pointeurs vers les structures du noyau). Par défaut, chaque processus hérite de trois descripteurs standard :
0: Entrée standard (stdin)1: Sortie standard (stdout)2: Erreur standard (stderr)
Par conséquent, tout nouveau fichier ouvert se verra attribuer le plus petit descripteur disponible, commençant généralement à 3.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int descriptors[5];
const char *target_file = "system_log.txt";
// Ouverture de plusieurs instances du même fichier
for (int i = 0; i < 5; i++) {
descriptors[i] = open(target_file, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (descriptors[i] < 0) {
perror("Erreur lors de l'ouverture");
return 1;
}
printf("Instance %d - Descripteur alloué : %d\n", i + 1, descriptors[i]);
}
// Nettoyage des ressources
for (int i = 0; i < 5; i++) {
close(descriptors[i]);
}
return 0;
}
Règles d'Allocation des Descripteurs
Le noyau alloue les descripteurs en suivant une règle stricte : le plus petit indice disponible. Si un descripteur standard est fermé, le prochain appel à open réutilisera cet indice.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
// Fermeture explicite de la sortie standard
close(STDOUT_FILENO);
// Le prochain open récupérera le descripteur 1
int log_fd = open("output_capture.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (log_fd < 0) {
perror("open");
return 1;
}
// Cette instruction ne s'affichera pas sur le terminal,
// car stdout (fd 1) pointe désormais vers le fichier.
printf("Descripteur alloué : %d\n", log_fd);
close(log_fd);
return 0;
}
Dans cet exemple, l'appel à printf écrit sur le descripteur 1. Puisque 1 a été réassigné à output_capture.txt, le texte est écrit dans le fichier au lieu du terminal.
Mécanismes de Redirection
La redirection consiste à modifier la cible d'un descripteur de fichier standard sans changer le code de l'application. Au niveau du noyau, cela revient à modifier le pointeur dans la table des descripteurs du processus pour qu'il pointe vers une autre structure de fichier.
L'appel système dup2(int oldfd, int newfd) est l'outil principal pour cette opération. Il ferme newfd s'il est ouvert, puis le fait pointer vers la même structure de fichier que oldfd.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int custom_fd = open("redirected_output.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (custom_fd < 0) {
perror("open");
return 1;
}
// Redirection : stdout (1) pointe maintenant vers le même fichier que custom_fd
if (dup2(custom_fd, STDOUT_FILENO) < 0) {
perror("dup2");
return 1;
}
// L'écriture sur stdout va maintenant dans le fichier
printf("Ce message est capturé dans le fichier journal.\n");
fprintf(stdout, "Ceci est une autre ligne de log.\n");
close(custom_fd);
return 0;
}
Pour une redirection en mode ajout (append), il suffit de modifier les drapeaux lors de l'ouverture du fichier en utilisant O_APPEND au lieu de O_TRUNC.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int input_fd = open("input_data.txt", O_RDONLY);
if (input_fd < 0) {
perror("open");
return 1;
}
// Redirection de l'entrée standard (0) vers notre fichier
if (dup2(input_fd, STDIN_FILENO) < 0) {
perror("dup2");
return 1;
}
char buffer[128];
// La lecture sur stdin lira désormais depuis input_data.txt
while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
printf("Lu depuis le fichier : %s", buffer);
}
close(input_fd);
return 0;
}