当前位置: 首页 > news >正文

Linux 用户态内存分配:glibc malloc

在日常开发里,不管是 C 还是 C++ 语言,我都习惯用malloc申请内存。就像下面这段简单的 C 语言代码:

#include <stdio.h> #include <stdlib.h> int main() { int *ptr = (int *)malloc(4 * sizeof(int)); if (ptr == NULL) { perror("malloc failed"); return 1; } // 使用内存 for (int i = 0; i < 4; i++) { ptr[i] = i; printf("%d ", ptr[i]); } printf("\n"); // 释放内存 free(ptr);

调用malloc分配内存、free释放,整个流程看似简单,但背后的逻辑远比我们看到的复杂。

多线程同时调用malloc时,怎么避免内存冲突?为什么小块内存分配往往比大块更快?这些问题,其实都和glibc malloc的实现逻辑相关。

Linux系统中,glibc malloc是默认的用户态内存分配器,程序运行时的内存分配、释放,全靠它在背后调度。它的性能好坏,直接影响程序的内存利用率,尤其是多线程场景下,更是决定了程序的并发能力。今天我们就一步步拆解它的实现,把这些底层逻辑讲清楚。

一、glibc malloc的底层依赖:brk与mmap系统调用

1.1 brk与mmap的适用场景

glibc malloc本身不直接向操作系统申请内存,它的底层依赖两个关键系统调用:brk和mmap。这两个调用分工不同,glibc会根据内存分配的大小,选择用哪个来完成申请。默认情况下,这个区分阈值是128KB,不过也可以手动调整。

当程序请求分配的内存大小小于 128KB 时 ,glibc malloc会优先选择brk系统调用。brk的工作方式相对直接,它通过调整程序数据段的结束地址(也就是堆顶指针_edata),将其往高地址方向推移,从而在堆空间中为程序分配新的内存 。这种方式就像是在已有的堆空间 “蛋糕” 上,直接切下一块合适大小的部分给程序使用 。由于堆空间是连续的,而且在进程启动时就已经有了一定的初始大小,所以通过brk分配内存的过程相对简单高效,不需要额外去寻找其他内存区域 。并且,在这个过程中,brk分配的内存地址通常比较靠近程序的数据段,这对于程序访问这些内存数据来说,在内存访问局部性原理上有一定优势,能减少内存访问的时间开销 。

而当程序申请的内存大小大于或等于 128KB 时 ,glibc malloc则会启用mmap系统调用 。mmap采用的是在进程的虚拟地址空间中,于堆和栈之间的文件映射区域(也称为匿名映射区域)寻找一块空闲的虚拟内存进行分配 。它就像是在堆和栈之间的 “空地” 上,开辟出一块新的区域专门给这次的内存请求使用 。这样做的好处是,对于大块内存的分配,通过mmap可以避免对堆空间造成 “污染”,防止因为频繁分配和释放大块内存导致堆空间产生过多的内存碎片,从而影响后续小块内存的分配效率 。而且,mmap分配的内存相对独立,在释放时可以直接归还给操作系统,不像brk分配的内存释放后可能还留在堆空间的空闲列表中 。值得一提的是,这个 128KB 的阈值并不是固定不变的,我们可以通过mallopt函数来灵活调整它 。例如,如果我们希望在程序中让更多的内存分配使用mmap方式,可以通过mallopt(M_MMAP_THRESHOLD, new_threshold)来降低这个阈值,这样当内存申请大小达到新的阈值时就会使用mmap进行分配 ;反之,如果希望更多地使用brk分配方式,就可以提高这个阈值 。

1.2 brk与mmap的地址分布差异

从虚拟内存布局来看,brk和mmap分配的内存地址,差异非常明显。以 32 位 Linux 系统为例,其虚拟地址空间总共有 4GB,其中低 3GB 是用户空间,高 1GB 是内核空间 。在用户空间中,从低地址到高地址依次分布着代码段、数据段、BSS 段、堆、文件映射区域和栈 。

