5.6.5 new_heap()
New_heap() 函数负责从 mmap 区域映射一块内存来作为 sub_heap ,在 32 位系统上,该函数每次映射 1M 内存,映射的内存块地址按 1M 对齐;在 64 为系统上,该函数映射 64M 内存,映射的内存块地址按 64M 对齐。 New_heap() 函数只是映射一块虚拟地址空间,该空间不可读写,不会被 swap 。 New_heap() 函数的实现源代码如下:
/* If consecutive mmap (0, HEAP_MAX_SIZE << 1, ...) calls return decreasing addresses as opposed to increasing, new_heap would badly fragment the address space. In that case remember the second HEAP_MAX_SIZE part aligned to HEAP_MAX_SIZE from last mmap (0, HEAP_MAX_SIZE << 1, ...) call (if it is already aligned) and try to reuse it next time. We need no locking for it, as kernel ensures the atomicity for us - worst case we'll call mmap (addr, HEAP_MAX_SIZE, ...) for some value of addr in multiple threads, but only one will succeed. */ static char *aligned_heap_area; /* Create a new heap. size is automatically rounded up to a multiple of the page size. */ static heap_info * internal_function #if __STD_C new_heap(size_t size, size_t top_pad) #else new_heap(size, top_pad) size_t size, top_pad; #endif { size_t page_mask = malloc_getpagesize - 1; char *p1, *p2; unsigned long ul; heap_info *h; if(size+top_pad < HEAP_MIN_SIZE) size = HEAP_MIN_SIZE; else if(size+top_pad <= HEAP_MAX_SIZE) size += top_pad; else if(size > HEAP_MAX_SIZE) return 0; else size = HEAP_MAX_SIZE; size = (size + page_mask) & ~page_mask;
调整size 的大小, size 的最小值为 32K, 最大值 HEAP_MAX_SIZE 在不同的系统上不同,在 32 位系统为 1M , 64 位系统为 64M ,将 size 的大小调整到最小值与最大值之间,并以页对齐,如果 size 大于最大值,直接报错。
/* A memory region aligned to a multiple of HEAP_MAX_SIZE is needed. No swap space needs to be reserved for the following large mapping (on Linux, this is the case for all non-writable mappings anyway). */ p2 = MAP_FAILED; if(aligned_heap_area) { p2 = (char *)MMAP(aligned_heap_area, HEAP_MAX_SIZE, PROT_NONE, MAP_PRIVATE|MAP_NORESERVE); aligned_heap_area = NULL; if (p2 != MAP_FAILED && ((unsigned long)p2 & (HEAP_MAX_SIZE-1))) { munmap(p2, HEAP_MAX_SIZE); p2 = MAP_FAILED; } }
全局变量 aligned_heap_area 是上一次调用 mmap 分配内存的结束虚拟地址,并已经按照 HEAP_MAX_SIZE 大小对齐。如果 aligned_heap_area 不为空,尝试从上次映射结束地址开始映射大小为 HEAP_MAX_SIZE 的内存块,由于全局变量 aligned_heap_area 没有锁保护,可能存在多个线程同时 mmap() 函数从 aligned_heap_area 开始映射新的虚拟内存块,操作系统会保证只会有一个线程会成功,其它在同一地址映射新虚拟内存块都会失败。无论映射是否成功,都将全局变量 aligned_heap_area 设置为 NULL 。如果映射成功,但返回的虚拟地址不是按 HEAP_MAX_SIZE 大小对齐的,取消该区域的映射,映射失败。
if(p2 == MAP_FAILED) { p1 = (char *)MMAP(0, HEAP_MAX_SIZE<<1, PROT_NONE, MAP_PRIVATE|MAP_NORESERVE);
全局变量 aligned_heap_area 为 NULL ,或者从 aligned_heap_area 开始映射失败了,尝试映射 2 倍 HEAP_MAX_SIZE 大小的虚拟内存,便于地址对齐,因为在最坏可能情况下,需要映射 2 倍 HEAP_MAX_SIZE 大小的虚拟内存才能实现地址按照 HEAP_MAX_SIZE 大小对齐。
if(p1 != MAP_FAILED) { p2 = (char *)(((unsigned long)p1 + (HEAP_MAX_SIZE-1)) & ~(HEAP_MAX_SIZE-1)); ul = p2 - p1; if (ul) munmap(p1, ul); else aligned_heap_area = p2 + HEAP_MAX_SIZE; munmap(p2 + HEAP_MAX_SIZE, HEAP_MAX_SIZE - ul);
映射2 倍 HEAP_MAX_SIZE 大小的虚拟内存成功,将大于等于 p1 并按 HEAP_MAX_SIZE 大小对齐的第一个虚拟地址赋值给 p2 , p2 作为 sub_heap 的起始虚拟地址, p2+ HEAP_MAX_SIZE 作为 sub_heap 的结束地址,并将 sub_heap 的结束地址赋值给全局变量 aligned_heap_area ,最后还需要将多余的虚拟内存还回给操作系统。
} else { /* Try to take the chance that an allocation of only HEAP_MAX_SIZE is already aligned. */ p2 = (char *)MMAP(0, HEAP_MAX_SIZE, PROT_NONE, MAP_PRIVATE|MAP_NORESERVE); if(p2 == MAP_FAILED) return 0; if((unsigned long)p2 & (HEAP_MAX_SIZE-1)) { munmap(p2, HEAP_MAX_SIZE); return 0; }
映射2 倍 HEAP_MAX_SIZE 大小的虚拟内存失败了,再尝试映射 HEAP_MAX_SIZE 大小的虚拟内存,如果失败,返回;如果成功,但该虚拟地址不是按照 HEAP_MAX_SIZE 大小对齐的,返回。
} } if(mprotect(p2, size, PROT_READ|PROT_WRITE) != 0) { munmap(p2, HEAP_MAX_SIZE); return 0; } h = (heap_info *)p2; h->size = size; h->mprotect_size = size; THREAD_STAT(stat_n_heaps++);
调用 mprotect() 函数将 size 大小的内存设置为可读可写,如果失败,解除整个 sub_heap 的映射。然后更新 heap_info 实例中的相关字段。
return h; }
5.6.6 get_free_list()和reused_arena()
这两个函数在开启了 PER_THRAD 优化时用于获取分配区( arena ), arena_get2 首先调用 get_free_list() 尝试获得 arena ,如果失败在尝试调用 reused_arena() 获得 arena ,如果仍然没有获得分配区,调用 _int_new_arena() 创建一个新的分配区。 Get_free_list() 函数的源代码如下:
static mstate get_free_list (void) { mstate result = free_list; if (result != NULL) { (void)mutex_lock(&list_lock); result = free_list; if (result != NULL) free_list = result->next_free; (void)mutex_unlock(&list_lock); if (result != NULL) { (void)mutex_lock(&result->mutex); tsd_setspecific(arena_key, (Void_t *)result); THREAD_STAT(++(result->stat_lock_loop)); } } return result; }
这个函数实现很简单,首先查看 arena 的 free_list 中是否为 NULL ,如果不为 NULL ,获得全局锁 list_lock ,将 free_list 的第一个 arena 从单向链表中取出,解锁 list_lock 。如果从 free_list 中获得一个 arena ,对该 arena 加锁,并将该 arena 加入线程的私有实例中。
reused_arena() 函数的源代码实现如下:
static mstate reused_arena (void) { if (narenas <= mp_.arena_test) return NULL;
首先判断全局分配区的总数是否小于分配区的个数的限定值( arena_test ), 在 32 位系统上 arena_test 默认值为 2 , 64 位系统上的默认值为 8 ,如果当前进程的分配区数量没有达到限定值,直接返回 NULL 。
static int narenas_limit; if (narenas_limit == 0) { if (mp_.arena_max != 0) narenas_limit = mp_.arena_max; else { int n = __get_nprocs (); if (n >= 1) narenas_limit = NARENAS_FROM_NCORES (n); else /* We have no information about the system. Assume two cores. */ narenas_limit = NARENAS_FROM_NCORES (2); } } if (narenas < narenas_limit) return NULL;
设定全局变量 narenas_limit ,如果应用层设置了进程的最大分配区个数( arena_max ),将 arena_max 赋值给 narenas_limit ,否则根据系统的 cpu 个数和系统的字大小设定 narenas_limit 的大小, narenas_limit 的大小默认与 arena_test 大小相同。然后再次判断进程的当前分配区个数是否达到了分配区的限制个数,如果没有达到限定值,返回。
mstate result; static mstate next_to_use; if (next_to_use == NULL) next_to_use = &main_arena; result = next_to_use; do { if (!mutex_trylock(&result->mutex)) goto out; result = result->next; } while (result != next_to_use); /* No arena available. Wait for the next in line. */ (void)mutex_lock(&result->mutex); out: tsd_setspecific(arena_key, (Void_t *)result); THREAD_STAT(++(result->stat_lock_loop)); next_to_use = result->next;
全局变量 next_to_use 指向下一个可能可用的分配区,该全局变量没有锁保护,主要用于记录上次遍历分配区循环链表到达的位置,避免每次都从同一个分配区开始遍历,导致从某个分配区分配的内存过多。首先判断 next_to_use 是否为 NULL ,如果是,将主分配区赋值给 next_to_use 。然后从 next_to_use 开始遍历分配区链表,尝试对遍历的分配区加锁,如果加锁成功,退出循环,如果遍历分配区循环链表中的所有分配区,尝试加锁都失败了,等待获得 next_to_use 指向的分配区的锁。执行到 out 的代码,意味着已经获得一个分配区的锁,将该分配区加入线程私有实例,并将当前分配区的下一个分配区赋值给 next_to_use 。
return result; }