Académique Documents
Professionnel Documents
Culture Documents
: GESTION MEMOIRE.
1
mémoire dynamique, malloc, possède un paramètre d’entrée qui indique la taille de
l’allocation requise en octets. Malloc alloue un bloc de plus grande taille, qui est constitué
d’un ou de plusieurs blocs de taille fixe plus petits. La taille de ce grand bloc mémoire est au
moins aussi grande que la taille requise ; c’est le nombre le plus proche du multiple de la taille
unitaire (unit size). Par exemple, si une allocation demande 100 octets, le bloc retourné aura
une taille de 128 octets (4 unités * 32 octets par unités). En conséquence, le demandeur
n’utilisera pas 28 octets (128-100 octets), ce qui est appelé fragmentation mémoire (memory
fragmentation). Cette forme spécifique de fragmentation est appelée fragmentation interne car
elle est interne au bloc alloué.
La table d’allocation peut être représentée comme une table de bits (bitmap), dans laquelle
chaque bit représente une unité de 32 octets. La figure suivante montre l’état de la table
d’allocation après une série d’appels des fonctions malloc et free. Dans cet exemple, le heap
est de 256 octets.
L’étape 6 montre deux blocks libres de 32 octets chacun. L’étape 7, au lieu de conserver 3
blocs libres séparés, montre que tous les blocs libres ont été combinés en un bloc de 128
octets. Comme ce nouveau bloc fait 128 octets, une future allocation de 96 octets se traduira
par un succès.
La figure suivante montre un autre exemple de l’état de la table d’allocation. Notez la présence
de deux blocs libres de 32 octets. Un bloc est à l’adresse 0x10080, tandis que le second bloc
est en 0x101C0, qui ne peut être utilisé pour une allocation mémoire supérieure à 32 octets.
Comme ces blocs isolés ne peuvent pas participer à la réalisation d’un espace mémoire libre
contigu, leur existence pose des problèmes (impossibilité) en cas de demande d’allocation
mémoire d’une taille supérieure à 32 octets. On parle de fragmentation externe car la
fragmentation existe dans la table mais pas dans les blocs eux-mêmes.
Une façon d’éliminer ce type de fragmentation est de compacter les zones adjacentes à ces
deux blocs. Le contenu de la zone mémoire depuis l’adresse 0x100A0 (immédiatement après
le premier bloc libre) jusqu’à l’adresse 0x101BF (immédiatement précédent le second bloc
libre) est décalé de 32 octets vers le bas de la mémoire, ce qui donne une nouvelle zone allant
de 0x10080 jusqu’à 0x1019F, qui effectivement combine deux blocs libres en un bloc de 64
octets. Ce nouveau bloc libre est encore considéré comme de la fragmentation mémoire si de
2
futures allocations demandent une taille mémoire supérieure à 64 octets. En conséquence, la
compaction mémoire continue jusqu’à ce que tous les blocs libres soient combinés en un large
morceau.
Plusieurs problèmes peuvent apparaitre avec la compaction mémoire. Elle consume du temps
pour transférer les contenus mémoires d’un endroit vers un autre. Le cout de l’opération de
copie dépend de la longueur des blocs contigus utilisés. Les taches qui désire obtenir l’accès
concurrent à ces zones mémoires sont empêchés d’y accéder jusqu’à la fin de la copie. La
compaction mémoire est très rarement utilisée dans les systèmes embarqués. Les blocs
mémoire libres sont combinés seulement s’ils sont des voisins proches.
En conclusion, une gestion mémoire efficace doit réaliser les actions suivantes :
Déterminer si un bloc libre assez grand existe pour satisfaire l’allocation requise. Ce
travail est une partie de la fonction malloc().
Mettre à jour les informations de gestion interne. Ce travail est réalisé par malloc() et
par free().
Déterminer si le bloc qui vient d’être libéré peut être combiné avec les blocs libres de
son voisinage pour former une zone plus large. C’est une partie du travail de la
fonction free().
La structure de la table d’allocation est la clé pour une gestion efficace de la mémoire, car la
structure détermine la manière dont les opérations listées plus tôt peuvent être implémentées.
La table d’allocation occupe de l’espace mémoire, il est important qu’elle soit de faible taille.
3
taille fixe correspondant. Dans ce sens, ce tableau est identique à celui présenté plus haut et
représenté une nouvelle fois ci-dessous, mais ce dernier utilise un schéma de codage différent.
Le nombre d’entrées contenues dans le tableau est le nombre de blocs de taille fixe disponible
dans la zone mémoire gérée. Par exemple, 1 MB de mémoire peut être divisé 32768 blocs de
32 octets chacun. En conséquence, le tableau aura 32768 entrées.
4
Commençons avec l’index du tableau d’allocation débutant à 0. Avant l’allocation d’une zone
mémoire, un bloc libre de grande taille est présent, il contient 12 unités mémoire disponibles.
Le tableau d’allocation utilise un schéma d’encodage simple pour repérer les blocs mémoires
libres et ceux qui sont alloués. Pour indiquer la plage des blocs libres contigus, un nombre
positif est placé dans la première entrée et la dernière entrée pour représenter la plage. Dans
notre exemple, dans le premier tableau présenté à gauche, le nombre d’unités libres (12 dans
notre cas) est placé dans les entrées d’index 0 et d’index 11.
Placer un nombre négatif dans la première entrée et un zéro dans la dernière indique une zone
de blocs alloués. Le nombre placé dans la première entrée est égal à -1 fois le nombre de blocs
alloués.
Dans notre exemple, la première allocation nécessite 3 unités. Le tableau avec le label 1 de la
figure précédente représente l’état du tableau alloué après la première allocation. La valeur -3
est placée à l’index 9 et la valeur 0 est placée à l’index 11, ce qui marque la zone des blocs
alloués. La taille des blocs libres est réduite à 9.
Le tableau portant le label 3 montre l’état du tableau d’allocation après la fin de trois requetés
d’allocations. Cet arrangement du tableau et le marquage des blocs alloués simplifie les
opération de fusion qui prendront place durant l’opération free(), ce qui sera expliqué un peu
plus loin.
Non seulement le tableau d’allocation indique les blocs libres, mais il indique aussi les
adresses de départ de chaque bloc, car il existe une relation simple entre les indices du tableau
et les adresses de départ :
Lors de l’allocation d’un bloc mémoire, malloc() utilise cette formule pour calculer l’adresse
de départ du bloc. Par exemple, dans la figure précédente, la première allocation de trois
unités commence à l’index 9. Si l’offset est 0x10000 et la taille unité du bloc est 0x20 (32 en
décimal), l’adresse retournée pour l’index 9 est :
5
La taille de chaque bloc libre est déterminante dans l’arrangement du tas. Le bloc libre de plus
grande taille est placé au sommet du heap. L’algorithme employé par malloc() découpe
l’allocation du bloc libre de plus grande taille. La portion restante est réinsérée dans le heap.
Puis le heap est réarrangé, ce qui constitue la dernière étape de l’allocation mémoire.
Bien que la taille de chaque zone libre soit déterminante dans l’arrangement du tas, chaque
nœud du heap est en réalité une structure de données contenant au moins deux informations :
la taille de la zone libre et l’index de départ dans le tableau d’allocation. L’opération malloc()
implique les étapes suivantes :
Examiner le heap pour déterminer s’il existe un bloc libre assez grand pour
l’allocation requise.
Si il n’y a pas de blocs libre assez grand, renvoyer une erreur à l’appelant.
Récupérer l’index de départ de la zone libre du tableau d’allocation en partant du
sommet du heap.
Mettre à jour le tableau d’allocation en marquant le bloc récemment alloué, comme
indiqué dans l’avant dernière figure.
Si le bloc entier est utilisé pour satisfaire l’allocation, mettre à jour le heap en effaçant
le plus grand nœud. Sinon, mettre à jour la taille.
Réarranger le heap.
Avant que n’importe quelle mémoire soit allouée, le heap a juste un nœud, ce qui signifie que
la mémoire est disponible (grand bloc libre). Le heap continue d’avoir un nœud unique soit si
la mémoire est allouée consécutivement sans aucune opération free() ou si chaque opération
free() provoque une fusion du bloc dé alloué avec ses voisins immédiats. La structure du heap,
présentée à figure précédente, représente les blocs libres entrelacés avec des blocs utilisés ;
elle est similaire à l’avant avant dernière figure.
Le heap peut être implémenté en utilisant un autre tableau statique appelé heap array
présenté à la figure précédente. Les index du tableau commencent à 1 au lieu de 0 pour
simplifier le codage en langage C. Dans cet exemple, 6 blocs libres de 20, 18, 12,11, 9 et 4
6
blocs sont disponibles. La prochaine allocation mémoire utilise une zone de 20 blocs
indépendamment de la taille requise dans la requête d’allocation. Notez que le heap array est
une manière compacte d’implémenter un arbre binaire. Il ne stocke pas de pointeurs vers les
nœuds enfants ; au lieu de cela, les relations enfants-parents sont indiquées par les positions
des nœuds dans le tableau.
Si l’index de départ du bloc n’est pas 0, vérifier la valeur du tableau à l’index -1. Si la
valeur est positive (donc pas négative ou 0), ce voisin peut être fusionné.
Si l’index + nombres de blocs ne dépasse la valeur maximale de l’index du tableau,
vérifier que la valeur du tableau à l’index + nombres de blocs. Si la valeur est positive,
ce voisin peut être fusionné.
Ces règles sont illustrées à la figure suivante.
Cette figure montre deux situations importantes. Dans la première, le bloc débutant à l’index 3
est libéré. En suivant la règle 1, on regarde la valeur à l’index 2. Cette valeur est 3. Donc, le
bloc voisin peut être fusionné. La valeur 3 indique que le bloc voisin a une taille de 3 unités.
Le bloc qui vient d’être libéré à une taille de 4, donc en appliquant la règle 2, on regarde la
valeur à l’index 7. La valeur vaut -2, donc le bloc voisin est encore utilisé ne peut pas être
fusionné. Le résultat de l’opération free() dans la première situation est montré dans la
seconde table de la figure ci-dessus.
Dans la seconde situation, le bloc à l’index 7 vient d’être libéré. En suivant la règle 1, on
observe que la valeur à l’index 6 est 0. Cette valeur indique que le bloc voisin est encore
7
occupé. En suivant la règle 2, on regarde la valeur à l’index 9, elle vaut -3. Une fois de plus,
cette valeur indique que ce bloc est occupé. Les blocs nouvellement libérés restent des pièces
indépendantes. Après avoir appliqué les deux règles de fusion, la prochaine opération free()
sur le bloc débutant à l’index 3 donne une table d’allocation indiquée sur la droite de la figure
précédente.
Lorsqu’un bloc est libéré, le heap doit également être mis à jour. L’opération free() implique
les étapes suivantes :
Mise à jour du tableau d’allocation et fusion avec les blocs voisins si possible.
Si le nouveau bloc libéré ne peut être fusionné avec un de ses voisins, ajouter une
nouvelle entrée dans le heap array.
Si le nouveau bloc libéré peut être fusionné avec l’un de ses voisins, l’entrée dans le
heap représentant le bloc voisin doit être mise à jour, et l’entrée mise à jour réarrangée
en fonction de sa nouvelle taille.
Si le nouveau bloc libéré peut être fusionné avec tous ses voisins, l’entrée dans le
heap représentant un des blocs voisins doit effacée du heap, et l’entrée du heap
représentant les autres blocs voisins doit être mise à jour réarrangée en fonction de sa
nouvelle taille.
Chaque groupe mémoire possède une structure de contrôle qui maintient les informations
telles que la taille du bloc, le nombre total de blocs, et le nombre de blocs libres. Dans cet
exemple, les groupes mémoires sont liés ensemble et classé par la taille. Pour trouver la plus
petite taille adéquate à une allocation, il faut effectuer une recherche dans la liaison et
examiner chaque structure de contrôle pour le premier bloc de taille adéquate.
Lorsque allocation réussie provoque la suppression d’une entrée du groupe mémoire. Un dé
allocation réussie provoque la réinsertion de l’entrée dans le groupe mémoire. La structure du
groupe mémoire présenté à la figure précédente est une liste liée uniquement. Cette méthode
8
n’est pas aussi efficace que l’algorithme présenté au paragraphe 2 et possède des
inconvénients. Dans les systèmes temps réels embarqués, le besoin mémoire d’une tache
dépend de son environnement. Cet environnement peut être dynamique. Cette méthode ne
fonctionne pas bien pour des applications embarquées qui opèrent de façon constante dans un
environnement dynamique car il est presque impossible d’anticiper les tailles des blocs
mémoires qu’une tache peut couramment utiliser. Ce problème génère l’augmentation de la
fragmentation mémoire par allocation. De plus, le nombre de blocs à allouer pour chaque taille
est aussi impossible à prédire. Dans de nombreux cas, les groupes mémoires ont une
construction basée sur un nombre d’hypothèses. Il en résulte que certains groupe mémoires
sont peu ou pas utilisés, alors que d’autres le sont trop.
D’un autre côté, cette méthode d’allocation mémoire peut vraiment réduire la fragmentation
interne et fournir une utilisation intensive pour les applications embarquées statiques. Ce sont
les applications où l’environnement est prédictible, où le connait le nombre de taches au
démarrage de l’application, et où on connait les tailles des blocs mémoire nécessaires.
Un avantage de cette méthode de gestion mémoire est qu’elle est plus déterministe que
l’algorithme basé sur le heap. Dans la méthode basée sur le heap, chaque opération malloc()
ou free() peut potentiellement déclencher une réorganisation du heap. Dans la méthode des
groupes mémoire, les blocs sont pris ou retourné depuis le commencement de la liste de telle
sorte que les opérations prennent un temps constant. La méthode de gestion par groupe
mémoire ne nécessite pas de restructuration.
9
Comme le montre la figure suivante, une fonction d’allocation mémoire bloquante peut être
implémentée en utilisant à la fois un sémaphore à compte et un mutex. Ces primitives de
synchronisation sont créées pour chaque groupe mémoire et conservées dans une structure de
contrôle.
Le sémaphore à compte est initialisé avec le nombre total de blocs mémoire disponibles lors
de la création du groupe mémoire. Les blocs mémoires sont alloués et libérés à partir de début
de la liste.
De multiples taches peuvent accéder à la liste des blocs libres des groupes mémoire. La
structure de contrôle est mise à jour chaque fois qu’une allocation ou dé allocation apparait.
En conséquence, le verrouillage du mutex est utilisé pour garantir à la tache un accès exclusif
à la liste des blocs libres et à la structure de contrôle. Une tache peut attendre qu’un bloc soit
disponible, acquérir le bloc, puis continuer son exécution. Dans ce cas, un sémaphore à
compte est utilisé.
Pour qu’une requête d’allocation réussisse, la tache doit d’abord acquérir avec succès le
sémaphore à compte, puis acquérir avec succès le mutex.
L’acquisition du sémaphore à compte réserve un morceau de blocs disponible depuis le pool.
Une tache tente d’abord s’acquérir le sémaphore à compte. Si aucun bloc est disponible, la
tache se bloque sur le sémaphore à compte, en supposant que la tache est conçue pour
l’attendre. Si une ressource est disponible, la tache réussit à acquérir le sémaphore à compte.
La valeur du sémaphore à compte est maintenant décrémentée d’une unité. A ce niveau, la
tache a réservé un ensemble de blocs disponibles mais doit cependant obtenir le bloc.
Ensuite, la tache tente de verrouiller le mutex. Si une autre tache est en train d’acquérir un bloc
en dehors du pool ou si une autre tache est en train de libérer un bloc à destination du pool, le
mutex est dans l’état verrouillé. Les taches sont dans l’état bloqué en attendant la libération du
mutex. Après que la tache verrouille le mutex, la tache retire la ressource de la liste. Le
sémaphore à compte est relâché lorsque la tache finit d’utiliser le bloc mémoire.
10
Le pseudo code pour l’allocation mémoire avec sémaphore à compte et mutex est présenté ci-
dessous.
Le pseudo code pour la dé-allocation mémoire avec sémaphore à compte et mutex est
présenté ci-dessous.
Les deux pseudos code précédents sont des fonctions d’allocation mémoire et dé-allocation
fiables pour du traitement multitâches. L’utilisation du sémaphore à compte et du mutex
élimine les problèmes d’inversion de priorité lorsqu’un blocage d’allocation mémoire est
validé avec ces primitives de synchronisation.
11