Cours Bonus ‐ Gestion de la mémoire - vbridonneau/CoursSysteme GitHub Wiki

Gestion de la mémoire

Dans cette partie, nous allons nous concentrer sur la ressource mémoire. Nous allons voir comment celle-ci est géré par un système d'exploitation ainsi que l'élément matériel qui permet son traitement. Nous verrons ensuite comment la mémoire est organisée. Cela comprends la mémoire vive et son découpage, ainsi que les différents niveaux de caches que l'on retrouve dans les ordinateurs modernes.

Unité de gestion de mémoire (MMU)

Le premier point que l'on va voir concerne la gestion de la mémoire. Comme expliqué précédemment, le système d'exploitation permet de manipuler les ressources matérielles que l'on possède. En particulier, il met à disposition un moyen d'accéder et de modifier la mémoire d'un ordinateur : la MMU ou Unité de Gestion de Mémoire en français. Son rôle est de faire l'intermédiaire entre les adresses physiques et les adresses logiques. Les adresses logiques sont les adresses générés par le CPU qui les manipules pour accéder à des variables ou des données stockées dans le tas. Les adresses physiques elles correspondent à un emplacement réel des dans la mémoire. La MMU ne fait que la traduction pour le CPU des adresses logiques en adresses physiques.

TLB (Translation Lookaside Buffer)

Le TLB est un outil de traduction. Le point le plus important à retenir est que son rôle est de traduire les adresses virtuelles en adresses physiques. Pour ce faire, il va utiliser une table associant à toute adresse virtuelle une adresse physique. Plus précisément, cette table contient les pages de la mémoires physiques où sont inscrites des adresses physiques associées à des variables ou autre. Ainsi, dès qu'une variable ou un pointeur est accédé (lecture ou écriture), le TLB va chercher dans quelle page cette donnée est située afin que le CPU puisse l'utiliser. Si aucune page n'est associée à une adresse logique, un signal est levé : pagefault. Lorsque ce signale est levé, une nouvelle page est alors créée dans la table afin de faire correspondre l'adresse logique à une adresse physique.

Mémoire vive

Dans cette section nous allons voir comment la mémoire est organisée. La mémoire est constituée d'un tas, d'une pile, d'une zone de texte et d'une partie correspondant au noyau. Tous les processus ont un accès à ces différentes zones et

Le tas (heap)

C'est là que sont principalement allouées les données de façon dynamique (retour de malloc).

La pile (stack)

Contient principalement le code ainsi que les variables. La pile a un fonctionnement légèrement différente des autres zones de la mémoire. Lorsqu'un programme est exécuté, il faut naturellement marquer dans le code, l'endroit où l'on se trouve. Un registre nommé stack pointer (abrégé en sp) tient ce rôle. Lorsqu'une instruction est exécutée et que l'on passe à la suivante, ce pointeur diminue au lieu d'augmenter. En effet, avancer dans la pile ne se fait pas en augmentant la valeur d'un pointeur, mais en le diminuant. On dit que la pile se lit haut en bas.

Hiérarchie dans la mémoire vive

La mémoire vive dans un ordinateur ne se situe pas uniquement dans les barrettes de RAM. En effet, lorsqu'un CPU a besoin d'une adresse mémoire, il effectue une requête pour accéder à cette valeur. Si la valeur accédé n'a jamais été lu ou l'a été il y a longtemps, cette adresse se situe très certainement dans la RAM. Or, un accès à la RAM coûte cher (quelques millisecondes). Pour pallier ce problème, des mémoires intermédiaires ont été créées, que l'on appelle caches, qui permettent de stocker temporairement des valeurs de variables ou du code récemment utilisé. On distingue trois niveaux de cache. !()["./topologie.png"]

Cache L1

C'est le niveau de cache le plus proche du processeur. Il est d'ailleurs souvent collé à un cœur de processeur. Accéder à ce cache est très rapide. Par ailleurs, se cache est souvent divisé en deux. Une partie contient le code que le processeur est en train d'exécuter. Le second contient des données. Souvent provenant du tas.

