Introduction Ă  la Virtualisation (Partie 1)

Lecture 18 min. ‱


Table des matiĂšres

Bonjour à toutes et tous 🙂

Bienvenue dans un article encore une fois bien trop long.

Ce devait ĂȘtre l’introduction d’un article sur la crĂ©ation d’image disque dans un pipeline Gitlab CI, je vais pas vous le cacher, ça a un peu dĂ©rapé  đŸ€­

Je vous propose un tour d’horizon des concepts de la virtualisation que j’utiliserai par la suite dans l’article prĂ©vu au dĂ©but.

Mais comme je sais pas m’arrĂȘter et que j’aime que n’importe qui puisse comprendre quelque soit son niveau de dĂ©part, il y aura de l’extra 😁

Avant de commencer.

D’abord, un grand merci Ă  Samuel Ortez, Julien Durillon et Pierre-Antoine GrĂ©goire pour la prĂ©sentation que je vais tenter de rĂ©sumer et complĂ©ter sans dĂ©former dans les lignes qui vont suivre.

Prenez une inspiration, on descends ! đŸ€ż

Code Machine et Hardware

Oui, comme ça direct. ^^

Ne vous inquietez pas je vais vous guider.

Code Machine

PremiĂšrement ce qu’il faut savoir, c’est qu’un ordinateur est extrĂȘment idiot, il ne parle ni français ni anglais, il ne connais qu’un langage, le Code Machine, basiquement des 0 et des 1.

Impossible de lui donner à manger du C, du Java ou du JavaScript, il n’est pas fait pour ça.

Dans un ordinateur, ce qui est responsable de comprendre et d’exĂ©cuter du code se nomme un CPU (Central Processing Unit).

le CPU ne prend que du code machine

De mĂȘme, un CPU parle un Code Machine bien spĂ©cifique et ne saura comprendre un Code Machine diffĂ©rent.

Ce code machine doit ĂȘtre adaptĂ© au CPU

 

J’ai fait abstraction de l’interprĂ©tation et de la semi-compilation, plus de dĂ©tail dans cet article

CPU

Voyons un peu plus en dĂ©tail les diffĂ©rents composants d’un ordinateur.

Le premier, nous l’avons dĂ©jĂ  nommĂ© est le CPU.

Anatomie d'un CPU

Il est composé de deux parties principales (je fais abstraction de ce qui nous intéresse pas ).

Les Registres sont des petites mémoires qui ne permettent (en simplifiant) que de stocker une valeur.

Plus d’infos dans cet article

Ils sont en quelques sortes l’établi utiliser par un artisan pour poser ses planches avant la dĂ©coupe.

Dans ces registres, un est un peu spĂ©cial et se nomme l’Accumulateur, ici je le reprĂ©sente par un Acc.

Si les registres sont l’établi, alors l’ALU sont les outils de l’artisan, son travail est de manipuler les registres pour faire des opĂ©rations mathĂ©matique ou des transferts de donnĂ©es.

RAM

Mais je l’ai dit plus haut, un registre ne contient qu’une seule valeur, ici on a 5 registres, donc on peut stocker 5 valeurs, nos programmes vont ĂȘtre courts đŸ€­

L’idĂ©e est alors de stocker ailleurs la donnĂ©e, la place sur le CPU Ă©tant limitĂ©e, on vient stocker dans un autre composant appelĂ© la RAM.

Connexion entre le CPU et la RAM

Le CPU communique avec la RAM au moyen de deux canaux :

Précisions

Ici je fais abstraction du cache CPU, d’ailleurs c’est en partie pour ça que les Apple M1 sont trùs rapides, ils ont beaucoup de cache ^^

J’assimile toute mĂ©moire de travail Ă  de la RAM pour plus de facilitĂ©.

Le programme dans la RAM

On peut globalement y stocker n’importe quoi, tant que ce sont des 0 et des 1. Et ça tombe bien notre programme est du Code Machine pouvant se reprĂ©senter par du binaire.

Chaque donnée est adressée par un nombre.

Dans cet exemple le CPU demande le contenu de l’adresse @500 à la RAM.

Chouette on peut stocker plus de 5 valeurs. 🙂

