cursus.

Cours 1 · GénéralitésLeçon 1 sur 2

Rôle et structure d'un système

4 h de lecture9 sections Version PDF

À la fin de cette leçon, vous saurez

De la machine nue au besoin d'abstraction ; gestion des ressources, interface et protection ; noyau monolithique et micro-noyau ; mode utilisateur, mode noyau et appels système.

Écrivons un programme qui affiche une ligne de texte, sur une machine sans système d'exploitation. Il faudrait connaître le modèle exact du contrôleur d'écran, l'adresse de ses registres, la séquence de commandes qu'il attend. Changer d'écran, et le programme est à réécrire. Faire tourner deux programmes à la fois, et il faudrait qu'ils se soient mis d'accord à l'avance sur le partage de la mémoire.

Ce que le chapitre 5 d'architecture appelait la machine de von Neumann est en réalité inutilisable telle quelle. Entre elle et les programmes, il manque une couche : le système d'exploitation. Ce chapitre dit ce qu'elle fait, comment elle est bâtie, et par quelle frontière on y accède — une frontière matérielle, héritée directement des interruptions du chapitre 8 d'architecture.

Trois fonctions, pas une

On résume souvent le système à « il gère le matériel ». C'est trop vague pour être utile. Il remplit trois fonctions distinctes, et les confondre empêche de comprendre le reste du cours.

Abstraire. Le système transforme des dispositifs hétérogènes en objets uniformes. Un fichier est la même chose sur un disque magnétique, un SSD ou une clé USB. Un processus est la même chose quel que soit le processeur. Cette uniformité est ce qui rend un programme portable, et c'est le service que le programmeur perçoit en premier.

Gérer les ressources. Processeur, mémoire, disque et réseau sont en quantité finie, et plusieurs programmes les veulent en même temps. Le système arbitre : à qui donner le processeur, dans quel ordre (chapitre 4), quelle page garder en mémoire (chapitre 6), quel accès disque servir d'abord (chapitre 8). Ces arbitrages sont des décisions d'allocation, et ils forment l'essentiel du cours.

Protéger. Un programme fautif ou hostile ne doit pas pouvoir lire la mémoire d'un autre, ni écrire directement sur le disque, ni monopoliser le processeur. Cette fonction n'est pas purement logicielle : elle exige un appui matériel, et c'est tout l'objet de la dernière section.

Quelques familles de systèmes

Ces trois fonctions se pondèrent différemment selon l'usage, et cela explique des choix de conception qui paraîtraient sinon incohérents.

Le traitement par lots, historiquement premier, enchaîne des travaux sans interaction : il optimise le débit, c'est-à-dire le nombre de travaux par heure. Le temps partagé donne le processeur à tour de rôle à des utilisateurs interactifs : il optimise le temps de réponse, quitte à perdre du débit — le va-et-vient entre programmes coûte. C'est le modèle de tous les systèmes de bureau.

Le temps réel ajoute une contrainte de nature différente : une échéance à respecter. Un système de freinage doit réagir en moins de dix millisecondes, et un système temps réel dur qui rate cette échéance a échoué, même si sa réponse est correcte. Ce n'est plus une question de vitesse moyenne mais de garantie, ce qui interdit la plupart des optimisations statistiques du cours — à commencer par le cache et la mémoire virtuelle, dont les temps sont imprévisibles.

L'embarqué vise l'empreinte mémoire et la consommation ; le distribué, la coordination de plusieurs machines.

Monolithique ou micro-noyau

Le noyau est la partie du système qui s'exécute avec tous les privilèges. Sa frontière — ce qu'on met dedans, ce qu'on laisse dehors — est le grand choix d'architecture, et il oppose deux écoles.

Le noyau monolithique met tout dans le noyau : ordonnancement, mémoire, systèmes de fichiers, pilotes, pile réseau. Tout ce petit monde partage le même espace d'adressage, donc communique par simple appel de fonction, ce qui est rapide. Le revers est que le moindre défaut dans un pilote peut corrompre le noyau entier : un bogue de pilote graphique fait tomber la machine.

Le micro-noyau ne garde dans le noyau que le strict minimum — communication entre processus, ordonnancement, gestion élémentaire de la mémoire — et sort tout le reste en services ordinaires, en mode utilisateur. Un pilote qui plante ne fait alors tomber que lui-même, et peut être relancé. Le prix est le coût des échanges : ce qui était un appel de fonction devient un envoi de message entre deux processus, avec deux changements de contexte.

