Aller au contenu principal

Systèmes d'exploitation — fondations · L2 · Section 4/12

Fichiers

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Fichiers

Prérequis

  • Introduction du module: appels système, strace.
  • C de base: gestion des erreurs, errno, perror.

Objectifs d'apprentissage

  • Expliquer le rôle d'un descripteur de fichier et de la table des fichiers ouverts.
  • Prédire le comportement de read/write/lseek, y compris les retours partiels.
  • Observer la différence de coût entre écritures tamponnées et non tamponnées.

#Le descripteur: une poignée vers le noyau

Dans un OS moderne, un fichier est d'abord une abstraction. Derrière un simple chemin se cachent des métadonnées (droits, propriétaire, dates), des blocs de données, des caches et parfois des verrous. Le développeur n'accède jamais directement au disque: il passe par une poignée appelée descripteur de fichier.

#1. Tables et descripteurs

Lorsqu'un processus appelle open, le noyau vérifie les permissions, crée une entrée dans la table des fichiers ouverts du processus et renvoie un entier. Ce nombre n'a pas de signification intrinsèque: c'est un indice dans un tableau. Les descripteurs 0, 1 et 2 sont réservés à l'entrée standard, la sortie standard et la sortie d'erreur; c'est exactement ce que le shell branche avec <, > et 2>.

Chaque entrée pointe vers une structure globale contenant la position courante, les indicateurs (lecture seule, ajout à la fin) et un lien vers l'inode. Ainsi, deux processus peuvent partager le même descripteur via dup ou fork, ce qui explique pourquoi un enfant hérite des fichiers ouverts par son parent. La position est partagée: si parent et enfant lisent le même descripteur, chacun fait avancer le curseur pour les deux.

Vérification directe dans le pseudo-système de fichiers du noyau:

bashbash

1ls -l /proc/$$/fd2# 0 -> /dev/pts/0   1 -> /dev/pts/0   2 -> /dev/pts/0

#Les trois fichiers standard, vus du shell

Le cours d'Unix formule la même chose en termes d'utilisateur, et cette formulation est celle des épreuves: chaque programme Unix accède à au moins trois fichiers standard — l'entrée, numérotée 0, la sortie, numérotée 1, et la sortie d'erreur, numérotée 2. Sauf indication contraire, ils sont associés au terminal du processus: entrée depuis le clavier, sortie et sortie d'erreur dans la fenêtre.

Tout fichier standard peut être redirigé vers un autre fichier, et c'est le shell qui s'en charge, indépendamment du programme qui s'exécute. C'est le point décisif: un programme qui se contente de lire son entrée standard et d'écrire sa sortie standard devient automatiquement un filtre, utilisable dans un tube, sans avoir été écrit pour cela.

bashbash

1commande < fichier        # entrée standard prise sur le fichier2commande > fichier        # sortie standard envoyée dans le fichier3commande 2> fichier       # sortie d'erreur envoyée dans le fichier4commande >> fichier       # sortie standard CONCATÉNÉE à la fin du fichier5commande >! fichier       # remplace le fichier même s'il existe6commande <> fichier       # le fichier sert à la fois d'entrée et de sortie7commande << mot           # entrée sur place, jusqu'à une ligne ne contenant que mot

Deux règles à retenir. Le fichier en entrée doit exister. Le fichier en sortie ne doit pas exister — c'est vrai en zsh, pas nécessairement dans les autres shells, où > écrase: le comportement dépend de l'option clobber, et à Valrose elle est activée par défaut. Cette subtilité a déjà été posée en épreuve.

L'intérêt de séparer les deux flux de sortie se voit sur un exemple où la commande échoue:

bashbash

1ls -l toto >& f1    # stdout ET stderr dans f1: le message d'erreur y est2ls -l toto 1> f2    # stdout seule: f2 est VIDE, l'erreur reste à l'écran3ls -l toto 2> f3    # stderr seule: l'erreur est dans f3, rien à l'écran

#2. Lecture, écriture, repositionnement

read lit jusqu'à N octets, renvoie le nombre réellement lus et fait avancer la position; 0 signale la fin de fichier. write ajoute des données; si le fichier a été ouvert avec O_APPEND, le noyau décale automatiquement le curseur en fin de fichier avant l'écriture, c'est ainsi que plusieurs processus peuvent écrire dans le même journal sans s'écraser. lseek repositionne la tête de lecture.

Il est crucial de vérifier systématiquement le nombre d'octets réellement transférés, car les appels peuvent être interrompus par des signaux et retourner partiels. Un read sur un tube peut livrer moins d'octets que demandé sans que ce soit une erreur; un write sur un disque plein renvoie -1 avec errno à ENOSPC. Les bibliothèques standard (stdio) enveloppent ces appels dans un tampon utilisateur pour limiter les transitions noyau-utilisateur, mais le comportement fondamental reste identique.

