Glibc内存管理--ptmalloc2源代码分析(二十一)
5.6?多分配区支持
由于只有一个主分配区从堆中分配小内存块,而稍大的内存块都必须从mmap映射区域分配,如果有多个线程都要分配小内存块,但多个线程是不能同时调用sbrk()函数的,因为只有一个函数调用sbrk()时才能保证分配的虚拟地址空间是连续的。如果多个线程都从主分配区中分配小内存块,效率很低效。为了解决这个问题,ptmalloc使用非主分配区来模拟主分配区的功能,非主分配区同样可以分配小内存块,并且可以创建多个非主分配区,从而在线程分配内存竞争比较激烈的情况下,可以创建更多的非主分配区来完成分配任务,减少分配区的锁竞争,提高分配效率。
Ptmalloc怎么用非主分配区来模拟主分配区的行为呢?首先创建一个新的非主分配区,非主分配区使用mmap()函数分配一大块内存来模拟堆(sub-heap),所有的从该非主分配区总分配的小内存块都从sub-heap中切分出来,如果一个sub-heap的内存用光了,或是sub-heap中的内存不够用时,使用mmap()分配一块新的内存块作为sub-heap,并将新的sub-heap链接在非主分配区中sub-heap的单向链表中。
分主分配区中的sub-heap所占用的内存不会无限的增长下去,同样会像主分配区那样进行进行sub-heap收缩,将sub-heap中top chunk的一部分返回给操作系统,如果top chunk为整个sub-heap,会把整个sub-heap还回给操作系统。收缩堆的条件是当前free的chunk大小加上前后能合并chunk的大小大于64KB,并且top chunk的大小达到mmap收缩阈值,才有可能收缩堆。
一般情况下,进程中有多个线程,也有多个分配区,线程的数据一般会比分配区数量多,所以必能保证没有线程独享一个分配区,每个分配区都有可能被多个线程使用,为了保证分配区的线程安全,对分配区的访问需要锁保护,当线程获得分配区的锁时,可以使用该分配区分配内存,并将该分配区的指针保存在线程的私有实例中。
当某一线程需要调用malloc分配内存空间时,该线程先查看线程私有变量中是否已经存在一个分配区,如果存在,尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果失败,该线程搜分配区索循环链表试图获得一个空闲的分配区。如果所有的分配区都已经加锁,那么malloc会开辟一个新的分配区,把该分配区加入到分配区的全局分配区循环链表并加锁,然后使用该分配区进行分配操作。在回收操作中,线程同样试图获得待回收块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行回收操作。
5.6.1 Heap_info
/* A heap is a single contiguous memory region holding (coalesceable) malloc_chunks. It is allocated with mmap() and always starts at an address aligned to HEAP_MAX_SIZE. Not used unless compiling with USE_ARENAS. */typedef struct _heap_info { mstate ar_ptr; /* Arena for this heap. */ struct _heap_info *prev; /* Previous heap. */ size_t size; /* Current size in bytes. */ size_t mprotect_size; /* Size in bytes that has been mprotected PROT_READ|PROT_WRITE. */ /* Make sure the following data is properly aligned, particularly that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of MALLOC_ALIGNMENT. */ char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];} heap_info;
?ar_ptr是指向所属分配区的指针,mstate的定义为:typedefstruct malloc_state *mstate;
prev字段用于将同一个分配区中的sub_heap用单向链表链接起来。prev指向链表中的前一个sub_heap。
size字段表示当前sub_heap中的内存大小,以page对齐。
mprotect_size字段表示当前sub_heap中被读写保护的内存大小,也就是说还没有被分配的内存大小。
Pad字段用于保证sizeof(heap_info) + 2 * SIZE_SZ是按MALLOC_ALIGNMENT对齐的。MALLOC_ALIGNMENT_MASK为2 * SIZE_SZ - 1,无论SIZE_SZ为4或8,-6 * SIZE_SZ & ?MALLOC_ALIGN_MASK的值为0,如果sizeof(heap_info) + 2 * SIZE_SZ不是按MALLOC_ALIGNMENT对齐,编译的时候就会报错,编译时会执行下面的宏。
/* Get a compile-time error if the heap_info padding is not correct to make alignment work as expected in sYSMALLOc. */extern int sanity_check_heap_info_alignment[(sizeof (heap_info) + 2 * SIZE_SZ) % MALLOC_ALIGNMENT ? -1 : 1];
?为什么一定要保证对齐呢?作为分主分配区的第一个sub_heap,heap_info存放在sub_heap的头部,紧跟heap_info之后是该非主分配区的malloc_state实例,紧跟malloc_state实例后,是sub_heap中的第一个chunk,但chunk的首地址必须按照MALLOC_ALIGNMENT对齐,所以在malloc_state实例和第一个chunk之间可能有几个字节的pad,但如果sub_heap不是非主分配区的第一个sub_heap,则紧跟heap_info后是第一个chunk,但sysmalloc()函数默认heap_info是按照MALLOC_ALIGNMENT对齐的,没有再做对齐的工作,直接将heap_info后的内存强制转换成一个chunk。所以这里在编译时保证sizeof (heap_info) + 2 * SIZE_SZ是按MALLOC_ALIGNMENT对齐的,在运行时就不用再做检查了,也不必再做对齐。
5.6.2 获取分配区
static tsd_key_t arena_key;static mutex_t list_lock;#ifdef PER_THREADstatic size_t narenas;static mstate free_list;#endif/* Mapped memory in non-main arenas (reliable only for NO_THREADS). */static unsigned long arena_mem;/* Already initialized? */int __malloc_initialized = -1;
?arena_key存放的是线程的私用实例,该私有实例保存的是分配区(arena)的malloc_state实例的指针。arena_key指向的可能是主分配区的指针,也可能是非主分配区的指针。
list_lock用于同步分配区的单向环形链表。
如果定义了PRE_THREAD,narenas全局变量表示当前分配区的数量,free_list全局变量是空闲分配区的单向链表,这些空闲的分配区可能是从父进程那里继承来的。全局变量narenas和free_list都用锁list_lock同步。
arena_mem只用于单线程的ptmalloc版本,记录了非主分配区所分配的内存大小。
__malloc_initializd全局变量用来标识是否ptmalloc已经初始化了,其值大于0时表示已经初始化。
?
Ptmalloc使用如下的宏来获得分配区:
/* arena_get() acquires an arena and locks the corresponding mutex. First, try the one last locked successfully by this thread. (This is the common case and handled with a macro for speed.) Then, loop once over the circularly linked list of arenas. If no arena is readily available, create a new one. In this latter case, `size' is just a hint as to how much memory will be required immediately in the new arena. */#define arena_get(ptr, size) do { \ arena_lookup(ptr); \ arena_lock(ptr, size); \} while(0)#define arena_lookup(ptr) do { \ Void_t *vptr = NULL; \ ptr = (mstate)tsd_getspecific(arena_key, vptr); \} while(0)#ifdef PER_THREAD#define arena_lock(ptr, size) do { \ if(ptr) \ (void)mutex_lock(&ptr->mutex); \ else \ ptr = arena_get2(ptr, (size)); \} while(0)#else#define arena_lock(ptr, size) do { \ if(ptr && !mutex_trylock(&ptr->mutex)) { \ THREAD_STAT(++(ptr->stat_lock_direct)); \ } else \ ptr = arena_get2(ptr, (size)); \} while(0)#endif/* find the heap and corresponding arena for a given ptr */#define heap_for_ptr(ptr) \ ((heap_info *)((unsigned long)(ptr) & ~(HEAP_MAX_SIZE-1)))#define arena_for_chunk(ptr) \ (chunk_non_main_arena(ptr) ? heap_for_ptr(ptr)->ar_ptr : &main_arena)
?arena_get首先调用arena_lookup查找本线程的私用实例中是否包含一个分配区的指针,返回该指针,调用arena_lock尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果对该分配区加锁失败,调用arena_get2获得一个分配区指针。如果定义了PRE_THREAD,arena_lock的处理有些不同,如果本线程拥有的私用实例中包含分配区的指针,则直接对该分配区加锁,否则,调用arena_get2获得分配区指针,PRE_THREAD的优化保证了每个线程尽量从自己所属的分配区中分配内存,减少与其它线程因共享分配区带来的锁开销,但PRE_THREAD的优化并不能保证每个线程都有一个不同的分配区,当系统中的分配区数量达到配置的最大值时,不能再增加新的分配区,如果再增加新的线程,就会有多个线程共享同一个分配区。所以ptmalloc的PRE_THREAD优化,对线程少时可能会提升一些性能,但线程多时,提升性能并不明显。即使没有线程共享分配区的情况下,任然需要加锁,这是不必要的开销,每次加锁操作会消耗100ns左右的时间。
每个sub_heap的内存块使用mmap()函数分配,并以HEAP_MAX_SIZE对齐,所以可以根据chunk的指针地址,获得这个chunk所属的sub_heap的地址。heap_for_ptr根据chunk的地址获得sub_heap的地址。由于sub_heap的头部存放的是heap_info的实例,heap_info中保存了分配区的指针,所以可以通过chunk的地址获得分配区的地址,前提是这个chunk属于非主分配区,arena_for_chunk用来做这样的转换。
#define HEAP_MIN_SIZE (32*1024)#ifndef HEAP_MAX_SIZE# ifdef DEFAULT_MMAP_THRESHOLD_MAX# define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX)# else# define HEAP_MAX_SIZE (1024*1024) /* must be a power of two */# endif#endif
?HEAP_MIN_SIZE定义了sub_heap内存块的最小值,32KB。HEAP_MAX_SIZE定义了sub_heap 内存块的最大值,在32位系统上,HEAP_MAX_SIZE默认值为1MB,64为系统上,HEAP_MAX_SIZE的默认值为64MB。
?
?