Périphériques

Mais le CPU Ă  part faire des calculs, il ne sert pas Ă  grand chose, il faut au moins une imprimante comme dans l’ancien temps pour lire le rĂ©sultat.

C’est pour cela que l’on a inventĂ© les PĂ©riphĂ©riques. Ceux-ci peuvent ĂȘtre n’importe quoi: un disque dur, la carte graphique, la carte rĂ©seau, l’imprimante qui marche jamais quand vous en avez besoin, etc 


Périphériques

Assembleur et Instructions

C’est bien beau tout ça mais le binaire et l’esprit humain cela fait 2 (sans mauvais jeu de mots ^^’), c’est pour cela que l’on ne manipule pas directement les 0 et les 1.

On le remplace par une sorte de langage qui est transcriptible en 0 et 1.

Assembleur

Ce langage se nomme de l’Assembleur ou ASM

Il existe autant d’assembleur qu’il y a de de Code Machine diffĂ©rents et plusieurs Assembleur diffĂ©rents peuvent mener au mĂȘme Code Machine.

Cet assembleur est la premiĂšre abstraction créé par l’Humain pour l’aider Ă  parler le Machine sans devenir fou!

Un exemple d’assembleur (rassurez-vous on dĂ©taille aprĂšs 🙂).

LOD @5

Va ĂȘtre transcrit en utilisant le Code Machine de notre CPU fictif en 0101010

Load opcode

Notre ligne de programme en Assembleur peut alors se décomposer en deux parties

Un autre exemple d’instruction

STD @5

Qui devient

Store opcode

On voit ici que le OpCode change la transcription binaire, il aurait Ă©tĂ© de mĂȘme si l’opĂ©rande avait Ă©galement Ă©voluĂ©.

La langue du Code Machine est appelĂ© un set d’instructions.

Instructions

Nous avons l’alphabet attaquons-nous aux mots.

La langue du CPU est décomposé en instructions, voici celle de notre CPU fictif.

OpCodeOperande 1Operande 2Signification
MOVRXDDĂ©place la valeur D vers le registre RX, D peut ĂȘtre un autre registre
LODRX@ACharge la valeur à l’adresse @A depuis la RAM vers le registre RX
STDRX@AStocke la valeur contenu dans le registre RX à l’adresse @A de la RAM
ADDXAjoute la valeur X Ă  Acc, X peut ĂȘtre un registre
OUTX#VXEcrire X sur le périphérique #VX
JMP@ASauter à l’instruction à l’adresse @A
JNZRX@ASauter à l’instruction à l’adresse @A, si la valeur dans le registre RX n’est pas 0
HLTStop le CPU et met fin au programme

Maintenant que nous avons des mots, nous allons pouvoir faire des phrases.

ExĂ©cution d’un programme

Ces phrases vont ĂȘtre notre programme.

Je vous propose celui-ci.

Programme d'exemple

Chaque ligne du programme est stockĂ©e Ă  une adresse, ici on dĂ©bute Ă  l’adresse @500, biensĂ»r c’est la reprĂ©sentation binaire qui est stockĂ©e et non l’assembleur.

Nous avons donc 3 questions à résoudre :

Pour cela, nous allons introduire 3 notions:

Fetch - Decode - Execute qui se chaĂźnent avec des flĂšches

Notre curseur d’exĂ©cution est Ă  l’adresse @500, ce qui veut dire que la ligne d’instruction Ă  rechercher dans la RAM est Ă  l’adresse @500.

Le CPU demande donc le contenu de l’adresse @500 à la RAM au travers du bus d’adresses, la RAM lui renvoie ce contenu.

Cette opération est appelée le Fetch.

Opération de Fetch

Une fois le contenu binaire de l’adresse @500 en main, celui-ci est dĂ©codĂ©.

Opération de Decode

Ici le dĂ©codage nous annonce que l’opĂ©ration Ă  rĂ©aliser de mettre la valeur 1 dans le registre Acc.

MOV ACC, 1

Maintenant que le CPU, sait quoi faire, il va pouvoir l’exĂ©cuter.

Opération de Decode

Dans le cas présent, mettre un 1 dans le registre Acc.

