PROFDINFO.COM

Votre enseignant d'informatique en ligne

Organisation de la mémoire

Description de la mémoire

La mémoire physique sur un système se divise en deux catégories :

  • la mémoire vive : composée de circuit intégrés, donc très rapide
  • la mémoire de masse : composée de supports magnétiques (disque dur, bandes magnétiques...), beaucoup plus lente

La mémoire physique sert de zone de stockage temporaire pour les programmes et données que vous utilisez. De façon générale, plus la quantité de mémoire est importante, plus vous pouvez lancer d'applications simultanément. D'autre part, plus celle-ci est rapide plus votre système réagit vite, il s'agit donc (pour le système d'exploitation) de l'organiser au mieux pour en tirer le maximum de performances.

La gestion de la mémoire

La gestion de la mémoire est un difficile compromis entre les performances (temps d'accès) et la quantité (espace disponible). On désire en effet tout le temps avoir le maximum de mémoire disponible, mais l'on souhaite rarement que cela se fasse au détriment des performances. La gestion de la mémoire doit de plus remplir les fonctions suivantes :

  • permettre le partage de la mémoire entre différentes applications (pour un système multi-tâches);
  • permettre d'allouer des blocs de mémoire aux différentes tâches;
  • protéger les espaces mémoire utilisés (empêcher par exemple à un utilisateur de modifier une tâche exécutée par un autre utilisateur);
  • optimiser la quantité de mémoire disponible, notamment par des mécanismes d'extension de la mémoire.

L'extension de la mémoire

Il est possible d'étendre la mémoire de deux manières :

  • En découpant un programme en une partie résidente en mémoire vive et une partie chargée uniquement en mémoire lorsque l'accès à ces données est nécessaire.
  • En utilisant un mécanisme de mémoire virtuelle, consistant à utiliser le disque dur comme mémoire principale et à stocker uniquement dans la RAM les instructions et les données utilisées par le processeur. Le système d'exploitation réalise cette opération en créant un fichier temporaire (appelé swap file, traduisez "fichier d'échange") dans lequel sont stockées les informations lorsque la quantité de mémoire vive n'est plus suffisante.
    • Cette opération se traduit par une baisse considérable des performances, étant donné que le temps d'accès du disque dur est beaucoup plus long que celui de la RAM.
    • Lors de l'utilisation de la mémoire virtuelle, il est courant de constater que la DEL du disque dur reste quasiment constamment allumée et, dans le cas du système Microsoft Windows, qu'un fichier appelé win386.swp (ou pagefile.sys pour les systèmes plus récents -- vous devrez sans doute modifier les options d'affichage des fichiers système pour le voir) d'une taille conséquente, proportionnelle aux besoins en mémoire vive, fait son apparition.
    • Notez que les systèmes Linux créent à l'installation une partition complète qu'ils utiliseront ensuite comme extension de la mémoire vive. La mémoire virtuelle n'est donc pas sur la même partition que le système d'exploitaiton, les programmes et les fichiers.

Les mécanismes de découpage de la mémoire

La mémoire centrale est découpée de deux façons :

  • La pagination: elle consiste à diviser la mémoire en blocs, et les programmes en pages de longueur fixe.
    • Toute la mémoire est partitionnée en pages de taille fixe (et normalement petite, quelque chose autour de 512 octets à 4 Ko)
    • Les pages sont numérotées et chaque page a son adresse
    • Le système d'exploitation maintient une liste des pages (pour faire correspondre numéros de page et adresses) et sait lesquelles sont libres
    • Chaque processus en mémoire est divisé en pages. L'adresse d'un octet en mémoire est donc défini par le numéro de la page et l'offset de l'octet, c'est-à-dire sa distance du début de la page. L'offset seul donne donc une adresse relative au processus (qui ne connaît pas l'existence des pages). Le numéro de page + l'offset donne une adresse absolue pour l'OS.
    • Les pages ne sont pas visibles pour les processus en exécution, qui ne voient qu'un grand espace contigu leur appartenant, et rien d'autre.
    • Une erreur de page (page fault) est une erreur qui se produit lorsqu'un processus tente d'accéder à une page définie dans la table de pages, mais n'ayant pas réellement été chargée en mémoire. Quelques causes possibles pour cette erreur:
      • Il est possible que ça soit tout simplement parce que cette page a été envoyée en mémoire virtuelle (sur le disque dur), ou n'ait jamais été chargée encore dans un but d'économiser la mémoire vive. Dans ce cas, ce n'est pas réellement une "erreur" (même si on appellera ça une erreur de page majeure (major page fault)) -- l'OS chargera simplement la page demandée en mémoire lorsque l'erreur de page surviendra (c'est d'ailleurs la seule façon qu'il a de savoir que c'est le temps de le faire!)
      • Il est également possible que la page ait été chargée en mémoire mais que la table des pages n'ait pas été mise à jour. Dans ce cas, on parlera d'un erreur de page mineure (minor page fault), puisqu'il ne suffit que de mettre à jour la table des pages.
      • Enfin, il est toujours possible qu'un programme tente d'accéder à une page qui n'existe pas où à laquelle il n'a pas accès. On appelera alors le phénomène une erreur de page invalide (invalid page fault). L'OS devra alors retourner une erreur au programme.
  • La segmentation : les programmes sont découpés en parcelles ayant des longueurs variables appelées «segments».
    • La segmentation est visible pour le programmeur. La mémoire peut donc être vue comme plusieurs espaces d'adressages différents (les segments)
    • Chaque composante du processus (le code, les données, la pile d'exécution (voir plus loin), le tas, etc) peut avoir son propre segment.
    • Il est possible de mettre des droits d'accès aux segments pour déterminer qui peut y faire quoi -- en effet, plusieurs processus peuvent partager un même segment.
    • Un segment s'étend sur plusieurs pages et les deux systèmes sont donc utilisés conjointement.
    • L'adresse d'un octet est composée de l'adresse du segment (donc du début du segment) + un offset. Cette adresse est une adresse linéaire qui correspondra à une page et un offset, et donc à une case mémoire précise. On a deux couches logiques une par-dessus l'autre au-dessus de la couche physique!
    • Une erreur de segmentation (segmentation fault) peut survenir lorsque l'on tente d'accéder à un segment invalide, auquel on n'a pas accès, ou d'une façon qui n'est pas permise (écrire dans une zone de lecture seule, par exemple).
      • Plusieurs erreurs de programmation amèneront un message de segmentation fault. Bien que les segments ne soient pas nécessairement impliqués directement dans l'opération, on gardera ce nom pour toute erreur d'accès en mémoire. La plupart de ces cas d'erreur surviendront dans votre formation à partir de la session prochaine.

(Ces notes de cours ont été grandement inspirées du document intitulé « La gestion de la mémoire » issu de Comment Ça Marche (www.commentcamarche.net), qui est mis à disposition sous les termes de la licence Creative Commons.)

La pile (stack) et le tas (heap)

Le tas n'a pas de vocation particuliere et est un bloc de mémoire allouable au byte près. Allouer un bloc de mémoire signifie le réserver pour y mettre des données, sous forme d'objet, de tableau ou autre. Dans le tas, des bloc mémoires peuvent être alloués et supprimés, laissant des « trous » inutilisés. Si un récupérateur de mémoire (garbage collector) ne passe pas en arrière pour compacter l’espace laissé vacant, la performance se dégrade et on peut manquer plus rapidement de mémoire. Néanmoins, l’espace mémoire du tas est essentiel pour contenir des nouvelles informations non prévues. Le tas est surtout utilisé avec les pointeurs, que vous découvrirez à la session prochaine. Pour l'instant, retenez que c'est un gros bloc d'adresses qu'on peut diviser comme on veut à mesure qu'on en a besoin.

La pile des appels (aussi appelée "pile d'exécution", "call stack" ou, souvent, "le stack") est une zone de la mémoire (paginée par pages de 4 Ko) où on empile des contextes d'exécution (un grand terme qui signifie au fond des sous-programmes). Elle est accompagnée d'un pointeur d'exécution qui pointe vers le contexte du haut de la pile.

Une pile est une structure de données qui ne permet que deux (parfois trois) opérations:

La pile est donc une structure FILO (First In Last Out -- le premier entré est le dernier sorti) ou LIFO (Last In First Out -- le dernier entré est le premier sorti). Elle est particulièrement efficace par sa simplcité: les blocs mémoire sont toujours contigus, ne laissant pas de « trous », contrairement au tas. Elle est généralement considérée comme opposée à la structure file (FIFO). Pensez à une pile d'assiette dans un buffet chinois, versus une file d'attente à l'épicerie.

Un contexte d'exécution contient:

On ne peut jamais voir les contextes d'exécution qui se trouvent en dessous du contexte courant. On est donc toujours en train d'exécuter le contexte du dessus de la pile.

Lorsqu'on appelle une fonction, on empile son contexte: on empile les valeurs des paramètres qu'on lui passe, puis on empile l'adresse mémoire de la ligne qui vient d'appeler la fonction. On change le pointeur d'exécution.

La fonction va empiler ses variables locales sur le dessus de la pile, va prendre les valeurs de ses paramètres et va s'exécuter. Lorsqu'elle aura fini, elle va: "dépiler" le contexte d'exécution, empiler sa valeur de retour et changer le pointeur d'exécution pour le replacer à l'adresse mémoire qui lui a été fournie.

On se retrouvera donc de nouveau dans la fonction appelante. Cette fonction va prendre la valeur de retour sur le dessus de la pile et va en faire ce qu'elle veut, puis va poursuivre son exécution.

En empilant les contextes à l'appel et en les "dépilant" au retour, on sait toujours exactement où on est rendu et d'où on vient. Ça explique aussi pourquoi:

Pile des appels

(source du graphique: Wikipedia)

Adressage

On parle d’une architecture 32-bits lorsque les mots manipulés par le processeur ont une largeur de 32 bits, ce qui leur permet de varier entre les valeurs 0 et 4 294 967 295 pour un mot non signé (232), et entre −2 147 483 648 et 2 147 483 647 pour un mot signé (232-1). Avec la version 64-bits de Windows, la mémoire maximale n’est pas 264, mais limitée par la version de Windows. Par exemple, pour Windows 7, on aura:

  • Starter, Familiale Basique (Home Basic) : 8 Go
  • Familiale Premium (Home Premium) : 16 Go
  • Professionnel (Professional), Entreprise (Enterprise), Intégrale (Ultimate) : 192 Go

Lorsqu'un mot de 32 bits sert à stocker l’adresse d’une case mémoire (toujours d'une taille d'un octet), on pourra adresser une mémoire de taille 4 Go (puisqu'on aura 4 294 967 296 adresses possibles (toutes les combinaisons d'un mot de 32 bits) et que chaque adresse contient un octet). Donc un système d'exploitation Microsoft Windows de 32 bits ne pourra pas utiliser plus de 4 Go de RAM.


Lire la mémoire

Il faut savoir que les adresses mémoires sont décroissantes, ce qui signifie en fait qu'on écrit "à l'envers" dans la mémoire. On commence par écrire "au bas", ou à la fin, et on remonte progressivement vers "le haut" ou vers le début.

Par exemple, si je stocke le nombre 1 500 000 en mémoire dans un int (donc 4 octets), je sais que je stockerai en fait le nombre 0000 0000 0001 0110 1110 0011 0110 0000. Pour plus de simplicité, nous "compresserons" cette notation en hexadécimal. On obtiendra un caractère pour chaque paquet de 4 bits, donc deux caractères pour chaque octet: 0x0016E360. La convention veut qu'on regroupe ces caractères en paquets de 4 pour plus de clarté: 0x0016 E360.

Supposons que mon int est placé en mémoire à l'adresse 0x00000C800F24. Cette adresse représente en fait l'adresse de l'octet au poids le plus faible, c'est à dire normalement l'octet de droite, ici 0x60. L'octet 0xE3 sera stocké à l'adresse suivante (0x00000C800F25), puis l'octet 0x16 à l'adresse suivante (0x00000C800F26) et finalement l'octet au poids le plus lourd, 0x00, à l'adresse 0x00000C800F27. On aura donc la situation suivante:

 

Adresse mémoire 0x00000C800F24 0x00000C800F25 0x00000C800F26 0x00000C800F27
Contenu 0x60 0xE3 0x16 0x00

On appelle cette façon de stocker le nombre "à l'envers" le Little Endian, c'est à dire qu'on commence par socker le little end, ou le "petit bout" du nombre, dans l'adresse de départ, puis qu'on stocke progressivement vers le haut. Comme cette façon de faire est un peu contre-intuitive, on préférera alors représenter les adresses mémoires de façon décroissante. Ainsi, notre nombre semblera "à l'endroit":

 

Adresse mémoire 0x00000C800F27 0x00000C800F26 0x00000C800F25 0x00000C800F24
Contenu 0x00 0x16 0xE3 0x60

On lit mieux notre nombre 0x0016 E30 ainsi.

Il faut donc retenir que:

Par exemple, on pourra écrire le contenu d'une plage mémoire ainsi:

 

0x00000C800F2B-0x00000C800F24 : 3041 C042 D043 6944

Cela signifie que l'intervalle de mémoire allant de 0x00000C800F2B à 0x00000C800F24 contient le nombre 0x3041 C042 D043 6944. Donc la case au numéro finissant par 24 contient l'octet 0x44, que celle finissant par 25 contient l'octet 0x69, et ainsi de suite jusqu'à la case finissant par 2B qui contient 0x30. L'intervalle d'adresse est décroissant, donc le nombre que l'on lit est le "vrai" nombre contenu dans la plage.

On peut également représenter le tout dans un tableau, ainsi:

 

Adresse mémoire 0x00000C800F2B 0x00000C800F2A 0x00000C800F29 0x00000C800F28 0x00000C800F27 0x00000C800F26 0x00000C800F25 0x00000C800F24
Contenu 0x30 0x41 0xC0 0x42 0xD0 0x43 0x69 0x44

Évidemment, la façon dont ces données seront interprétées dépendra totalement du type de données de la variable.

 

Questions et exercices

1- Pourquoi un système qui utilise de la mémoire virtuelle est-il plus lent qu'un système qui n'en utilise pas?

2- Pourquoi utiliser la mémoire virtuelle si elle ralentit l'ordinateur?

3- Donnez deux grandes différences entre les pages et les segments en mémoire.

4- Pourquoi une erreur de page majeure n'est pas réellement une erreur en tant que telle?

5- Nommez trois situations qui peuvent mener à une erreur de segmentation?

6- Quelles sont les trois opérations que l'ont peut faire sur une pile et en quoi consistent-elles?

7- Qu'empile-t-on sur la pile d'exécution?

8- Prenez l'exercice 4.6 de votre cours d'algorithmie. Dessinez le contenu de la pile d'exécution une fois que le main a appelé GererMenu et que GererMenu a appelé AfficherMenu. Imaginez les adresses des fonctions mais soyez cohérents!

9- Pourquoi est-ce que GererMenu ne peut pas accéder aux variables déclarées dans AfficherMenu?

10- Lorsque les mots du processeur avaient une taille de 16 bits, quel était la capacité maximale de la mémoire vive (RAM) qui était adressable (et donc utilisable)? Expliquez avec des calculs.

11- Si l'intervalle de mémoire suivant contient les données suivantes:

 

0x00000B33341D-0x00000B333416 : 1900 DDA0 2113 4735

a) Quel caractère (char) se trouve à l'adresse 0x00000B333419? (nommez le caractère qui serait affiché à l'écran)

b) À quelle adresse se trouve l'entier (int) 0x1900 DDA0?

c) Quel entier court non signé (unsigned short) se trouve à l'adresse 0x000000B33419? (donnez le nombre en système décimal et indiquez votre démarche)

d) Quel entier (int) se trouve à l'adresse 0x00000B333416? (donnez le nombre en système décimal et indiquez votre démarche)

e) Quel flottant (float) se trouve à l'adresse 0x00000B333416? (donnez le nombre en système décimal et indiquez votre démarche)