Note: ce texte est également disponible en version audio (version m4b, ou chapitres mp3), produite par ABS multimedia, lue par Claude Alain, sous licence Creative Commons Attribution-ShareAlike.

Il y a également une version anglaise maintenue sur github: What happens when you type google.com into your browser and press enter?, qui s'attache surtout aux questions réseau, mais ayant des défauts dans les parties systèmes :)

C'est l'histoire d'un 'a'...

Ou "comment ce que je tape apparaît à l'écran ?"

Lorsque l'on tape sur une touche de notre clavier, la lettre imprimée sur la touche se retrouve affichée à l'écran. Nos ancêtres auraient tout simplement utilisé une ficelle qui tire un masque pour obtenir cet effet. Plus récemment, dans les machines à écrire, c'était une tige de métal qui propulsait un caractère vers un ruban imbibé d'encre situé au-dessus d'une feuille. Il se trouve qu'entre-temps on a développé des trésors d'ingéniosité pour rendre cela infiniment plus complexe...

Ici est racontée l'histoire de cet héroïque 'a' tout au long de ses exploits, depuis la touche sur laquelle il est imprimé, jusqu'à l'écran qui l'affiche. Le cas pris en exemple ici n'est pas forcément exactement ce qui se passe dans votre machine, puisque cela dépend par exemple de la connectique de votre clavier (AT, PS/2, ou USB), il faut surtout y lire le principe, qui est assez générique.

À noter qu'il arrive que cette question soit posée telle quelle pendant un entretien pour un poste à haute qualification ;)

De bêtes machines en mode texte

Je prends volontairement d'abord l'exemple des bonnes vieilles machines en mode texte, plus faciles à comprendre dans un premier temps.

schéma clavier - unité centrale - écran

Un clavier, c'est (pour simplifier) juste un ensemble de boutons pressoirs au-dessus d'une série de fils horizontaux (les lignes de touches) et d'une série de fils verticaux (les colonnes de touches à peu près). Il y a une puce dans le clavier qui passe son temps à envoyer du courant sur les différentes lignes horizontales (par exemple), et regarde sur quelles lignes verticales elle récupère du courant, ce qui lui donne une(des) combinaison(s) ligne verticale / ligne horizontale qui permet(tent) de déterminer quelle(s) touche(s) est(sont) enfoncée(s). À l'aide d'une table interne, elle traduit ces combinaisons en nombres (scancodes): 1 pour echap, 2 pour la touche [1/&] (sur un clavier azerty), 3 pour la touche [2/é/~], etc. Ici, pour un 'a' de clavier azerty, c'est 16.

Ce scancode est envoyé à l'unité centrale via un câble série: les bits codant ce scancode sont envoyés les uns après les autres, par niveaux électriques: une tension positive pour 1, une tension négative pour 0, par exemple (la ligne étant à 0V au repos). Savoir si c'est le bit de poids le plus fort ou de poids le plus faible en premier est un éternel débat bien connu de Mr Swift.

Il y a une puce (i8042) dans l'unité centrale qui récupère ce scancode et le stocke dans un registre: une série de boîtes contenant 1 ou 0, autant qu'il en faut pour pouvoir coder tous les scancodes qui vont typiquement de 0 à 127, donc 7 suffisent. La puce envoie un signal (appelé interruption) au processeur. Celui-ci arrête de faire tourner le logiciel en cours et se met à exécuter le traitant d'interruption mis en place par le système d'exploitation de la machine. Ce traitant demande à la puce le scancode, via le bus principal de la machine, qui est par exemple une série de fils en parallèle: les bits du scancode sont envoyés en même temps en parallèle, un par fil (à l'époque il n'y avait que 8 fils, ça suffit). une vidéo de Ben Eater donne le détail de la transmission, en PS/2 et en USB.