On passe alors Ă  la ligne suivante en incrĂ©mentant le curseur d’exĂ©cution.

Et du coup, fetch de l’adresse @501 cette fois-ci.

Opération de Fetch

Décodage

Opération de Decode

en

MOV R1, 4

Puis exécution

Opération d'Execute MOV

Nous avons maintenant un 1 dans l’accumulateur et un 4 dans le registre R1.

On déroule le programme.

Celui-ci nous demande d’additionner le contenu de l’accumulateur avec le contenu du registre R1 et de mettre le rĂ©sultat dans l’accumulateur.

ADD R1
Opération d'Execute ADD

Nous avons maintenant 5 dans l’accumulateur.

On continue.

Plus compliquĂ©, on nous demande de stocker le contenu de l’accumulateur dans la RAM Ă  l’adresse @15.

STD ACC, @15
Opération d'Execute STD

Le CPU va adresser la RAM pour que la valeur 5 contenue dans l’accumulateur soit stockĂ©e Ă  l’adresse @15.

Ligne suivante, on fait le chemin inverse, on demande de charger le contenu de l’adresse @15 dans le registre R1.

Opération d'Execute LOD

Nous avons maintenant 5 Ă  la place de 1 dans le registre R1

On déroule.

Nous devons déplacer ce qui est dans R1 dans R2. Facile.

MOV R2, R1
Opération d'Execute MOV

Nous avons maintenant 5 dans le registre R2.

Plus difficile, nous avons utilisĂ© les registres, l’ALU, la RAM, mais pas encore les pĂ©riphĂ©riques.

Nous allons demander d’écrire le contenu du registre R2 sur le pĂ©riphĂ©rique #12.

Ici, nous allons dire que c’est du disque dur.

Chaque périphérique est un micro ordinateur, qui possÚde également des registres.

OUT R2, #12 

Vient écrire sur le périphérique en voie 1 dans le registre X2, la valeur contenue par le registre R2.

Opération d'Execute OUT

5 est maintenant écrit dans le registre, le disque viendra alors faire quelque chose avec cette valeur, mais ceci est hors scope de notre article.

La derniÚre instruction, éteint le CPU.

HLT

Multi-Processus

Race condition

GĂ©nĂ©ralement, nous n’avons pas qu’un seul programme qui tourne sur un ordinateur.

Chacun de ces programmes sont stockés dans la RAM à des addresses différentes.

Deux programme en RAM

Comme nous n’avons qu’un seul CPU, et qu’il ne peut pas faire deux choses en mĂȘme temps, il faut faire les choses de maniĂšre sĂ©quentielle.

Nous allons départager le temps que nous passons dans chaque programme.

D’ailleurs lorsqu’un programme tourne, nous allons l’appeler un processus.

Schedule

Chaque passage entre processus sera appelé un context switch, on verra par la suite de quoi il en retourne.

Voici les deux programmes en questions.

Maintenant expert en assembleur, vous devriez les comprendre. ^^

Deux programmes

On accĂ©lĂšre la montre pour se placer Ă  l’instruction

STD ACC, @15

du programme 1, celle-ci Ă©crit comme tout Ă  l’heure la valeur 5 Ă  l’adresse @15.

C’est alors qu’intervient le context switch, le processus 1 doit laisser la main au processus 2.

Instruction STD processus 1

Nous sommes juste avant le dĂ©but du programme 2, le switch de contexte ne peut pas faire grand chose, il est sensĂ© remettre les valeurs du contexte d’exĂ©cution du processus 2.

Mais comme le programme n’a jamais tournĂ©, c’est plus du bruit qui est dĂ©fini ou bien les valeurs prĂ©cĂ©dentes restent inchangĂ©es.

En tout cas, en l’état le processus 2, ne sait pas ce qu’il y a dans les registres.

Context switch entre processus 1 et processus 2

Le programme 2 se déroule et atteint une instruction étrangement familiÚre.

STD R1, @15

Comme un ordinateur est dĂ©terministe, une mĂȘme action, provoque le mĂȘme rĂ©sultat.

A ceci prĂȘt que l’opĂ©ration prĂ©cĂ©dente avait dĂ©fini la valeur de R1 comme Ă©tant 0

