C5 — Fichiers et périphériquesDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 1 · Systèmes d'exploitation

Cours 5Fichiers et périphériques

Ce qui reste quand la machine s'éteint : fichiers, droits, organisation physique du disque.

2 chapitres · 8 h de travail estimé

  1. 1. Système de fichiers5 h
  2. 2. Entrées/sorties et sécurité3 h

Chapitre 1 · 5 h

Système de fichiers

Fichier, répertoire et chemin ; droits et propriétaires ; blocs, inodes et FAT ; montage, et journalisation en survol.

Sous Unix, on peut supprimer un fichier qu'un programme est en train de lire. Le programme continue de lire, jusqu'au bout, sans erreur — et l'espace n'est libéré qu'à sa fermeture. Sous Windows, la même opération est refusée : le fichier est verrouillé.

Cette différence n'est pas un détail d'implémentation. Elle révèle ce qu'est réellement un fichier dans un système Unix : le nom et le contenu sont deux objets distincts. rm n'efface rien, il retire un nom. Le contenu disparaît quand plus personne n'y renvoie.

Ce chapitre décrit cette organisation. Il ferme la boucle du cours : le chapitre 6 rangeait des pages sur un disque sans dire comment ce disque était structuré, et la mémoire virtuelle suppose exactement ce que nous allons construire.

Fichier, répertoire, chemin

Un fichier est une suite d'octets nommée et persistante. C'est tout — le système ne connaît ni « fichier texte », ni « image ». L'extension .jpg est une convention entre applications, et la commande file, qui reconnaît un format, le fait en examinant les premiers octets, pas le nom.

Un répertoire est un fichier lui aussi, dont le contenu est une table de correspondances entre des noms et des numéros. Ces numéros sont ceux des inodes, et c'est la structure centrale du chapitre.

Répertoire /home/ana┌────────────┬────────┐│ nom        │ inode  │├────────────┼────────┤│ .          │  1204  │   lui-même│ ..         │   902  │   son parent│ rapport.md │  3311  ││ copie.md   │  3311  │   ← le MÊME inode : deux noms, un seul contenu└────────────┴────────┘

L'inode porte tout ce qu'on sait d'un fichier sauf son nom : taille, propriétaire, droits, dates, et les adresses des blocs de données. Le nom est dans le répertoire, pas dans l'inode — et c'est ce qui permet à deux entrées de désigner le même inode. C'est un lien physique, et l'inode compte ses références.

rm décrémente ce compteur et retire l'entrée du répertoire. Les blocs ne sont libérés que lorsque le compteur atteint zéro et qu'aucun processus n'a le fichier ouvert. D'où le comportement de l'introduction, et deux conséquences pratiques : la suppression ne détruit pas les données, elle les rend seulement inaccessibles — ce qui fonde la récupération de fichiers et justifie l'effacement sécurisé ; et un fichier volumineux ouvert par un processus continue d'occuper le disque après un rm, ce qui explique bien des « disques pleins » où du ne trouve rien.

Il faut distinguer le lien symbolique, qui est un fichier contenant un chemin. Il peut pointer sur un autre système de fichiers, mais il devient invalide si la cible disparaît, alors qu'un lien physique reste toujours valide.

Droits et propriétaires

Chaque fichier appartient à un utilisateur et à un groupe, et porte neuf bits de permission — trois catégories, trois droits :

-rwxr-xr--   1 ana  etudiants  4096  rapport.md │└┬┘└┬┘└┬┘ │ │  │  └── autres    : r--  lecture seule │ │  └───── groupe    : r-x  lecture et exécution │ └──────── propriétaire : rwx └────────── type : - fichier, d répertoire, l lien

L'écriture octale du chapitre 1 d'architecture prend ici tout son sens : chaque groupe de trois bits est un chiffre octal, d'où chmod 754. Les valeurs se composent — 4 pour lire, 2 pour écrire, 1 pour exécuter — et 755 ou 644 sont les deux motifs qu'on écrit sans réfléchir.

Le point qui trompe : sur un répertoire, les droits n'ont pas le même sens. r permet de lister les noms, w d'y créer ou supprimer des entrées, et x de le traverser, c'est-à-dire d'accéder à ce qu'il contient si l'on en connaît le nom. Un répertoire en r-- laisse voir les noms sans pouvoir ouvrir les fichiers ; en --x, il laisse ouvrir un fichier dont on connaît le chemin exact sans pouvoir découvrir la liste. Et surtout, supprimer un fichier ne demande aucun droit sur le fichier : c'est une modification du répertoire, donc seul w sur le répertoire compte. Un fichier en lecture seule dans un répertoire où vous écrivez est effaçable par vous.

Sur le disque : blocs et allocation

Le disque est découpé en blocs de taille fixe, typiquement 4 Kio — la même que la page du chapitre 6, et ce n'est pas un hasard. Reste à savoir quels blocs appartiennent à quel fichier, et trois réponses ont été essayées.

L'allocation contiguë met le fichier dans des blocs consécutifs. Lecture séquentielle très rapide, accès direct immédiat — et exactement la fragmentation externe du chapitre 6, plus l'impossibilité d'agrandir un fichier coincé entre deux voisins. On ne la trouve plus que sur les supports en lecture seule, comme les disques optiques.

L'allocation chaînée met dans chaque bloc l'adresse du suivant. Plus de fragmentation, mais l'accès direct devient impossible : atteindre le millième bloc demande de lire les 999 premiers. La variante FAT sort les pointeurs des blocs et les met dans une table unique, chargée en mémoire : l'accès direct redevient possible en parcourant la table plutôt que le disque. C'est l'organisation des clés USB et des cartes mémoire, et sa limite est que la table doit tenir en mémoire, donc grandit avec le disque.

L'allocation indexée est celle d'Unix. Chaque fichier a un inode qui contient un tableau d'adresses de blocs. L'accès direct est immédiat, il n'y a pas de fragmentation externe — et le tableau est de taille fixe, ce qui poserait une limite si l'on s'arrêtait là.

Les blocs indirects

La solution est élégante et se retrouve dans presque tous les systèmes de fichiers modernes. L'inode contient :

12 pointeurs DIRECTS            → 12 blocs de données 1 pointeur INDIRECT SIMPLE     → un bloc rempli de pointeurs 1 pointeur INDIRECT DOUBLE     → un bloc de pointeurs vers des blocs de pointeurs 1 pointeur INDIRECT TRIPLE     → un niveau de plus

Avec des blocs de 4 Kio et des pointeurs de 4 octets, un bloc contient 1024 pointeurs. La capacité totale se calcule alors ainsi :

direct          12 × 4 Kio                    =      48 Kioindirect simple 1024 × 4 Kio                  =       4 Mioindirect double 1024 × 1024 × 4 Kio           =       4 Gioindirect triple 1024 × 1024 × 1024 × 4 Kio    =       4 Tio

