Le langage C:

Allocateur de mémoire



Par :

Guilhem de Wailly (gdw@erian-concept.com)



Résumé

Voici le dernier article de cette longue série de 24 qui a durée un peu plus de deux années. Nous finissons notre allocateur de mémoire en C avec les dernière fonctions qui restaient en suspens.

En effet, ma nouvelle fonction chez Tornado Technology ne permet plus de soutenir le rythme de deux articles par mois. J'espère sincèrement que ces articles auront permis à certains de découvrir le langage C de manière pratique et donné le goût à l'écriture de logiciels en C.

Je remercie tous les lecteurs qui m'ont soutenu et corrigé les (quelques :-) erreurs qui se sont glissées de temps en temps. Je remercie aussi Linux Magazine, son rédacteur Denis Bodor et toute l'équipe d'ouvrir leurs colonnes de cette manière et d'avoir la patience de relire, corriger et mettre en forme nos textes.

Il me reste maintenant à rassembler tout ce matériel en un livre qui sera placé sur le WEB et disponible librement pour tous.

Un hommage maintenant : ces articles ont été écrits sur un portable sous Linux Slackware (www.slackware.com), en utilisant la suite bureautique Applixware (www.applixware.com) qui à su, sans broncher, transcrire mon blabla. La correction grammaticale a été assurée par Corector101 (www.machinasapiens.com) qui laisse loin derrière les autres correcteurs. L'ensemble de ces trois outils revient à moins de 1000 francs, soit 152,43 euro, ou 20 places de cinéma. Deux choses : je n'ai aucune actions ni chez l'un ni chez l'autre, et je ne vois pas pourquoi je ne dirais pas ce que j'en pense car il est temps de faire savoir que l'on peut travailler correctement dans un environnement Linux, même en bureautique. Ces éloges ne diminuent cependant en rien la qualité des autres solutions, libres ou non.

Quant à moi, je continue les articles sur le langage Scheme (sauf ce mois-ci) en finissant le serveur WEB, puis j'entamerai une nouvelle série d'articles destinée à présenter la 3D en Scheme, en utilisant scrupuleusement la norme R4/5RS de manière à ce que le code soit exécutable par la plupart des environnements Scheme, dont OpenScheme.

Description

Le mois dernier, nous avons présenté un allocateur de mémoire dynamique qui permet de gérer la ressource 'mémoire' de manière optimale. Il nous reste à terminer cet allocateur en réalisant la fonction de libération de blocs, et les fonctions d'allocation secondaires, basées sur la fonction d'allocation présentée le mois dernier.

Nous montrerons ensuite comment utiliser cette bibliothèque de manière automatique à l'aide d'un jeu de macros.

Réalisation de la bibliothèque

Libération

La libération des blocs alloués consiste à permettre au bloc d'être à nouveau alloué en le replaçant dans la liste des blocs libres. Si on n'y prend pas garde, la mémoire va se trouver morcelée en blocs de plus en plus petits au fil de son utilisation. La fonction de désallocation essaye de palier à cet inconvénient en vérifiant si le bloc désalloué ne peut pas être associé à un bloc libre placé avant ou après lui, ou les deux :

/* Libération d'un bloc de mémoire. */
void heap_free (char * pointer) {
Header p, q;

/* Si le pointeur est nul, provoquer
* une erreur. C'est le comportement
   * standardisé par l'ANSI. */
if (! pointer) {
abort();
}
/* Supprimer le pointeur du tableau
* de pointeurs */
_debug_heap_del (pointer);

/* Récupérer l'entête */
p = (Header) pointer - 1;
_current -= p->s.size;

  /* Recherche du bloc libre situé juste
   * avant le bloc à libérer */

for (q =_allocp;
! (p > q && p < q->s.ptr);
q = q->s.ptr) {
if (q >= q->s.ptr &&
(p > q || p < q->s.ptr)) {
break;
}
}
  /* si les blocs sont adjacents, les
   * joindre */

if (p + p->s.size == q->s.ptr) {
p->s.size += q->s.ptr->s.size;
p->s.ptr = q->s.ptr->s.ptr;
}
else {
p->s.ptr = q->s.ptr;
}
if (q + q->s.size == p) {
q->s.size += p->s.size;
q->s.ptr = p->s.ptr;
}
else {
q->s.ptr = p;
}
_allocp = q;
}

La fonction de libération recherche dans la liste des blocs libres le bloc situé juste avant le bloc à libérer. Elle tente ensuite de relier le bloc à libérer aux blocs adjacents situés avant et après lui de manière à limiter le morcellement de la mémoire.

Allocation avec initialisation à zéro

Voici maintenant la réalisation de la fonction équivalente à calloc :

/* Équivalent de la fonction calloc */
char * heap_calloc (unsigned size,
unsigned number) {
/* Calculer la taille demandée */
unsigned s = size * number;

/* Allouer de la mémoire non initialisée */
char * p = (char *) heap_malloc (s);

if (p) {
/* Initialiser la mémoire */
memset (p, 0, s);
}
/* Retourner le pointeur */
return (p);
}

Cette fonction se contente de calculer la taille du bloc, de l'allouer et de l'initialiser à zéro.

Réallocation