Instruction STD processus 2

Donc maintenant l’adresse @15 vaut 0 !!!

Nous venons de crĂ©er une race condition : deux acteurs qui utilisent la mĂȘme ressources sans se synchroniser sur l’écriture.

Temps écoulé pour le processus 2, il passe la main.

Le context switch restaure les états des registres pour que le processus 1 puisse de nouveau travailler.

On remet :

On restaure Ă©galement le pointeur d’exĂ©cution.

Context switch entre processus 2 et processus 1

On incrĂ©mente le curseur d’exĂ©cution.

LOD R1, @15
LOD processus 1

Ah bah oui, mais non 


Le 5 n’est plus lĂ . Il a Ă©tĂ© prĂ©cĂ©demment remplacĂ©.

Le programme 1, n’est dĂ©sormais plus dĂ©terministe, il est capable de faire n’importe quoi.

Echec et mat !

Sommes-nous condamner Ă  n’exĂ©cuter qu’un seul programme par CPU.

Non, biensûr que non.

Mais pour ça, nous allons avoir besoin de plus de choses.

Kernel et MMU

Il va nous falloir un chef de gare pour gérer un peu la discipline dans les rangs.

Je vous le prĂ©sente, il s’agit du Kernel.

Kernel

Son rĂŽle va ĂȘtre de gĂ©rer les processus et de les empĂȘcher de faire n’importe quoi.

Kernel et processus

Et par n’importe quoi, je veux dire Ă©crire n’importe oĂč dans la mĂ©moire.

Le but Ă©tant d’éviter la race condition, mais plus gĂ©nĂ©ralement que chaque processus reste chez lui et ne puisse pas aller taper de la mĂ©moire qui ne lui appartient pas ou qui est dĂ©jĂ  utilisĂ©e.

Il a encore un milliard de rĂŽles diffĂ©rents supplĂ©mentaires, mais on reste simple, c’est de la vulgarisation, pas un cours.

Pour l’aider dans sa tñche de cloisonnement, le Kernel peut faire appel à un outil.

Celui-ci se nomme la Memory Management Unit.

MMU

Il s’agit globalement d’une liste d’adresse virtuelles qui pointent sur des adresses physique.

Pourquoi virtuelle ? Parce qu’elles n’ont aucune existence physique, elle n’existent pas dans la RAM, au contraire de l’adresse physique, qui est et bien physiquement prĂ©sente dans le silicium.

Pour permettre de s’y retrouver et aussi d’avoir plusieurs fois la mĂȘme adresse virtuelle pour deux adresses physiques diffĂ©rentes, on dĂ©coupe la MMU en tables.

Ici par exemple on a deux tables, dans ces deux tables l’adresses virtuelle 15 existe, mais ne pointe pas vers la mĂȘme adresse physique.

Table du MMU

Dans la table 1

$$15 \Rightarrow 1005$$

Dans la table 2

$$15 \Rightarrow 22$$

La MMU est un composant physique à la différence du Kernel qui est du logicielle.

La MMU en action

Nous allons utiliser cette propriĂ©tĂ© pour rĂ©soudre notre problĂšme de tout Ă  l’heure.

Revenons dans le temps.

Nous sommes dans le processus 1, juste avant le stockage de la valeur en RAM.

Nous allons interposer la MMU sur le chemin du bus d’adressage.

Le CPU fait ce qu’il faisait avant, demander de stocker Ă  l’adresse @15, mais la MMU est configurĂ©e par le Kernel pour utiliser la table p1.

Ainsi, l’adresse subit la modification suivante:

$$15 \Rightarrow 1013$$

Translation d'adresse

Notre 5 est alors stockĂ© Ă  l’adresse physique @1013 et non @15.

Cette fois-ci, c’est au processus 2 de vouloir stocker son 0 à l’adresse @15.

Mais mĂȘme principe, le Kernel dĂ©fini la table de la MMU sur p2.

Ainsi, l’adresse rĂ©ellement utilisĂ©e devient

$$15 \Rightarrow 2862$$

Translation d'adresse

Son 0 est alors stockĂ© Ă  l’adresse physique @2862 et non @15.