L'intérêt de cette structure asymétrique est qu'elle s'adapte à la distribution réelle des tailles de fichiers. L'immense majorité des fichiers font moins de 48 Kio : leur inode suffit, et un accès demande une seule lecture. Les gros fichiers, rares, paient un ou deux accès supplémentaires — ce qui est acceptable puisqu'ils sont rares, et que le cache du système gardera de toute façon leurs blocs d'index.

Quiz · 1 question

Un utilisateur ne peut pas modifier un fichier appartenant à quelqu'un d'autre, mais il parvient à le supprimer. Comment est-ce possible ?

  • C'est un bogue du système de fichiers : supprimer devrait exiger le droit d'écriture sur le fichierbogue
  • Supprimer un fichier, c'est retirer une entrée d'un RÉPERTOIRE : seul le droit d'écriture sur le répertoire est requis, les droits du fichier n'entrent pas en jeul'entrée est dans le répertoire
  • L'utilisateur appartient au groupe propriétaire du fichierappartenance au groupe

Réponse : C'est la conséquence directe de la séparation entre le nom et le contenu. Le nom vit dans le répertoire, sous forme d'un couple (nom, numéro d'inode) ; les droits vivent dans l'inode. Supprimer, c'est retirer le couple du répertoire et décrémenter le compteur de références de l'inode : une modification du répertoire, qui ne demande que le droit w sur celui-ci. Un fichier en lecture seule placé dans un répertoire où vous écrivez est donc effaçable par vous — comportement qui surprend systématiquement, et qui explique le bit « sticky » posé sur /tmp, lequel restreint justement la suppression au propriétaire du fichier.

Le montage

Le chapitre 2 l'a annoncé : Unix n'a qu'un seul arbre. Le montage est l'opération qui greffe le système de fichiers d'un périphérique sur un répertoire existant, appelé point de montage.

mount /dev/sdb1 /media/cle

Après quoi /media/cle/photo.jpg désigne un fichier de la clé, atteint par les mêmes appels système et les mêmes commandes que n'importe quel autre. Le contenu que le point de montage avait avant l'opération est masqué tant que le montage dure — il n'est pas détruit, mais il n'est plus atteignable.

C'est l'abstraction du chapitre 1 poussée à son terme : la table de montage, plus une couche appelée système de fichiers virtuel qui définit une interface commune, permettent à ext4, à NTFS, à un partage réseau et à /proc de coexister sous les mêmes commandes. Un programme qui lit un fichier ne sait pas — et n'a pas besoin de savoir — sur quel type de support il se trouve.

La journalisation

Une opération apparemment simple — créer un fichier — modifie plusieurs structures : l'inode, la table des blocs libres, l'entrée de répertoire. Une coupure de courant au milieu laisse un système incohérent : un bloc marqué occupé qu'aucun inode ne réclame, ou pire, un inode pointant vers un bloc réattribué à un autre fichier.

L'ancienne parade était de tout vérifier au démarrage — un parcours complet du disque, qui prenait des heures sur un gros volume.

La journalisation procède autrement : avant de modifier quoi que ce soit, le système écrit dans un journal ce qu'il s'apprête à faire, puis exécute, puis efface l'entrée du journal. Après une coupure, il suffit de relire le journal et de rejouer ou d'annuler les opérations inachevées — quelques secondes au lieu de quelques heures.

La plupart des systèmes ne journalisent que les métadonnées, pas les données. C'est un compromis explicite : la structure est garantie cohérente, mais un fichier peut se retrouver avec des blocs au contenu indéterminé. Journaliser aussi les données double les écritures.

Quiz · 1 question

Avec des blocs de 4 Kio et des pointeurs de 4 octets, quelle taille maximale un fichier peut-il atteindre en n'utilisant que les 12 pointeurs directs et l'indirect simple ?

  • 48 Kio + 4 Mio, car le bloc indirect contient 4096/4 = 1024 pointeurs vers des blocs de 4 Kio1024 pointeurs
  • 48 Kio + 4 Kio, car l'indirect simple ajoute un seul bloc de donnéesun bloc de plus
  • 16 × 4 Kio = 64 Kio, car l'inode contient 16 pointeurs au total16 pointeurs

Réponse : Le raisonnement se fait en deux temps. Combien de pointeurs tiennent dans un bloc ? 4096 octets ÷ 4 octets par pointeur = 1024. Ce bloc indirect ne contient AUCUNE donnée : il ne contient que des adresses, qui désignent 1024 blocs de données, soit 1024 × 4 Kio = 4 Mio. Les 12 pointeurs directs, eux, désignent directement 12 × 4 Kio = 48 Kio. Total : 4 Mio et 48 Kio. La deuxième réponse est l'erreur classique — confondre le bloc d'index et un bloc de données. La troisième oublie que les trois derniers pointeurs de l'inode ne sont pas de même nature que les douze premiers.

À vous

L'exercice est le calcul d'examen du chapitre, écrit une fois pour toutes. Vous implémentez la résolution d'un déplacement en octets vers un numéro de bloc à travers la structure de l'inode : direct, indirect simple, indirect double — en comptant au passage combien d'accès disque la résolution a coûté.

C'est ce compteur qui rend la structure compréhensible. On voit que les premiers 48 Kio coûtent un accès, que le mégaoctet suivant en coûte deux, et pourquoi c'est le bon compromis compte tenu de la distribution réelle des tailles de fichiers.

Exercice de code

Résolvez un déplacement en octets à travers l'inode, et comptez les accès disque.

Point de départ

const BLOC = 4096;                 // 4 Kio
const POINTEUR = 4;                // 4 octets par adresse
const PAR_BLOC = BLOC / POINTEUR;  // 1024 pointeurs dans un bloc d'index
const DIRECTS = 12;

// Capacités cumulées, en blocs de données.
const CAP_DIRECT = DIRECTS;                       // 12
const CAP_SIMPLE = PAR_BLOC;                      // 1024
const CAP_DOUBLE = PAR_BLOC * PAR_BLOC;           // 1 048 576

// Résout un déplacement en octets vers un bloc logique, en disant par où l'on
// est passé et combien d'accès disque cela a coûté.
function resoudre(octet) {
  const blocLogique = Math.floor(octet / BLOC);
  const dansLeBloc = octet % BLOC;

  if (blocLogique < CAP_DIRECT) {
    return { voie: "direct", indices: [blocLogique], dansLeBloc, acces: 1 };
  }

  let reste = blocLogique - CAP_DIRECT;
  if (reste < CAP_SIMPLE) {
    // Un accès pour lire le bloc d'index, un pour la donnée.
    return { voie: "indirect simple", indices: [reste], dansLeBloc, acces: 2 };
  }

  reste -= CAP_SIMPLE;
  if (reste < CAP_DOUBLE) {
    return null;   // ← à écrire : indirect double
  }

  return { erreur: "au-delà de l'indirect double" };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Complétez la branche « indirect double » : deux indices (quel bloc du
//    premier niveau, quelle entrée dedans) et le bon nombre d'accès.
// 2. Écrivez tailleMax(), qui rend la taille maximale d'un fichier avec
//    directs + simple + double.
// 3. Vérifiez qu'un fichier de moins de 48 Kio coûte toujours UN accès.

function tailleMax() {
  return 0;   // ← à écrire
}

const CAS = [0, 40000, 49152, 60000, 4 * 1024 * 1024, 100 * 1024 * 1024];
for (const o of CAS) {
  const r = resoudre(o);
  console.log(String(o).padStart(10) + " octets -> " +
    (r === null ? "à écrire" : r.erreur ?? (r.voie.padEnd(16) + " indices " +
      JSON.stringify(r.indices).padEnd(16) + " " + r.acces + " accès disque")));
}
console.log("taille maximale :", tailleMax(), "octets");

Solution

const BLOC = 4096;
const POINTEUR = 4;
const PAR_BLOC = BLOC / POINTEUR;
const DIRECTS = 12;

const CAP_DIRECT = DIRECTS;
const CAP_SIMPLE = PAR_BLOC;
const CAP_DOUBLE = PAR_BLOC * PAR_BLOC;

function resoudre(octet) {
  const blocLogique = Math.floor(octet / BLOC);
  const dansLeBloc = octet % BLOC;

  if (blocLogique < CAP_DIRECT) {
    return { voie: "direct", indices: [blocLogique], dansLeBloc, acces: 1 };
  }

  let reste = blocLogique - CAP_DIRECT;
  if (reste < CAP_SIMPLE) {
    return { voie: "indirect simple", indices: [reste], dansLeBloc, acces: 2 };
  }

  reste -= CAP_SIMPLE;
  if (reste < CAP_DOUBLE) {
    // Deux niveaux d'index : le quotient désigne l'entrée du premier bloc
    // d'index, le reste désigne l'entrée du bloc de second niveau. Trois
    // accès disque : index 1, index 2, donnée.
    const premier = Math.floor(reste / PAR_BLOC);
    const second = reste % PAR_BLOC;
    return { voie: "indirect double", indices: [premier, second], dansLeBloc, acces: 3 };
  }

  return { erreur: "au-delà de l'indirect double" };
}

function tailleMax() {
  return (CAP_DIRECT + CAP_SIMPLE + CAP_DOUBLE) * BLOC;
}

const CAS = [0, 40000, 49152, 60000, 4 * 1024 * 1024, 100 * 1024 * 1024];
for (const o of CAS) {
  const r = resoudre(o);
  console.log(String(o).padStart(10) + " octets -> " +
    (r.erreur ?? (r.voie.padEnd(16) + " indices " +
      JSON.stringify(r.indices).padEnd(16) + " " + r.acces + " accès disque")));
}

const max = tailleMax();
console.log("");
console.log("taille maximale : " + max + " octets = " +
            (max / (1024 * 1024 * 1024)).toFixed(2) + " Gio");

// Combien d'accès selon la taille du fichier ?
console.log("");
console.log("— coût d'un accès selon la position —");
for (const [nom, o] of [["fichier de 10 Kio", 10 * 1024],
                        ["fichier de 40 Kio", 40 * 1024],
                        ["dernier octet des directs", 48 * 1024 - 1],
                        ["premier octet au-delà", 48 * 1024],
                        ["à 3 Mio", 3 * 1024 * 1024],
                        ["à 50 Mio", 50 * 1024 * 1024]]) {
  console.log("   " + nom.padEnd(28) + resoudre(o).acces + " accès");
}
// La marche se voit nettement à 48 Kio. C'est là que se justifie la
// structure asymétrique : l'immense majorité des fichiers d'un système
// tiennent sous cette barre et se lisent en un seul accès, tandis que les
// gros fichiers, rares, paient deux ou trois accès — dont les blocs d'index
// resteront de toute façon dans le cache du système.

En travaux pratiques

Travaux pratiques 7 · 3 h

Un fichier n'est pas son nom

Séparer le nom, l'inode et le contenu — la distinction qui explique les liens, la suppression, l'espace qui ne se libère pas, et la récupération d'un fichier effacé.

Avant de commencer

  • Les TP 2 et 3
  • Les commandes ls -i, stat, lsof, df, du

Énoncé

  1. Voir l'inodeCréez un fichier, affichez son inode et ses métadonnées. Listez ce qui est stocké dans l'inode et ce qui ne l'est pas. Où est le nom ?
  2. Deux noms, un fichierCréez un lien dur vers ce fichier. Comparez les inodes, modifiez par un nom, lisez par l'autre. Supprimez le premier nom : que devient le contenu ?
  3. Le lien symboliqueCréez un lien symbolique vers le même fichier et refaites les mêmes essais. Supprimez la cible et observez. Établissez le tableau des différences.
  4. L'espace qui ne revient pasCréez un fichier d'un gigaoctet, ouvrez-le depuis un programme qui dort, supprimez-le pendant ce temps. Comparez ce que disent df et du. Expliquez. Indice : Le contenu disparaît quand le dernier NOM et le dernier DESCRIPTEUR ont disparu.
  5. Récupérer l'irrécupérableSans arrêter le programme précédent, retrouvez le contenu du fichier supprimé en passant par /proc. Vous venez de récupérer un fichier effacé.
  6. Épuiser autre chose que l'espaceSur une petite partition de test, créez des centaines de milliers de fichiers vides jusqu'à l'échec. Regardez df puis df -i, et expliquez le message.
  7. Écriture atomiqueÉcrivez un programme qui remplace un fichier de configuration, et tuez-le au milieu de l'écriture. Constatez la corruption, puis réécrivez-le avec le motif écrire-puis-renommer et refaites l'essai.

C'est réussi quand

  • Vous savez dire où est stocké le nom d'un fichier, et ce n'est pas dans l'inode
  • Vous récupérez le contenu d'un fichier supprimé encore ouvert
  • Le disque est plein alors que df annonce de la place, et vous savez pourquoi
  • Votre configuration n'est jamais corrompue, même tuée en pleine écriture

Correction

Ce que contient l'inode
stat essai.txt
Inode: 1179654   Links: 1
Access: (0644/-rw-r--r--)  Uid: 1000  Gid: 1000
Size: 12   Blocks: 8
Modify: 2026-09-07 10:14:02

dans l'inode : type, droits, propriétaire, tailles, dates,
             pointeurs vers les blocs de données
PAS dans l'inode : le NOM

Le nom est dans le RÉPERTOIRE, qui n'est qu'une table associant des noms à des numéros d'inode. Un répertoire ne contient donc pas les fichiers : il contient leurs noms. Toute la suite du TP découle de cette seule séparation.

Lien dur et lien symbolique
                     lien dur          lien symbolique
même inode ?         oui               non (inode propre)
cible supprimée      contenu intact    lien CASSÉ
entre partitions     impossible        possible
sur un répertoire    interdit          autorisé
contenu du lien      —                 un CHEMIN, en texte

ls -i
1179654 essai.txt    1179654 copie.txt     ← même inode
1179654 essai.txt    1179701 raccourci     ← inodes différents

Un lien dur n'est pas une copie ni un raccourci : c'est un SECOND NOM, exactement au même titre que le premier. Il n'y a pas d'original. Le lien symbolique, lui, est un petit fichier contenant un chemin — d'où sa fragilité, et d'où sa capacité à traverser les partitions, qu'un lien dur n'a pas puisque les numéros d'inode sont locaux à un système de fichiers.

Suppression : deux compteurs, pas un
du → 0        (plus aucun NOM dans aucun répertoire)
df → 1 Go pris (le contenu existe toujours)

un inode est libéré quand :
 nombre de liens == 0   ET   nombre de descripteurs ouverts == 0

/* d'où le grand classique : */
rm /var/log/enorme.log   sans redémarrer le service qui l'écrit
→ l'espace n'est PAS rendu. Il faut fermer le fichier
 (ou tronquer : > /var/log/enorme.log)

C'est l'incident de production le plus banal qui soit : un disque plein, un gros journal supprimé, et rien qui ne se libère. La commande lsof avec l'option +L1 liste précisément ces fichiers sans nom mais encore ouverts. Corollaire : la vraie suppression, c'est unlink — « retirer un nom » — et le nom de l'appel système le dit.

Récupérer un fichier supprimé
lsof +L1                     → pid 4217, descripteur 3
cp /proc/4217/fd/3 recupere.txt

/* /proc/PID/fd/N est un lien vers l'inode ouvert :
 tant que le processus vit, le contenu est accessible */

Le fichier n'avait plus de nom ; il en a un de nouveau. Cela vaut pour un journal supprimé par erreur comme pour une analyse après incident — et c'est aussi une raison de ne pas croire qu'un rm efface quoi que ce soit. Effacer réellement demande d'écraser, et sur un disque à mémoire flash même cela ne suffit pas, à cause de la répartition d'usure.

Le disque plein sans être plein
touch: cannot touch 'f200000': No space left on device

df    → 62 % utilisé      ← il reste de la place !
df -i → 100 % d'inodes utilisés

le nombre d'inodes est FIXÉ à la création du système de fichiers

Deux ressources indépendantes s'épuisent : les blocs de données et les inodes. Des millions de petits fichiers — un cache, des sessions, des courriels — épuisent les seconds bien avant les premiers. Le message d'erreur, lui, est le même dans les deux cas, ce qui rend le diagnostic déroutant si l'on ne connaît pas l'option -i.

Le remplacement atomique
/* NON atomique : une fenêtre de corruption */
fd = open("config.conf", O_WRONLY|O_TRUNC);   ← le fichier est vidé ICI
write(fd, contenu, taille);                    ← tué ici = fichier vide
close(fd);

/* atomique */
fd = open("config.conf.tmp", O_WRONLY|O_CREAT, 0644);
write(fd, contenu, taille);
fsync(fd);                     /* les données sont sur le disque */
close(fd);
rename("config.conf.tmp", "config.conf");   /* ATOMIQUE */

rename est garanti atomique par le système : à tout instant, un lecteur voit soit l'ancien fichier complet, soit le nouveau, jamais un intermédiaire. Le fsync avant est indispensable — sans lui, le renommage peut être enregistré avant les données, et une coupure laisse un fichier vide portant le bon nom. C'est ainsi qu'écrivent les éditeurs, les gestionnaires de paquets et les bases de données.

Ce que la suite en fait

Le chapitre 8 termine le cours en descendant sous le système de fichiers : les pilotes qui parlent au contrôleur, les tampons qui absorbent l'écart de vitesse, et l'ordonnancement des requêtes disque — car les blocs que ce chapitre a numérotés se trouvent physiquement quelque part, et l'ordre dans lequel on les demande change le temps total.

Les droits posés ici y reviendront aussi, du côté sécurité : ce sont eux qui portent l'isolation entre utilisateurs, et le chapitre montrera par quels mécanismes on va au-delà.

À retenir

Flashcards · 5 cartes

Qu'est-ce qu'un inode, et pourquoi rm n'efface-t-il rien ?
L'inode porte tout ce qu'on sait d'un fichier SAUF son nom : taille, propriétaire, droits, dates, adresses des blocs. Le nom vit dans le répertoire, sous forme d'un couple (nom, numéro d'inode) — d'où la possibilité que deux noms désignent le même inode (lien physique). rm retire une entrée de répertoire et décrémente le compteur de références ; les blocs ne sont libérés qu'à zéro ET si aucun processus n'a le fichier ouvert.
Que signifient r, w et x sur un RÉPERTOIRE ?
r : lister les noms qu'il contient. w : y créer ou supprimer des entrées. x : le traverser, donc atteindre ce qu'il contient si l'on en connaît le nom. Conséquence majeure : supprimer un fichier ne demande AUCUN droit sur le fichier, seulement w sur le répertoire — un fichier en lecture seule dans un répertoire où vous écrivez est effaçable par vous. C'est ce que corrige le bit sticky sur /tmp.
Comparez allocation contiguë, chaînée et indexée.
CONTIGUË : blocs consécutifs, très rapide en séquentiel et en accès direct, mais fragmentation externe et impossibilité d'agrandir — réservée aux supports en lecture seule. CHAÎNÉE : chaque bloc pointe le suivant, pas de fragmentation mais aucun accès direct ; la variante FAT sort les pointeurs dans une table en mémoire, qui doit tenir en RAM. INDEXÉE (Unix) : un inode contient le tableau des adresses, accès direct immédiat et pas de fragmentation externe.
Comment un inode dépasse-t-il la limite de son tableau de pointeurs ?
Par les blocs INDIRECTS : 12 pointeurs directs, puis un indirect simple (un bloc rempli de pointeurs), un indirect double (un bloc de pointeurs vers des blocs de pointeurs), un indirect triple. Avec des blocs de 4 Kio et des pointeurs de 4 octets — donc 1024 pointeurs par bloc — cela donne 48 Kio, puis 4 Mio, puis 4 Gio, puis 4 Tio. La structure est asymétrique parce que la distribution des tailles de fichiers l'est : les petits fichiers, majoritaires, coûtent un seul accès.
Qu'apporte la journalisation, et que journalise-t-on en pratique ?
Créer un fichier modifie plusieurs structures (inode, table des blocs libres, entrée de répertoire) ; une coupure au milieu laisse un système incohérent, qu'il fallait autrefois vérifier en parcourant tout le disque. La journalisation écrit d'abord dans un journal ce qu'elle va faire, exécute, puis efface l'entrée : après une coupure, on rejoue ou annule les opérations inachevées en quelques secondes. En pratique on ne journalise que les MÉTADONNÉES — la structure est cohérente, mais le contenu d'un fichier peut rester indéterminé.

Chapitre 2 · 3 h

Entrées/sorties et sécurité

Pilotes et tampons, ordonnancement disque, et les notions d'utilisateur, de permission et d'isolation.

Un disque magnétique est la dernière pièce mécanique d'un ordinateur. Ses plateaux tournent, son bras se déplace, et sa performance obéit à Newton plutôt qu'à Moore : depuis trente ans, les processeurs ont gagné un facteur mille, les temps de déplacement de tête un facteur trois.

C'est pourquoi ce dernier chapitre existe. Tout ce que le cours a construit — processus, ordonnancement, mémoire virtuelle, système de fichiers — finit par toucher un périphérique, et c'est là que le temps se perd. Il s'achève sur la question qui traverse tout le cours sans avoir jamais été traitée pour elle-même : au nom de quoi le système refuse-t-il quelque chose à quelqu'un ?

Pilotes et tampons

Le chapitre 8 d'architecture a décrit le contrôleur et ses registres. Le pilote est la partie du système qui connaît ce modèle-là de contrôleur, et qui présente au reste du noyau une interface uniforme. C'est l'abstraction du chapitre 1, à son étage le plus bas : au-dessus du pilote, plus personne ne sait de quel matériel il s'agit.

Deux familles suffisent à ranger presque tout. Un périphérique en mode bloc — disque, SSD, clé — transfère des blocs de taille fixe et supporte l'accès direct : c'est le support du chapitre 7. Un périphérique en mode caractère — clavier, souris, port série — transfère un flux d'octets, sans accès direct.

Entre les deux se trouvent les tampons, et ils servent à trois choses distinctes qu'il ne faut pas confondre. Ils absorbent la différence de vitesse entre un producteur et un consommateur — le tube du chapitre 2, exactement. Ils absorbent la différence de taille : un programme qui écrit un octet à la fois ne peut pas provoquer une écriture disque par octet, puisque l'unité de transfert est le bloc. Et ils permettent la sémantique de copie : un appel write peut rendre la main dès que les données sont copiées dans le noyau, sans attendre le matériel, ce qui libère le processus.

Au-dessus, le système maintient un cache disque en mémoire vive, gouverné par les mêmes principes que le cache du chapitre 7 d'architecture — localité, remplacement LRU. Sur une machine de bureau, il occupe couramment plusieurs gigaoctets, et c'est lui qui explique qu'un fichier relu soit instantané.

Ce cache a un revers, à connaître : les écritures sont différées. Un programme dont write a réussi n'a aucune garantie que les octets soient sur le disque, et une coupure de courant les perd. Les applications qui ne peuvent pas se le permettre — bases de données, systèmes de fichiers journalisés — appellent explicitement fsync, qui bloque jusqu'à l'écriture réelle. Le coût est considérable, et c'est pourquoi on ne le fait pas partout.

Ordonnancement disque

Sur un disque magnétique, le temps d'accès se décompose en trois :

déplacement de la tête (seek)   ~ 5 ms    ← dominant, et le seul qu'on peut réduirerotation jusqu'au secteur       ~ 2 mstransfert                       ~ 0,1 ms

Puisque le déplacement domine, et puisque plusieurs processus demandent des blocs en même temps, le système peut réordonner les requêtes en attente pour réduire la distance totale parcourue. C'est exactement l'ordonnancement du chapitre 4, appliqué à une autre ressource — et on y retrouve les mêmes compromis.

Prenons une file de requêtes sur les cylindres 98, 183, 37, 122, 14, 124, 65, 67, la tête étant en 53.

Premier arrivé, premier servi. On sert dans l'ordre. Total : 640 cylindres parcourus. Équitable, et mauvais — la tête traverse le disque de part en part sans raison.

Le plus proche d'abord (SSTF). On sert la requête la plus proche de la position courante. Total : 236 cylindres, presque trois fois moins. C'est le SJF du chapitre 4, avec le même défaut : la famine. Une requête sur un cylindre éloigné peut attendre indéfiniment si des requêtes proches continuent d'arriver.

L'ascenseur (SCAN). La tête balaie dans un sens, sert tout ce qu'elle rencontre, puis repart dans l'autre sens. Le nom dit tout : un ascenseur ne redescend pas chercher quelqu'un au troisième quand il monte au dixième. La famine disparaît, puisque le balayage garantit un passage. Total ici : 236 également, mais avec une borne sur l'attente.

C-SCAN ne sert que dans un sens et revient à vide au début, ce qui rend l'attente plus uniforme : avec SCAN, les cylindres du milieu sont visités deux fois plus souvent que les extrémités.

Un mot pour finir, parce qu'il change la conclusion : sur un SSD, tout ceci ne s'applique plus. Il n'y a pas de tête, l'accès est uniforme, et réordonner ne réduit aucune distance. Le système bascule donc sur d'autres priorités — regrouper les écritures pour ménager l'usure des cellules, et exploiter le parallélisme interne du support en gardant plusieurs requêtes en vol. Le noyau Linux propose d'ailleurs un ordonnanceur none pour les disques rapides : la meilleure décision est parfois de ne pas décider.

Quiz · 1 question

Un administrateur remplace des disques magnétiques par des SSD et conserve l'ordonnanceur « ascenseur ». Que faut-il en penser ?

  • C'est le bon choix : l'ascenseur reste l'algorithme le plus efficace sur tout supporttoujours valable
  • L'ascenseur n'a plus d'objet — il minimise un déplacement de tête qui n'existe pas — et son surcoût de tri retarde inutilement les requêtes ; les priorités deviennent le regroupement des écritures et le parallélisme internesans objet sur SSD
  • Il faut le remplacer par SSTF, mieux adapté aux accès rapidespasser à SSTF

Réponse : Tous les algorithmes de ce chapitre optimisent une seule grandeur : la distance parcourue par une tête de lecture. Un SSD n'en a pas, et son temps d'accès est indépendant de l'adresse : réordonner ne fait plus rien gagner, et le tri lui-même consomme du temps processeur et ajoute de la latence. SSTF a exactement le même défaut, plus la famine. Sur SSD, les enjeux sont ailleurs : regrouper les écritures pour limiter l'amplification d'écriture et l'usure des cellules, et surtout maintenir plusieurs requêtes en vol pour exploiter le parallélisme des puces — c'est ce que fait NVMe avec ses files multiples. D'où l'existence d'un ordonnanceur « none » dans Linux : sur un support assez rapide, la meilleure politique est de ne pas ordonnancer.

Utilisateurs, permissions, isolation

Le chapitre 1 a posé la protection comme troisième fonction du système, et le mécanisme de base — le bit de mode. Voici ce qui se construit dessus.

Chaque processus porte un identifiant d'utilisateur et des identifiants de groupe, hérités de son parent au fork du chapitre 3. Toute décision d'accès s'y ramène : quand un processus ouvre un fichier, le noyau compare ces identifiants au propriétaire et aux neuf bits de permission du chapitre 7.

L'utilisateur d'identifiant 0, root, échappe à ces contrôles. C'est une simplification grossière, et elle est reconnue comme telle : elle viole le principe de moindre privilège, qui veut qu'un programme n'ait que les droits strictement nécessaires à sa tâche. Un serveur web a besoin d'écouter sur le port 80 — une opération privilégiée — et n'a aucun besoin de lire les fichiers de mots de passe ; pourtant, s'il s'exécute en root, il peut tout. D'où les mécanismes de capacités, qui découpent les pouvoirs de root en droits séparés et attribuables un par un.

Un cas mérite une attention particulière, parce qu'il est à la fois indispensable et dangereux : le bit setuid. Un exécutable qui le porte s'exécute avec les droits de son propriétaire, et non de celui qui le lance. C'est ainsi que la commande de changement de mot de passe, lancée par un utilisateur ordinaire, peut modifier un fichier système auquel il n'a pas accès. Le programme devient alors une frontière de privilège : tous ses arguments, toutes ses variables d'environnement, toutes ses entrées viennent d'un utilisateur non fiable et doivent être validées. Historiquement, c'est l'une des principales sources d'élévation de privilèges, et la règle est de n'en poser qu'exceptionnellement.

Au-delà des permissions, l'isolation se renforce par degrés, chaque niveau coûtant plus cher que le précédent :

MécanismeCe qui est isoléCoût
Droits utilisateurles fichiers, selon l'identiténul
chrootla vue de l'arborescencefaible, contournable
Espaces de noms et cgroupsprocessus, réseau, montages, quotasfaible
Conteneurl'ensemble des précédents, noyau partagémodéré
Machine virtuelletout, y compris le noyauélevé

La ligne à retenir est celle des conteneurs : ils reposent sur des mécanismes du noyau — espaces de noms pour la visibilité, cgroups pour les quotas — et le noyau reste partagé avec la machine hôte. Un conteneur n'est donc pas une machine virtuelle : une faille du noyau traverse l'isolation. C'est un compromis parfaitement raisonnable pour séparer des services qu'on maîtrise, et insuffisant pour exécuter du code hostile — ce que fait alors une machine virtuelle, au prix d'un noyau complet par instance.

Quiz · 1 question

Pourquoi un programme portant le bit setuid et appartenant à root demande-t-il une vigilance particulière ?

  • Parce qu'il consomme plus de ressources qu'un programme ordinaireconsommation
  • Parce qu'il s'exécute avec les droits de root tout en recevant ses arguments, son environnement et ses entrées d'un utilisateur non fiable : c'est une frontière de privilège, et tout ce qui la traverse doit être validéfrontière de privilège
  • Parce qu'il ne peut être lancé que par root, ce qui limite son utilitéréservé à root

Réponse : Le bit setuid inverse la règle habituelle : le programme s'exécute avec les droits de son PROPRIÉTAIRE, pas de celui qui le lance. C'est indispensable — la commande de changement de mot de passe doit modifier un fichier système au nom d'un utilisateur ordinaire — et c'est exactement ce qui en fait une cible. Tout ce qui entre vient de l'appelant : arguments, variables d'environnement (à commencer par PATH, vu au chapitre 2), descripteurs déjà ouverts, répertoire courant, limites de ressources. Un seul de ces canaux mal validé, et l'appelant obtient les droits de root. C'est historiquement l'une des premières sources d'élévation de privilèges, d'où la règle : n'en poser qu'exceptionnellement, et préférer les capacités, qui ne donnent qu'un pouvoir précis au lieu de tous.

À vous

L'exercice compare les quatre algorithmes d'ordonnancement disque sur la file classique, en comptant les cylindres parcourus et, pour chaque requête, son temps d'attente.

Le second chiffre est celui qui compte, et il révèle ce que le total cache : SSTF gagne sur la distance totale mais fait attendre très longtemps une requête isolée en bout de disque, alors que l'ascenseur borne cette attente. C'est la même opposition qu'au chapitre 4 entre moyenne et équité, sur une autre ressource.

Exercice de code

Écrivez l'ascenseur, puis comparez distance totale et pire attente aux autres politiques.

Point de départ

const FILE = [98, 183, 37, 122, 14, 124, 65, 67];
const DEPART = 53;
const MAX = 199;

// Rend l'ordre de service, puis on en déduit distance et attentes.
function fcfs(file) {
  return [...file];
}

function sstf(file, depart) {
  const restants = [...file];
  const ordre = [];
  let tete = depart;
  while (restants.length > 0) {
    // Le plus proche de la position courante, dans un sens ou dans l'autre.
    let meilleur = 0;
    for (let i = 1; i < restants.length; i++) {
      if (Math.abs(restants[i] - tete) < Math.abs(restants[meilleur] - tete)) meilleur = i;
    }
    tete = restants[meilleur];
    ordre.push(tete);
    restants.splice(meilleur, 1);
  }
  return ordre;
}

function scan(file, depart, max) {
  return [];   // ← à écrire : monter en servant, puis redescendre
}

function bilan(nom, ordre, depart) {
  let tete = depart, distance = 0;
  const attentes = new Map();
  for (const c of ordre) {
    distance += Math.abs(c - tete);
    tete = c;
    attentes.set(c, distance);       // « temps » = cylindres parcourus avant service
  }
  const liste = [...attentes.values()];
  const moyenne = liste.reduce((a, b) => a + b, 0) / liste.length;
  console.log(nom.padEnd(22) +
    "distance " + String(distance).padStart(4) +
    " | attente moyenne " + moyenne.toFixed(1).padStart(6) +
    " | pire attente " + String(Math.max(...liste)).padStart(4));
  console.log("   ordre : " + ordre.join(" -> "));
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez scan : servir en montant jusqu'au bout, puis en descendant.
// 2. Comparez la PIRE attente de SSTF et celle de SCAN. Laquelle des deux
//    politiques peut affamer une requête, et pourquoi ?
// 3. Ajoutez c-scan (montée seule, puis retour à vide en 0) et observez
//    l'effet sur l'écart entre la meilleure et la pire attente.

bilan("FCFS", fcfs(FILE), DEPART);
bilan("SSTF", sstf(FILE, DEPART), DEPART);

Solution

const FILE = [98, 183, 37, 122, 14, 124, 65, 67];
const DEPART = 53;
const MAX = 199;

function fcfs(file) { return [...file]; }

function sstf(file, depart) {
  const restants = [...file];
  const ordre = [];
  let tete = depart;
  while (restants.length > 0) {
    let meilleur = 0;
    for (let i = 1; i < restants.length; i++) {
      if (Math.abs(restants[i] - tete) < Math.abs(restants[meilleur] - tete)) meilleur = i;
    }
    tete = restants[meilleur];
    ordre.push(tete);
    restants.splice(meilleur, 1);
  }
  return ordre;
}

function scan(file, depart, max) {
  // L'ascenseur : on sert tout ce qui est devant en montant, on va jusqu'au
  // bord, puis on redescend en servant le reste. Aucune requête ne peut être
  // repoussée indéfiniment, puisque le balayage passe forcément par elle.
  const montee = file.filter((c) => c >= depart).sort((a, b) => a - b);
  const descente = file.filter((c) => c < depart).sort((a, b) => b - a);
  return [...montee, ...descente];
}

function cscan(file, depart, max) {
  // Une seule direction utile : après le bord, retour à vide en 0. L'attente
  // devient plus UNIFORME — avec SCAN, le milieu du disque est visité deux
  // fois plus souvent que les extrémités.
  const montee = file.filter((c) => c >= depart).sort((a, b) => a - b);
  const reste = file.filter((c) => c < depart).sort((a, b) => a - b);
  return [...montee, ...reste];
}

function bilan(nom, ordre, depart) {
  let tete = depart, distance = 0;
  const attentes = new Map();
  for (const c of ordre) {
    distance += Math.abs(c - tete);
    tete = c;
    attentes.set(c, distance);
  }
  const liste = [...attentes.values()];
  const moyenne = liste.reduce((a, b) => a + b, 0) / liste.length;
  console.log(nom.padEnd(22) +
    "distance " + String(distance).padStart(4) +
    " | attente moyenne " + moyenne.toFixed(1).padStart(6) +
    " | pire attente " + String(Math.max(...liste)).padStart(4));
  console.log("   ordre : " + ordre.join(" -> "));
}

bilan("FCFS", fcfs(FILE), DEPART);
bilan("SSTF", sstf(FILE, DEPART), DEPART);
bilan("SCAN (ascenseur)", scan(FILE, DEPART, MAX), DEPART);
bilan("C-SCAN", cscan(FILE, DEPART, MAX), DEPART);

console.log("");
console.log("— la famine de SSTF, en pratique —");
// On ajoute un flux continu de requêtes proches de la tête : la requête
// lointaine (14) n'est jamais servie tant qu'il en arrive de plus proches.
let file = [14, 55, 58, 60];
let tete = DEPART, servies = 0;
for (let arrivee = 0; arrivee < 12; arrivee++) {
  const ordre = sstf(file, tete);
  const premiere = ordre[0];
  tete = premiere;
  servies++;
  file = file.filter((c) => c !== premiere);
  file.push(tete + 2);                       // une nouvelle requête, tout près
  if (servies % 4 === 0) {
    console.log("   après " + servies + " services, la requête 14 est " +
      (file.includes(14) ? "TOUJOURS en attente" : "servie"));
  }
}
// L'ascenseur ne peut pas produire cela : son balayage atteint forcément le
// cylindre 14 au plus tard à la fin de la descente. C'est exactement le
// couple SJF / tourniquet du chapitre 4, transposé au bras du disque.

En travaux pratiques

Travaux pratiques 8 · 3 h

Les tuyaux du mini-shell, et les droits qui les encadrent

Terminer le shell commencé au TP 3 en implémentant redirections et tuyaux, puis auditer les droits d'un système en cherchant ce qui ne devrait pas y être.

Avant de commencer

  • Le mini-shell du TP 3
  • Les TP 5 et 7

Énoncé

  1. Rediriger la sortieAjoutez à votre shell la reconnaissance du chevron. Le fils doit ouvrir le fichier et remplacer sa sortie standard AVANT d'appeler exec. Indice : dup2 remplace un descripteur par un autre ; il faut le faire entre fork et exec.
  2. Rediriger l'entréeMême travail pour le chevron entrant. Testez avec une commande qui lit son entrée standard, et vérifiez qu'elle s'arrête bien en fin de fichier.
  3. Le tuyauImplémentez une commande avec un seul tuyau : deux fils, un pipe, chacun redirigeant un bout. Testez avec la chaîne du TP 2.
  4. Le blocage à trouverSi votre tuyau ne se termine jamais, cherchez quel descripteur n'a pas été fermé. Expliquez pourquoi il faut le fermer dans TROIS processus.
  5. Compter les droits en tropListez tous les fichiers setuid root du système, et tous ceux inscriptibles par tout le monde. Pour trois d'entre eux, dites à quoi sert le privilège.
  6. Écrire un setuid, et le casserÉcrivez un petit programme setuid qui affiche un fichier dont le chemin est donné en argument. Trouvez ensuite comment lui faire afficher un fichier qu'il ne devrait pas.
  7. Le réparerCorrigez en abandonnant les privilèges dès que possible, et en validant le chemin. Vérifiez que l'attaque précédente échoue.
  8. RestreindreExécutez votre shell avec les capacités réduites, ou dans un espace de noms séparé. Vérifiez ce qu'il ne peut plus faire.

C'est réussi quand

  • Votre shell exécute une chaîne de deux commandes avec un tuyau
  • Vous savez expliquer quel descripteur non fermé bloque un tuyau
  • Vous décrivez une attaque contre votre propre programme setuid, puis sa correction

Correction

La redirectionshell.c
pid_t p = fork();
if (p == 0) {
  if (fichier_sortie) {
      int fd = open(fichier_sortie, O_WRONLY|O_CREAT|O_TRUNC, 0644);
      dup2(fd, STDOUT_FILENO);   /* 1 devient une copie de fd */
      close(fd);                 /* l'original ne sert plus */
  }
  execvp(args[0], args);
  _exit(127);
}

C'est ici que la séparation fork/exec du TP 3 prend tout son sens : entre les deux, le fils est encore VOTRE code, et peut préparer l'environnement du programme qui va le remplacer. Les descripteurs survivent à exec — c'est précisément ce qui rend la redirection possible. Sur un système où création et lancement seraient un seul appel, il faudrait une interface bien plus compliquée.

Le tuyau
int tube[2];
pipe(tube);                    /* tube[0] = lecture, tube[1] = écriture */

if (fork() == 0) {             /* GAUCHE : écrit dans le tube */
  dup2(tube[1], STDOUT_FILENO);
  close(tube[0]); close(tube[1]);
  execvp(gauche[0], gauche);
}
if (fork() == 0) {             /* DROITE : lit dans le tube */
  dup2(tube[0], STDIN_FILENO);
  close(tube[0]); close(tube[1]);
  execvp(droite[0], droite);
}
close(tube[0]); close(tube[1]);   /* LE PÈRE AUSSI */
wait(NULL); wait(NULL);

Les deux commandes tournent EN MÊME TEMPS, pas l'une après l'autre : c'est ce qui permet à la chaîne du TP 2 de traiter un flux plus gros que la mémoire. Le tampon du tuyau, environ 64 Ko, régule tout seul — si le lecteur est lent, l'écrivain se bloque sur write.

Le descripteur oublié
symptôme : la commande de droite ne se termine JAMAIS

cause : elle attend la fin de fichier, qui n'arrive que quand
      TOUS les descripteurs d'écriture du tube sont fermés

il y en a trois : celui du fils gauche (fermé par sa fin),
                celui du fils droit  (à fermer explicitement),
                celui du PÈRE        (le plus souvent oublié)

Chaque fork duplique tous les descripteurs ouverts : le tube existe alors en trois exemplaires. La fin de fichier n'est signalée que lorsque le compteur tombe à zéro, et un seul descripteur oublié suffit à faire attendre indéfiniment. C'est le bogue emblématique de ce TP, et il apprend une règle générale — un descripteur dont on ne se sert pas se ferme, toujours.

L'audit
find / -perm -4000 -type f 2>/dev/null       # setuid
/usr/bin/passwd    modifie /etc/shadow, lisible de root seul
/usr/bin/sudo      change d'identité, par définition
/usr/bin/mount     modifie la table des montages

find / -perm -0002 -type f 2>/dev/null      # inscriptible par tous
→ tout résultat ici est un problème : n'importe qui peut
  en remplacer le contenu

Un binaire setuid root s'exécute avec TOUS les privilèges, quel que soit l'appelant : c'est un point de passage volontaire à travers l'isolation du système. Chacun doit donc être justifié, minimal et audité. Les capacités POSIX permettent aujourd'hui de n'accorder que le privilège nécessaire — ping n'a besoin que de CAP_NET_RAW, pas des pleins pouvoirs.

L'attaque, et la correction
/* vulnérable */
int main(int argc, char **argv) {
  char cmd[256];
  sprintf(cmd, "cat %s", argv[1]);   /* argument NON validé */
  system(cmd);                        /* + PATH héritée */
}

attaque 1 : ./afficher "/etc/shadow"
attaque 2 : ./afficher "x; /bin/sh"        → un shell root
attaque 3 : PATH=/tmp ./afficher f         → /tmp/cat est exécuté

/* corrigé */
int main(int argc, char **argv) {
  int fd = open(argv[1], O_RDONLY);      /* pas de shell du tout */
  setuid(getuid());                       /* privilèges ABANDONNÉS */
  if (fd < 0) { perror("open"); return 1; }
  …copie octet par octet…
}

Trois principes, dans l'ordre d'efficacité. Ne jamais construire une commande shell par concaténation — utiliser l'appel système directement. Abandonner le privilège dès qu'il n'est plus nécessaire, et le plus tôt possible. Ne jamais faire confiance à l'environnement hérité, PATH comprise. Le moindre privilège n'est pas un principe abstrait : c'est la différence entre lire un fichier de trop et donner un interpréteur de commandes administrateur.

Ce que le cours laisse

Huit chapitres plus tôt, la question était : que fait le système que les programmes ne peuvent pas faire eux-mêmes ? La réponse tient en quatre points, et chacun a occupé un bloc.

Il fait exister plusieurs exécutions là où il n'y a qu'un processeur, et il choisit laquelle avance. Il les empêche de se piétiner, sur la mémoire comme sur les données partagées. Il leur donne plus de mémoire qu'il n'y en a, en pariant sur la localité. Et il conserve ce qui doit survivre à l'extinction, dans une structure qu'aucun programme ne pourrait maintenir seul.

Ces quatre services ont un point commun, et c'est peut-être ce qu'il faut retenir de l'ensemble : aucun n'a de solution optimale. L'ordonnanceur arbitre entre attente et équité ; le remplacement de pages entre coût et clairvoyance ; le noyau entre performance et robustesse ; l'isolation entre sûreté et prix. Un système d'exploitation n'est pas un ensemble d'algorithmes justes, c'est un ensemble de compromis explicites — et savoir lequel a été fait, et pourquoi, est exactement ce qui distingue quelqu'un qui utilise un système de quelqu'un qui le comprend.

À retenir

Flashcards · 6 cartes

À quoi servent les tampons entre un programme et un périphérique ?
À trois choses distinctes. Absorber la différence de VITESSE entre producteur et consommateur (le tube du chapitre 2). Absorber la différence de TAILLE : un programme qui écrit octet par octet ne peut pas provoquer une écriture disque par octet, l'unité de transfert étant le bloc. Et permettre la sémantique de COPIE : write rend la main dès que les données sont dans le noyau, sans attendre le matériel.
Pourquoi un write réussi ne garantit-il pas que les données soient sur le disque ?
Parce que le système maintient un cache disque en mémoire vive et diffère les écritures. Une coupure de courant perd ce qui n'a pas encore été écrit. Les applications qui ne peuvent pas se le permettre — bases de données, systèmes de fichiers journalisés — appellent fsync, qui bloque jusqu'à l'écriture réelle, à un coût considérable. D'où le fait qu'on ne le fasse pas partout.
Comparez FCFS, SSTF, SCAN et C-SCAN pour l'ordonnancement disque.
FCFS sert dans l'ordre d'arrivée : équitable, mais la tête traverse le disque sans raison. SSTF sert le plus proche : bien plus court, mais c'est le SJF du chapitre 4 et il provoque la FAMINE des cylindres éloignés. SCAN (l'ascenseur) balaie dans un sens puis dans l'autre : aussi efficace, et l'attente est bornée. C-SCAN ne sert que dans un sens et revient à vide, ce qui rend l'attente plus uniforme entre le milieu et les extrémités.
Pourquoi ces algorithmes disparaissent-ils sur un SSD ?
Ils optimisent tous une seule grandeur, la distance parcourue par une tête de lecture — qui n'existe pas sur un SSD, dont le temps d'accès ne dépend pas de l'adresse. Réordonner ne gagne rien et le tri lui-même ajoute de la latence. Les priorités deviennent le regroupement des écritures (usure des cellules) et le maintien de plusieurs requêtes en vol pour exploiter le parallélisme interne. Linux propose d'ailleurs un ordonnanceur « none ».
Qu'est-ce que le bit setuid, et pourquoi est-il dangereux ?
Un exécutable qui le porte s'exécute avec les droits de son PROPRIÉTAIRE, pas de celui qui le lance — c'est ainsi qu'un utilisateur ordinaire peut changer son mot de passe dans un fichier système. Le programme devient une frontière de privilège : arguments, variables d'environnement, descripteurs ouverts, répertoire courant viennent tous d'un appelant non fiable et doivent être validés. Un seul canal oublié donne les droits de root. On lui préfère les capacités, qui n'accordent qu'un pouvoir précis.
En quoi un conteneur diffère-t-il d'une machine virtuelle ?
Un conteneur repose sur des mécanismes du noyau — espaces de noms pour la visibilité, cgroups pour les quotas — et le NOYAU RESTE PARTAGÉ avec l'hôte : une faille du noyau traverse l'isolation. Une machine virtuelle isole tout, y compris le noyau, au prix d'un noyau complet par instance. Le conteneur est un bon compromis pour séparer des services qu'on maîtrise, insuffisant pour exécuter du code hostile.