Le traitant traduit le scancode en caractère: le scancode 16 est alors d'abord traduit en keycode, qui est un numéro de touche physique indépendant du type de matériel (clavier PC, mac, USB, ...), puis en la lettre 'a' (numéroté 97 par le standard ASCII) car j'ai configuré mon système pour un clavier français, qui place effectivement la lettre 'a' sur la touche de numéro physique 16. Le traitant décide de ce qu'il en fait: par exemple, il le fournit à l'application (là c'est du logiciel, plus grand chose à expliquer au niveau matériel, j'y reviens au prochain épisode), et celle-ci décide de l'afficher: elle envoie ce 97 au système, qui l'envoie à la carte vidéo, en repassant de la même façon par le même bus principal de la machine que tout à l'heure. la carte vidéo le stocke dans sa mémoire.

À chaque rafraichissement de l'écran (i.e. de l'ordre de 60 fois par seconde), la carte vidéo examine sa mémoire pour savoir quels caractères afficher. Elle a également une police de caractères qui lui donne le bitmap (dessin point par point) de chaque caractère. Elle peut donc, pour chaque ligne du balayage écran, envoyer une ligne de chaque bitmap de chaque caractère de la ligne de texte qu'elle est en train d'afficher, et ainsi donc pour toutes les lignes de l'écran, qui forment au final tout le texte.

Et une couche de TTY...

À l'épisode précédent, on a esquivé toute une tranche d'explications, qui est purement logicielle: comment l'application décide-t-elle qu'il faut afficher le a tapé au clavier ? Il y a bien sûr de nombreux cas selon les applications, on va prendre ici le cas d'une application textuelle interactive qui gère elle-même son affichage.

L'application tourne « sur » ce qu'on appelle un « tty » (TeleTYpe), c'est-à-dire l'association d'une notion de clavier et d'une notion d'écran (ça tombe bien, c'est ce qui nous occupe). On peut l'observer dans la sortie du programme ps, dans la colone TTY. On va prendre le cas relativement simple de la console texte Linux, qui comporte classiquement 6 TTYs virtuels, entre lesquels on peut basculer à l'aide de [alt]-[Fx]. Notre application tourne « sur » un seul d'entre eux, par exemple tty1, que l'on suppose être le tty actuellement actif. On suppose ici que l'application a fini ses traitements et attend simplement l'entrée de l'utilisateur, i.e. elle est bloquée dans un appel read() sur son entrée standard, qui se trouve être tty1, donc.

