Managing virtual address spaces with MMAP

January 11, 2013

Being able to manage your own virtual address space(s) is sometimes really useful. Some platforms (Win32/Xbox360/PS3) allow you to do this in a very elegant fashion: you first allocate your address space and then you start mapping physical memory into it. OSX and Linux (to my limited knowledge) only support managing virtual memory through the MMAP system calls.

The details for mmap can be found here.

If you look through the manpage, you’ll notice there is no notion of address space, so how can we achieve what we want to achieve using this (primitive) system call API?

Well, it’s not that simple, but assuming (I know…I always say that assumption is the mother of all fuckups…) the mmap implementation is true to the design document it will implement demand paging.This is something that we can really use. Since I can’t talk about Xbox360 or PS3 publicly, I can illustrate this with Win32’s VirtualAlloc.

In order to be able to manage our own memory space successfully we need to have access to the following 4 operations:

The minimalistic implementation of the four operations using the Win32 API is as follows:

void* AllocateAddressSpace(size_t size)
{
    return VirtualAlloc(NULL, size, MEM_RESERVE , PAGE_NOACCESS);
}

void* CommitMemory(void* addr, size_t size)
{
    return VirtualAlloc(addr, size, MEM_COMMIT, PAGE_READWRITE);
}

void DecommitMemory(void* addr, size_t size)
{
    VirtualFree((void*)addr, size, MEM_DECOMMIT);
}

void FreeAddressSpace(void* addr, size_t size)
{
    VirtualFree((void*)addr, 0, MEM_RELEASE)
}

Now, with MMAP there is no “ownership” of pages until they’re mapped and used, and that makes the implementation of the address space concept really tricky.
In order to keep the pages reserved in your virtual address space, all you need to do is to just remap the address and then trick the TLB to remap the area as a freshly mapped region.
This, together with the demand paging will basically return the address into a reserved & uncommitted state hence returning the physical memory back to the OS.
The interesting (and a bit upside-down from what you’d expect to see there) is in DecommitMemory.
The equivalent MMAP implementation would be somewhat like this:

void* AllocateAddressSpace(size_t size)
{
    void * ptr = mmap((void*)0, size, PROT_NONE, MAP_PRIVATE|MAP_ANON, -1, 0);
    msync(ptr, size, MS_SYNC|MS_INVALIDATE);
    return ptr;
}

void* CommitMemory(void* addr, size_t size)
{
    void * ptr = mmap(addr, size, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_SHARED|MAP_ANON, -1, 0);
    msync(addr, size, MS_SYNC|MS_INVALIDATE);
    return ptr;
}

void DecommitMemory(void* addr, size_t size)
{
    // instead of unmapping the address, we're just gonna trick 
    // the TLB to mark this as a new mapped area which, due to 
    // demand paging, will not be committed until used.

    mmap(addr, size, PROT_NONE, MAP_FIXED|MAP_PRIVATE|MAP_ANON, -1, 0);
    msync(addr, size, MS_SYNC|MS_INVALIDATE);
}

void FreeAddressSpace(void* addr, size_t size)
{
    msync(addr, size, MS_SYNC);
    munmap(addr, size);
}

I hope this will help someone save some time by not debugging nonsensical crashes in random and seemingly unrelated places…
It took around 2 days of debugging GPU driver and Mono crashes until I could come up with a solution and an explanation that would make sense to me…

Share this:

#c0decafe