Glibc内存管理--ptmalloc2源代码分析(二十三)
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_Cnew_heap(size_t size, size_t top_pad)#elsenew_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 mstateget_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 mstatereused_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;}
页:
[1]