Entrées/sorties et sécuritéDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Systèmes d'exploitation · C5 Fichiers et périphériques · 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.