Aller au contenu principal

Architecture & systèmes · L1 · Section 3/6

Systèmes d’exploitation

Progression

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

#Systèmes d'exploitation

#Prérequis et objectifs

Prérequis: manipuler un terminal (lister, exécuter un programme), comprendre ce qu'est un fichier, et avoir lu la page « Architecture de Von Neumann » pour le lien mémoire/CPU.

Objectifs d'apprentissage:

  • énumérer les services d'un noyau et les relier aux abstractions visibles depuis un programme;
  • dérouler le trajet d'un appel système de la libc jusqu'au pilote, et le constater avec strace;
  • expliquer la mémoire virtuelle (pages, TLB, fautes de page) et pourquoi deux processus ne se marchent pas dessus;
  • reconnaître les politiques d'ordonnancement (Round-Robin, priorités, MLFQ) et leurs compromis latence/débit.

#Services offerts

Le noyau maintient une couche de fichiers virtuelle (VFS) qui uniformise les systèmes de fichiers et expose des opérations simples (open, read, write). Il fournit des API réseau via les sockets, crée et planifie les processus ou threads, et expose des horloges/timers pour cadencer les tâches. La mémoire virtuelle repose sur la MMU: pages, tables multi-niveaux et mmap pour partager des segments ou mapper un fichier. Côté sécurité, le noyau gère permissions, utilisateurs/groupes, mécanismes d'isolation (cgroups, namespaces) et, selon les distributions, des politiques renforcées (SELinux, AppArmor).

Pour relier cette liste à la pratique: chaque service correspond à des appels système que vous pouvez observer. Le tableau mental à garder est « abstraction visible = syscall derrière »: un descripteur de fichier cache un open, une connexion réseau cache un socket + connect.

#Boot & userland

Au démarrage, le bootloader charge le noyau en mémoire puis lui passe la main. Le noyau initialise les pilotes essentiels, monte le système de fichiers racine et lance init (ou systemd). Ce dernier démarre les services, monte les volumes restants et, en bout de chaîne, offre un shell ou un environnement graphique à l'utilisateur.

#Appels système (exemples)

Le noyau expose un catalogue d'appels système: open/read/write/close pour manipuler des fichiers, fork/exec/wait pour créer des processus, socket/connect/accept pour établir des connexions réseau, ou encore clone, sendfile, mmap, ioctl pour des scénarios spécifiques.

Exemple minimaliste (C) lecture d'un fichier:

cc

1#include <fcntl.h>2#include <unistd.h>3#include <stdio.h>4 5int main() {6  int fd = open("/etc/hostname", O_RDONLY); // syscall via libc7  if (fd < 0) return 1;8  char buf[128];9  ssize_t n = read(fd, buf, sizeof(buf)-1);10  if (n > 0) { buf[n] = '\0'; printf("%s\n", buf); }11  close(fd);12}

Pour observer ce que ce programme fait réellement, lancez:

bashbash

1strace -e openat,read,close ./lecteur2# openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 33# read(3, "myhost\n", 127)                = 74# close(3)                                = 0

Observez le numéro de descripteur: le noyau renvoie 3 car 0, 1 et 2 sont déjà pris par stdin/stdout/stderr. C'est une observation directe du lien entre abstraction et noyau.

#Parcours interactif d'un syscall

Parcours d’un appel noyau

Choisissez un scénario pour voir comment la requête traverse l’espace utilisateur, le noyau et le matériel, avec la part de latence associée à chaque phase.

Lecture auto

Un appel read() classique depuis un programme utilisateur. On suit la traversée user → noyau → contrôleur NVMe, puis la remontée des données.

Progression cumulative : 30 µs / 5.43 ms
Étape actuelle
Appel read(fd, buf, size)

Libc vérifie les paramètres et prépare le tampon utilisateur.

Espace utilisateur

Répartition des durées
Espace utilisateur1.7 % (90 µs)
Noyau2.6 % (140 µs)
Matériel95.8 % (5.20 ms)
À retenir
  • Les accès NVMe dominent la latence totale : la phase matérielle représente > 90 % du temps.
  • La copie mémoire peut être remplacée par mmap pour éviter un aller-retour supplémentaire.

#Mémoire mappée et périphériques

Sous Unix, on parle à un périphérique à travers un descripteur de fichier ; sur un microcontrôleur, on lui parle à travers une adresse. C'est la même idée : une abstraction qui cache du matériel derrière un nom. Le noyau Linux pousse la logique jusqu'au bout avec /dev et sysfs, où l'état matériel apparaît comme un fichier texte.

Le plan mémoire de l'ATmega2560 montre la version matérielle, sans noyau : les registres de configuration des périphériques sont rangés dans les plages d'entrée/sortie de l'espace de données, et les instructions qui les manipulent sont les mêmes que celles qui accèdent à la mémoire. Un port parallèle y occupe trois adresses pour une seule broche :

  • DDRB — le registre de direction (1 = sortie, 0 = entrée) ;
  • PORTB — le registre de sortie, qui fixe la tension de la broche, et active la résistance de tirage quand la broche est en entrée ;
  • PINB — l'état réellement lu sur la broche.

