glibc堆内存管理
背景
应用出现SIGABRT crash,报错信息为:malloc(): invalid size (unsorted),即是在应用调用malloc分配内存时出现异常导致的crash。
管理结构
进程虚拟地址空间被划分为代码段、数据段、堆和栈。其中,堆内存从低地址到高地址增长,并通过brk系统调用从系统中分配实际的物理内存空间,同时建立物理内存到虚拟内存的映射。堆内存在应用程序运行过程中存在反复的分配和释放过程,为提高内存的分配效率和减少内存碎片,需要利用相关的策略对堆内存进行管理。
Arena(分配区)
当一个进程中包含多个线程时,线程之间共享同一个堆内存空间(即共享资源)。由于linux中共享资源的申请需要获取对应的资源锁,同一时刻仅有一个线程能过获取到锁并操作共享资源,其他线程只能够等待锁的释放,这样就造成了堆内存分配和释放效率的低下。为解决这个问题,可为每个线程单独划分一片堆内存区域,用于各个线程的堆内存分配。基于这种思想,glibc中引入了Arena的概念,用于多线程的堆内存管理。其中,对于进程中的主线程来说,堆内存空间通过brk系统调用进行扩展,而对于其他线程的堆内存(即Arean)则通过mmap系统调用来扩展。如下图:
heap(堆)
brk和mmap每次以固定大小的size从系统中获取内存,对于主线程来说,每次都是通过brk系统调用申请内存,内存空间地址连续,但是对于其他线程对应的堆内存来说,每次通过mmap获取的内存空间不一定连续,所以为每一次通过mmap获得的内存增加一个heap_info的数据结构,并且通过prev指针将同属于一个Arena的各个heap_info连接起来,所有同属于一个Arena的heap_info的ar_ptr指向同一个Arena数据管理结构malloc_state。如下图:
malloc_struce的结构如下所示:
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
chunk(块)
对于同属于一个Arena的所有heap,可以将其抽象的视为一块地址连续的内存片段。当在该内存片段中申请释放内存时,通过chunk(块)对其进行管理。如下图:
对于已经分配的chunk称之为allocated chunk,分配之后释放过的称之为free chunk,未被分配的称之为top chunk。初始状态时,整个heap都是top chunk,经过分配之后产生allocated chunk,allocated chunk被释放之后产生free chunk。当再次进行堆内存分配时,为减少内存碎片,首先尝试从free chunk中进行分配,当free chunk中没有满足条件的内存片时,才从top chunk中进行分配。而对于free chunk来说,由于其size各不相同,在free chunk中查找满足条件的内存片段时,需要对free chunk进行遍历。为提高遍历的效率,glibc中对free chunk做了进一步的划分,用于管理不同大小的free chunk,管理的单元为bins。
bins(箱)
按照free chunk的大小,将各个free chunk交由如下的四种bin进行管理:
-
fast bin
大小介于16 - 80 bytes的chunk被称之为fast chunk,每一种大小的fast chunk通过链表组成一个fast bin,所有的fast bin组成一个数组。并且,fast bin在内存分配时具有最高的优先级。其结构如下所示:
其中,fast bin通过malloc_chunk结构体中的fd指针链接成为一个单向链表,并且每一种fast bin内的chunk 大小相差8 bytes。
-
unsorted bin
当small chunk和large chunk被释放时,并不是加入到small bin或者large bin,而是首先加入到unsort bin中。用于快速找到最近被释放的chunk以加速内存分配的效率。unsorted bin只有一个,并且unsorted bin内的chunk大小可以各不相同。unsorted bin和small bins以及large bins共用一个数组,如下图:
三种bin都是双向链表,其中数组中的元素只是记录了fd和bk两个指针,并不是一个malloc_chunk的结构体指针。如fd = 0x804b000指向前一个malloc_chunk,相对的前一个malloc_chunk的bk指针则应该指向本身,此时看前一个bk指针为0xb7fd8430,并非数组首地址0xb7fd8438,这里前一个malloc_chunk是将包含了fd的这个节点当做了malloc_chunk结构体看待,以保证双向循环链表操作函数的统一。
-
small bin
大小小于512bytes的chunk为small chunk,由small chunk组成的双向链表为一个small bin。small bin共62个,占据数组的第2 - 124项,每一个bin中的small chunk的大小是相同的,并且相邻的bin包含的small chunk的大小相差8bytes。
-
large bin
大小超过512bytes的chunk为large chunk,由large chunk组成的双向链表为一个large bin。large bin共63个,bin内包含的chunk大小可以不同,而是在一个范围内并且按大小的顺序排列在双向链表中。其中前面32个bin包含的large chunk的大小范围以64bytes递增,比如第一个large bin中的large chunk大小为512 到512 + 64 bytes,那么第二个bin的大小范围则是介于512 + 64 到512 + 128bytes之间,以此类推。后面的16个bin内的large chunk大小范围以512bytes递增,之后的8个bin内的large chunk大小范围以4096bytes递增,之后的4个bin内的large chunk大小范围以32768bytes递增,在此之后的2个bin内的large chunk大小范围则是以262144bytes递增,最后的1个bin其中的large chunk大小为保留项。
实际上,并不是每一个线程都对应一个Arena,当线程过多时会导致数据结构过度的资源占用。对于Arena的数量,一般有如下规定:
For 32 bit systems:
Number of arena = 2 * number of cores.
For 64 bit systems:
Number of arena = 8 * number of cores.
当线程的数量超过Arena的数量时,新的线程将遍历各个Arena,并且尝试获取该Arena的锁,如果获取到了就直接从中分配,如果没有便等待。当下一次再次尝试分配堆内存时,该线程将直接从此前分配成功的Arena中分配(如果没有获取到锁,便等待),并不会重新再次遍历所有的Arena。
内存合并
内存合并是指当与free chunk相邻的allocated chunk被释放时,将allocated chunk和相邻的free chunk合并为一个新的free chunk,并根据free chunk的大小和类型进行转移(如转移到unsorted bin/small bin)。其中,fastbin中的free chunk不会进行合并,以保证fast bin中的chunk足够碎片化,能够尽快满足小内存的申请。当allocated chunk释放的时候怎么去判断相邻的前后是否是free chunk呢?这一点通过chunk的数据结构来实现:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
malloc_chunk结构体内的元素含义根据allocated chunk和free chunk的不同而有所差异:对于allocated chunk来说,它只需要记录用于和free chunk合并的信息,而对于free chunk来说,则需要利用malloc_chunk将自身链接入不同的bin中。allocated chunk和free chunk对应的malloc_chunk结构体示意图分别如下:
判断一个chunk是否为free chunk则根据mchunk_size后的标志位来判断:P(PREV_INUSE),表示当前chunk的前一个chunk被分配了,即为allocated chunk;M(IS_MAPPED),表示该chunk被mapped;N(NON_MAIN_ARENA),表示该chunk属于thread arena而不是mian arena(即主线程的arena)。
当allocated chunk之前为一个free chunk,则此时allocated chunk的头部malloc_chunk的P标志位被置1,其中的mchunk_prev_size则记录了之前free chunk的size,根据当前allocated chunk的地址和mchunk_prev_size,则可以计算出free chunk的起始地址了。而判断当前allocated chunk之后的一个chunk是否为free chunk,则可以直接通过当前allocated chunk的size和地址,计算出下一个chunk的地址,然后根据该处的malloc_chunk中的标志位进行判断。对于allocated chunk来说,malloc_chunk的其他元素都没有作用,用户可以直接使用该部分空间。
对于free chunk来说,因为要将free chunk分类到不同的bin中,所以其中有两个链表节点,用于和其他chunk组成链表,用于堆内存分配时的chunk索引。
回到开头的问题,报malloc(): invalid size (unsorted)的错误,是由于在对chunk的size进行检查的时候,发现其size异常,导致的主动abort,其代码位置:
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (size <= 2 * SIZE_SZ) //SIZE_SZ为malloc_chunk的size
|| __glibc_unlikely (size > av->system_mem)) //av->system_mem为arena的内存大小,chunk的size必须介于两者之间
malloc_printerr ("malloc(): invalid size (unsorted)");
参考
- Understanding glibc malloc