Rewriting malloc

Table of Contents

Mandatory part

Rewrite malloc, free, and realloc functions.

Also, write a show_alloc_mem function to print memory allocations.

Like the real malloc, the allocated memory must be stored in pre-allocated “zones” (bins), unless the requested size is too big, then the allocation must be fitted.

Note

Fitted allocations are allocations that are outside of a bin, meaning it’s allocated directly with a call to mmap.

Two zone types are required:

Zones size must be a multiple of the page size, acquired with sysconf(_SC_PAGESIZE). Allocations (and internally required allocations) must be executed with mmap, de-allocations with munmap.

Bonus part

Mandatory part code explanation

Slightly inspired by debian glibc malloc

Important

When words related to allocation are put between double quotes, it means that it is not necessarily an allocation. Attributing a portion of a bin is not an allocation; a fitted allocation is indeed an allocation.

Since most of the explanations are not linked to real memory allocations made with mmap and applies to bin or fitted “allocations”, whether it is an attribution or an allocation does not change how it is organized.

Brief explanation of memory alignment

The computer reads memory by sections of 4/8 bytes, or 32/64 bits; this determines the name of the architecture.

Aligning memory corresponds to setting variables at the start of 4/8 bytes sections. If this is not respected, it raises:

Considering this, compilers will automatically align memory when possible by padding memory with char. When the fields of a structure are not positioned properly or the total size of a structure is not a multiple of 4/8, it will take more memory than it should, due to padding.

However, when freely attributing values in memory, it is required to be careful with alignment to prevent unnecessary slowdowns.

Chunks

Every memory “allocation” (bin or fitted) is preceded by a header. The combination of a memory “allocation” (data) and its header is called a chunk.

The header bears the following information:

For a total of 16/32 bytes on 32/64 bits architectures. The structure itself is memory aligned, and the start of the data is also aligned. The data size is always set to a multiple of 16, forcing memory alignment on every architecture.

The data size is often larger than what was asked, to keep memory aligned. The portion of data that matches the size asked by the user (real size) is called provided data, the remainder is called tail.

To sum things up, this is how a chunk is organized in memory (assuming a line is 4/8 bytes):

SectionData storedType of data
Headersize of data (size)unsigned long
Headersize of provided data (real size)unsigned long
Headerpointer to next chunk (next)void *
Headerpointer to previous chunk (prev)void *
dataprovided data (could be any size)N/R
datatail (could be any size)N/R

The size of the provided data is only here for debug information and show_alloc_mem.

Pros and cons

The pros of overhead instead of exported bookkeeping are that it is faster to retrieve data related to a chunk with a pointer (in free or realloc, for example) since it only requires subtracting the header size from the pointer to retrieve its header.

It also takes less space since it does not contain pointers to data.

It also requires fewer calls to mmap (which are fairly slow) since it is not allocated elsewhere; data and header are allocated in a single call.

The cons are that a non-cautious user writing out of provided data bounds might ruin the bookkeeping data and ruin malloc… But well, nobody is supposed to write out of bounds, right?

Bins

Similarly to a chunk, bins have a preceding header. The only differences are that the size corresponds to the full zone size, header included, that the real size field is unused, and that pointers point to bins instead of chunks.

malloc

When calling malloc, if the size is not too high for a bin (then it just performs a fitted allocation), it will iterate in the proper bin (corresponding to the requested size) and find a free chunk with the proper size. This helps to prevent bin fragmentation. If it reaches the end of a bin and there is enough space left in the bin, it will attribute a chunk in the bin. If there is not enough space left in the bin, it will create a new bin.

If several contiguous freed chunks’ total size matches the new chunk’s size, they will be joined as a single chunk, again, to prevent fragmentation.

free

When calling free, if the size is not too high for a bin (then it will just unmap the chunk and sort out next/prev pointers), it will check if the provided chunk is at the end of a bin. If so, the chunk will be de-attributed, then it will iterate backward to find contiguous chunks to de-attribute them. This helps to prevent bin fragmentation.

If the bin ends up being empty, it is de-allocated to reduce the program’s memory footprint.

realloc

When calling realloc, if the size is not too high for a bin (which will be covered in a few lines), it will run a few checks:

In the two previous cases, if the chunk is at the end of a bin, the chunk will be expanded (if there is enough space left) or trimmed to fit the new size.

Similarly, if the size is too big for a bin, it will try to expand or trim the chunk with mmap. If it works, the chunk’s size and real size are changed accordingly and the provided pointer is returned. If it does not work, it will copy the chunk’s data to the new chunk and return a pointer to the new chunk’s data.

Bonus part code explanation

None of the bonuses really require an explanation, besides eventually de-fragmentation…

Preventing fragmentation is already covered in the mandatory part (to be fair, if one wants to do this bonus, it is recommended to code the mandatory part with de-fragmentation in mind…).

Making the lib thread-safe is not that hard; the easiest implementation is to lock/unlock a mutex before each call of the exposed functions. It could eventually be optimized, but for little to no gain at my scale.

To make it fancier, I added macros to exclude thread-safe logic at build time when not required.

I also added some debug information on show_alloc_mem when building with a specific flag, it prints a dump of the chunks, along with more details about the sizes of the chunks and bins.


Go to Top