Vous double-cliquez sur une icône, et une fraction de seconde plus tard une fenêtre apparaît. Cela ressemble à une seule action. En dessous, presque tous les niveaux de ce site entrent en jeu : un périphérique d’entrée déclenche une interruption, un programme décide quoi lancer, le noyau construit un processus, un chargeur assemble des dizaines de fichiers, le système mémoire fait venir le code depuis le stockage page par page, et enfin le CPU exécute la première instruction du programme.
Ce chapitre suit ce chemin de haut en bas. Chaque étape ouvre sur un chapitre plus profond ; ici, on ne fait que passer devant, dans l’ordre, pour voir comment elles s’emboîtent.
Un clic, tous les niveaux
| Niveau | Ce qu’il fait quand on ouvre une application |
|---|---|
| Applications | le Finder (ou l’Explorateur, ou votre bureau Linux) voit un double-clic sur une icône |
| Système d’exploitation | un nouveau processus est créé, et le fichier du programme est lu |
| Assembleur et ISA | le chargeur prépare les registres et la pile, puis saute au point d’entrée |
| Microarchitecture | les premières instructions ratent tous les caches et attendent la mémoire |
| Logique numérique et composants | le contrôleur de stockage et les puces mémoire livrent les octets |
On peut décrire un ordinateur comme un empilement de niveaux (le chapitre sur les niveaux d’abstraction raconte cette histoire), chacun étant une machine dont le langage est mis en œuvre par le niveau inférieur, soit par traduction (un compilateur convertit d’abord tout le programme dans le langage inférieur), soit par interprétation (un programme du niveau inférieur l’exécute pas à pas). Les six niveaux classiques s’arrêtent aux langages orientés problème dans lesquels écrivent les programmeurs. Ce site en ajoute deux au-dessus : les algorithmes que ces programmes mettent en œuvre, et les applications que les gens utilisent réellement. Ouvrir une application est le moment où tous interviennent à la fois.
Avant de regarder chaque étape, suivez tout le trajet une fois. Appuyez sur Lecture : l’échelle à gauche montre quel niveau est à l’œuvre, et la pastille sous le texte ce qui est transmis à ce moment-là.
À essayer : Appuyez sur Lecture pour suivre l’action à travers les niveaux, ou avancez à votre rythme avec les flèches et les points.
- 0Apps
- 1Algorithmes
- 2C
- 3Assembleur
- 4OS
- 5Code machine
- 6µops & bus
- 7Portes logiques
- 8Transistors
- 9Physique
Le bouton de la souris s’enfonce
La souris détecte l’appui et envoie un petit rapport à l’ordinateur, par USB ou Bluetooth.
1. Le clic devient un événement
La souris signale qu’un bouton s’est enfoncé. Ce message voyage par USB ou Bluetooth, une interruption prévient le noyau que quelque chose est arrivé, un pilote le décode, et le serveur de fenêtres (le processus qui possède l’écran, WindowServer sous macOS) détermine quelle fenêtre se trouvait sous le pointeur et envoie un événement à l’application concernée. Deux clics assez proches dans le temps et dans l’espace deviennent un double-clic. Le chapitre sur la frappe au clavier suit ce chemin en détail pour une touche ; un bouton de souris emprunte la même route.
Ici, la fenêtre sous le pointeur appartient au Finder, qui n’est lui-même qu’une application. Sa boucle d’événements reçoit le double-clic, constate que l’élément est une application, et demande qu’on la lance.
2. Quelqu’un décide de lancer un programme
Lancer un programme, c’est toujours la demande d’un processus au noyau. Qui la formule dépend du système :
- Sous Linux, un bureau graphique ou un shell utilise le couple UNIX classique :
forkcopie le processus courant, et l’enfant appelleexecpour se remplacer par le nouveau programme. Le chapitre sur les processus détaille les deux. - Sous Windows, l’Explorateur appelle
CreateProcess, qui crée le processus et charge le programme en un seul appel. - Sous macOS, le Finder s’adresse au framework LaunchServices, qui demande à
launchd(le processus 1, l’ancêtre de tous les autres) de lancer l’application. C’est pourquoi le parent d’une application Mac n’est pas le Finder. Sur ce Mac, chaque processus principal de Chrome en cours d’exécution avait pour parent le processus 1.
POSIX propose aussi posix_spawn, qui fait fork puis exec en un seul appel, et que macOS préfère en interne. Quel que soit le chemin, le noyau se retrouve avec la même tâche : créer un nouveau processus et y placer un programme.
3. Le noyau crée un processus
Le noyau alloue un bloc de contrôle de processus (un identifiant, un propriétaire, une table de fichiers ouverts, un emplacement pour sauvegarder les registres) et un espace d’adressage vide. Puis il ouvre le fichier du programme et lit son en-tête. Sur un Mac, c’est un fichier Mach-O ; sous Linux un fichier ELF ; sous Windows un fichier PE. Le chapitre sur les fichiers exécutables ouvre les trois octet par octet. L’en-tête indique pour quel CPU est le code, quelles parties du fichier vont où en mémoire, avec quels droits, et quel éditeur de liens dynamique exécuter en premier.
La Calculette d’Apple montre la première vérification. Son exécutable est un binaire universel qui contient deux programmes complets, l’un pour les Mac Intel, l’autre pour les puces Apple :
$ lipo -archs /System/Applications/Calculator.app/Contents/MacOS/Calculator
x86_64 arm64e
Le noyau choisit la tranche arm64e sur cette machine. Sur puce Apple, il refuse aussi d’exécuter du code qui ne porte pas de signature de code valide, et la première fois que vous ouvrez une application téléchargée sur Internet, macOS vérifie la signature du développeur et la notarisation d’Apple avant que quoi que ce soit ne s’exécute.
4. Le chargeur projette le programme et ses bibliothèques
Très peu du code d’une application se trouve dans son propre fichier. L’exécutable de la Calculette fait 3,4 Mo, et il nomme 44 bibliothèques et frameworks dont il a besoin : AppKit, SwiftUI, Foundation, CoreGraphics et d’autres :
$ otool -L /System/Applications/Calculator.app/Contents/MacOS/Calculator
/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI
/usr/lib/libSystem.B.dylib
… (44 in all)
Chacune de ces bibliothèques en demande d’autres. L’éditeur de liens dynamique (dyld sous macOS, ld-linux sous Linux) les trouve toutes, projette chacune dans l’espace d’adressage, et remplit les tables par lesquelles le programme les appelle, comme le décrit le chapitre assembleur, éditeur de liens et chargeur. Même le plus petit programme C attire du monde. Celui-ci n’est lié qu’à une seule bibliothèque, libSystem, et demande combien d’images le chargeur a réellement amenées :
#include <stdio.h>
#include <mach-o/dyld.h>
int main(void) {
printf("%u images\n", _dyld_image_count());
}
45 images
Quarante-cinq : le programme, libSystem, et les 43 bibliothèques système dont libSystem est faite et dont elle dépend, parmi elles libsystem_c (la bibliothèque C proprement dite), libsystem_malloc (malloc et free) et libsystem_kernel (les petites fonctions enveloppes qui exécutent les vrais appels système). Sous Linux, le chapitre sur les appels système a tracé le même travail pour un programme « hello » : 32 appels système, dont 31 dépensés par le chargeur pour trouver, projeter et protéger la bibliothèque C.
Si le chargeur ouvrait et analysait des milliers de fichiers de bibliothèques à chaque lancement, démarrer un programme serait lent. macOS pré-lie donc toutes ses bibliothèques système dans un unique fichier géant, le cache partagé de dyld (dyld shared cache). Sur ce Mac, il contient 3 649 bibliothèques dans environ 5,8 Go de fichiers pour l’architecture arm64e. Il est projeté au même endroit dans chaque processus, si bien que ses pages sont partagées par tous, et c’est aussi pourquoi ls /usr/lib/libSystem.B.dylib ne trouve aucun fichier de ce nom : la bibliothèque n’existe qu’à l’intérieur du cache.
5. Rien n’est lu avant d’être nécessaire
On se représente souvent un programme chargé en mémoire puis exécuté. L’image plus juste est la pagination à la demande, et c’est ce que fait tout système moderne. Rien n’est lu à l’avance. Le chargeur projette des fichiers : il dit au noyau « ces adresses correspondent à telle partie de tel fichier », et le noyau note la promesse sans lire un seul octet. La première fois que le programme touche l’une de ces adresses, le matériel ne trouve aucune traduction valide et lève un défaut de page, une exception au sens du chapitre sur les déroutements. Le noyau trouve alors la page, la projette et relance l’instruction. Tout le mécanisme fait l’objet du chapitre sur la mémoire virtuelle.
Il y a deux sortes de défauts de page, et le /usr/bin/time -l de macOS compte les deux. Une récupération de page (page reclaim, un défaut mineur) trouve la page déjà en mémoire (dans le cache de fichiers, ou dans le cache partagé utilisé par tous les autres processus) et n’a qu’à la projeter. Un défaut de page proprement dit (un défaut majeur) doit la lire sur le stockage. Voici ce que coûte une exécution de quatre programmes, mesurée sur l’Apple M2 Ultra sur lequel ce chapitre a été écrit (deux exécutions chacun) :
| Commande | Instructions exécutées | Récupérations de page | Défauts de page | Mémoire utilisée |
|---|---|---|---|---|
/usr/bin/true | 9,5 millions | 243 | 0 | 1,4 Mo |
ls / | 11,1–11,4 millions | 255 | 1–2 | 1,5 Mo |
python3 -c pass | 192–194 millions | ~1 725 | 41–43 | 16 Mo |
node -e 0 | 537–540 millions | ~3 150 | 74–127 | 48 Mo |
true ne fait rien du tout (son seul travail est de sortir avec le statut 0), et pourtant l’exécuter coûte 9,5 millions d’instructions : le noyau qui crée le processus, dyld qui lie 45 images, l’initialisation de libSystem, puis la sortie. Presque toutes ses pages de 16 Kio étaient déjà en mémoire. Python et Node.js doivent démarrer un interpréteur, un gestionnaire de mémoire et une bibliothèque standard avant de pouvoir exécuter une seule ligne de votre code, et cela se voit.
6. main s’exécute, puis attend
Quand tout est projeté, le chargeur saute au point d’entrée du programme. Un petit morceau de code d’exécution prépare l’environnement du langage et appelle main. Pour un outil en ligne de commande comme ls, main fait son travail, appelle exit, et le noyau démonte le processus.
Une application avec une fenêtre fait autre chose : une fois prête, elle entre dans une boucle d’événements et attend l’événement suivant. Attendre ne coûte rien : le processus est bloqué dans un appel système, et l’ordonnanceur donne son cœur à quelqu’un d’autre. C’est pour cela qu’un ordinateur peut avoir tant de programmes ouverts. Pendant l’écriture de ce chapitre, top indiquait :
Processes: 1231 total, 12 running, 1219 sleeping, 10197 threads
1 231 processus sur 24 cœurs, et 99 % d’entre eux endormis, en attente d’un événement.
Ce que cela coûte
En chronométrant l’aller-retour complet (posix_spawn, exécution, sortie, waitpid) depuis un petit programme C, 20 à 50 fois chacun (le minimum correspond au cas non perturbé ; la moyenne inclut l’interférence de tout ce que faisait la machine par ailleurs) :
| Programme | Minimum | Moyenne |
|---|---|---|
/usr/bin/true | 1,4 ms | 2,8–3,3 ms |
ls / | 1,4–1,6 ms | 3,4–4,1 ms |
python3 -c pass | 24–25 ms | 31–37 ms |
node -e 0 | 56–66 ms | 70–75 ms |
Une milliseconde ou deux, c’est le plancher pour n’importe quel processus sur cette machine ; un environnement d’exécution de langage ajoute des dizaines de millisecondes. Une grosse application graphique, avec sa fenêtre, ses polices, son contexte GPU et son état à restaurer, prend plus longtemps encore, et le premier lancement après un redémarrage est le plus lent de tous, parce que ses pages ne sont pas encore en mémoire et que bien plus de ses défauts sont des défauts majeurs.
D’où les astuces qu’on voit partout : des applications qui restent actives en arrière-plan, des navigateurs qui gardent des processus prêts pour le prochain onglet, des téléphones qui figent les applications au lieu de les quitter. Lancer un programme coûte cher, et ne pas en lancer est l’optimisation la moins chère qui soit.
À retenir
- Ouvrir une application traverse tous les niveaux : un événement d’entrée, une demande au noyau, un nouveau processus, le chargeur, des défauts de page, et enfin le CPU qui exécute
main. - Ce sont d’autres programmes qui lancent les programmes :
fork+execsous UNIX,CreateProcesssous Windows,posix_spawnoulaunchdsous macOS, où le parent de chaque application est le processus 1. - L’exécutable est surtout une liste de bibliothèques : la Calculette en nomme 44, et même un programme C trivial se retrouve avec 45 images, pour la plupart tirées du cache partagé de dyld, pré-lié par macOS.
- Le chargement est paresseux : les fichiers sont projetés, et les défauts de page amènent les pages au premier accès : défauts mineurs si elles sont déjà en mémoire, majeurs s’il faut les lire sur le stockage.
- Ne rien faire coûte 9,5 millions d’instructions et 1,4 ms ; démarrer Python ou Node.js coûte des dizaines de millisecondes. Garder des programmes en marche coûte moins cher que de les lancer.