Notre fonction de réallocation est une version simplifiée qui, en fait, alloue toujours un nouvel objet au lieu de tenter d'agrandir la taille du bloc déjà alloué :

/* Équivalent de la fonction realloc */
char * heap_realloc (char * pointer,
unsigned size) {
unsigned old;
char * ptr = heap_malloc (size);

if (pointer) {
/* Calculer la taille de l'ancien bloc */
old = ((Header) pointer - 1) -> s.size;
memcpy (ptr, pointer, size > old ? old : size);
}
/* Retourner le pointeur */
return (ptr);
}

Il faudrait que cette fonction vérifie la présence d'un espace libre suffisamment grand situé juste après le bloc. Si cet espace existe, l'espace libre est scindé en deux et la taille du bloc mise à jour. Si cet espace n'existe pas, la fonction d'allocation est invoquée et un nouveau bloc est retourné, comme c'est le cas maintenant.

Taille d'un bloc

Cette fonction permet d'obtenir la taille d'un bloc de mémoire. Cette fonction n'a pas d'équivalence dans la norme ANSI :

/* Retourne la taille d'un bloc. */
unsigned heap_size (char * pointer) {
if (! pointer) {
return (0);
}
return (((Header) pointer - 1) -> s.size);
}

Obtention de statistiques

Cette fonction permet d'obtenir les statistiques concernant l'utilisation de la mémoire :

/* Retourne les statistiques concernant
* le tas. */
void heap_stats (unsigned * current,
unsigned * maximum,
double * total,
unsigned * mallocs) {
(* current) = _current;
(* maximum) = _maximum;
(* total) = _total;
(* mallocs) = _mallocs;
}

Les valeurs retournées sont placées dans les arguments pointeurs. On retourne le nombre d'octets actuellement alloués, le maximum d'octets qui furent alloués simultanément, le cumul du nombre d'octets alloués et le nombre d'allocations effectuées.

Ces statistiques permettent notamment de savoir s'il reste des blocs alloués et non libérés.

Utilisation

Pour utiliser les fonctions de cette bibliothèque de manière transparente à la place des fonctions standards, il est possible de modifier légèrement le fichier d'entête :

#ifndef ___HEAP_H
#define ___HEAP_H


void heap_init (unsigned heap_size);
void heap_end (void);
char * heap_malloc (unsigned nbytes);
char * heap_calloc (unsigned size,
unsigned number);
char * heap_realloc (char * pointer,
unsigned size);
void heap_free (char * pointer);
unsigned heap_size (char * pointer);
void heap_stats (unsigned * current,
unsigned * maximum,
double * total,
unsigned * mallocs);

#ifdef HEAP_DEBUG
# define malloc(n) heap_malloc(n)
# define calloc(s,n) heap_calloc ((s), (n))
# define realloc(p,s) heap_realloc ((p), (s))
# define free(p) heap_free(p)
#endif /* HEAP_DEBUG */

#endif /* ___HEAP_H */

L'inclusion de ce fichier sera faite avant toute autre inclusion. Lorsque la macro DEBUG_HEAP sera définie, en ajoutant l'option -D DEBUG_HEAP sur la ligne de commande de gcc, nos fonctions d'allocation seront utilisées en lieu et place des fonctions standards.

Améliorations

On pourra modifier notre fonction d'allocation pour que des mots magiques soient placés juste avant et juste après le bloc de mémoire retourné à l'utilisateur. La fonction de libération devra vérifier que les mots magiques sont toujours présents ; si ce n'est pas le cas, le programme utilisateur aura commit une erreur mémoire et si c'est le cas, on est sûrs de rien.


Figure 1: Utilisation de mots magiques pour contrôler les débordements dans l'écriture du bloc de mémoire.

Ce contrôle est partiel et ne détecte que les débordements en écriture et non en lecture. Pour effectuer des vérifications strictes, on pourra utiliser la bibliothèque ElectricFence de Bruce Perens (www.perens.com) qui détecte tout débordement tant à la lecture qu'à l'écriture.

L'allocation de blocs de taille constante est plus simple et plus efficace que l'allocation de blocs de taille variable. On pourra modifier l'allocateur pour que pour les tailles allant, par exemple, de 1 à 100 octets, une gestion spécifique soit réalisée de manière à optimiser l'allocateur.

Voila maintenant terminé notre allocateur de mémoire simpliste.

Il me reste à vous remercier de vos lectures attentives et à vous souhaiter bonne route ...

L'auteur

Guilhem de Wailly, gérant de la société Erian Concept, partenaire NetUltra (www.netultra.net) et Getek (www.getek.fr) et Vice-Président R&D de Tornado Technology (www.aecviz.com).

Votre interlocuteur Linux !

Support, formations, configurations, administration, développements Linux.

http://www.erian-concept.com

Références

Langage C - norme ANSI : bon ouvrage de référence
Ph. DRIX
Masson

Programmer en C++: une bonne référence
S.C. DEWHURST et K.T. STARK
Masson

Le Langage C: la bible!
B.W. KERNIGHAN et D.M. RITCHIE
Masson

gcc: compilateur C du GNU, et tous les outils de développement
http://www.gnu.org

Kdevel: un environnement KDE de programmation C
http://samuel.cs.uni-potsdam.de/~smeier/kdevelop_new/index.html