brk分配的内存位于堆区,堆区的起始地址通常在 0x8048000 附近 ,随着程序不断通过brk分配内存,堆顶指针(_edata)会不断向高地址方向扩展,就像一个不断生长的 “高塔” 。例如,在一个进程启动后,堆区可能最初只有很小的一块空间,当程序调用malloc分配小于 128KB 的内存时,brk会将堆顶指针向上移动相应的大小,在堆区中划分出一块新的内存区域供程序使用 。如果后续又有多次小于 128KB 的内存分配请求,堆顶指针会继续向上扩展,这些分配的内存块在堆区中是连续排列的 。

mmap分配的内存则位于堆和栈之间的文件映射区域 。在早期的 Linux 内核版本(2.6.9 之前)中,mmap分配内存的默认起始地址通常在 0x40000000 附近 ;而在 2.6.9 及之后的内核版本中,这个起始地址可以通过/proc文件系统中的相关参数进行配置 。mmap分配的内存区域从文件映射区域的某个位置开始,向高地址方向增长 。当程序使用mmap分配内存时,它会在这个文件映射区域中找到一块合适大小的空闲空间进行分配 。而且,当通过mmap分配的内存被释放时,这块内存会直接归还给操作系统,操作系统可以立即将其重新分配给其他需要的进程 ;而brk分配的内存释放后,并不会立即归还给操作系统,而是会被glibc malloc存入空闲列表中,等待后续的内存分配请求复用 。这种地址分布和内存释放机制的差异,使得brk和mmap在不同的内存分配场景下,各自发挥着独特的作用,共同支撑着glibc malloc高效地管理内存 。

二、多线程并发核心:Arena机制

2.1 Arena的定义与分类

多线程场景下,arena是glibc malloc实现高效并发的核心,相当于每个线程专属的内存“小仓库”。从定义上来说,Arena是glibc malloc内部管理的一个独立的堆内存分配区域,每个Arena都拥有一套独立的内存管理数据结构,包括空闲列表(bins)、内存块(chunks)等 ,这些数据结构用于管理和维护该Arena内的内存分配与释放操作 。

Arena主要分为两种类型:主分配区(main arena)和非主分配区(non-main arena) 。主线程在程序启动时,会默认绑定到main arena 。main arena基于brk系统调用进行内存分配,它的堆空间从进程数据段的末尾(_edata)开始,向上增长 。当主线程调用malloc函数分配内存时,首先会在main arena的空闲列表中查找合适的空闲内存块 。如果找到,则直接从该空闲块中分割出所需大小的内存返回给主线程;如果没有找到合适的空闲块,main arena会尝试通过brk系统调用向操作系统申请更多的内存 。

而对于其他线程,当它们首次调用malloc函数时,会创建一个专属自己的non-main arena 。non-main arena是基于mmap系统调用在进程的虚拟地址空间中的文件映射区域分配内存 。这样每个线程都有了自己独立的内存分配区域,在进行内存分配和释放操作时,各个线程之间互不干扰 。例如,在一个多线程的数据库查询程序中,当多个线程同时进行数据库查询操作时,每个线程都可能需要分配内存来存储查询结果 。通过Arena机制,每个线程在自己的non-main arena中独立分配内存,避免了多个线程同时访问同一个内存分配区域带来的锁竞争问题,大大提高了程序的并发性能 。

2.2 Arena的数量限制

Arena的数量并非无限制的,它与系统的硬件架构,尤其是 CPU 核心数紧密相关 。在 32 位系统中,Arena的数量上限通常为8 * CPU核心数 ;在 64 位系统中,同样也是8 * CPU核心数 。这个上限设置是综合考虑了系统资源的利用和性能平衡 。当线程数量小于或等于Arena的上限时,每个线程都能拥有自己独立的non-main arena,从而实现高效的并发内存分配 。

一旦线程数量超过了Arena的上限,情况就会变得复杂一些 。此时,新创建的线程无法再拥有自己独立的non-main arena,它们需要尝试复用已有的Arena 。这些新线程会竞争已有的Arena,通过加锁的方式来访问和使用Arena中的内存资源 。如果在某个时刻,所有的Arena都被其他线程占用,没有可用的Arena供新线程使用,那么新线程就会被阻塞,进入等待状态,直到有Arena被释放并可用 。例如,在一个具有 8 个 CPU 核心的 64 位系统中,Arena的上限为 64 个 。当线程数量达到 65 个时,第 65 个线程就需要竞争复用已有的 64 个Arena 。在高并发的多线程应用场景中,这种对Arena的竞争和复用可能会成为性能瓶颈,因为频繁的加锁和解锁操作会带来额外的开销 。所以,在进行多线程程序开发时,合理控制线程数量,避免线程数量过度超过Arena上限,对于提高程序的性能至关重要 。

