Réécriture de malloc

Table des matières

Partie obligatoire

Réécrire les fonctions malloc, free, et realloc.

Écrire aussi une fonction show_alloc_mem pour afficher les allocations de mémoire.

Comme le vrai malloc, la mémoire allouée doit être stockée dans des “zones” pré-allouées (bacs), sauf si la taille demandée est trop grande, auquel cas l’allocation doit être fitted.

Note

Les allocations fitted sont des allocations qui sont en dehors d’un bac, ce qui signifie qu’elles sont allouées directement avec un appel à mmap.

Deux types de zones sont nécessaires :

La taille des zones doit être un multiple de la taille de page, déterminée par sysconf(_SC_PAGESIZE). Les allocations (et les allocations internes requises) doivent être exécutées avec mmap, les désallocations avec munmap.

Partie Bonus

Explication du code de la partie obligatoire

Légèrement inspiré du malloc de la glibc de Debian.

Important

Lorsque les mots relatifs à l’allocation sont mis entre guillemets, ça signifie qu’il ne s’agit pas nécessairement d’une allocation. L’attribution d’une partie d’un bac n’est pas une allocation ; une allocation fitted est, elle, bien une allocation.

Puisque la plupart des explications ne sont pas liées à des allocations de mémoire réelles faites avec mmap, elles s’appliquent à des “allocations” bac ou fitted. Le fait qu’il s’agisse d’une attribution ou d’une allocation ne change pas la façon dont elle est organisée.

Explication brève de l’alignement mémoire

L’ordinateur lit la mémoire par sections de 4/8 octets, ou 32/64 bits ; ça détermine le nom de l’architecture.

Aligner la mémoire signifie placer les variables au début des sections de 4/8 octets. Si ce n’est pas respecté, ça cause :

En prenant ça en compte, c’est pourquoi les compilateurs alignent automatiquement la mémoire lorsque c’est possible en remplissant la mémoire avec des char. Lorsque les champs d’une structure ne sont pas positionnés correctement ou que la taille totale d’une structure n’est pas un multiple de 4/8, elle prendra plus de mémoire qu’elle ne le devrait, à cause du remplissage.

Cependant, lors de l’attribution “libre” de valeurs en mémoire, il est nécessaire de faire attention à l’alignement afin d’éviter des ralentissements inutiles.

Chunks

Chaque “allocation” de mémoire (bac ou fitted) est précédée d’un en-tête. La combinaison d’une “allocation” de mémoire (données) et de son en-tête s’appelle un chunk.

L’en-tête contient les informations suivantes :

Pour un total de 16/32 octets sur les architectures 32/64 bits. La structure elle-même est alignée sur la mémoire, le début des données est donc également aligné. La taille des données est toujours fixée à un multiple de 16, ce qui force l’alignement de la mémoire sur toutes les architectures.

La taille des données est souvent supérieure à ce qui a été demandé, afin de maintenir l’alignement de la mémoire. La partie des données qui correspond à la taille demandée par l’utilisateur (taille réelle) est appelée données fournies, le reste est appelé queue.

Pour résumer, voici comment un chunk est organisé en mémoire :

SectionDonnées stockéesType de données
En-têteTaille des données (size)unsigned long
En-têteTaille des données fournies (real_size)unsigned long
En-têtePointeur sur le chunk suivant (next)void *
En-têtePointeur sur le chunk précédent (prev)void *
DonnéesDonnées fourniesPas pertinent
DonnéesQueuePas pertinent

La taille des données fournies n’est utilisée que pour les informations de debug et pour show_alloc_mem.

Avantages et inconvénients

L’avantage de l’overhead plutôt qu’un carnet d’adresses exporté est qu’il est plus rapide de récupérer les données liées à un chunk avec un pointeur (dans free ou realloc par exemple), puisqu’il suffit de soustraire la taille de l’en-tête du pointeur pour récupérer son en-tête.

Il prend également moins de place puisqu’il ne contient pas de pointeurs sur les données.

Il nécessite également moins d’appels à mmap (qui sont assez lents) puisqu’il n’est pas alloué ailleurs ; les données et l’en-tête sont alloués en un seul appel.

L’inconvénient est qu’en cas d’écriture en dehors des limites des données fournies, ça pourrait corrompre les données d’en-tête et donc ruiner malloc… Mais personne n’est censé écrire en dehors des limites, n’est-ce pas ?

Bacs

De la même manière qu’un chunk, les bacs ont un en-tête qui les précède. Les seules différences sont que la taille correspond à la taille complète de la zone, en-tête inclus, que le champ de taille réelle n’est pas utilisé, et que les pointeurs pointent vers les bacs au lieu des chunks.

malloc

Lors de l’appel à malloc, si la taille n’est pas trop élevée pour un bac (auquel cas il effectue simplement une allocation fitted), malloc va itérer dans le bon bac (correspondant à la taille demandée) et trouver un chunk libre de la bonne taille. Ça permet d’éviter la fragmentation des bacs. S’il atteint la fin d’un bac et qu’il reste suffisamment d’espace dedans, il attribue un chunk dans le bac. S’il n’y a pas assez d’espace dans le bac, il en crée un nouveau.

Si la taille totale de plusieurs chunks contigus libre correspond à la taille du nouveau chunk, ils seront réunis en un seul chunk, encore une fois pour éviter la fragmentation.

free

Lors de l’appel à free, si la taille n’est pas trop élevée pour un chunk (il suffit alors de unmap le chunk et de modifier les pointeurs next/prev), il vérifiera si le chunk fourni se trouve à la fin d’un bac. Si c’est le cas, le chunk sera désattribué, puis il itère vers l’arrière pour trouver des chunks contigus afin de les désattribuer. Ça permet d’éviter la fragmentation du bac.

Si le bac est vide, il est désalloué pour réduire l’empreinte mémoire du programme.

realloc

Lors de l’appel à realloc, si la taille n’est pas trop élevée pour un bac (ce qui sera abordé dans quelques lignes), quelques vérifications sont effectuées :

Dans les deux cas précédents, si le chunk se trouve à la fin d’un bac, le chunk sera étendu (s’il reste assez d’espace) ou tronqué pour s’adapter à la nouvelle taille.

De la même manière, si la taille est trop grande pour un bac (fitted), il essaiera d’étendre ou de réduire le chunk avec mmap. Si ça fonctionne, la taille du chunk et la taille réelle sont modifiées en conséquence et le pointeur fourni est retourné. Si ça ne fonctionne pas, il copiera les données du chunk dans le nouveau chunk et retournera un pointeur vers les données du nouveau chunk.

Explication du code de la partie bonus

Aucun des bonus ne nécessite vraiment d’explication, à part éventuellement la dé-fragmentation.

La prévention de la fragmentation est déjà couverte dans la partie obligatoire (honnêtement, pour faire ce bonus, il est recommandé de coder la partie obligatoire en pensant dès le départ à la dé-fragmentation).

Rendre la librairie thread-safe n’est pas si difficile ; l’implémentation la plus simple est de verrouiller/déverrouiller un mutex avant chaque appel des fonctions exposées. Ça pourrait éventuellement être optimisé, mais ce serait pour un gain minime à mon échelle.

Parce que c’est plus sympa, j’ai ajouté des macros pour exclure la logique thread-safe au moment du build lorsqu’elle n’est pas nécessaire.

J’ai également ajouté des informations de debug sur show_alloc_mem ajoutables lors du build avec un flag spécifique, il imprime un dump des chunks, ainsi que plus de détails sur les tailles des chunks et des bacs.

Revenir en haut