Écrire dans PORTB change une tension physique ; lire PINB lit ce qu'un composant extérieur impose. Un seul périphérique, plusieurs registres, chacun avec un rôle distinct : la même structure se retrouve dans toutes les cartes, du microcontrôleur à la carte réseau.

Les périphériques plus complexes ajoutent un registre d'état et un drapeau d'interruption. Le convertisseur analogique-numérique en est l'illustration : on écrit dans ADCSRA pour démarrer une conversion, et le bit ADIF passe à 1 quand le résultat est disponible dans les registres de données. On a alors le choix, exactement comme côté noyau :

  • attente active : boucler en lisant ADIF — c'est le busy-waiting, équivalent d'un read bloquant ;
  • interruption : autoriser ADIE et laisser le matériel prévenir le processeur — équivalent d'une E/S asynchrone.

C'est le même arbitrage latence/occupation du CPU que celui qui sépare les E/S bloquantes des E/S non bloquantes côté système d'exploitation, à une échelle mille fois plus petite.

#Planification (ordonnancement)

Les politiques courantes incluent Round-Robin, les priorités statiques ou dynamiques, et les files multi-niveaux à rétroaction (MLFQ). Un ordonnanceur préemptif interrompt un processus à l'expiration de son quantum pour donner la main à un autre, ce qui réduit la latence mais nécessite de surveiller la famine (starvation) via des mécanismes de vieillissement. Les métriques suivies sont la latence de réponse, le débit global, l'équité et, pour certaines charges, les garanties temps réel (classes SCHED_FIFO, SCHED_RR).

Repère de lecture: Round-Robin maximise l'équité au prix de la latence moyenne, les priorités favorisent les tâches critiques au risque d'affamer les autres, et le MLFQ tente de concilier les deux en démarrant tous les processus en priorité haute puis en rétrogradant les gourmands.

Exercice observable sous Linux: lancez stress-ng --cpu 4 --timeout 30 dans un terminal et htop dans un autre; repérez les colonnes NI (priorité nice) et S (état: R en cours, S endormi, D en attente d'E/S). Puis relancez avec nice -n 19 stress-ng --cpu 4 et observez la baisse de part CPU. Vous venez de voir l'ordonnanceur arbitrer.

#Mémoire virtuelle

Les espaces d'adressage sont découpés en pages (souvent 4 KiB) référencées par des tables multi-niveaux. La TLB accélère les traductions, et une faute de page déclenche le chargement ou l'allocation du bloc manquant. Les processus peuvent obtenir de la mémoire via brk/sbrk ou mmap; le noyau sur-alloue et ne réserve réellement qu'au premier accès (copy-on-write après fork). La protection repose sur des bits d'exécution (NX/DEP), l'aléa ASLR et la distinction entre segments privés ou partagés, anonymes ou mappés sur fichier.

Vérification en une commande: cat /proc/self/maps liste les segments du processus qui l'affiche, avec leurs droits r, w, x. Cherchez une plage en rw-p (données, non exécutable) et une plage en r-xp (code): la séparation code/données y est visible, page par page.

#Systèmes de fichiers

Les systèmes de fichiers stockent des inodes (métadonnées) et des répertoires, gèrent liens durs et symboliques et choisissent un mode de protection: journalisation (ext4, XFS) ou copy-on-write (btrfs, ZFS). Le cache page/buffer évite de toucher le disque pour chaque écriture; fsync ou les barrières de mémoire forcent la persistance. Les permissions POSIX et les ACLs raffinent le contrôle d'accès, tandis que les namespaces permettent de monter des environnements isolés (mount, pid, net).

#Exercices observables

  1. Comptez les appels système d'une lecture lente. Comparez strace -c cat gros_fichier.bin > /dev/null (lecture en blocs via le cache page) et strace -c dd if=gros_fichier.bin of=/dev/null bs=1 (un aller-retour par octet). Notez le nombre d'appels read dans chaque cas et le temps total. L'écart d'ordre de grandeur est votre preuve du coût fixe par appel système.
  2. Identifiez un processus et son parent. Lancez sleep 100 & dans un shell, puis ps -o pid,ppid,stat,cmd --pid $! pour voir votre enfant dans l'état S (endormi). Pour croiser un zombie, exécutez bash -c 'sleep 1 & exec sleep 100' et repérez la ligne <defunct> ou l'état Z avec ps aux.

Correction attendue: l'exercice 1 montre typiquement quelques dizaines de read pour cat contre des centaines de milliers pour dd bs=1 sur un même fichier. L'exercice 2 vous fait lire les colonnes PID, PPID et STAT; un état Z (zombie) signale un enfant terminé que le parent n'a pas encore récolté avec wait.

#Quiz

Quelle séquence crée un nouveau processus et remplace son image par un programme ?
Quelle séquence crée un nouveau processus et remplace son image par un programme ?
Pourquoi lire un fichier octet par octet est-il beaucoup plus lent que par blocs de 4 KiB ?
Pourquoi lire un fichier octet par octet est-il beaucoup plus lent que par blocs de 4 KiB ?