2.3 实战案例

在 ptmalloc2 中,当两个线程同时调用 malloc 时,内存均会得以立即分配——每个线程都维护着单独的堆,各个堆被独立的空闲列表数据结构管理,因此各个线程可以并发地从空闲列表数据结构中申请内存。这种为每个线程维护独立堆与空闲列表数据结构的行为就「per thread arena」。

案例代码

/* 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); 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(1000); 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; }

2.3.1 案例输出

(1)在主线程 malloc 之前

从如下的输出结果中我们可以看到,这里还没有堆段也没有每个线程的栈,因为 thread1 还没有创建!

sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread b7e05000-b7e07000 rw-p 00000000 00:00 0 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

(2) 在主线程 malloc 之后

从如下的输出结果中我们可以看到,堆段已经产生,并且其地址区间正好在数据段(0x0804b000 - 0x0806c000)上面,这表明堆内存是移动Program Break的位置产生的(也即通过 brk 中断)。此外,请注意,尽管用户只申请了 1000 字节的内存,但是实际产生了 132KB的堆。这个连续的堆区域被称为「arena」。因为这个 arena 是被主线程建立的,因此其被称为「main arena」。接下来的申请会继续分配这个 arena 的 132KB 中剩余的部分。当分配完毕时,它可以通过继续移动 Program Break 的位置扩容。扩容后,「top chunk」的大小也随之调整,以将这块新增的空间圈进去;相应地,arena 也可以在 top chunk 过大时缩小。

注意:top chunk 是一个 arena 位于最顶层的 chunk。有关 top chunk 的更多信息详见后续章节「top chunk」部分。

sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread ... sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7e05000-b7e07000 rw-p 00000000 00:00 0 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

(3)在主线程 free 之后

从如下的输出结果中我们可以看到,当分配的内存区域 free 掉时,其并不会立即归还给操作系统 ,而仅仅是移交给了作为库函数的分配器。这块 free 掉的内存添加在了「main arenas bin」中(在 glibc malloc 中,空闲列表数据结构被称为「bin」)。随后当用户请求内存时,分配器就不再向内核申请新堆了,而是先试着各个「bin」中查找空闲内存。只有当 bin 中不存在空闲内存时,分配器才会继续向内核申请内存。

sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread ... sploitfun@sploitfun-VirtualBox:~/lsploits/hof/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7e05000-b7e07000 rw-p 00000000 00:00 0 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

(4)在 thread1 malloc 之前

从如下的输出结果中我们可以看到,此时 thread1 的堆尚不存在,但其栈已产生。

sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread Before malloc in thread 1 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7604000-b7605000 ---p 00000000 00:00 0 b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594] ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

(5)在 thread1 malloc 之后

从如下的输出结果中我们可以看到,thread1 的堆段(b7500000 - b7521000,132KB)建立在了内存映射段中,这也表明了堆内存是使用 mmap 系统调用产生的,而非同主线程一样使用 sbrk 系统调用。类似地,尽管用户只请求了 1000B,但是映射到程地址空间的堆内存足有 1MB。这 1MB 中,只有 132KB 被设置了读写权限,并成为该线程的堆内存。这段连续内存(132KB)被称为「thread arena」。

注意:当用户请求超过 128KB(比如 malloc(132*1024)) 大小并且此时 arena 中没有足够的空间来满足用户的请求时,内存将通过 mmap 系统调用(不再是 sbrk)分配,而不论请求是发自 main arena 还是 thread arena。

ploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread Before malloc in thread 1 After malloc and before free in thread 1 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7500000-b7521000 rw-p 00000000 00:00 0 b7521000-b7600000 ---p 00000000 00:00 0 b7604000-b7605000 ---p 00000000 00:00 0 b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594] ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

(6)在 thread1 free 之后

从如下的输出结果中我们可以看到,free 不会把内存归还给操作系统,而是移交给分配器,然后添加在了「thread arenas bin」中。

sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread After malloc and before free in main thread After free in main thread Before malloc in thread 1 After malloc and before free in thread 1 After free in thread 1 ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ cat /proc/6501/maps 08048000-08049000 r-xp 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 08049000-0804a000 r--p 00000000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804a000-0804b000 rw-p 00001000 08:01 539625 /home/sploitfun/ptmalloc.ppt/mthread/mthread 0804b000-0806c000 rw-p 00000000 00:00 0 [heap] b7500000-b7521000 rw-p 00000000 00:00 0 b7521000-b7600000 ---p 00000000 00:00 0 b7604000-b7605000 ---p 00000000 00:00 0 b7605000-b7e07000 rw-p 00000000 00:00 0 [stack:6594] ... sploitfun@sploitfun-VirtualBox:~/ptmalloc.ppt/mthread$

往期文章推荐

图解 TCP/IP协议,看图秒懂

图解 C/C++ 多线程,看图秒懂

爆肝整理!嵌入式开发必知10种调试手段

【图解】SSH(安全外壳协议)工作原理

👉【就业避坑】C++ 就业前景全解析:为什么劝退声不断,大厂核心岗仍刚需 C++?

👉【大厂标准】Linux C/C++ 后端开发系统学习路线

👉【音视频】音视频流媒体高级开发核心学习路径

👉Qt进阶】C++ Qt 桌面 & 嵌入式开发一条龙学习攻略

👉【内核底层】Linux 内核硬核修炼指南

👉【面试冲刺】C/C++ 高频八股面试题 1000 题(三)

👉【项目实战】手撕线程池:C++ 程序员的能力试金石

三、内存管理的最小单元:Chunk

glibc malloc管理内存的最小单元,是chunk,,所有的内存分配与释放操作,归根结底都是围绕着Chunk展开 。Chunk可以简单理解为glibc malloc内部管理的一个内存块,它既可以是已经分配给用户程序使用的内存块,也可以是处于空闲状态,等待被分配的内存块 。每个Chunk都包含了用于管理该内存块的元数据(metadata),这些元数据就像是每个Chunk的 “身份标识” 和 “管理标签”,记录了该Chunk的大小、状态(已分配或空闲)、以及与其他Chunk之间的关联关系等重要信息 。通过这些元数据,glibc malloc能够高效地对Chunk进行分配、释放、合并等操作,从而实现对内存的精细管理 。根据Chunk的状态不同,我们可以将其分为已分配Chunk和空闲Chunk,它们在结构和用途上都有着各自的特点 。

3.1 已分配Chunk的结构

已分配Chunk,顾名思义,就是已经被分配给用户程序使用的内存块 。在 64 位系统中,其结构主要包含两个关键的元数据字段 。第一个是prev_size字段,它占用 8 个字节 。这个字段的作用很特殊,如果当前Chunk的前一个Chunk是空闲状态,那么prev_size就用于记录前一个Chunk的大小 ;而如果前一个Chunk处于已分配状态,那么prev_size这个字段就会被前一个Chunk复用,用于存储用户数据 。例如,假设有两个连续的已分配Chunk,前一个Chunk分配给用户的大小是 32 字节,后一个Chunk的prev_size字段在这种情况下就会被前一个Chunk用来存储它的用户数据 。

另一个关键字段是size字段,同样占用 8 个字节 。这个字段不仅记录了当前Chunk的大小,还包含了一些重要的标志位信息 。其中最低 3 位是标志位,第一位(最低位)是PREV_INUSE标志位,当它的值为 1 时,表示前一个Chunk处于已分配状态;为 0 时,表示前一个Chunk是空闲状态 。这对于glibc malloc在释放内存时判断是否可以合并相邻的空闲Chunk非常重要 。第二位是IS_MMAPPED标志位,若为 1,说明当前Chunk是通过mmap系统调用分配的;为 0 则表示是从堆中分配 。第三位是NON_MAIN_ARENA标志位,值为 1 时,表明当前Chunk来自非主分配区(即线程专属的non-main arena);为 0 则来自主分配区 。当我们要获取当前Chunk的实际大小时,需要通过size & ~0x7操作,清除最低 3 位的标志位 。例如,若size的值为 0x21(二进制为 0010 0001),那么实际大小就是0x21 & ~0x7 = 0x20,即 32 字节 。这些元数据字段都隐藏在用户真正使用的内存地址之前,并且它们的存在并不会占用用户通过malloc申请的内存空间 。比如,当用户调用malloc(16)申请 16 字节内存时,实际分配的Chunk大小可能会大于 16 字节,多出来的部分就是用于存储这些元数据 。

3.2 空闲Chunk的结构与特性

空闲Chunk,是那些已经被释放,处于空闲状态,等待再次被分配使用的内存块 。它在已分配Chunk的结构基础上,又增加了两个重要的指针字段 。fd(forward pointer)指针,也就是前向指针,它指向双向空闲列表中的下一个空闲Chunk;bk(backward pointer)指针,即后向指针,指向双向空闲列表中的前一个空闲Chunk 。通过这两个指针,空闲Chunk能够被串联成一个双向链表结构,方便glibc malloc进行快速的查找和分配操作 。当有新的内存分配请求时,glibc malloc可以直接从这个双向空闲列表中查找合适大小的空闲Chunk 。

空闲Chunk还有一个重要的特性,就是它会自动与相邻的空闲Chunk进行合并 。当一个Chunk被释放并标记为空闲时,glibc malloc会检查其相邻的Chunk是否也处于空闲状态 。如果相邻的Chunk也是空闲的,那么就会将它们合并成一个更大的空闲Chunk 。假设我们有三个连续的Chunk,中间的Chunk被释放后,glibc malloc发现其前后两个Chunk也都是空闲的,就会将这三个Chunk合并成一个大的空闲Chunk 。这样做的好处是,能够有效地减少内存碎片的产生,提高内存的复用率 。因为如果不进行合并,随着内存的不断分配和释放,可能会产生大量的小块空闲Chunk,这些小块空闲Chunk在面对较大的内存分配请求时,可能无法满足需求,从而导致内存浪费 。而通过合并操作,就可以将这些零散的空闲Chunk整合起来,形成更大的空闲内存块,以便更好地满足后续的内存分配需求 。

四、空闲内存管理:Bins家族

空闲chunk的管理,靠的是bins家族,相当于glibc的“内存收纳系统”。不同类型的bins分工不同,分别管理不同大小、不同状态的空闲chunk,目的就是让内存分配更高效,避免频繁遍历查找。

4.1 Fast Bin:小内存块的快速分配

Fast Bin专门负责管理小尺寸的内存块 。在默认情况下,对于 64 位系统,它主要管理大小小于等于 64 字节(即0x40)的内存块 ;对于 32 位系统,则管理小于等于 32 字节(即0x20)的内存块 。Fast Bin采用的是单链表数据结构,这种结构就像是一串紧密相连的 “小格子”,每个 “格子” 里存放着一个空闲Chunk 。当程序请求分配小块内存时,Fast Bin能够迅速响应 。它会从链表头部取出一个空闲Chunk,直接分配给程序,这个过程几乎不需要进行复杂的查找和比较操作,大大节省了时间 。例如,在一个频繁进行小块内存分配的图形渲染程序中,当需要不断分配小块内存来存储图形的顶点数据时,Fast Bin能够快速地将空闲Chunk分配出去,保证图形渲染的流畅性 。

Fast Bin还有一个独特的设计,就是当内存块被释放并放入Fast Bin时,它不会立即去合并相邻的空闲内存块 。这是为了进一步提高分配速度,因为合并操作需要额外的时间和计算资源 。不过,这种设计也可能会导致一定程度的内存碎片问题 。为了平衡分配速度和内存碎片,Fast Bin设定了一个内存块数量阈值 。当某个Fast Bin中的内存块数量达到这个阈值(默认是 64 个)时,glibc malloc就会触发合并操作 。它会将这些内存块从Fast Bin中取出,合并相邻的空闲块,然后再将合并后的大内存块放入Unsorted Bin中 。例如,当一个Fast Bin中存储了 64 个大小为 16 字节的空闲Chunk时,就会进行合并操作,将它们合并成更大的空闲块,再放入Unsorted Bin,以便后续更合理地分配和利用内存 。

4.2 Unsorted Bin:空闲内存的临时中转

unsorted bin相当于空闲chunk的“临时中转站”,当一个内存块被释放时,如果它不属于Fast Bin管理的范围,或者虽然属于Fast Bin但当前Fast Bin已满触发了合并操作,那么这个内存块就会被放入Unsorted Bin中 。Unsorted Bin采用双向循环链表结构,这使得它在数据的插入和删除操作上都比较高效 。

在内存分配时,glibc malloc会优先检查Unsorted Bin 。这是因为刚释放的内存块很可能很快又会被再次使用,通过先在Unsorted Bin中查找,可以大大提高内存复用的效率 。当程序请求分配内存时,如果Unsorted Bin中有大小合适的内存块,glibc malloc就会直接将其分配给程序 。假设一个程序在处理网络数据包时,先分配了一块内存用于存储数据包,处理完后释放,紧接着又有新的数据包需要存储,此时这块刚释放的内存很可能就会从Unsorted Bin中被再次分配出去 。如果Unsorted Bin中没有合适大小的内存块,glibc malloc就会将Unsorted Bin中的内存块进行分类,根据其大小将它们分别放入Small Bin或Large Bin中,以便后续更精准地进行内存分配 。这种先将释放的内存块放入Unsorted Bin,再根据需求进行分类的方式,有效地减少了内存分配过程中频繁分类带来的系统开销 。

4.3 Small Bin与Large Bin:不同尺寸内存的精准匹配

small bin和large bin,负责精准匹配不同大小的chunk,两者分工明确,覆盖了大部分内存分配场景。

Small Bin主要管理大小小于等于 512 字节(即0x200)的内存块 。它包含 62 个双向循环链表,每个链表都对应一种固定大小的内存块 。第一个Small Bin链表中的内存块大小为 16 字节(即0x10),之后每个链表中的内存块大小依次递增 8 字节 ,最后一个链表中的内存块大小为 512 字节 。这种设计就像是一个有序排列的 “小件收纳架”,每个格子都存放着固定尺寸的 “小件物品” 。在内存分配时,Small Bin采用最佳适配(Best-fit)算法 。当程序请求分配一个小于等于 512 字节的内存块时,glibc malloc会根据请求的大小,直接找到对应的Small Bin链表,从链表中取出一个内存块分配给程序 。在一个数据库索引构建程序中,需要频繁分配一些大小固定的小块内存来存储索引节点,Small Bin就能通过这种精准匹配的方式,快速地将合适大小的内存块分配出去,提高索引构建的效率 。

Large Bin则负责管理大小大于 512 字节的内存块 。它同样采用双向链表结构,但与Small Bin不同的是,Large Bin中的内存块大小不是固定的,而是按照一定的尺寸范围进行分组 。Large Bin共有 63 个链表,前 32 个链表的内存块大小范围以 64 字节为步长递增 ,例如第一个链表中的内存块大小范围是 512 - 575 字节(即0x200 - 0x23F),第二个链表是 576 - 639 字节(即0x240 - 0x27F);紧接着的 16 个链表以 512 字节为步长递增 ;之后的 8 个链表以 4096 字节为步长递增 ;再之后的 4 个链表以 32768 字节为步长递增 ;最后的 2 个链表以 262144 字节为步长递增 ,剩余的超大内存块则放入最后一个Large Bin链表中 。在分配内存时,Large Bin采用首次适配(First-fit)策略 。当程序请求分配一个大于 512 字节的内存块时,glibc malloc会先确定请求大小所在的Large Bin链表,然后从该链表的尾部开始遍历,找到第一个大小大于或等于请求大小的内存块进行分配 。如果找到的内存块大于请求大小,glibc malloc会将其分割成两部分,一部分返回给程序,另一部分则作为新的空闲块,根据其大小放入合适的Bin中 。在一个大型文件处理程序中,当需要分配较大内存块来存储文件数据时,Large Bin就能通过这种方式,在众多不同大小的内存块中找到合适的进行分配,既满足了程序对大块内存的需求,又能合理地管理剩余的空闲内存 。

4.4 Top Chunk与Last Remainder Chunk:空闲内存兜底机制

除了前面说的几种bins,还有两个特殊的chunk,负责兜底——top chunk和last remainder chunk,当其他bins没有合适的空闲chunk时,就靠它们来满足分配需求。

Top Chunk位于Arena的顶部,是一块特殊的空闲内存块 。当Fast Bin、Unsorted Bin、Small Bin和Large Bin中都没有合适的空闲内存块来满足程序的分配请求时,glibc malloc就会从Top Chunk中切割出一块合适大小的内存分配给程序 。例如,在一个不断进行复杂数据处理的程序中,随着内存分配和释放的不断进行,其他Bin中的空闲内存块都被用尽,此时如果又有新的内存分配请求,就会从Top Chunk中获取内存 。如果切割后的Top Chunk还剩余一部分内存,那么这部分剩余内存就会成为新的Top Chunk,继续等待下一次的分配请求 。当Top Chunk的大小不足以满足当前内存分配请求时,glibc malloc会根据请求的大小,通过brk或mmap系统调用向操作系统申请更多的内存,以扩充Top Chunk的大小 。

Last Remainder Chunk是在进行小内存分配时产生的 。当从一个较大的空闲内存块中分配出一块小内存后,剩余的部分就会成为Last Remainder Chunk 。这个Last Remainder Chunk会被保留下来,用于满足后续的小内存分配请求 。因为它的大小通常比较适合小内存分配,所以可以避免频繁地从Top Chunk中切割内存,减少内存碎片的产生 。假设一个程序在进行一系列小内存分配操作时,先从一个较大的空闲块中分配出一块小内存,剩余的部分成为Last Remainder Chunk,当后续又有小内存分配请求时,就可以直接从这个Last Remainder Chunk中分配,而不需要再次从Top Chunk切割,从而提高了内存分配的效率 。

五、内存分配策略

5.1 申请流程

申请流程glibc中malloc内存分配大体逻辑:

  • 分配内存 < DEFAULT_MMAP_THRESHOLD,走brk,从内存池获取,失败的话走brk系统调用
  • 分配内存 > DEFAULT_MMAP_THRESHOLD,走mmap,直接调用mmap系统调用

其中,DEFAULT_MMAP_THRESHOLD默认为128k,可通过mallopt进行设置。 重点看下小块内存(size > DEFAULT_MMAP_THRESHOLD)的分配,

glibc在内存池中查找合适的chunk时(此处不考虑fastbin和tcache),采用了最佳适应的伙伴算法。

1、如果分配内存<512字节,则通过内存大小定位到smallbins对应的index上(floor(size/8))

  • smallbins[index]为空,进入步骤3
  • smallbins[index]非空,直接返回第一个chunk

2、如果分配内存>512字节,则定位到largebins对应的index上

  • largebins[index]为空,进入步骤3
  • largebins[index]非空,扫描链表,找到第一个大小最合适的chunk,如size=12.5K,则使用chunk B,剩下的0.5k放入unsorted_list中

3、遍历unsorted_list,查找合适size的chunk,如果找到则返回;否则,将这些chunk都归类放到smallbins和largebins里面

4、index++从更大的链表中查找,直到找到合适大小的chunk为止,找到后将chunk拆分,并将剩余的加入到unsorted_list中

5、如果还没有找到,那么使用top chunk

6、或者,内存<128k,使用brk;内存>128k,使用mmap获取新内存

5.2 释放流程

free释放内存到其内存池时,有两种情况:

  • chunk和top chunk相邻,则和top chunk合并
  • chunk和top chunk不相邻,则直接插入到unsorted_list中

5.3 内存碎片

按照glibc的内存分配策略,我们考虑下如下场景(假设brk其实地址是512k):

  • malloc 40k内存,即chunkA,brk = 512k + 40k = 552k
  • malloc 50k内存,即chunkB,brk = 552k + 50k = 602k
  • malloc 60k内存,即chunkC,brk = 602k + 60k = 662k
  • free chunkA。

此时,由于brk = 662k,而释放的内存是位于[512k, 552k]之间,无法通过移动brk指针,将区域内内存交还操作系统,因此,在[512k, 552k]的区域内便形成了一个内存碎片。 按照glibc的策略,free后的chunkA区域由于不和top chunk相邻,因此,无法和top chunk 合并,应该挂在unsorted_list链表上。

5.4 多线程下的竞争抢锁

并发条件下,main_arena引发的竞争将会成为限制程序性能的瓶颈所在,因此glibc采用了多arena机制,线程A分配内存时获取main_arena锁成功,将在main_arena所管理的内存中分配;此时线程B获取main_arena失败,glibc会新建一个arena1,此次内存分配从arena1中进行。

这种策略,一定程度上解决了多线程下竞争的问题;但是随着arena的增多,内存碎片出现的可能性也变大了。例如,main_arena中有10k、20k的空闲内存,线程B要获取20k的空闲内存,但是获取main_arena锁失败,导致留下20k的碎片,降低了内存使用率。

普通arena结构

  • 一个arena由多个Heap构成
  • 每个Heap通过mmap获得,最大为1M,多个Heap间可能不相邻
  • Heap之间有prev指针指向前一个Heap
  • 最上面的Heap,也有top chunk

每个Heap里面也是由chunk组成,使用和main_arena完全相同的管理方式管理空闲chunk。

main arena和普通arena的区别 main_arena是为一个使用brk指针的arena,由于brk是堆顶指针,一个进程中只可能有一个,因此普通arena无法使用brk进行内存分配。普通arena建立在mmap的机制上,内存管理方式和main_arena类似,只有一点区别,普通arena只有在整个arena都空闲时,才会调用munmap把内存还给操作系统。

http://www.gsyq.cn/news/1604904.html

相关文章:

  • WinUtil:Windows系统优化终极工具 - 一键完成软件安装、系统调优与故障修复
  • 14-already flash encrypt or secure boot提示:ESP32S3误烧熔丝的补救方法
  • 猫抓浏览器扩展:全网视频音频资源一键抓取的终极指南
  • 高颜值出差住地铁口可猫咪的酒店步行 3 分钟到地铁
  • volatile有什么用
  • 告别繁琐操作:原神脚本让你的提瓦特冒险更智能高效
  • PCB 新手 18 类常见错误汇总
  • EtherCAT重学之二: EtherCAT 系统硬件架构
  • 大湾区EMBA特色测评:科学选型理性指南
  • 【LeetCode】第1题 两数之和
  • CBDC安全架构:密码学签名与硬件防护核心技术解析
  • 【单片机毕业设计】基于 STM32 的多模式智能路灯控制系统设计, 基于单片机的光照自适应路灯亮度调节系统设计(014001)
  • 为什么顶尖AI团队拒绝“通用提示词”?——稀缺首发:金融/医疗/法律三大垂直领域217条经审计Prompt资产包(限时开放下载)
  • Java 多线程:继承 Thread 与实现 Runnable 两种创建方式完整对比
  • 自动定期备份服务器数据
  • python下载M3U8视频脚本
  • AI截图工具免费下载,基于DeepSeek的OCR截图软件支持Mac和Win
  • 【单片机毕业设计】基于 STM32 的超重声光报警电子秤设计与实现,基于 STM32 的阈值式重量监测报警系统设计(013701)
  • Burp Suite实战:验证码场景下的自动化渗透测试与绕过技术
  • ABB工业机器人编程基础(十三)功能程序(FUNC)
  • 第八、九次作业
  • 考四级的资料|过四级必备资料书|英语六级备考资料
  • MySQL数据库期末复习②
  • 英语四级考资料|四级考试英语资料|英语四级考试资料
  • 2026学生降AI率工具盘点: 学术打磨+逻辑优化哪家强?
  • 使用Hermes 排查OpenClaw 从 5.12 升级到 6.10 的故障
  • 第八次作业和第九次作业
  • 【小白也能轻松玩转龙虾】虾壳云一键部署办公增效,批量文件处理 OpenClaw v2.7.9 教学(附最新安装包)
  • Linux基础指令(一):命令行入门
  • 【ChatGPT结构化提示词黄金法则】:20年AI工程实战提炼的7大不可绕过的设计范式