malloc()
, free()
, calloc()
et realloc()
. Vous avez évidemment étudié et utilisé ces fonctions, ou au moins les deux premières; si vous avez quelques lacunes, un coup de man -S 3 malloc
vous renseignera.
La réalisation de telles fonctions peut être extrêmement dépendante du système d'exploitation pour être efficace. Pour ce TP sous Linux, on s'appuiera sur la fonction sbrk()
: lisez la page de manuel correspondante. Celle-ci est jugée obsolète dans les versions récentes de la norme POSIX
: à la place, on recommande malloc()
, qui est plus flexible. C'est-à-dire que quand on utilise la bibliothèque standard pour écrire des programmes portables, on doit utiliser malloc()
et pas sbrk()
.
Dans ce TP, cependant, l'objectif est différent: on veut réécrire des fonctions de la bibliothèque standard; il est donc légitime d'utiliser des fonctions de bas niveau. La fonction sbrk()
repose sur l'appel système le moins coûteux et le plus simple sous Linux pour demander de la mémoire au système: brk()
. Ces deux fonctions sont déclarées dans l'en-tête unistd.h
. Consultez la page de manuel de ces fonctions avant de commencer: man -S 2 brk
.
malloc()
, free()
et compagnie, même indirectement, au risque d'introduire une dépendance cyclique. C'est le cas par exemple des fonctions de la bibliothèque stdio.h
(printf()
et consorts). Pour afficher des messages, on utilisera plutôt les fonctions définies dans les fichiers affiche.h
et affiche.c
fournis. sbrk()
et celle qui déclare les fonctions d'affichage: #include <unistd.h> #include "affiche.h"
Vous coderez votre allocateur dans des fichiers alloc.h
et alloc.c
. Le fichier d'en-têtes alloc.h
ne contiendra pas forcément les déclarations de toutes les variables et fonctions demandées ci-dessous, seulement celles qui doivent être accessibles à l'utilisateur pour:
malloc()
et ses amis;
En plus de l'allocateur, vous écrirez quelques programmes de test simples, en essayant de mettre en évidence des cas dans lesquels des stratégies d'allocation différentes produisent des effets différents. Les sources de ces tests sont à joindre à votre projet. Vous fournirez un Makefile
permettant de compiler ces programmes de test facilement.
Testez également la compatibilité votre code avec des programmes qui n'ont pas été explicitement écrits pour ce TP, mais qui utilisent malloc()
, free()
, calloc()
et/ou realloc()
. Par exemple vous pourrez récuperer les sources de programmes en C qui utilisent l'allocation dynamique, que vous avez pu étudier ou écrire vous même au cours de votre cursus.
Vous fournirez tous les fichiers sources (*.c
, *.h
et Makefile
) de votre projet dans une archive obligatoirement au format tar.gz
ou tar.bz2
et avec un nom explicite: par exemple INFO-502_TP3_Nom1-Nom2.tar.gz
pour un binôme Nom1-Nom2. Cette archive est à transmettre par e-mail à votre encadrant de TP: Thibault Carron, Pierre Hyvernat ou Lionel Vaux, à l'adresse habituelle ( Nom.Prenom
@
univ-savoie.fr
).
La notation prendra en compte la correction de votre code, évidemment, mais également sa lisibilité et sa modularité. L'écriture de toute fonction auxiliaire qui vous parait utile est encouragée.
Toute erreur à la compilation sera lourdement sanctionnée. Les options du compilateur renseignées dans le Makefile
doivent comprendre au moins -Wall
et -Werrors
. C'est-à-dire qu'un avertissement sera également considéré comme une erreur.
taille
, on cherche dans la liste un bloc libre de taille au moins taille
. Le bloc obtenu dépend de la stratégie utilisée: on considèrera les stratégies first fit, best fit, worst fit ou next fit. Si aucun bloc libre n'est assez grand, on fait un appel à sbrk()
pour créer un nouveau bloc libre. La liste des blocs libres est maintenue dans l'ordre croissant des adresses.Chaque bloc est muni d'un en-tête qui renseigne sur la taille de la zone allouée. De plus, si le bloc est libre, on stocke les adresses des blocs libres précédent et suivant: la liste des blocs libres est cyclique et doublement chaînée. On partira des définitions de types suivantes:
struct alloc_entete { size_t taille ; struct alloc_entete *prec ; struct alloc_entete *suiv ; } ; typedef struct alloc_entete *alloc_bloc_t ;
alloc_premier_bloc_libre
et alloc_bloc_suivant_pour_nextfit
de type alloc_bloc_t
.
size_t
est normalement défini par l'inclusion de l'en-tête unistd.h
mentionné plus haut.alloc_bloc_t alloc_cherche_firstfit(size_t taille) ; alloc_bloc_t alloc_cherche_bestfit(size_t taille) ; alloc_bloc_t alloc_cherche_worstfit(size_t taille) ; alloc_bloc_t alloc_cherche_nextfit(size_t taille) ;
taille
obtenu en suivant la stratégie correspondante, ou NULL
s'il n'y en a pas. alloc_bloc_t b ;
Le pointeur b
représente un bloc d'allocation composé d'un en-tête et d'une zone allouée. Cette zone contient exactement b->taille
octets.
Notez que l'adresse de la zone mémoire allouée n'est pas renseignée directement dans la structure struct alloc_entete
: l'idée est que cette zone commence juste après le champ taille
du struct alloc_entete
correspondant, c'est à dire à l'adresse de b->prec
. Cette méthode est contre-intuitive: la zone de données empiète sur les champs du struct alloc_entete
. Elle est cependant efficace: on ne perd que la taille d'un size_t
(ou un peu plus suivant les exigences d'alignement de l'architecture) dans l'en-tête d'un bloc occupé. C'est-à-dire que la taille en mémoire du bloc lui-même serait plutôt sizeof(b->taille)+b->taille
, considérations d'alignement mises à part. Dans un bloc libre, la zone de données n'est pas utilisée; on a donc toute latitude pour l'occuper à des fins internes, avec les champs prec
et suiv
.
On a donc les deux situations suivantes pour la représentation des blocs en mémoire.
b
est occupé (la zone a été attribuée par un malloc()
):
Les champs b->prec
et b->suiv
sont masqués par la zone de données: on ne s'en sert pas de toute façon, puisque le bloc n'est plus dans la liste des blocs libres.
b
est libre:
En particulier, il est nécessaire que la taille
soit toujours suffisante pour stocker au moins les deux pointeurs prec
et suiv
: cette remarque sera importante par la suite.
void *alloc_bloc_mem(alloc_bloc_t b) ;
b
(en supposant qu'il est alloué). Par définition, c'est pareil que l'adresse de b->prec
. alloc_bloc_t alloc_bloc_entete(void *p) ;
p
(c'est l'inverse de la précédente): si b
est de type alloc_bloc_t
, alors on doit avoir b==alloc_bloc_entete(alloc_bloc_mem(b))
; réciproquement, si p
est de type void *
, alors p==alloc_bloc_mem(alloc_bloc_entete(p))
. typedef enum { ALLOC_FIRSTFIT, ALLOC_BESTFIT, ALLOC_WORSTFIT, ALLOC_NEXTFIT } alloc_strategie_t ;
alloc_strategie_courante
de ce type.
Quand on veut allouer une zone pour l'utilisateur, il faut d'abord chercher un bloc libre dans lequel l'insérer (ou en fabriquer un nouveau). Puis on sépare éventuellement le bloc en deux, si la taille demandée par l'utilisateur est sensiblement inférieure à la taille du bloc libre trouvé. On ajoutera une variable globale alloc_taille_min
qui définit la taille minimale d'un bloc libre: si pour couper un bloc libre en deux (pour donner la première partie à l'utilisateur), il fallait créer un bloc de taille inférieure, on donnerait plutôt tout le bloc libre initial à l'utilisateur. On s'assurera du fait que alloc_taille_min
est toujours au moins suffisante pour stocker les deux pointeurs prec
et suiv
d'un bloc libre.
void *alloc_occupe(alloc_bloc_t cible, size_t taille) ;
cible
est libre et de taille au moins taille
effectue les opérations suivantes: cible
en deux blocs contigus si c'est pertinent; alloc_bloc_mem()
évidemment). void *malloc(size_t taille) ;
taille
suivant la stratégie définie par alloc_strategie_courante
; sbrk()
pour obtenir au moins autant de mémoire que nécessaire (on pourra demander plus, si on pense cette méthode plus efficace: penser à tester) et construit un bloc libre avec cette nouvelle zone de mémoire; alloc_occupe()
; alloc_bloc_suivant_pour_nextfit
. void *calloc(size_t nmemb, size_t size) ;
Si le bloc qu'on libère est le dernier bloc alloué (juste avant la limite du segment de données obtenue avec sbrk(0)
), on est dans un cas spécial: on peut restituer la mémoire libérée au système, plus tôt que de créer un bloc libre.
void alloc_fusion(alloc_bloc_t b) ;
b
est dans la liste des blocs libres, fusionne b
avec b->suiv
s'ils sont contigus. void free(void *p) ;
b
de l'en-tête du bloc dont les données commencent au pointeur p
; b
dans la liste des blocs libres; sbrk()
avec un argument négatif et restituer de la mémoire au système quand on libère le dernier bloc: dans ce cas, on n'insère pas ce bloc dans la liste des blocs libres. malloc()
et free()
, on peut facilement écrire realloc()
de manière naïve: allouer un nouveau bloc, copier les données, et libérer l'ancien bloc. Évidemment, on peut faire mieux dans certains cas: sbrk()
, sans riquer d'écraser un autre bloc alloué. void *realloc(void *ptr, size_t taille) ;
malloc()
et un appel à free()
que si c'est nécessaire. alloc_seuil_scinde
). sbrk()
et on augmente la taille de la zone allouée du dernier bloc. sbrk()
; malloc()
ou les cas pertinents de realloc()
); sbrk()
. void alloc_info() ;
Souvenez-vous que cette fonction ne peut pas utiliser printf()
ni les fonctions de stdio.h
en général, seulement les fonctions de affiche.h
et celles que vous avez vous-même définies.
Bien que cette tâche figure en dernière position, vous devriez avoir le réflexe de faire le premier item de ce qui suit avant même qu'on vous le demande. On ne le rappelle que pour enfoncer le clou.
alloc.h
et affiche.h
. Assurez-vous que votre allocateur n'a pas un comportement aberrant. printf()
. Regardez ce que donnent vos statistiques.