cc

1#include <fcntl.h>2#include <unistd.h>3#include <stdio.h>4#include <string.h>5 6int main(void) {7    int fd = open("journal.log", O_WRONLY | O_CREAT | O_APPEND, 0640);8    if (fd == -1) { perror("open"); return 1; }9 10    const char *ligne = "evenement\n";11    ssize_t n = write(fd, ligne, strlen(ligne));12    if (n == -1) { perror("write"); close(fd); return 1; }13 14    if (close(fd) == -1) { perror("close"); return 1; }

À lire: le mode 0640 aligne les permissions (rw-r-----) sur ce que donnerait un umask 027; close aussi peut échouer et mérite son contrôle.

#3. Fichiers spéciaux et uniformité Unix

L'une des grandes forces d'Unix réside dans le principe « tout est fichier ». Les périphériques blocs et caractères situés dans /dev, les tubes (pipe), les sockets Unix, le pseudo-système /proc qui expose l'état du noyau: tout se manipule avec les mêmes appels open, read, write. Cette uniformité permet de brancher des composants entre eux sans modification, et c'est elle qui rend le pipe du shell possible. Elle impose aussi de maîtriser les permissions et les modes, faute de quoi un programme peut se retrouver bloqué à attendre éternellement un périphérique absent: ouvrir en lecture une FIFO sans écrivain bloque jusqu'à ce qu'un écrivain se présente.

#Les quatre catégories de fichiers

Le cours d'Unix part d'un principe plus général encore: toutes les informations extérieures au processus sont des fichiers. Un fichier peut être associé au clavier, à l'écran, à l'imprimante. Quatre catégories en découlent.

  • Le fichier ordinaire est une suite d'octets sans structure particulière; il contient des données ou du programme. Les fichiers de texte sont structurés en lignes par une marque de fin, de nombre ordinal 10, ce qui impose des conversions depuis et vers MS-DOS ou Mac-OS — c'est le fameux problème des fins de ligne.
  • Le répertoire est un nœud de la hiérarchie: un fichier de références à d'autres fichiers. Il ne contient donc pas les fichiers, seulement des noms associés à des numéros d'inode.
  • Le fichier spécial est un fichier virtuel, représentation d'un organe périphérique, auquel on accède par un programme pilote spécifique du périphérique. C'est la catégorie qui matérialise le principe « tout est fichier ».
  • Le lien symbolique est un fichier contenant la chaîne de caractères qui représente le nom d'un autre fichier: une référence indirecte, qui permet de construire un graphe quelconque et non plus seulement une arborescence.

Cette dernière propriété explique une limite du système de fichiers: une arborescence est un cas particulier de graphe, mais un lien symbolique peut pointer n'importe où — y compris vers un nom qui n'existe pas. Le noyau ne le vérifie pas à la création.

#4. Examiner et comparer le contenu

Le cours range les opérations sur le contenu en deux familles: celles qui traitent un fichier, et celles qui parcourent une hiérarchie entière.

bashbash

1file fichier.txt          # détermine la NATURE du fichier2cmp a.txt b.txt           # dit simplement s'ils sont identiques ou non3diff a.txt b.txt          # montre TOUTES les différences4wc fichier.txt            # compte caractères, mots et lignes5wc -l fichier.txt         # seulement les lignes

file est la commande qui répond à la question « quelle est la nature de ce fichier? » — elle inspecte le contenu plutôt que l'extension. cmp s'arrête à la première différence et convient au binaire; diff détaille les écarts ligne à ligne et convient au texte. wc (word count) compte les trois grandeurs d'un coup, ses options permettant de n'en retenir qu'une.

Deux commandes parcourent une hiérarchie entière, et leur différence est importante: find sélectionne des fichiers selon des critères de nom, de type, de propriétaire ou de taille; grep cherche des expressions rationnelles dans leur contenu. Les combiner couvre la plupart des besoins.

bashbash

1find . -type f -name "*.txt"2find . -name "*.txt" -exec grep -n technologie {} \; -print

head et tail affichent le début et la fin d'un fichier, dix lignes par défaut, avec -n nombre pour changer ce nombre. tail -f suit en temps réel un fichier dont le contenu augmente: c'est l'outil universel de surveillance d'un journal.

bashbash

1head -n 5 journal.log2tail -n 50 journal.log3tail -f /var/log/syslog     # surveillance en direct