Aucune des deux écoles n'a gagné. Linux est monolithique, mais modulaire — les pilotes se chargent et se déchargent à chaud, ce qui récupère une partie de la souplesse. Windows et macOS sont hybrides. Les systèmes où la fiabilité prime sur le débit, comme QNX en automobile, restent à micro-noyau. C'est le premier compromis performance/robustesse du cours, et il n'est pas le dernier.

Quiz · vérifiez votre compréhension Sans réponse

Dans un système à micro-noyau, un pilote de périphérique défectueux provoque une erreur fatale. Quelle est la conséquence, et à quel prix cette propriété est-elle obtenue ?

La frontière : deux modes d'exécution

Voici le point central du chapitre, et celui sur lequel tout le reste du cours s'appuie.

La protection ne peut pas être assurée par le logiciel seul. Si un programme pouvait, par ses propres moyens, décider de ne pas être protégé, la protection n'existerait pas. Il faut donc un appui matériel, et il est d'une grande simplicité : le processeur possède un bit de mode.

En mode noyau (dit superviseur), tout est permis : toutes les instructions, toute la mémoire, tous les registres de périphériques. En mode utilisateur, certaines instructions sont privilégiées et refusées — modifier la table des pages, masquer les interruptions, accéder directement à un contrôleur, changer soi-même le bit de mode. Tenter l'une d'elles ne provoque pas une exécution partielle : le processeur déclenche une exception, et la main revient au noyau.

Une application s'exécute donc en mode utilisateur, dans une prison. Mais elle a besoin de services que seul le noyau peut rendre : lire un fichier, créer un processus, envoyer sur le réseau. Il faut donc une porte, et une seule.

L'appel système

Cette porte est l'appel système. Son mécanisme mérite d'être connu en détail, parce qu'il explique à la fois la sécurité du modèle et son coût.

1. L'application place un NUMÉRO d'appel dans un registre convenu,   et ses arguments dans d'autres registres.2. Elle exécute une instruction spéciale — `syscall`, `int 0x80`, `svc` —   qui déclenche une interruption LOGICIELLE.3. Le matériel bascule le bit de mode en mode noyau et saute à une adresse   FIXE, définie par le noyau au démarrage. L'application ne choisit pas   où elle atterrit : c'est là toute la sécurité du dispositif.4. Le noyau lit le numéro, valide les arguments, exécute le service.5. Il place le résultat, restaure le mode utilisateur et rend la main   à l'instruction qui suivait l'appel.

Trois remarques sur ce mécanisme.

Ce n'est pas un appel de fonction. Un appel de fonction saute où on lui dit ; un appel système saute où le noyau a décidé, et change de mode au passage. Le programme ne peut donc pas « appeler le milieu » d'un service pour en sauter les contrôles.

Il coûte cher. Changement de mode, sauvegarde d'état, validation des arguments, et souvent perte du contenu du cache : de l'ordre de quelques centaines à quelques milliers de cycles, contre une poignée pour un appel de fonction. D'où l'usage systématique de tampons — la bibliothèque C accumule les écritures et n'appelle write qu'une fois le tampon plein, ce qui explique qu'un affichage puisse apparaître en retard, ou disparaître si le programme plante avant la purge.

On ne l'écrit presque jamais à la main. La bibliothèque standard offre des fonctions — printf, fopen, malloc — qui enveloppent les appels système, en font moins que ce qu'on croit, et parfois pas du tout : malloc ne demande de la mémoire au noyau que lorsque sa réserve est épuisée.

Le lien avec l'architecture est direct. L'interruption matérielle du chapitre 8 vient d'un périphérique et arrive n'importe quand ; l'interruption logicielle de l'appel système vient du programme lui-même et arrive au moment qu'il choisit. Le traitement est le même : sauvegarde d'état, bascule en mode noyau, exécution d'une routine, restauration. Un seul mécanisme matériel, deux usages.

Quiz · vérifiez votre compréhension Sans réponse

Pourquoi un appel système ne peut-il pas être remplacé par un simple appel de fonction vers le code du noyau, ce qui serait bien plus rapide ?

À vous

L'exercice construit la frontière en miniature : un noyau qui expose une table d'appels système, une application qui s'exécute en mode utilisateur, et un répartiteur qui bascule le mode, valide, puis rend la main.

Trois choses à faire tomber, et ce sont les trois propriétés du modèle : une instruction privilégiée tentée en mode utilisateur doit lever une exception, un numéro d'appel invalide doit être rejeté sans planter le noyau, et un argument non validé — un descripteur de fichier qui n'appartient pas à l'appelant — doit être refusé.

Exercice · JavaScript · à vous de jouer

Faites tenir la frontière utilisateur/noyau : instruction privilégiée, numéro invalide, argument non validé.

En attente
// ── Le matériel ───────────────────────────────────────────────────────────
const machine = {
  mode: "utilisateur",
  memoire: { 0: "code noyau", 100: "données du processus 1" },
};