Cache L2

Un peu plus grand que le cache L1, ce cache est également associé à un CPU et se trouve au niveau du processeur. Un accès à ce cache est légèrement plus lent qu'un accès au cache L1 pour le processeur.

Cache L3

C'est souvent le dernier niveau de cache que l'on trouve sur une machine. Sa fonction principale est de faire un pont entre les différents CPU sans avoir à passer par la RAM. Quand un processeur a besoin d'une variable qu'un autre est en train d'utiliser, l'information est remonté dans ce cache, ce qui évite d'avoir à aller chercher plus haut.

Illustration

Voici une illustration des niveaux de caches et de l'architecture matérielle. Celle-ci s'obtient à l'aide de la commande lstopo. Ce n'est pas une commande native. Elle permet d'obtenir les informations quant à l'architecture d'un processeur ainsi que des différents niveaux de caches. Une illustration des niveaux de caches et de l'architecture matérielle

Allocation/Désallocation

Lorsque l'on souhaite allouer des données de façon dynamique dans le tas, il existe plusieurs moyen. Le plus simple et le plus sécurisé reste encore d'utiliser la fonction malloc, mais il est possible de descendre un cran plus bas en utilisant des appels systèmes. Pour cela, on va regarder ce qu'il se passe lorsque l'on utilise les appels systèmes mmap et munmap :

/* Allocation */
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

/* Desallocation */
void munmap(void *addr, size_t length);

Allouer dans le tas

L'appel système mmap permet, entre autre, d'allouer de la mémoire dans le tas. Les paramètres de cette fonction permette d'obtenir un comportement précis et un contrôle sur la façon d'allouer la mémoire. Notre objectif étant d'allouer de la mémoire comme le ferait malloc, on va écrire le code suivant :

void *donnee = mmap(NULL, taille, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

Prenons chaque paramètre un à un :

  1. Le premier paramètre que l'on renseigne est l'endroit où la mémoire sera allouée. Comme on ne sait pas où allouer la mémoire, on donne NULL comme argument. La fonction se charge pour nous de nous trouver une adresse dans le tas.
  2. Le second paramètre correspond à la quantité que l'on souhaite allouée.
  3. Le troisème paramètre correspond au droit que l'on souhaite avoir sur la mémoire allouer. Ici, PROT_READ | PROT_WRITE signifie que l'on souhaite pouvoir lire et écrire dans la mémoire.
  4. Le quatrième paramètre correspond aux options que l'on souhaite transmettre à mmap.
    • MAP_PRIVATE est nécessaire et indique comment les autres processus verrons les modifications lorsque la zone allouée sera modifiée : la zone allouée ne sera visible que par le processus et par personne d'autre.
    • MAP_ANONYMOUS indique que la zone allouée n'est pas associée à un fichier.
  5. Le cinquième élément correspond à un descripteur de fichier. Comme ici, on ne projette pas le contenu d'un fichier, on précise la valeur -1 afin que mmap en soit informé.
  6. Le sixième argument est l'offset/décalage. Comme on ne manipule pas de fichier, on passe la valeur 0.

Vérification du retour de fonction

Il se peut que mmap ne puisse pas nous allouer de mémoire pour une quelconque raison. Pour vérifier que l'appel système a bien fonctionné, il faut comparer le retour avec MAP_FAILED comme suit :

if (donnee == MAP_FAILED) {
    perror("mmap");
    exit(EXIT_FAILURE);
}

Il est très important de toujours vérifier les retours d'appels systèmes.

Désallocation

Lorsque l'on alloue de la mémoire avec mmap il faut la désallouer avec munmap. A l'instar de free qui ne demande que l'adresse d'un pointeur alloué avec malloc, munmap requiert en plus de donner la taille allouée avec mmap. Le code permettant de désallouer nos données est alors :

munmap(donnee, taille);