Reprenons où l'on en était de l'épisode précédent. Dans le traitant d'interruption, une fois que le noyau a traduit le scancode en keycode puis en code ASCII, il le met en attente dans le tampon d'entrée du tty1 (puisqu'étant le tty actif), et réveille l'application bloquée sur read(), puisque des données sont maintenant disponibles pour lecture. Le traitant a fini son travail, il termine, et le noyau redonne la main à l'application qui était en cours d'exécution (pas forcément celle qui nous intéresse, donc). Au bout d'un moment, le noyau décide de consacrer du temps processeur à notre application, puisqu'elle est désormais réveillée. L'appel read() retourne alors un charactère, le fameux 'a'. L'application teste ce caractère, se rend compte que c'est une simple lettre, et décide alors de l'afficher. Elle appelle donc write() sur sa sortie standard, qui se trouve être tty1 aussi. Le noyau écrit alors ce caractère dans le tampon de sortie de tty1, et puisque c'est le tty actif, répercute l'affichage en l'écrivant dans la mémoire de la carte vidéo, on reboucle ainsi sur l'épisode précédent.

Une variante de ce scénario est le mode canonique: si l'application a la flemme de gérer elle-même l'affichage des lettres tapées au clavier, elle peut utiliser tcsetattr pour activer le drapeau ICANON sur son tty. Lorsque ce mode est activé, dans le traitant d'interruption, plutôt que fournir la lettre à l'application, elle est directement copiée dans le tampon de sortie du tty (et donc répercutée à l'écran). Il n'y a donc pas d'aller-retour avec l'application. Ce n'est que lorsque la touche [entrée] est pressée que tout ce qui a été entré dans le tampon d'entrée du tty1 (depuis la dernière pression de [entrée]) est retourné d'un seul tenant à l'appel read(). Ce n'est pas le seul changement: il y a par exemple dans la configuration du tty le paramètre erase, qui contient en général le caractère correspondant à la touche backspace (effacement arrière). Lorsque cette touche est pressée au clavier, le noyau enlève alors du tampon d'entrée du tty le dernière charactère qui s'y trouvait. Ainsi, le mode canonique fait faire au noyau un certain traitement sur les lignes de textes tapées au clavier, avant même qu'elles soient retournées à l'application. On peut le remarquer en lançant strace cat . De même pour [Control]-U, [Control]-W, [Control]-V

Un dernier détail: le tty a également un drapeau ISIG, qui indique si les paramètres tels que intr sont actifs. intr est typiquement configuré à [Control]-C: lorsque ce caractère est tapé au clavier, plutôt que le mettre dans le tampon d'entrée du tty, le noyau envoie un signal SIGINT à l'application qui tourne sur le tty, qui bien souvent fait donc terminer l'application. C'est ainsi le comportement bien connu du raccourci [Control]-C. De même pour [Control]-\ (SIGQUIT), [Control]-Z (SIGTSTP).

Et une couche de graphisme...

De nos jours, il est devenu rare que l'on utilise un terminal virtuel en mode texte, on utilise plutôt des XTerm (ou rxvt, gnome-terminal, konsole, ...) en mode graphique, c'est-à-dire avec un serveur X. Cela rajoute un certain nombre de détours à notre cher 'a', car il y a désormais 4 acteurs: l'application, le XTerm qui fait tourner l'application, le serveur X qui fait tourner le XTerm, et le noyau qui fait la glue entre le tout: un pseudo-terminal (PTY) entre l'application et l'XTerm, et une socket Unix entre l'XTerm et le serveur X. Pseudo-terminal et socket Unix sont tous deux essentiellement de bêtes tuyaux bidirectionnels.

Tout d'abord, le serveur X pioche en général directement à la sortie du driver clavier. Il récupère ainsi directement un keycode, sans aucune autre traduction. Un tout petit driver XKB traduit cela en un évènement X, qui est envoyé à l'XTerm via la socket Unix. Cet évènement contient juste le keycode, l'heure de l'évènement, et l'état des modificateurs shift/control/alt/capslock/numlock/...

L'XTerm réception l'évènement, et le confie à un gros bout d'XKB, qui convertit ce keycode en keysym, qui représente ce qui est écrit sur la touche qui a été pressée, 'a' ici, donc (code 97 de nouveau). C'est un gros bout car c'est ici que sont gérées toute la traduction dans le cas de X11: mise en majuscule, combinaison avec accent circonflexe, etc.

XTerm interprète finalement ce keysym 'a' comme devant être traduit en la lettre 'a', et l'écrit sur son côté du PTY, le côté maître. Le noyau retransmet alors à l'application.

L'application décide alors d'écrire cette lettre 'a' sur sa sortie standard. Elle l'écrit donc sur son côté du PTY, le côté esclave. Le noyau retransmet alors au XTerm.

L'XTerm réceptionne le 'a', et décide donc de l'afficher. Il effectue lui-même le rendu graphique, en utilisant une police de caractères logicielle, et produit ainsi un pixmap contenant l'ensemble des pixels à afficher pour produire le 'a', et l'envoie sur la socket Unix

Le serveur X réceptionne le pixmap, et le confie à un driver vidéo, qui dialogue en général directement avec la carte vidéo, sans passer par le noyau.

Dans notre exemple, c'est l'XTerm qui effectue le rendu du texte. Auparavant, il laissait au serveur X le soin de le faire. C'était cependant relativement rigide et pénible, de nos jours on préfère faire le rendu dans l'XTerm pour pouvoir y implémenter facilement anti-crénelage, internationalisation, ...

Le pixmap n'est en général pas envoyé tel quel sur la socket Unix. Il y a plutôt une zone de mémoire partagée entre l'XTerm et le serveur X, dans lequel l'XTerm peut faire directement le rendu, et dans lequel le driver vidéo va piocher directement.

Le mode canonique garde tout son sens ici et rend le PTY un chouilla moins "bête" qu'un simple tuyau: lorsque le PTY est en mode canonique, dès que l'XTerm a écrit son 'a' sur le côté maître, le noyau fait l'echo en lui renvoyant ce même 'a' immédiatement, sans passer par l'application.

De même, le drapeau ISIG positionné par l'application garde tout son sens: si c'est [Control]-C qui a été pressé, c'est-à-dire que l'XTerm a reçu du serveur X un évènement contenant le Keysym C avec l'état "control appuyé", et a donc écrit sur le côté maître du PTY le caractère control-C (0x03 en ASCII), le noyau envoie alors le signal SIGINT à l'application qui est sur le côté esclave. De même pour [Control]-\ etc.

Et une couche de screen

screen est un outil très pratique qui permet de laisser tourner des applications dans un "coin". Le principe est de le lancer avant de lancer l'application que l'on veut faire tourner.

schéma avec screen

screen va alors créer un PTY, sur lequel l'application va tourner. Supposons que screen a été lancé sur un TTY quelconque. Lorsqu'il reçoit un "a" depuis celui-ci, il le transmet sur son côté maître du PTY, et le "a" se trouve donc transmis à l'application par le côté esclave. Lorsqu'application affiche le "a" sur son côté esclave du PTY, screen le récupère sur son côté maître, et le retransmet alors sur son propre TTY. L'utilisateur n'y voit donc que du feu, et croit que l'application tourne directement sur le TTY.

Sauf que screen a en fait un modèle client/serveur: dans le dessin ci-dessus, la partie "client" peut en fait se détacher de la partie serveur (en appuyant sur control-a d). L'application ne recevra donc plus de pression de touches, mais elle peut continuer à afficher sur le PTY, la partie serveur de screen met à jour la représentation de l'écran dans un tampon interne. Lorsque le client se reconnecte au serveur, le serveur lui donne l'état actuel de la représentation interne, le client peut alors l'afficher sur son TTY. L'utilisateur a ainsi l'impression d'être revenu sur son application.

Bien sûr, le client qui se reconnecte peut être lancé depuis un autre TTY, il peut même y avoir plusieurs clients qui se connectent, c'est très pratique pour effectuer de la téléassistance par exemple.

Et une couche de virtualisation...

La virtualisation est à la mode. Cela permet de lancer un Linux dans un Linux, un Windows dans un Linux, etc. Mais que se passe-t-il lorsque l'on presse la touche 'a' ?

schéma avec machine virtuelle

Le début de l'histoire, on le connait déjà: le (vrai) matériel transmet un scancode au noyau, qui le convertit en la lettre 'a', et la passe via un TTY à l'application.

Il se trouve qu'ici, l'application, c'est un outil de virtualisation. Il contient un "clavier virtuel", c'est-à-dire un bout de code qui, du point de vue du noyau invité, se comporte comme un vrai clavier, sous forme logicielle. En fait il va se contenter de simuler la puce i8042 dont on a parlé plus haut: lorsque que le clavier virtuel récupère le 'a' depuis le TTY, il stocke le scancode y correspondant dans une variable (qui correspond au registre de l'i8042), et génère une interruption.

À partir de là, on connait aussi déjà l'histoire: le traitant d'interruption du noyau invité interroge la puce i8042 virtuelle pour savoir quelle touche est pressée, la transmet à la couche TTY, puis l'application la récupère, décide de l'afficher sur le TTY, un driver vidéo s'occupe alors de l'afficher.

Intervient alors une carte graphique virtuelle. Dans le cas d'un vrai matériel, la carte vidéo balaye périodiquement sa mémoire pour savoir ce qu'elle doit afficher. La carte graphique de l'outil de virtualisation fonctionne typiquement de la même manière: régulièrement elle observer ce qui a été écrit dans la mémoire vidéo virtuelle. Pour optimiser, les outils de virtualisation utilise souvent des méthodes de bit "dirty" pour savoir efficacement quelles zones de l'écran ont été modifiées.

L'outil de virtualisation envoie alors une version rafraîchie de l'écran sur le TTY, et de nouveau, à partir de là, on connait la musique: un driver vidéo s'occupe de l'envoyer dans la mémoire vidéo de la vraie carte graphique, et l'écran affiche finalement notre 'a'.

Alors bien sûr, pour simplifier j'ai pris l'exemple du mode texte. Le lecteur pourra s'amuser à faire le dessin avec en plus une couche graphique.

Bon, mais est-ce qu'il n'y a pas eu une entourloupe ? Eh si. Comment l'outil de virtualisation fait pour deviner comment transformer le "a" en scancode ? Eh bien il essaie de deviner. Et il y a des cas où il se plante forcément.

Dans le cas des outils de virtualisation en mode graphique, puisque le serveur X leur envoie le keycode, ils peuvent retraduire cela en scancode sans se tromper, quelle que soit la disposition de touches utilisée dans la machine hôte. C'est ensuite à l'utilisateur de configurer la disposition de la machine invitée pour avoir ce qu'il désire.

Dans le cas des outils de virtualisation en mode texte (ou bien via vnc), ils ne récupèrent qu'un code ascii. Ils ne peuvent pas savoir la disposition du clavier de la machine hôte, et ne peuvent donc qu'essayer de le deviner pour reconvertir ce code ascii en scancode. Éventuellement l'utilisateur peut indiquer à l'outil quel clavier il utilise (c'est l'option -k de qemu/kvm). S'il y a une erreur à ce niveau, le résultat peut être assez loufoque: si à la fois l'OS hôte et invité sont configurés en azerty, mais que l'outil croit que l'OS hôte est en qwerty, le 'a' va être converti en scancode 30 (car sur un clavier qwerty, le 'a' est sur la première touche de la deuxième rangée de lettre). L'OS invité, étant configuré en azerty, va donc le traduire en la lettre ascii "q", ce qui n'est pas ce qui était désiré. Si par contre l'OS invité est configuré en qwerty (typiquement parce qu'il est en train d'être installé), il va traduire correctement (mais à tort, en fait) le scancode 30 (qu'il croit être un 'a' de qwerty) en 'a'. Comportement on ne peut plus bizarre: pour avoir des lettres correctes, on a l'impression qu'il faut laisser l'OS invité en qwerty ! Le problème c'est qu'il y a des tas de lettres qui ne vont pas fonctionner pour autant: par exemple celles pour lesquelles il faut appuyer sur shift pour l'obtenir en azerty, mais pas pour l'obtenir en qwerty, ou vice-versa, etc. Configurer toute la chaîne en azerty (ou en qwerty) permet de corriger la grande majorité des soucis, il reste juste le problème des raccourcis claviers qui n'ont pas d'équivalent en ascii.

Le seul moyen vraiment sûr de s'en sortir est d'utiliser des interfaces de rendu de virtualisation qui récupèrent directement des scancodes: l'interface sdl de qemu par exemple. Ainsi c'est le scancode qui est donné à l'OS invité, et la configuration clavier de l'OS hôte n'influe plus du tout.

Inversement, lorsque l'OS invité écrit dans la mémoire vidéo, comment est-on sûr que c'est un "a" ? En effet, l'encodage utilisé par la police de caractères chargée par l'OS invité n'est pas nécessairement un encodage ASCII ! Il se trouve par chance que c'est le cas la plupart du temps.

Et une couche de réseau

TODO: en fait je suis connecté par telnet à une machine sur mon réseau local, basculement dans le monde réseau

Et une couche d'Internet

TODO: en fait je suis connecté par telnet à une machine quelque part sur Internet, auquel je suis connecté par ADSL, basculement dans le monde tout court !

Conclusion

On peut se demander par quel miracle tout ce bazar parvient à fonctionner...


Copyright (c) 2011-2013 Samuel Thibault

Ce texte est fourni sous la licence Creative Commons Attribution-ShareAlike (V3.0), ainsi que décrite sur http://creativecommons.org/licenses/by-sa/3.0/ et dont le texte complet est disponible sur http://creativecommons.org/licenses/by-sa/3.0/legalcode . Me contacter si la clause ShareAlike pose souci.