Page préc.
Introduction
Fin de page Page suiv.
Mémoire virtuelle

Mémoire réelle

Mono-programmation

    Dans ce premier cas, le plus simple, une partie de la mémoire est occupée par la partie résidente du S.E., le reste est tout ou partie occupé par le programme de l'utilisateur, comme l'illustre la figure suivante :

Monoprogrammation

    Cette situation, générale dans le début des années 60, ne se rencontre plus que rarement, essentiellement dans le domaine de la "petite" micro-informatique (anciens ordinateurs du type Z-80, carte à microprocesseur, etc.). Son plus gros avantage est la simplicité des compilateurs, traducteurs et chargeurs correspondants. En effet, le programme est toujours chargé à la même adresse mémoire pour y être exécuté (adresse 0x100 dans le S.E. CP/M sur Z80). Lors de la compilation, toutes les adresses générées peuvent donc être absolues, c'est-à-dire correspondre exactement à l'implantation du programme à l'exécution.

    Il est actuellement admis que le code des programmes est statique en cours d'exécution, c'est-à-dire ne peut être modifié dynamiquement au cours de son exécution. Il n'en a pas toujours été ainsi, en particulier sur les ordinateurs individuels. La possibilité que donnent les langages assembleurs de modifier des instructions, d'en créer de nouvelles, donc d'avoir un programme dont la taille croît pendant son exécution, entraîne la possibilité de provoquer de graves problèmes, volontaires ou non. En effet, si le programme vient se superposer partiellement au S.E., cela provoque inéluctablement une catastrophe la plupart du temps bien visible. L'utilisation de l'ordinateur par une seule personne à la fois limite les conséquences désastreuses d'un tel événement, qui n'en reste pas moins très gênant pour la victime. Généralement involontaire, cette violation de la mémoire interdite est parfois une technique utilisée de façon malfaisante, par exemple pour la propagation de "virus". Parfois plus perfide, l'effet d'une telle action peut être une modification imperceptible du comportement des fonctions du système.

    Il est d'ailleurs (malheureusement !) facile d'obtenir un tel effet sur un ordinateur personnel, même en utilisant des langages de programmation de haut niveau (Pascal, C, ADA) en utilisant de façon incorrecte les pointeurs, en débordant la taille d'un vecteur, en utilisant des données dynamiques dont l'espace mémoire n'a jamais été alloué, etc.

    Le débordement de la mémoire disponible peut aussi être obtenu (c'est une erreur fréquente) par la création de nouvelles données (mémoire dynamique) ou par des dépassements de pile (récursivité infinie ou trop importante).

    Le problème de la protection doit donc être pris en compte, à la fois de façon matérielle et de façon logicielle - en utilisant au maximum les fonctions permettant de tester si les opérations voulues sont possibles (appel de la fonction MemAvail() avant l'opérateur new en Pascal Borland, test de la valeur de retour de l'opérateur new en C++ ou utilisation de la fonction set_new_handler(), test systématique des valeurs de retour des fonctions système sous UNIX, etc.)

    Il existe sur certains processeurs un ou deux registres appelés registres limites (figure suivante) pointant sur les adresses mémoire extrêmes utilisables par l'utilisateur. A chaque accès mémoire d'un programme utilisateur par le processeur, celui-ci vérifie que l'adresse est à l'intérieur de ces valeurs limites.

Registres limites

Mono-programmation et recouvrement

    Le recouvrement (en anglais overlay) est une méthode très largement utilisée, en particulier sur les micro-ordinateurs (Macintosh, PC, etc.). Le Pascal de Borland sous DOS par exemple (versions 3 à 7) le permet (la version 4 l'ayant temporairement supprimé).

    Lorsque la zone mémoire qui peut accueillir le programme est insuffisante, une solution assez simple à mettre en œuvre est le recouvrement. Elle consiste à découper le programme (et les données statiques associées) en blocs de tailles différentes :

Schéma de recouvrement

    En général, une partie du programme, appelée racine, est chargée une fois pour toutes dans la mémoire principale. Les autres blocs sont chargés à partir du disque au fur et à mesure des besoins. Chacun écrase le précédent et est lui-même écrasé par le suivant. Les seules limites sont :

    Cette technique, très utile, comporte cependant quelques restrictions :     Le S.E. et le processeur doivent avoir été conçus pour cette opération. Ils nécessitent en effet l'utilisation d'un registre de base qui contient l'adresse de début de la zone de recouvrement. Dans un bloc, l'adressage est relatif et ne peut en principe pas accéder à une zone mémoire d'un autre bloc.

Va-et-vient (swapping)

    Il s'agit d'une transposition à un système multitâches du recouvrement vu ci-dessus (pour un seul utilisateur). Cette technique fut utilisée surtout au début de l'apparition du traitement en temps partagé (time sharing). A un instant donné, un seul programme est physiquement en mémoire, mais pour un temps assez court, puis il est recopié sur disque et écrasé par un nouveau programme, et ainsi de suite. Chaque utilisateur a l'illusion d'occuper seul la machine (temps partagé), mais en réalité, son programme peut faire plusieurs fois par seconde l'aller-retour entre le disque et la mémoire centrale. Chaque programme est stocké suivant le mode d'adressage absolu. C'est le S.E. qui joue le rôle de la racine.

Va-et-vient

    Compte tenu de la lenteur relative des accès disques, cette méthode est assez lourde et n'a pas été conservée dans sa forme la plus simple. Elle a été améliorée de plusieurs façons :

    La technique de va-et-vient peut d'ailleurs être couplée avec celle du recouvrement si les programmes sont trop volumineux. Elle est à l'origine de la méthode la plus répandue : la pagination.

Partitions fixes à plusieurs files d'attentes

    L'allocation de la mémoire avec partitions fixes est assez semblable à celle présentée sur la figure suivante :


Partitions fixes avec files d'attente

    Dans un contexte de multiprogrammation, elle consiste à diviser la mémoire en partitions de tailles prédéterminées (à la génération du système) et fixes (pas forcément égales). Dans son implémentation la plus simple, les programmes sont compilés en absolu, c'est-à-dire sont destinés à être exécutés dans une partition particulière. A chaque partition correspond une file d'attente des processus destinés à cette partition qui se remplit au fur et à mesure des lancements des programmes. Une fois son tour arrivé, le programme est chargé définitivement en mémoire où il demeurera jusqu'à la fin de son exécution.

    Le premier inconvénient de cette méthode est la nécessité pour un processus d'être exécuté dans une partition particulière. On peut y remédier en utilisant un adressage relatif. Au moment du chargement du programme en mémoire, il est relogé par recalcul de toutes les adresses

    Le deuxième inconvénient est l'impossibilité pour un processus dans une file d'attente d'en changer si une partition devient inutilisée :


Partitions fixes avec files d'attente vides

    Cela peut conduire à une sous-utilisation de la mémoire centrale, certaines partitions étant saturées alors que d'autres sont inoccupées. Ce phénomène est très probable. En effet, lorsqu'un programme est lancé, il est logique de l'affecter à la partition de taille immédiatement supérieure à la sienne. On peut donc penser que les partitions de tailles extrêmes (les plus petites et les plus grandes) seront les moins souvent occupées. Cela a conduit à la technique des partitions fixes à une seule file d'attente.

Partitions fixes à une seule file d'attente

    Dans cette méthode, les requêtes de chargement des programmes sont stockées dans une seule file. Il s'agit encore d'une allocation statique puisqu'elle est faite une fois pour toutes avant le début de l'exécution du programme :


Partitions fixes à une seule file d'attente

    Cependant, l'affectation à une partition est différée au dernier moment, lors du chargement du programme en mémoire. C'est l'allocateur de mémoire qui décide à quelle partition le programme est affecté, en fonction d'un algorithme d'affectation :

Affectation par allocateur mémoire

"Le premier d'abord"

    Lorsqu'une partition se libère, l'algorithme du "premier d'abord" consiste à parcourir la liste d'attente à partir du premier et à choisir le premier programme de taille inférieure ou égale à la taille de la partition libre.

"Le meilleur d'abord"

    Lorsqu'une partition se libère, l'algorithme du "meilleur d'abord" consiste à parcourir complètement la liste d'attente et à choisir le programme de taille la plus proche par défaut de la taille de la partition libre.

    Le problème de la protection doit aussi être traité dans le cas de la multiprogrammation. Il est ici impossible d'associer deux registres limites pour chaque partition. Il suffit d'en avoir deux qui sont associés à la partition active. A chaque partition seront associées deux zones mémoire qui seront chargées dans les registres lorsque la partition est activée.

Partitions variables

Allocation de la mémoire contiguë

    Au fur et à mesure que les programmes se présentent dans la file d'attente, ils sont placés en mémoire centrale à des adresses contiguës. Bien sûr, on considère ici que chaque programme pris séparément peut entrer totalement en mémoire centrale. Cette première étape est illustrée ci-dessous :


Allocation en mémoire contiguë

    Puis un ou plusieurs programmes se terminent, libérant des zones mémoire qui peuvent ne pas être contiguës :

    Les différentes solutions qui peuvent être adoptées pour gérer ces espaces mémoire libres sont présentées ci-après.

Algorithme dit "du premier espace libre suffisant"

    A partir de l'adresse la plus basse l'allocateur mémoire cherche le premier espace libre de taille au moins égale à celle du programme. Ce choix est illustré ci-dessous :


Algorithme dit "du premier espace libre suffisant"

    Un inconvénient important de cet algorithme est de concentrer le placement en début de mémoire sans se soucier d'utiliser au mieux l'espace mémoire. C'est un algorithme rapide car la recherche est arrêtée dès qu'une solution est trouvée.

Algorithme dit "du prochain espace libre suffisant"

    Il s'agit ici de chercher le prochain espace mémoire libre suffisant à partir de la dernière allocation. Cet algorithme répartit mieux les programmes dans l'espace mémoire. Mais il ne résout pas plus que le précédent la fragmentation et la création de "trous" trop petits. Des simulations ont montré qu'il est légèrement moins bon que le premier.

Algorithme dit "du meilleur espace libre"

    La figure suivante montre le résultat de l'allocation suivant cet algorithme. L'allocateur choisit l'espace libre dont la taille est la plus proche par excès de la taille du programme à placer. L'objectif de cet algorithme est d'obtenir des zones non affectées les plus petites possibles :

Algorithme "du meilleur espace libre"

    Cette méthode s'est aussi révélée moins efficace que celle du premier espace disponible. Knuth a montré que l'algorithme dit du "first fit" est souvent meilleur que celui du "best fit".

Algorithme dit "du plus mauvais espace libre"

    Au contraire du précédent, cet algorithme cherche l'espace mémoire libre de taille maximale :


 Algorithme "du plus mauvais espace libre"

    Ainsi, après placement du programme, on espère que l'espace restant sera suffisant pour placer un nouveau programme. Là encore, il ne s'agit pas d'une très bonne idée.

    Quel que soit l'algorithme retenu, il provoque l'apparition de trous (espaces mémoire inutilisés) qu'il faut bien gérer. Là aussi, plusieurs solutions ont été proposées.

Le compactage ou "garbage collector "

    La méthode consiste à garder une table des espaces libres au fur et à mesure que les programmes se terminent, mais à n'allouer de l'espace mémoire que dans l'espace inutilisé "en haut" de la mémoire. Lorsque l'espace inutilisé est trop petit pour permettre une nouvelle allocation, le système transfère les programmes en mémoire en les rendant contigus :


Compactage

    Tous les trous disjoints sont alors réunis en un seul espace mémoire libre. Cette méthode offre pour unique avantage de ne pas chercher d'espace mémoire à chaque nouvelle allocation.

    Au contraire elle présente plusieurs inconvénients importants. En premier lieu, le moment où le système lance une opération de compactage est totalement imprévisible. Cette opération prend un temps non négligeable et consomme les ressources du système, pénalisant fortement le travail en cours d'exécution à ce moment-là. Les effets dans un contexte "temps réel" sont catastrophiques. De plus, le compactage nécessite de reloger le programme. Si le mode d'adressage n'utilise pas un registre de base, il est nécessaire de conserver tout au long de l'exécution les tables de relogement (table des adresses présentes dans le programme). Enfin, cette méthode est incompatible avec une rotation rapide des programmes.

Coalescence des espaces libres

    Les quatre algorithmes supposent la gestion d'une ou de plusieurs listes de trous, consultée(s) à chaque allocation et mise à jour à chaque terminaison de programme. La première opération nécessaire consiste à pouvoir réunir deux trous contigus en un trou plus grand comme l'indique la figure ci-dessous :


Coalescence des espaces libres

    Cette coalescence peut être aisément obtenue si les trous sont conservés dans une liste triée par adresse mémoire. Il est ainsi facile de détecter qu'un trou dont on connaît l'adresse de début et le nombre d'unités d'allocation (par exemple l'octet), se termine à l'adresse voisine de l'adresse de début du trou suivant.

Table de description de la mémoire (mapping)

    Un "vecteur de bits" décrit l'état de la mémoire [1] : à chaque unité élémentaire d'allocation (1, 4, 16, n octets) correspond un bit, à 0 si la mémoire correspondante est libre, à 1 si elle est allouée. La figure suivante illustre un mapping de la mémoire où chaque bit correspond à 4Ko (tout à fait irréaliste dans la pratique, mais seulement destiné à l'illustration graphique).


mapping de la mémoire

    Un simple calcul montre que si 1 bit correspond à 4 octets, la table n'occupe qu'environ 3 % de la mémoire totale, ce qui est assez faible. Un avantage de cette méthode est de limiter la taille de la table à une valeur connue a priori (elle ne dépend que de la mémoire installée physiquement et de la valeur de n, et on peut donc savoir à quelle adresse elle sera implantée). Un inconvénient de cette méthode est la difficulté de trouver une suite de k bits à 0 si la taille mémoire recherchée est de k*n octets.

Utilisation de liste(s) chaînée(s)

    L'utilisation de liste(s) chaînée(s) est la plus naturelle, les espaces libres étant de tailles quelconques. De l'organisation de cette (ou ces) liste(s) dépend l'efficacité de l'algorithme d'allocation. Cette liste peut être dans un espace mémoire particulier. Dans ce cas, chaque élément contient un pointeur vers la zone libre, une indication de taille et le(s) lien(s) de chaînage. Au contraire, chaque espace libre peut commencer par un entête qui le lie au suivant : les trous constituent eux-mêmes la liste chaînée. Dans tous les cas, il semble préférable d'avoir au moins deux listes, l'une pour les espaces occupés par les programmes, l'autre pour les espaces libres.

Allocation par subdivision : "buddy system"

    Dans cette méthode, la mémoire est organisée en blocs dont la taille est une puissance de 2 d'une taille élémentaire, par exemple 1 Ko. On ne peut donc trouver que des blocs, libres ou affectés, de taille 1 Ko, 2 Ko, 4 Ko, 8 Ko, etc. Par exemple, un programme de taille 12 Ko nécessite un bloc mémoire de 16 Ko. S'il n'en existe pas, il faut chercher un bloc libre de 32 Ko qui est fractionné en deux blocs de 16 Ko, l'un affecté au programme, l'autre étant mis dans la liste des trous de 16 Ko. S'il n'existe pas non plus de trou de 32 Ko, on cherche un trou de 64 Ko, qui est alors découpé en 1 bloc libre de 32 Ko, un autre bloc libre de 16 Ko et un bloc de 16 Ko qui est attribué au programme.

    Comme le montre la figure ci-dessous, la mémoire initiale de 1024 Ko constitue un seul bloc libre. Pour placer le programme A, le bloc de 1024 Ko est subdivisé en 1 bloc de 512 Ko, 1 bloc de 256 Ko, 1 bloc de 128 Ko, le dernier bloc de 128 Ko étant affecté au programme A. Le programme B nécessite un espace de 64 Ko qui n'existe pas. On choisit donc de partitionner un bloc de 128 Ko en deux blocs de 64 Ko, l'un étant affecté au programme B, l'autre étant libre. Le placement du programme C nécessite un bloc de 128 Ko obtenu en partitionnant un bloc de 256 Ko. La fin des programmes A puis B libère un bloc de 128 Ko, puis un autre de 64 Ko dont le "compagnon" est libre. Ils sont donc fusionnés en un bloc de 128 Ko dont le compagnon est lui aussi libre. Leur réunion conduit à un bloc de 256 Ko.

    Suivant le même principe, on peut utiliser d'autres systèmes que le système binaire, par exemple la suite de Fibonacci définie par Si+1 = Si + Si-1, avec So = 1 et Si = 1, soit la suite 1, 1, 2, 3, 5, 8, 13, etc. Par exemple, un bloc de 13 Ko est partitionné en deux blocs, l'un de 8 Ko et l'autre de 5 Ko.


[1] La même technique est utilisée dans d'autres circonstances, par exemple pour décrire l'état d'occupation d'une zone disque (voir : Système de fichiers ext2).


Page préc.
Introduction
Début de page Page suiv.
Mémoire virtuelle
Dernière mise à jour : 07/11/2001