Novembre 2020
Alexandra Demski
L'objectif principal du projet est d'implémenter mes propres fonctions d'allocation dynamique malloc, free, calloc et realloc en utilisant les appels système permettant de manipuler la limite supérieure du tas.
int brk(void *addr);void *sbrk(intptr_t increment);Le projet va inclure les fonctions de base ainsi qu'un outil de debug.
xxxxxxxxxxvoid* malloc(size_t size);void* realloc(void *ptr, size_t size);void* calloc(size_t nmemb, size_t size);void free(void *ptr);void print_stats();Chaque bloque alloué comprendra une entête contenant toutes les informations administratives nécessaires au bon fonctionnement de malloc ainsi qu'un espace de données pour stocker des informations.
L'algorithme d'allocation va, dans un premier temps, chercher un bloque alloué libre dont la taille est suffisante. En cas de succès, il retourne l'adresse de la zone et indique le bloque comme utilisé, sinon, en cas d'échec, il crée un nouveau bloque en utilisant sbrk.
Avec cette implémentation minimaliste, désallouer un bloque en mémoire avec la fonction free va seulement changer son statut d'utilisé à libre.
L'outil de debug print_stats affichera des informations utiles comme le nombre total de bloque alloué, utilisé et libre, leur taille et la quantité d'espace gâché.
Le projet est divisé en plusieurs fichier. La majeure partie du code est stockée dans memory.c qui possède également un header memory.h incluant les fonctions. Les nombreux fichiers test.c sont utilisés pour confirmer l'efficacité de la librairie. Le dernier fichier appelé Makefile permet tout simplement de compiler tout le code : on utilisera la commande make pour créer tous les fichiers executables nécessaires.
Un bloque de mémoire possède des informations utiles qui seront toutes stockées dans un header.
xxxxxxxxxxstruct block { size_t size; size_t used_size; struct block *next; struct block *previous; int free;};Cette structure de données fonctionne comme une liste chaînée : chaque bloque va pointer vers le précédent et le suivant. Le premier bloque de la liste sera appelé first et initialisé à NULL puisque malloc n'a pas encore été appelé.
xxxxxxxxxxvoid *first = NULL;La première étape avant d'allouer un bloque est d'aligner la taille demandée. Cela permet de rendre le programme plus rapide et efficace. Ainsi, dans la fonction malloc nous allons appeler la méthode align qui retourne un multiple de 4 de la taille donnée.
xxxxxxxxxxsize_t align(size_t size){ size_t n = size; if(!multiple4(n)){ return ((n/4)+1)*4; } return n;}La fonction multiple4 vérifie seulement si la taille donnée n'est pas déjà un multiple de 4 et qui donc n'a pas besoin d'être alignée.
xxxxxxxxxxint multiple4(size_t n){ int x = (int)n; while(x > 0){ x = x-4; } if(x == 0){ return 1; } return 0;}Bien évidemment, après avoir aligné la taille, on vérifie si elle est inférieure ou égale à zéro pour ainsi éviter tout comportement anormal.
Après cela, on distingue deux possibilités : on utilise malloc pour la première fois et nous avons donc pas besoin de chercher de bloque libre ou on a déjà appelé malloc et on doit, par conséquence, parcourir la liste chaînée.
xvoid *_malloc (size_t size) { struct block *b; size_t s; s = align(size); if(s <= 0) return NULL; if(!first){ b = _new_block(NULL,s); if(!b) return NULL; first = b; } else { struct block *last = first; b = _find_block(&last, s); if(!b){ b = _new_block(last, s); if(!b) return NULL; } else { b->free = 0; b->used_size = s; } } return(b+1);}La fonction _new_block déplace le program break avec l'appel système sbrk et retourne le nouveau bloque créé.
xxxxxxxxxxstruct block *_new_block(struct block* last, size_t size){ struct block *b; b = sbrk(0); void *ptr = sbrk(size + META_SIZE); if(ptr == (void*)-1) return NULL; if(last){ last->next = b; b->previous = last; } b->size = size; b->next = NULL; b->free = 0; b->used_size = size; return b;}La fonction _find_block cherche à travers la liste chaînée le bloque le plus optimal. Si la fonction n'en trouve pas, elle retourne NULL.
xxxxxxxxxxstruct block *_find_block(struct block **last, size_t size){ struct block *current = first; struct block *optimal = NULL; while(current){ if(current->free && current->size >= size){ if(optimal){ if(optimal->size > current->size){ optimal = current; } } else { optimal = current; } } *last = current; current = current->next; }}Puisqu'il est impossible de désallouer l'espace mémoire avec sbrk la fonction free va seulement indiquer qu'un bloque est libre. Avant d'appeler la fonction, il est important de vérifier le pointeur passé en paramètre pour éviter des erreurs.
xxxxxxxxxxvoid _free(void *ptr) { if(!ptr) return; struct block *b; b = _block_ptr(ptr); b->free = 1; b->used_size = 0;}La fonction _block_ptr retourne l'adresse de l'espace alloué pour les données, sans l'en-tête.
xxxxxxxxxxstruct block *_block_ptr(void *ptr){ return (struct block*)ptr - 1;}La fonction calloc utilisera notre mallo pour allouer un bloque et mettre ses bits à 0 avec la fonction memset.
xxxxxxxxxxvoid *_calloc(size_t nmemb, size_t size){ size_t s = nmemb * size; void *ptr = malloc(s); memset(ptr, 0, size); return ptr;}Tout comme calloc la fonction realloc va simplement appeler les fonctions malloc et free pour, tout d'abord, copier les données du bloque passé en paramètre vers un nouveau avec la fonction memcpy, puis libérer ce même bloque. Il est important de vérifier le pointeur passé en paramètre pour éviter toute erreur.
xxxxxxxxxxvoid *_realloc(void *ptr, size_t size){ if(!ptr) return malloc(size); struct block *b_ptr = _block_ptr(ptr); if(b_ptr->size >= size) return ptr; void *new_ptr; new_ptr = malloc(size); if(!new_ptr) return NULL; memcpy(new_ptr, ptr, b_ptr->used_size); free(ptr); return new_ptr;}Pour améliorer la librairie, des fonctionnalités supplémentaires ont été ajoutées. En effet, notre librairie génère beaucoup de fragmentation et pour limiter son impact j'ai implémenté deux améliorations.
Avant qu'un bloque libre soit alloué par la fonction _find_block, si sa taille est suffisamment grande, le bloque sera divisé en deux pour générer un nouveau bloque ajouté à la liste chaînée.
xxxxxxxxxxif(optimal && (optimal->size >= (size + META_SIZE + 4))){ struct block *next, *new; next = optimal->next; new = _block_ptr(optimal) + size; new->next = next; optimal->next = new; new->previous = optimal; if(next != NULL){ next->previous = new; } new->size = optimal->size - size - META_SIZE; optimal->size = size; new->free = 1; new->used_size = 0;}L'amélioration complémentaire de la fonctionnalité précédente va fusionner les voisins d'un bloque à sa libération si eux aussi sont libres. Cette fonctionnalité fonctionne de façon similaire que la précédente mais au lieu d'ajouter un nouvel élément à la liste, on en supprime en les fusionnant.
xxxxxxxxxxstruct block *n, *p;n = b->next;p = b->previous;if(n && n->free){ b->next = n->next; b->size = b->size + n->size + META_SIZE;}if(p && p->free){ p->next = b->next; p->size = p->size + b->size + META_SIZE;}