// Certaines instructions n'ont le droit de s'exécuter qu'en mode noyau.
const PRIVILEGIEES = ["ecrireTablePages", "masquerInterruptions", "accesControleur"];

function instruction(nom) {
  if (PRIVILEGIEES.includes(nom) && machine.mode !== "noyau") {
    // ← à écrire : lever une exception plutôt que d'exécuter
  }
  return nom + " exécutée en mode " + machine.mode;
}

// ── Le noyau ──────────────────────────────────────────────────────────────
// Chaque processus n'a le droit qu'à ses propres descripteurs de fichier.
const PROCESSUS = { pid: 1, descripteurs: [0, 1, 2, 7] };

const TABLE_APPELS = {
  1: { nom: "ecrire", service: (fd, texte) => {
        if (!PROCESSUS.descripteurs.includes(fd)) return "ERREUR: descripteur " + fd + " interdit";
        return "écrit sur " + fd + " : " + texte;
      } },
  2: { nom: "obtenirPid", service: () => "pid = " + PROCESSUS.pid },
  3: { nom: "terminer", service: (code) => "processus terminé, code " + code },
};

// Le répartiteur : le SEUL point d'entrée du noyau.
function appelSysteme(numero, ...args) {
  machine.mode = "noyau";          // le matériel bascule le bit de mode
  let resultat;
  const entree = TABLE_APPELS[numero];
  resultat = entree.service(...args);   // ← et si le numéro n'existe pas ?
  machine.mode = "utilisateur";    // on rend la main
  return resultat;
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. instruction() doit lever une exception sur une instruction privilégiée
//    tentée en mode utilisateur.
// 2. appelSysteme() ne doit pas planter sur un numéro inconnu, et doit
//    restaurer le mode utilisateur MÊME si le service échoue.

console.log(appelSysteme(2));
console.log(appelSysteme(1, 1, "bonjour"));
console.log(appelSysteme(1, 42, "sur un descripteur qui n'est pas à moi"));
console.log(appelSysteme(99, "numéro inconnu"));
try { console.log(instruction("masquerInterruptions")); }
catch (e) { console.log("exception :", e.message); }
console.log("mode final :", machine.mode, "(doit être utilisateur)");

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 1 · sur machine

Voir la frontière

Rendre visible la ligne entre le mode utilisateur et le mode noyau, en observant les appels système d'un programme qu'on croit connaître.

2 h
Avant de commencer
  • Une machine Linux, réelle ou virtuelle
  • strace et gcc installés
  1. 1. Espionner une commande

    Lancez strace sur la commande ls et comptez les appels système. Repérez lesquels ouvrent des fichiers, lesquels écrivent, lesquels ne servent qu'à préparer le terrain.

  2. 2. Le programme le plus simple possible

    Écrivez un programme C qui ne fait rien du tout, compilez-le, tracez-le. Combien d'appels système avant votre première ligne de code ?

  3. 3. Un affichage

    Ajoutez un printf. Comparez la trace, et identifiez l'appel système qui affiche réellement. Combien de printf faut-il pour deux appels write ?

  4. 4. Sans bibliothèque

    Écrivez le même affichage en appelant directement write, puis en déclenchant l'appel système à la main. Comparez les trois tailles d'exécutable.

  5. 5. Franchir la frontière de force

    Écrivez un programme qui tente de lire l'adresse mémoire 0, puis un autre qui tente d'écrire dans le fichier /etc/passwd. Notez les deux messages d'erreur et dites qui les produit.

  6. 6. Mesurer la frontière

    Chronométrez un million d'appels à getpid, puis un million d'appels à une fonction vide que vous écrivez. Le rapport est le prix du passage en mode noyau.

C'est réussi quand
  • Vous savez nommer trois appels système exécutés avant votre main
  • Vous expliquez pourquoi dix printf ne font pas dix write
  • Vous mesurez le coût d'un appel système et vous le comparez à celui du TP 8 d'Architecture

Ce que la suite en fait

Le chapitre 2 prend l'autre porte d'entrée : le shell, qui n'est lui-même qu'une application ordinaire enchaînant des appels système. Le voir sous cet angle évite de le prendre pour une partie du système.

Puis le bloc II ouvre le vrai sujet. Le processus du chapitre 3 est l'objet que le mode utilisateur protège ; fork et exec y sont les appels système les plus caractéristiques du modèle Unix ; et l'ordonnancement du chapitre 4 repose entièrement sur l'interruption d'horloge qui, seule, permet au noyau de reprendre la main sans la demander.

À retenir

Flashcards · 1 / 4Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 9 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.