ptmalloc2分配器
ptmalloc2
分配给用户的内存都以 chunk 来表示,可以理解为 chunk 为分配释放内存的载体。下面根据glibc2.31
对ptmalloc进行了分析。
malloc_chunk
malloc_chunk用于管理内存块,该结构体维护了一张已使用和未使用内存块的链表
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk, if it is free. */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if this chunk is 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 this chunk is free. */
struct malloc_chunk* bk_nextsize;
};
typedef struct malloc_chunk* mchunkptr;
prev_size
代表上一个chunk的大小,是否有效取决于size
的属性位P
INTERNAL_SIZE_T
即size_t
用于屏蔽平台间的差异size
代表当前chunk的大小和属性根据,malloc_chunk结构体的大小的对齐原则,在64位系统中为8字节对齐,在32位的系统中为4字节对齐。因此在64位系统中,struct malloc_chunk结构体的大小的最后4个字节是没有被使用的,32位系统中,最后3个字节是没有被使用的分别代表
- NON_MAIN_ARENA:记录当前 chunk 是否不属于主线程。
- IS_MAPPED:记录当前chunk是否是由mmap分配的。
- PREV_INUSE:如果前面一个chunk处于分配状态,那么此位为1。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个chunk 的size 的P位为0时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并。
当chunk为空闲时,加入fd,bk
双链表中管理
fd_nextsize bk_nextsize
只用在 large bin
中,表示 上/下一个大小的指针,加快链表遍历。
ptmalloc2
分配器有以下特点:
- 当前一个chunk非空闲,
prev_size
无意义,可被前一个chunk使用(已经被使用知道了prev_size也没有意义) size
的低三位为属性位,size
一定是8的倍数A为是否为主分配区,M为是否为mmap P为前一个chunk是否被使用- chunk非空闲,
fd bk,fd_nextsize bk_nextsize
,都无意义,因此返回给用户的可用内存首先得返回size
- 分为主分配区和非主分配区
与块相关的宏定义如下
/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->size & PREV_INUSE)
/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena. This is only set immediately before handing
the chunk to the user, if necessary. */
#define NON_MAIN_ARENA 0x4
/* check for chunk from non-main arena */
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)
typedef struct malloc_chunk* mchunkptr;
#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define MINSIZE \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
mem
为用户可以使用的内存起始地址,如果不是large chunk的话chunk的最小大小至少为8*4=32字节
,fd_nextsize 和 bk_nextsize
只有在 large chunk 才用的上。
由于chunk可以复用,如果在当前chunk正在使用时,相邻prev_size无效,可当做chunk使用。request2size
将用户申请的内存大小转化为 需要分配的 chunk 大小,用户请求大小 (req + prev_size + MALLOC_ALIGN_MASK) = req + 8B + 16B=req + 24B
。这个计算公式的意义是:
1、由于chunk的复用,所以要在请求的大小基础上加上SIZE_SZ;
2、由于最终大小必须是2 * SIZE_SZ对齐,所以要向上对齐;
3、根据结果与MINSIZE比较,确定最终大小MINSIZE还是对齐后的计算结果。
chunk的实际分配模型如下
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
malloc_state
用户申请的chunk
可能来自三个地方
- mmap直接分配
- 主分配区分配:优先使用brk
- 非主分配区分配:优先使用mmap
malloc_state
是用来管理分配区的,非主分配区的出现是为了环节多线程竞争锁的情况,通常一个线程对应一个分配区,尽管是这样还是会加锁当线程数达到CPU核数,会停止创建分配区,对分配区进行复用,复用是通过轮询判断是否能加锁的。CPU核心数和分配区的对应关系如下
For [32 bit] systems: Number of arena = 2 * number of cores + 1.
For [64 bit] systems: Number of arena = 8 * number of cores + 1.
malloc_state
结构体如下
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;
};
-
mutex,为了支持多线程进入
-
flags,bit0 表示是否有 fast bin chunk,bit1 表示是否能返回连续的虚拟地址空间。
-
fastbinY,就是存储 fast bins 的数组,NFASTBINS 为 10。
-
top,top chunk ,直接与操作系统关联的地方,bin中申请不到内存从top chunk申请,top chunk不足则扩展top chunk,释放内存给操作系统也是从top chunk开始释放。
-
last_remainder,分配区若上次分配 small chunk 且还有剩余,则暂存入这个指针,会合并到其他bin中。
-
bins,即 unsorted bin + small bins + large bins = 1 + 62 + 63 = 125,bin[0] 和 bin[127] 没有用,NBINS为128,但是 bins 的大小
=2*128-2=254
,将所有bin保存在bins结构体中,然后构成一个双链表的结构体,可以减少内存。 -
binmap,标识 bit 指向的 bin 是否有空闲 chunk。
-
next,链接分配区。
-
system_mem,当前分配区已分配内存大小,可通过
malloc_stats(3)
进行查看。
Bin
bin通过显示链表将相同属性的空闲块串联起来方便管理,。bin
可以理解桶(数据结构类似于哈希桶),存放着 chunk
,在 ptmalloc
分配器存在四种 bin。
- fast bins:
fast bins
是小内存块的缓存,当小内存块被回收时,会先放入fast bins
,当下次分配小内存时,就会优先从fast bins
中找,节约时间。 - unsorted bin:
unsorted bin
只有一个,回收的chunk
若大于fast bins
的阈值即global_max_fast
,则放入unsorted bin
。 - small bins:
small bins
顾名思义,就是ptmalloc
觉得小的 chunk,就放进去,呈等差数列的形式递增,每个 bin 的 chunk 均为同一大小,通过fd, bk
链接 chunk 链表。 - large bins:
large bins
同上,不过每个 bin 中的 chunk 有大小排序,大的在前,小的在后,通过fd_nextsize, bk_nextsize
快速找到上/下 一个大小节点。
#define NBINS 128
bins
共有 small bins
有 62 个, large bins
有 63个, unsorted bin
为 1个,总共为 62+63+1 = 126 个
,其中 bin[0] 和 bin[127] 不用,因此 bins 总数为 128 个。要注意 fast bins
并不放入同一数组。每个分配区维护了一个bins位图
bin数组存储在malloc_state
的mchunkptr bins[NBINS * 2 - 2]
中映射关系如下图:每个bin由bin数组中的两个元素维护,两个元素构成双链表,起了节省空间的作用。
Fast bin
fast bins
是小内存块的缓存,大小小于DEFAULT_MXFAST
的chunk分配都会在fast bins
中先查找,默认为128字节,该宏可以通过mallopt
设置,最大值为160B,当小内存块被回收时,会先放入 fast bins
将相同大小的内存块存放在一起,当下一次分配小内存时,就会优先从fast bins
中找,节约时间。(16、24、32、40、48、56、64、72、80、88),fast chunk的特点就是两个相邻的fast chunk不需要合并fast chunk的maloc和free都是在对应的fast bin的链表头增加和删除,由于取用fast chunk
都是从第一个开始取用,因此fast bins
不需要维护双链表,使用后进先出的方式。Prev inuse
始终置1,避免块合并。
#ifndef DEFAULT_MXFAST
#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)
#endif
typedef struct malloc_chunk* mfastbinptr;
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int)(sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
#define NFASTBINS (fastbin_index(request2size(MAX_FAST_SIZE))+1)
#define FASTBIN_CONSOLIDATION_THRESHOLD (65536UL)
fast bins有以下特点
- 一共有10个
fast bin
,每个fast bin
维护一个单链表,增加和删除都在链表的头或者尾 - 初始化时候fast chunk最大的大小设置为64,因此默认16到64字节的被划分为快速块
- 8位对齐
fast bin
同样使用8字节对齐,数组第一个元素存储的都是接近16bytes的块列表,数组第二个存储的则是接近24字节块的列表 fast bin
设置了prev INUSE
所以快速块无法被合并,虽然可能产生外部碎片,但是能够快速找到合适大小的chunk- 刚开始
fast bin
为空,及时用户申请fast bin
也无法在fast bin
中分配到内存,但是会优先尝试。
NFASTBINS取值计算:
#define NFASTBINS (fastbin_index(request2size(MAX_FAST_SIZE))+1)
=fastbin_index((0x50*2+0x8+0x10) &~ 0xe) +1
=fastbin_index(0xb8) & 0xfffffff0 )+1
转换为 2进制
=fastbin_index((10111000) & 0xfffffff0 ) +1
= ( 10110000 >> 4 ) -2 +1
= 00001011 -2 +1
= 00001010
可得NFASTBINS的取值为 10
Unsorted bin
未排序的块,当 small chunk或者large chunk被free的时候不会直接加入到相应的small bin和large bin中,而是被添加到Unsorted bin;主要的目的是为了重新利用这些刚被释放的chunk;由free chunks组成的循环双链表,且没有大小限制;
Small bin
和快速块类似16、24……,504bytes在内存分配和释放方面,小容器比大容器快(但比快容器慢)。,插入发生在头,删除发生在尾部。
- 一共有62个
Small bin
- 每个
small bin
包含一共环形双链表,在small bin
中块与块之间无隔断,因此需要双链表维护
- 每个
- 8字节对齐
small bin
的链表也是每隔8字节一个元素,最小的small bin
(bins[2])位16字节- 同一个 bin中的所有chunk大小一致
- 相邻的空闲小块会被合并成一个块
- 小块在未初始化的时候也为空,当没有小块会在 未排序箱子中尝试
- 在第一次调用malloc时,
malloc_state
结构体可以看到结构,和bins指针,但是是空的 - 释放的时候会检查前一个块和后一个块是否空闲,空闲会合并到未排序块中
Large bin
大于等于512字节的块称为大块,装有大块的bin称为large bin
- 一共有63个
large bin
- 每个
large bin
维护一个空闲块的双链表,双链表可以在任何位置插入和删除。 - 32个bins的间隔是64字节
- 16个间隔512字节
- 8个间隔4096字节
- 4个间隔32768字节
- 2个间隔262144字节
- 剩下一个存储更大的
- 每个
- 不同与
small bin
每个large bin
里面的chunk大小不相同但会处于一定范围(上面的间隔即区间大小),大的存在前面小的存在binlist的后面,在添加和删除时按照顺序添加进行操作. - 最初所有的
large bin
都为空 - 优先会将最大的块分配给用户,如果最大块大于用户请求则从后遍历该binlist,当其接近或等于用户请求,该块会被分割成两个块
- 如果最大快大小小于用户请求,则会尝试从下一个非空的
large bin
来服务用户请求,如果没有找到,则尝试从top chunk分配 - 要注意的是
top chunk
位于分配区顶部边界的大块被称为top chunk ,它不属于任何bin,top chunk用于无法在bin中分配到内存时将top chunk 分割为两部分给用户使用,另一部分则称为remaining chunk
。remaining chunk
会变为新的top。
当top chunk 不足时则[使用sbrk
或mmap
扩展top chunk
Last remainder chunk
从最近拆分的一个请求中得到的余数。Last remainder chunk
是为了优化连续 小块的malloc 请求,这些请求最终可能会被紧密地分配给其他块。
分配原则
鉴于 bin 的个数较多,因为不同 bin中的 chunk极有可能在不同的内存页中,如果按照上一段中介绍的方法进行遍历的话,就可能会发生多次内存页中断操作,进而严重影响检索速度,所以 glibc
的 malloc
设计了 binmap
结构体来帮助提高 bin-by-bin 检索的速度。binmap
记录了各个 bin中是否为空,通过位图算法可以避免检索一些空的 bin 。如果通过 binmap
找到了下一个非空的 large bin的话,就将其分为两部分 ,否则就使用 top chunk 来分配合适的内存。
如果用户请求是一个小块,且unsorted bin中有块,该块会被分为两个,一个返回给用户另一个添加到unsorted bin中