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 还没有创建sploitfunsploitfun-VirtualBox:~/ptmalloc.ppt/mthread$ ./mthread Welcome to per thread arena example::6501 Before malloc in main thread ... sploitfunsploitfun-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 ... sploitfunsploitfun-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」部分。sploitfunsploitfun-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 ... sploitfunsploitfun-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 ... sploitfunsploitfun-VirtualBox:~/ptmalloc.ppt/mthread$3在主线程 free 之后从如下的输出结果中我们可以看到当分配的内存区域 free 掉时其并不会立即归还给操作系统 而仅仅是移交给了作为库函数的分配器。这块 free 掉的内存添加在了「main arenas bin」中在 glibc malloc 中空闲列表数据结构被称为「bin」。随后当用户请求内存时分配器就不再向内核申请新堆了而是先试着各个「bin」中查找空闲内存。只有当 bin 中不存在空闲内存时分配器才会继续向内核申请内存。sploitfunsploitfun-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 ... sploitfunsploitfun-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 ... sploitfunsploitfun-VirtualBox:~/ptmalloc.ppt/mthread$4在 thread1 malloc 之前从如下的输出结果中我们可以看到此时 thread1 的堆尚不存在但其栈已产生。sploitfunsploitfun-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 ... sploitfunsploitfun-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] ... sploitfunsploitfun-VirtualBox:~/ptmalloc.ppt/mthread$5在 thread1 malloc 之后从如下的输出结果中我们可以看到thread1 的堆段(b7500000 - b7521000132KB)建立在了内存映射段中这也表明了堆内存是使用 mmap 系统调用产生的而非同主线程一样使用 sbrk 系统调用。类似地尽管用户只请求了 1000B但是映射到程地址空间的堆内存足有 1MB。这 1MB 中只有 132KB 被设置了读写权限并成为该线程的堆内存。这段连续内存132KB被称为「thread arena」。注意当用户请求超过 128KB(比如 malloc(132*1024)) 大小并且此时 arena 中没有足够的空间来满足用户的请求时内存将通过 mmap 系统调用不再是 sbrk分配而不论请求是发自 main arena 还是 thread arena。ploitfunsploitfun-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 ... sploitfunsploitfun-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] ... sploitfunsploitfun-VirtualBox:~/ptmalloc.ppt/mthread$6在 thread1 free 之后从如下的输出结果中我们可以看到free 不会把内存归还给操作系统而是移交给分配器然后添加在了「thread arenas bin」中。sploitfunsploitfun-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 ... sploitfunsploitfun-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] ... sploitfunsploitfun-VirtualBox:~/ptmalloc.ppt/mthread$往期文章推荐图解 TCP/IP协议看图秒懂图解 C/C 多线程看图秒懂爆肝整理嵌入式开发必知10种调试手段【图解】SSH安全外壳协议工作原理【就业避坑】C 就业前景全解析为什么劝退声不断大厂核心岗仍刚需 C【大厂标准】Linux C/C 后端开发系统学习路线【音视频】音视频流媒体高级开发核心学习路径【Qt进阶】C Qt 桌面 嵌入式开发一条龙学习攻略【内核底层】Linux 内核硬核修炼指南【面试冲刺】C/C 高频八股面试题 1000 题三【项目实战】手撕线程池C 程序员的能力试金石三、内存管理的最小单元Chunkglibc 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的结构基础上又增加了两个重要的指针字段 。fdforward pointer指针也就是前向指针它指向双向空闲列表中的下一个空闲Chunkbkbackward 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]为空进入步骤3smallbins[index]非空直接返回第一个chunk2、如果分配内存512字节则定位到largebins对应的index上largebins[index]为空进入步骤3largebins[index]非空扫描链表找到第一个大小最合适的chunk如size12.5K则使用chunk B剩下的0.5k放入unsorted_list中3、遍历unsorted_list查找合适size的chunk如果找到则返回否则将这些chunk都归类放到smallbins和largebins里面4、index从更大的链表中查找直到找到合适大小的chunk为止找到后将chunk拆分并将剩余的加入到unsorted_list中5、如果还没有找到那么使用top chunk6、或者内存128k使用brk内存128k使用mmap获取新内存5.2 释放流程free释放内存到其内存池时有两种情况chunk和top chunk相邻则和top chunk合并chunk和top chunk不相邻则直接插入到unsorted_list中5.3 内存碎片按照glibc的内存分配策略我们考虑下如下场景(假设brk其实地址是512k)malloc 40k内存即chunkAbrk 512k 40k 552kmalloc 50k内存即chunkBbrk 552k 50k 602kmalloc 60k内存即chunkCbrk 602k 60k 662kfree 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把内存还给操作系统。

相关新闻

TAS2564评估板实战:从数字功放原理到立体声系统集成

TAS2564评估板实战:从数字功放原理到立体声系统集成

1. 从芯片到系统:TAS2564评估板的设计哲学与核心价值如果你正在为下一代便携式音频产品寻找一颗高性能、高集成度的数字功放芯片,或者你正头疼于如何将复杂的I2S、I2C总线与D类功放高效地整合到一个紧凑的系统中,那么德州仪器的TAS2564YBGEVM…

2026/6/30 1:28:07阅读更多 →
清宫后多久出门不怕风?分阶段防风与科学修护指

清宫后多久出门不怕风?分阶段防风与科学修护指

不少做完清宫手术的女性,纠结外出受风的时间节点,一部分人休养两三天便匆忙出门吹风,引发腹痛、恶露拖尾;还有人长期闭门不出,影响身心代谢。清宫属于宫腔侵入式操作,身体气血亏虚、毛孔疏松,短…

2026/6/30 1:28:07阅读更多 →
按照这个方法真的领到了8元

