Sub-heaps work mostly the same way as the initial program heap, with two main differences. Recall that the initial heap is located immediately after where the program is loaded into memory, and is dynamically expanded by sbrk. By contrast, each subheap is positioned into memory using mmap, and the heap manager manually emulates growing the subheap using mprotect.
When the heap manager wants to create a subheap, it first asks the kernel to reserve a region of memory that the subheap can grow into by calling mmap*. Reserving this region does not directly allocate memory into the subheap; it just asks the kernel to refrain from allocating things like thread stacks, mmap regions and other allocations inside this region.
*By default, the maximum size of a subheap–and therefore the region of memory reserved for the subheap to grow into–is 1MB on 32-bit processes and 64MB on 64-bit systems.
This is done by asking mmap for pages that are marked PROT_NONE, which acts as a hint to the kernel that it only needs to reserve the address range for the region; it doesn’t yet need the kernel to attach memory to it.
Where the initial heap grew using sbrk, the heap manager emulates “growing” the subheap into this reserved address range by manually invoking mprotect to change pages in the region from PROT_NONE to PROT_READ | PROT_WRITE. This causes the kernel to attach physical memory to those addresses, in effect causing the subheap to slowly grow until the whole mmap region is full. Once the entire subheap is exhausted, the arena just allocates another subheap. This allows the secondary arenas to keep growing almost indefinitely, only eventually failing when the kernel runs out of memory, or when the process runs out of address space.
To recap: the initial (“main”) arena contains only the main heap which lives just after the where the program binary is loaded into memory, and is expanded using sbrk. This is the only arena that is used for single-threaded applications. On multithreaded applications, new threads are given secondary arenas from which to allocate. Using arenas speeds up the program by reducing the likelihood that a thread will need to wait on a mutex before being able to perform a heap operation. Unlike the main arena, these secondary arenas allocate chunks from one or more subheaps, whose location in memory is first established using mmap, and which grow by using mprotect.