Pour l'impression, le cours distingue deux familles équivalentes: lpr/lp mettent un fichier en file d'attente, lprm/cancel l'en retirent, lpq/lpstat examinent la file. La mise en file implique une transformation cachée adaptée au modèle d'imprimante, la forme standard passant par le codage PostScript; a2ps (ASCII to PostScript) effectue des transformations plus élaborées.

#Atelier: mesurer le coût des appels

Étape 1: écrivez un programme qui ouvre un fichier dans /tmp et y écrit 10 000 lignes, un appel write par ligne. Mesurez avec time ./prog (temps réel, utilisateur, système).

Étape 2: même travail, mais en accumulant les lignes dans un tampon et en n'appelant write que tous les 1000 éléments, ou via fwrite (stdio). Comparez les temps système: l'écart d'un ordre de grandeur ou deux est l'observation attendue, signe du coût de chaque transition noyau.

Étape 3: tracez avec strace -c -e trace=openat,read,write ./prog les deux variantes: les compteurs d'appels racontent seuls l'histoire du tampon. Concluez sur la discipline côté application: le système de fichiers fixe les ordres de grandeur, mais le patron d'accès de l'application décide du coût final.

#Exercices

1. À quels fichiers chaque programme Unix accède-t-il au minimum? Comment sont-ils numérotés et à quoi sont-ils associés par défaut?

2. Qui effectue la redirection: le programme ou le shell? Quelle est la conséquence pour un programme qui n'a pas été écrit pour cela?

3. Aucun fichier toto n'existe. Que contiennent f1, f2 et f3 après les trois commandes ls -l toto >& f1, ls -l toto 1> f2 et ls -l toto 2> f3?

4. Qu'est-ce qu'un filtre? En quoi le tube diffère-t-il d'un enchaînement séquentiel par ;?

5. Citer les quatre catégories de fichiers d'Unix et donner, pour chacune, une propriété caractéristique.

6. Quelle commande détermine la nature d'un fichier? Laquelle montre toutes les différences entre deux fichiers? Laquelle suit en temps réel un fichier dont le contenu augmente?

Corrections détaillées

1. Chaque programme accède à au moins trois fichiers standard: l'entrée, numérotée 0, la sortie, numérotée 1, et la sortie d'erreur, numérotée 2. Sauf indication contraire, ils sont associés au terminal du processus: le clavier pour l'entrée, la fenêtre pour les deux sorties. Ce sont les descripteurs de fichiers 0, 1 et 2, hérités du shell par chaque processus lancé.

2. C'est le shell, et il le fait indépendamment du programme. Conséquence directe: un programme qui lit son entrée standard et écrit sa sortie standard devient un filtre utilisable dans un tube, sans avoir été écrit pour cela. C'est ce découplage qui permet à grep, sort ou wc de se composer à volonté.

3. La commande échoue, donc le message d'erreur part sur la sortie d'erreur. f1 contient le message d'erreur, car >& redirige les deux sorties au même endroit. f2 est créé mais vide: seule la sortie standard y était redirigée, et elle n'a rien produit; le message d'erreur s'est affiché à l'écran. f3 contient le message d'erreur, car 2> redirige la sortie d'erreur; rien ne s'est affiché à l'écran.

4. Un filtre est un programme qui effectue une transformation sur son entrée standard pour produire sa sortie standard; il s'utilise typiquement dans un tube. Le tube | connecte directement la sortie d'un processus à l'entrée du suivant sans fichier auxiliaire, et les processus s'exécutent simultanément, le système gérant leur synchronisation. Avec ;, au contraire, les commandes s'exécutent l'une après l'autre et sans rapport entre elles: ls -l /usr/bin > resultats ; more resultats ; rm resultats crée un fichier temporaire là où ls -l /usr/bin | more n'en a pas besoin.

5. Fichier ordinaire: suite d'octets sans structure particulière, contenant données ou programme, avec des lignes terminées par la marque de fin numéro 10. Répertoire: nœud de la hiérarchie, fichier de références à d'autres fichiers — il contient des noms, pas des fichiers. Fichier spécial: fichier virtuel représentant un organe périphérique, manipulé par un pilote; c'est l'incarnation du principe « tout est fichier ». Lien symbolique: fichier dont le contenu est un nom de fichier, ce qui autorise un graphe plutôt qu'une arborescence, et qui peut désigner une cible inexistante.

6. file détermine la nature d'un fichier, en inspectant son contenu. diff montre toutes les différences entre deux fichiers — cmp se contente de dire s'ils sont identiques, ce qui suffit pour un binaire. tail -f suit en temps réel un fichier dont le contenu augmente, ce qui en fait l'outil de surveillance d'un journal.