按照这个方法真的领到了8元

千问新用户专属220372上面的口令不输错夏天来了,世界杯也来了,按照上面的方法,赶快去试试吧,整一杯奶茶,哈哈哈

2026/6/30 1:23:07阅读更多 →
指针空置类型-nullptr

指针空置类型-nullptr

先看一段代码&#xff1a;#include <iostream> using namespace std;void func(char* p) {cout << "void func(char* p)" << endl;cout << p << endl; }void func(int p) {cout << "void func(int p)" << endl;…

2026/6/30 2:33:11阅读更多 →
开题报告毫无思路,有哪些好用的 AI 论文工具?保姆级实测推荐

开题报告毫无思路,有哪些好用的 AI 论文工具?保姆级实测推荐

每年毕业季&#xff0c;大批本科生、硕博生卡在开题第一步&#xff1a;选题毫无方向、文献综述梳理杂乱、研究框架逻辑断层、写好初稿查重 / AIGC 检测双双超标&#xff0c;熬几个通宵交出的稿子还被导师打回重改。通用 AI 大模型不懂学术规范&#xff0c;写出来的开题空泛无创…

2026/6/30 2:33:11阅读更多 →
Chroma报错chromadb.errors.InvalidArgumentError: Collection expecting embedding with dimension of 1024,

Chroma报错chromadb.errors.InvalidArgumentError: Collection expecting embedding with dimension of 1024,

如标题所示&#xff0c;在使用Chroma存储数据到向量库中后&#xff0c;进行检索操作报错&#xff0c;报错内容为chromadb.errors.InvalidArgumentError: Collection expecting embedding with dimension of 1024, got 1536 但这个错误在我把模型从text-embedding-v1切换为text-…

2026/6/30 2:33:11阅读更多 →
中小型园区网络交付全流程完整解析

中小型园区网络交付全流程完整解析

一、整体交付核心框架 中小型园区网络交付采用行业标准化三段式闭环流程&#xff1a;前期规划设计 → 现场施工部署 → 上线测试验收。整套流程兼顾网络稳定性、内网安全性与后期运维便捷性&#xff0c;广泛适用于企业厂区、小型产业园、院校校区、产业小院等终端规模在50–500…

2026/6/30 2:33:11阅读更多 →
从0x27服务看UDS安全访问:种子与密钥的实战解锁指南

从0x27服务看UDS安全访问:种子与密钥的实战解锁指南

1. 0x27服务与UDS安全访问的核心逻辑 第一次接触汽车电子诊断时&#xff0c;我被ECU上那个小小的锁形图标难住了整整三天。后来才知道&#xff0c;这背后是UDS协议中**安全访问服务&#xff08;0x27&#xff09;**的典型应用场景。简单来说&#xff0c;它就像汽车ECU的"门…

2026/6/30 2:33:11阅读更多 →
1.2 HSA的Topology sysfs 布局与发现机制

1.2 HSA的Topology sysfs 布局与发现机制

摘要&#xff1a; 本文聚焦 KFD Topology 的发现过程——内核如何通过 sysfs 暴露拓扑信息&#xff0c;libhsakmt 如何一次性加载为内存快照&#xff0c;以及 Node ID 映射、generation_id 等辅助机制。各 Properties 的字段详解见后续专题文档。 前文给出了描述异构系统的四个…

2026/6/30 2:28:11阅读更多 →
AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

6个月前的2025年12月&#xff0c;Boris Cherny 公开宣布自己卸载了 IDE。一时间&#xff0c;Vibe Coding 成了全行业最热的话题。6个月后&#xff0c;当我们回过头来拉一份真实账本&#xff0c;发现事情远没有"一句话生成一个App"那么浪漫。本文从产品经理和研发两个…

2026/6/29 3:27:55阅读更多 →
审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

引言&#xff1a;审计结束三个月了&#xff0c;审计员的权限还没关某城商行每年按照监管要求开展至少一次数据安全审计。审计期间&#xff0c;内审部门需要抽样检查各类业务数据——交易流水、客户信息、员工操作日志、权限配置记录。这些数据分布在不同系统中&#xff0c;审计…

2026/6/29 2:19:08阅读更多 →
为什么你需要Destiny 2 Solo Enabler:技术原理与实战指南

为什么你需要Destiny 2 Solo Enabler:技术原理与实战指南

为什么你需要Destiny 2 Solo Enabler&#xff1a;技术原理与实战指南 【免费下载链接】Destiny-2-Solo-Enabler Repo containing the C# and XAML code for the D2SE program. Included is also the dependency for the program, and image asset. 项目地址: https://gitcode…

2026/6/30 0:02:58阅读更多 →
第六章:PowerPoint 2010 核心功能与实战应用 —— 从入门到精通

第六章:PowerPoint 2010 核心功能与实战应用 —— 从入门到精通

1. PowerPoint 2010基础操作全攻略 刚接触PowerPoint 2010时&#xff0c;很多人会被它复杂的界面吓到。其实只要掌握几个核心区域&#xff0c;就能快速上手。我最开始用PPT时&#xff0c;经常找不到功能按钮在哪&#xff0c;后来发现主要操作都集中在顶部功能区。 工作窗口主要…

2026/6/30 0:02:58阅读更多 →
XGBoost超参数实战:从理论到调优策略

XGBoost超参数实战:从理论到调优策略

1. XGBoost超参数基础认知 第一次接触XGBoost时&#xff0c;我被它那密密麻麻的参数列表吓到了。这感觉就像面对一架波音747的驾驶舱——每个按钮都可能有神奇的效果&#xff0c;但按错了就可能坠机。经过多年实战&#xff0c;我发现其实掌握十几个核心参数就能解决90%的问题。…

2026/6/30 0:02:59阅读更多 →