转自:https://hanfeng.ink/post/understand_glibc_malloc/
本文是基于英文博客 Understanding glibc malloc ,对内容做了大量的补充和修改,主要阐释了malloc
分配内存的底层实现原理。
我一直在执着于堆的一些问题。比如以下问题:
虽然之前经常在想这些问题,但是光想并没有什么用。正好,最近我找到了点时间来好好思考这些问题。所以现在我就来分享一下这些知识的总结。此外,还有很多可用的内存分配器:
dlmalloc
– 通用分配器ptmalloc2
– glibcjemalloc
– FreeBSD and Firefoxtcmalloc
– Googlelibumem
– Solaris每种内存分配器都声称自己速度快、可扩展、空间利用高效!!但是并非所有的分配器都适合我们的程序。内存消耗大的应用,其性能很大程度依赖于内存分配器的性能。本文仅讨论 glibc malloc
内存分配器。
ptmalloc2
来自于 dlmalloc
的分支。在其基础上添加线程支持并于 2006 年发布。正式发布后,patmalloc2
集成到 glibc
源码中。随着源码集成,代码修改便直接在 glibc malloc
源码里进行。因此 ptmalloc2
的实现与 glibc malloc
有很多不同。
在早期的Linux
里,dlmalloc
被用做默认的内存分配器。但之后因为 ptmalloc2
添加了线程支持,ptmalloc2
成为了 Linux
默认内存分配器。线程支持可帮助提升内存分配器以及应用程序的性能。在 dlmalloc
里,当两个线程同时调用 malloc
时,只有一个线程能进入到临界段,因为这里的空闲列表是所有可用线程共用的。因此内存分配器要在多线程应用里耗费时间,从而导致性能降低。然而在 ptmalloc2
里,当两个线程同时调用 malloc
时,会立即分配内存。因为每个线程维护一个单独的堆分段,因此空闲列表维护的这些堆也是独立的。这种维护独立堆以及每一个线程享有空闲列表数据结构的行为被称为 Per Thread Arena
。
malloc
有两种方式获取内存,分别为sbrk
和 mmap
,以下为示意图:
我们来看一下关于这两个系统调用的官方解释:
sbrk
:
The brk() function sets the break or lowest address of a process’s data segment (uninitialized data) to addr (immediately above bss). Data addressing is restricted between addr and the lowest stack pointer to the stack segment.
mmap
:
The mmap() system call causes the pages starting at addr and continuing for at most len bytes to be mapped from the object described by fd, starting at byte offset offset.
两者一个明显区别在于,通过sbrk
获得的新的堆的内存地址和之前的地址是连续的,而 mmap
获得的地址由参数设定。下图为内存分配的示意图:
在进行进一步的分析之前,我们先要找到合适的方法,分析程序的内存分配情况。本文通过读取/proc/$pid/maps
,该文件的具体内容可以通过 man 5 proc
来了解,其的部分的解释如下:
address perms offset dev inode pathname 00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/dbus-daemon 00651000-00652000 r--p 00051000 08:02 173521 /usr/bin/dbus-daemon 00652000-00655000 rw-p 00052000 08:02 173521 /usr/bin/dbus-daemon 00e03000-00e24000 rw-p 00000000 00:00 0 [heap] 00e24000-011f7000 rw-p 00000000 00:00 0 [heap]
perms
代表了内存的权限,有5种格式:
r
= readw
= writex
= executes
= sharedp
= private (copy on write)offset
字段是文件中的偏移量;dev
是设备(主要:次要);inode
是该设备上的inode
。 0表示没有inode
与内存区域相关联,就像.BSS
(未初始化的数据存放的section
)
pathname
字段指向映射的文件。同时会提供几种伪地址: - [stack]
:进程的栈 - [stack:<tid>]
:各个线程的栈 - [vdso]
:虚拟动态共享对象 - [heap]
:进程的堆
我们用C语言编写一个简单的程序,在其中调用 malloc
来动态分配内存,并使用 getchar
使程序暂停,帮助我们查看程序的内存分配具体情况,源代码如下:
/* Per thread arena example. */ #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> void* threadFunc(void* arg) { printf("Before malloc in thread 1\n"); getchar(); char* addr = (char*) malloc(1000 * sizeof(char)); printf("After malloc and before free in thread 1\n"); getchar(); free(addr); printf("After free in thread 1\n"); getchar(); } int main() { pthread_t t1; void* s; int ret; char* addr; printf("Welcome to per thread arena example::%d\n", getpid()); printf("Before malloc in main thread\n"); getchar(); addr = (char*) malloc(6553500 * sizeof(char)); printf("After malloc and before free in main thread\n"); getchar() free(addr); printf("After free in main thread\n"); getchar(); ret = pthread_create(&t1, NULL, threadFunc, NULL); if(ret) { printf("Thread creation error\n"); return -1; } ret = pthread_join(t1, &s); if(ret) { printf("Thread join error\n"); return -1; } return 0; }
我们运行程序,检测输出的内容:
$ gcc main.c -lpthread -o mthread $ ./mthread Welcome to per thread arena example::13201 Before malloc in main thread
我们根据程序 PID
查看程序的内存分配情况:
$ cat /proc/13140/maps 56424b4c2000-56424b4c3000 r-xp 00000000 08:01 673794 /home/zhf/mthread 56424b6c2000-56424b6c3000 r--p 00000000 08:01 673794 /home/zhf/mthread 56424b6c3000-56424b6c4000 rw-p 00001000 08:01 673794 /home/zhf/mthread 56424c8db000-56424c8fc000 rw-p 00000000 00:00 0 [heap] ...
此时主进程并没有调用malloc
,但进程已经初始化部分内存空间作为进程的堆,地址为56424c8db000-56424c8fc000
,大小为132KB。这个堆内存的连续区域被称为arena
。这个 arena
是由主线程创建,则被称为main arena
。进一步的分配请求会继续使用这个 arena
直到 arena 空闲空间耗尽
。:
... After malloc and before free in main thread ... $ cat /proc/13201/maps 56424b4c2000-56424b4c3000 r-xp 00000000 08:01 673794 /home/zhf/mthread 56424b6c2000-56424b6c3000 r--p 00000000 08:01 673794 /home/zhf/mthread 56424b6c3000-56424b6c4000 rw-p 00001000 08:01 673794 /home/zhf/mthread 56424c8db000-56424c8fc000 rw-p 00000000 00:00 0 [heap] 7f6075cd0000-7f6076310000 rw-p 00000000 00:00 0 ...
由于我们在使用malloc
申请了一块较大的地址,原有的堆空间无法满足需求,因此会使用 mmap
系统调用,进一步扩张arena
的区域,新申请的内存区域与之前的堆相连,地址为7f6075cd0000-7f6076310000
。接着主线程free
内存块。
... After free in main thread ... $ cat /proc/13201/maps 56424b4c2000-56424b4c3000 r-xp 00000000 08:01 673794 /home/zhf/mthread 56424b6c2000-56424b6c3000 r--p 00000000 08:01 673794 /home/zhf/mthread 56424b6c3000-56424b6c4000 rw-p 00001000 08:01 673794 /home/zhf/mthread 56424c8db000-56424c8fc000 rw-p 00000000 00:00 0 [heap] ...
在主线程 free
之后: 在上面的输出里我们可以看到,当分配内存区域被释放时,其后内存不会被立即释放给操作系统。分配内存区域(1000 bytes 大小)只释放给 glibc malloc
库,在这里的释放掉的 Chunk
会被添加到 main arenas
中(在glibc malloc
里,freelist
被称为 bins
)。此后当用户申请内存时,glibc malloc
不会从内核中获得新的堆内存,而是尽量在bins
里找到一个空闲块(Free Chunk
)。只有当没有空闲块存在时,glibc malloc
才会从继续内核中申请内存。
... Before malloc in thread 1 ... $ cat /proc/13201/maps 56424b4c2000-56424b4c3000 r-xp 00000000 08:01 673794 /home/zhf/mthread 56424b6c2000-56424b6c3000 r--p 00000000 08:01 673794 /home/zhf/mthread 56424b6c3000-56424b6c4000 rw-p 00001000 08:01 673794 /home/zhf/mthread 56424c8db000-56424c8fc000 rw-p 00000000 00:00 0 [heap] 7f6075b0f000-7f6075b10000 ---p 00000000 00:00 0 7f6075b10000-7f6076310000 rw-p 00000000 00:00 0 ...
接着我们创建线程,我们可以看到,在线程调用malloc
之前,已经为线程分配好了线程堆,其地址为7f6075b10000-7f6076310000
。我们可以看到线程堆的地址进程堆的地址并不连续,这表明堆内存通过使用 mmap
系统调用而不是主线程(使用 sbrk
)创建。
... After malloc and before free in thread 1 ... $ cat /proc/13201/maps 56424b4c2000-56424b4c3000 r-xp 00000000 08:01 673794 /home/zhf/mthread 56424b6c2000-56424b6c3000 r--p 00000000 08:01 673794 /home/zhf/mthread 56424b6c3000-56424b6c4000 rw-p 00001000 08:01 673794 /home/zhf/mthread 56424c8db000-56424c8fc000 rw-p 00000000 00:00 0 [heap] 7f6070000000-7f6070031000 rw-p 00000000 00:00 0 7f6070031000-7f6074000000 ---p 00000000 00:00 0 7f6075b0f000-7f6075b10000 ---p 00000000 00:00 0 7f6075b10000-7f6076310000 rw-p 00000000 00:00 0 ...
在线程malloc
之后,出现一个新的线程堆地址,其中7f6070000000-7f6070031000
有读写权限,7f6070031000-7f6074000000
仅支持写时拷贝(copy on write
)。7f6070000000-7f6070031000
这块内存区域被称为thread arena
。
注意:当用户申请的内存大小超过 128KB( malloc(132*1024)
)并且当一个arena
里没有足够的空间来满足用户的请求时,内存是使用 mmap
系统调用来分配的(不使用 sbrk
) 无论这个请求是来自于 main arena
还是 thread arena
。
... After free in thread 1 ... $ cat /proc/13201/maps 56424b4c2000-56424b4c3000 r-xp 00000000 08:01 673794 /home/zhf/mthread 56424b6c2000-56424b6c3000 r--p 00000000 08:01 673794 /home/zhf/mthread 56424b6c3000-56424b6c4000 rw-p 00001000 08:01 673794 /home/zhf/mthread 56424c8db000-56424c8fc000 rw-p 00000000 00:00 0 [heap] 7f6070000000-7f6070031000 rw-p 00000000 00:00 0 7f6070031000-7f6074000000 ---p 00000000 00:00 0 7f6075b0f000-7f6075b10000 ---p 00000000 00:00 0 7f6075b10000-7f6076310000 rw-p 00000000 00:00 0 ...
在线程 free
之后后:在上面的输出我们可以看到,释放的堆内存并不会给操作系统。相反,这块内存区域还给了 glibc malloc
里,并将这个释放块添加到 thread arenas
的 bins
里,由 freelist
维护。
在以上示例中,我们看到了主线程包含main arena
同时每个线程包含了它自己的 thread arena
。那么是不是无论多少线程,每个线程都有自己独立的 arena
呢?显然不是,一个进程可以包含比 CPU
核数量更多的线程数量,在这样的情况下,每个线程单独有一个 arena
,其代价十分昂贵。因此,应用程序的arena 数量的限制是基于系统里现有的 CPU 核的数量。
For 32 bit systems: Number of arena = 2 * number of cores. For 64 bit systems: Number of arena = 8 * number of cores.
假设我们有一个多线程的程序(4线程 - 主线程 + 3个用户线程),在一个单核 32 位系统上运行。这里线程数量 > 2 * 核心数量。因此,glibc malloc
认定 Multiple Arena
被所有可用进程共享。但它是怎样共享的呢?
malloc
时,glibc malloc 会直接为它分配一个 main arena,不需要任何的附加条件malloc
时,会为这些线程创建一个新的 arena 。此时,各个线程与arena
是一一对应的。malloc
时, 此时 glibc malloc
能维持的 arena
数量已到达上限,因此尝试重用 现存的 arena
(main arena
、arena 1
或arena 2
)。
arena
,尽量去锁定可用的 arena
。main arena
被锁定成功),就向用户返回该 arena
。arena
是空闲的,那么就将线程3的malloc
操作 阻塞,直到有可用的arena为止。。malloc
时,malloc
会尽量使用上次访问的 arena (main arena
)。如果 main arena
是空闲的, 用户线程3会一直使用该 arena
并屏蔽其他线程的申请直到 main arena
被释放。main arena
就是这样在主线程和用户线程3间共享。在 glibc malloc
源代码里主要发现以下三种数据结构:
heap_info
—即 heap header
,因为一个thread arena
(注意:不包含main thread
) 可以包含多个heaps,所以为了便于管理,就给每个heap分配一个heap header
。那么在什么情况下一个 thread arena
会包含多个heaps呢。在当前heap不够用的时候,malloc
会通过系统调用mmap
申请新的堆空间,新的堆空间会被添加到当前thread arena
中,便于管理。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;
malloc_state
— 即Arena Header
,每个线程只含有一个Arena Header
。Area Header
包含bins信息,top chunk 以及 last remainder chunk 等。struct malloc_state { /* Serialize access. */ mutex_t mutex; /* Flags (formerly in max_fast). */ int flags; /* 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. */ struct malloc_state *next_free; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem; };
malloc_chunk
— 即chunk header
。一个 header 被分为多个 chunk,至于每个chunk 的大小,由用户的请求决定,也就是说用户调用malloc(size)
传递的size
参数”就是”chunk的大小(这里给“就是”加上引号,说明这种表示并不正确,但是为了方便立即就暂时这么描述了)。每个chunk都由一个结构体malloc_chunk
表示。struct malloc_chunk { /* #define INTERNAL_SIZE_T size_t */ INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. 这两个指针只在free chunk中存在*/ 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; };
注意:
Main arena
没有多重堆,因此没有 heap_info
结构体。当 main arena
耗尽空间时,就通过扩展sbrk
的heap segment来获取更多的空间,直到它碰到内存mapping区域为止。
不同于 thread arena
,main arena
的 Arena header
不是 sbrk sbrk heap segment
堆分段的一部分。它是一个全局变量,因此它属于 lib.so 的 data segment。
首先,通过内存分布图解释清malloc_state
与 heap_info
之间的组织关系。
下面是只有一个 heap segment 的 main arena
和 thread arena
的内存分布图::
下面是一个thread arena
中含有多个 heap segments 的情况:
上图可以看出,thread arena
只含有一个malloc_state
(即arena header),却有两个heap_info
(即heap header)。由于两个 heap segment 是通过 mmap
分配内存,两者 heap segments 是通过mmap
分配的内存,两者在内存布局上并不相邻而是分属于不同的内存区间,所以为了便于管理,libc malloc
将第二个heap_info
结构体的prev
成员指向第一个heap_info
结构体的起始位置(即ar_ptr
成员),而第一个heap_info
结构体的ar_ptr
成员指向了malloc_state
,这样就构成一个单链表,方便以后管理。
在堆里的块可以是以下几种类型中的一种:
Allocated chunk
—— 分配后的块Free chunk
—— 空闲块Top chunk
—— 开始块Last Remainder chunk
—— 最后剩余块从本质上来说,所有类型的 chunk 都是内存中一块连续的区域,只是通过该区域中特定位置的某些标识符加以区分。为了简便,我们先将这4类 chunk 简化为2类:allocate chunk
以及free chunk
,前者标识已经分配给用户使用的 chunk ,后者表示未使用的 chunk。
任何堆内存管理器都是以chunk为单位进行堆内存管理的,而这就需要一些数据结构来标志各个块的边界,以及区分已经分配块和空闲块。大多数堆内存管理都将这些边界信息作为 chunk 的一部分嵌入到 chunk 内部,下图为 allocated chunk 的结构:
size
:这部分包含了此处已分配的块的容量大小。
堆内存中要求每个chunk的大小必须为8的整数倍,因此chunk size的后3位是无效,为了充分利用内存,堆管理奖这3个bit比特位用作chunk的标志位,最后三位包含以下信息:
PREV_INUSE(P)
——如果之前的块已经被分配,该位置1IS_MMAPPED(M)
—— 如果块被映射,该位置1NON_MAIN_ARENA(N)
—— 如果该快属于一个 thread arena
,该位置1请注意以下几点:
malloc_chunk
(比如 fd — forward pointer,bk – back pointer
)。因此这部分区域会用来存储用户信息。malloc_chunk
需要一些额外的空间,用户请求的容量需要转换成实际需要的容量。转化不会改变最优 3 bits,因此它们用于存储关键信息。以下是空闲块各个部分内容的说明:
prev_size
:不能同时调整两个空闲的块。当两个块都空闲时,它就会与一个单独的空闲块连接。因此前一个块及当前这个释放的块会被分配,同时 prev_size
包含前一个块的用户数据。size
:这个部分包含有空闲块的 size
fd
:Forward pointer —— 同一bin
里指向下一块的指针(不是指向物理内存内的下一块)bk
:Backward pointer —— 同一bin
里指向前一块的指针(不是指向物理内存内的前一块)bin
是一种 freelist
数据结构,他们用来管理空闲的块。根据快的大小,以下为几不同的bins
:
用来装载这些 bins
的数据结构有:
fastbinsY
:装有 fast bins 的数组bins
:装有 unsorted, small, large bins 总共有126个,按照以下的规则被划分:
大小在 16 到 80 bytes 的块叫做 fast chunk
。支持 fast chunk
的 bin 叫做 fast bins。在上述的这些 bin 里,fast bins 在内存分配和重新分配上更快。(译者注:只有 fast bin 是 LIFO 其他的 bin
都是 FIFO)
malloc(fast chunk)
free(fast chunk)
当小块或大块被释放时,它会被添加到 unsorted bin 里。 这给了 glibc malloc
再次利用最近释放的块的机会。因此内存分配以及回收的速度都会有所提升(因为 unsorted bin)由于排除了用于查找合适容器的时间。
小于 512 bytes 的块叫做 small chunk。 支持 small chunks 的容器叫做 small bins。 Small bins 在内存分配和回收时比 large bins 快(比 fast bins 慢)。
malloc(small chunk)
malloc
期间,在 malloc_state 里发现的 small bin 和 large bin 数据结构(bins)被初始化(即,bins 会指向它自己表示他们是空的)。free(small chunk)
大小大于等于 512 bytes 的块称为 large chunk。支持 large chunks 的 bins 叫作 large bins。Large bins 在内存分配和回收中比 small bins 要慢。
malloc(large chunk)
malloc
期间,在 malloc_state 里发现的 small bin 和 large bin 被 初始化。即,bins 会指向它自己表示它们是空的。free(large chunk)
—— 过程与 free(small chunk)
类似处于 arena
顶部的块叫做 top chunk。它不属于任何 bin。而是在系统当前的所有 free chunk 都无法满足用户请求)的内存大小的时候,将此 chunk 分配给用户使用。如果 top chunk 比用户请求的容量要大,top chunk 将会分为两个块:
如果内存空间不足,top chunk 使用 sbrk (main arena
) 或 mmap
(thread arena
)系统调用来扩展内存空间。
last remainder chunk 是由最近一次请求,对块进行分裂而产生的。last remainder chunk 加强了引用的局部性(即,连续的 malloc small chunk
获取的空间可能彼此相邻)。
但是除了在一个 arena
里可利用的的块,哪些块有资格成为 last remainder chunk?
当用户请求一个small chunk,且该请求无法被 small bin,unsorted bin 满足的时候,就通过 bin maps 遍历合适的 chunk,如果该 chunk 有剩余部分的话,就将剩余部分变成一个新的 chunk 加入到 unsorted bin 中,另外,再将该新的 chunk 变成新的last remainder chunk。
那么如何使引用存在局部性?
当用户请求一个small chunk,且该请求无法被 small bin满足,那么就转而交由 unsorted bin处理。同时,假设当前 unsorted bin 中只有一个 chunk 的话——就是last remainder chunk,那么就将 chunk 分成两部分:前者分配给用户,剩下的部分放到 unsorted bin 中,并成为新的 last remainder chunk。这个就保证了连续malloc(small chunk)中,各个small chunk在内存分布中是相邻的,即提高内存分配的局部性。