Lorsque le processus 1 revient Ă  la vie.

Il va lors du chargement de l’adresse @15, là aussi passer par le MMU.

Ainsi ce n’est pas @15 mais Ă  nouveau @1013 qui est utilisĂ© pour adresser la RAM.

Translation d'adresse

Et donc c’est bien un 5 et non un 0 qui est dĂ©sormais stockĂ© dans le registre R1.

Le programme peut de nouveau continuer. 🌞

  • La MMU est un composant physique.

  • La MMU est une partie essentielle de la sĂ©curitĂ© d’un ordinateur, sans lui un programme malveillant ou mal conçu pourrait réécrire la mĂ©moire d’un autre programme ou mĂȘme le programme lui-mĂȘme.

  • Du fait que l’isolation soit matĂ©rielle, cela rend trĂšs difficile le contournement de la sĂ©curitĂ©.

Syscall

De la mĂȘme maniĂšre que nous ne voulons pas qu’un processus prenne la main sur la mĂ©moire de ses petits camarades, nous allons isoler nos processus de l’accĂšs direct aux ressources physiques.

Pour 2 raisons:

Syscall

Lorsque qu’un processus dĂ©sire rĂ©aliser une manipulation qui nĂ©cessite autre chose que de la RAM, ce processus va rĂ©aliser une dolĂ©ance au Kernel, cette demande est appelĂ©e un Syscall.

Le syscall que nous allons utiliser se nomme read. Son boulot est demander la lecture d’une ressource un fichier par exemple qui serait sur le disque dur.

Mais de la mĂȘme maniĂšre que le processus ne savait pas parler “Disque Dur”, le Kernel ne sait pas non plus.

Heureusement, le Driver lui parle Ă  la fois Kernel et Disque Dur, se sera donc notre interprĂšte dans la transaction.

Pour les besoins du dessin je simplifie tout, mais il y a bien plus de chose Ă  dire ^^

DĂ©taillons ce qu’il se passe lors de la lecture de ce fichier.

Syscall

Le processus vient dĂ©finir qu’il a besoin en mettant l’information dans un registre (ici pour l’exemple R2 ), le nombre est ce qu’on appelle un file descriptor, c’est un identifiant qui est connu du Kernel et qu’il associe Ă  une ressource, ici un fichier dĂ©jĂ  ouvert par un prĂ©cĂ©dent syscall open.

Syscall

Un JMP est une instruction appelĂ©e un Jump, celui-ci vient dĂ©placer le curseur d’exĂ©cution Ă  l’adresse dĂ©finie ici @809 qui correspond au dĂ©but de notre syscall imaginaire et simplifiĂ© read_fd.

Le syscall fait des trucs, dont stocker de la donnĂ©e Ă  l’adresse @10005.

C’est alors que des mĂ©canismes un peu complexes et que je n’ai pas totalement saisis vont aller rĂ©veiller le Kernel qui piquait une sieste en attendant qu’il se passe quelque chose d’intĂ©ressant.

Syscall

Cette magie noire se nomme une interruption.

Les instructions qui vont suivre sont appelĂ©es privilĂ©giĂ©es car elle ne peuvent ĂȘtre exĂ©cutĂ© que dans un mode spĂ©cifique du CPU, mode qui ne peut ĂȘtre atteint que par le Kernel et ses modules dont les drivers.

Il va alors faire ce que le syscall lui demande, lire le fichier, mais comme il ne sait toujours pas parler Disque dur, il fait appel au Driver qui lui sait.

Syscall

Finalement le Disque dur est lu, au travers de l’opĂ©ration

OUT R2, #12

Le trajet de retour vers le processus est une autre histoire, peut-ĂȘtre un jour dans un autre article ^^“

Petit rĂ©capitulatif de ce qui s’est dĂ©roulĂ©.

Syscall

Conclusion

Ainsi s’achĂšve la premiĂšre partie de notre Ă©popĂ©e dans la Virtualisation, vous avez l’intuition de ce qui se passe dans un ordinateur physique.

Dans la seconde partie on commencera Ă  esquisser le fonctionnement de la virtualisation elle mĂȘme.

Merci de votre lecture ♄