Re: malloc implementation
Posted: Wed Nov 06, 2019 1:59 pm
Gladly.Schol-R-LEA wrote:(Comments and corrections welcome; POSIX hasn't exactly been a major concern for me, since my implementation language and OS design plans do not resemble C and Unix in the slightest.)
First, I will point out that sbrk() and brk() have been removed from POSIX-2001 (they'd been obsoleted in the SUSv2). Linux still has the BRK system call, of course, and it is still used by several libcs in the malloc implementation, but even there it sees decreasing utility, and the allocators have to be designed with mmap() as a fallback if sbrk() fails, because sbrk() is likely to fail in quite a few scenarios.
Virtual memory in user space used to be divided up into three blocks: You have a code block, then a data block, then a large gap and then the stack. If you need more memory you can always increase the size of the data section. That is what sbrk() does. Also why it is usually incapable of shrinking again: Shrink too far and you loose static data. On Linux for instance, BRK is only capable of increasing the heap size, not reducing it.
But that only works if the application is linked statically, and there is no ASLR going on, and the application is not a PIE. But then dynamic libraries came: Suddenly, in order to run the main program, certain binary blobs have to be placed somewhere in memory, and sometimes they end up quite close to the end of the heap. ASLR was made the standard, so the values mmap() would return became unpredictable, even for the exact same situations (same files mapped into the same address space as before). PIE happened, and now nobody knows where the data section even of the main program will end up in relation to the stack and the BRK heap.
For all of these reasons BRK is only used as first choice these days, with a fallback on mmap().
Also, POSIX doesn't care about system calls. POSIX only cares about functions and semantics. You can implement sbrk() by allocating from a static memory block without hurting POSIX conformance (or SUSv2 conformance, as it were).
mmap() doesn't have to map file pages on most systems, though this isn't POSIX yet. Most systems allow you to map pages anonymously, without file backing, and those that don't allow you to map /dev/zero or something, so it is only technically file backed. Also of note here is that virtual memory and physical backing store are two different things. musl's free() implementation will never return managed pages to the system, but if the unused size is large enough, it will fire an madvise() that makes the kernel release the backing store. It will only be reallocated when the memory is accessed again.
Regarding implementations: DLmalloc appears to be the standard, TCmalloc is just DLmalloc with thread-local arenas, JEmalloc is a beefed up DLmalloc. musl's version is available here: https://git.musl-libc.org/cgit/musl/tre ... c/malloc.c. It does a few things quite well, managing to avoid bottlenecking multi-thread performance with a single lock (like so many other systems have). Unfortunately this very pro is also what's failing it. When a block is freed, the free() function will briefly incorporate any surrounding free blocks into the block to be freed, to combat fragmentation. Unfortunately, to a concurrent thread trying to allocate memory, this looks a lot like there not being any memory available at the moment, so now it has to allocate more memory from the system.
The only malloc I could find that does things truly differently is omalloc: https://github.com/emeryberger/Malloc-I ... /omalloc.c, which basically manages blocks by way of a hash table. OpenBSD has in the past, and so also in this case, affirmed that security is more important than speed, and so omalloc is not a fast implementation, but a robust one. Very hard to destroy this heap, where in DLmalloc you can just overwrite the footer and all is lost.