Cours 5 · Fichiers et périphériquesLeçon 2 sur 2
Entrées/sorties et sécurité
3 h de lecture7 sections Version PDF
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 msPuisque 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.
Un administrateur remplace des disques magnétiques par des SSD et conserve l'ordonnanceur « ascenseur ». Que faut-il en penser ?
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écanisme | Ce qui est isolé | Coût |
|---|---|---|
| Droits utilisateur | les fichiers, selon l'identité | nul |
chroot | la vue de l'arborescence | faible, contournable |
| Espaces de noms et cgroups | processus, réseau, montages, quotas | faible |
| Conteneur | l'ensemble des précédents, noyau partagé | modéré |
| Machine virtuelle | tout, 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.
Pourquoi un programme portant le bit setuid et appartenant à root demande-t-il une vigilance particulière ?
À 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.
Écrivez l'ascenseur, puis comparez distance totale et pire attente aux autres politiques.
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);
En travaux pratiques
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.
- Le mini-shell du TP 3
- Les TP 5 et 7
- 1. Rediriger la sortie
Ajoutez à votre shell la reconnaissance du chevron. Le fils doit ouvrir le fichier et remplacer sa sortie standard AVANT d'appeler exec.
- 2. Rediriger l'entrée
Mê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 tuyau
Implé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 à trouver
Si 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 trop
Listez 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éparer
Corrigez en abandonnant les privilèges dès que possible, et en validant le chemin. Vérifiez que l'attaque précédente échoue.
- 8. Restreindre
Exé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.
- 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
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
Vous avez parcouru les 7 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.