news 2026/4/22 11:37:49

【c++】glibc内存管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【c++】glibc内存管理

glibc 的内存分配器(Allocator)主要基于ptmalloc2(Doug Lea’s malloc 的多线程优化版)。

这是一个非常复杂且高度优化的系统。将从核心数据结构Malloc 的分配流程Free 的释放流程三个维度深入。


Glibc 内存管理深度解析 (ptmalloc2)

1. 核心概念与数据结构

在深入代码逻辑之前,必须理解 glibc 管理内存的物理形态。

1.1 Chunk (内存块)

在 glibc 中,内存不是以字节为单位管理的,而是以Chunk为单位。无论用户申请多少内存,glibc 都会将其封装成一个 Chunk。

malloc_chunk结构体 (简化版):

structmalloc_chunk{/* prev_size: 如果前一个物理相邻的 chunk 是空闲的,这里存储前一个 chunk 的大小。 如果前一个 chunk 正在使用,这里可能存储用户数据(空间复用)。 */INTERNAL_SIZE_T prev_size;/* size: 当前 chunk 的大小(必须是 8 或 16 字节对齐)。 低 3 位被用作标志位(因为 size 总是对齐的,低位总是0)。 */INTERNAL_SIZE_T size;/* fd, bk: 只有当 chunk 空闲(Freed)时才有意义。 用于在双向链表中指向前一个和后一个空闲 chunk。 如果是 Fastbins 或 Tcache,只使用 fd (单链表)。 */structmalloc_chunk*fd;structmalloc_chunk*bk;/* fd_nextsize, bk_nextsize: 仅用于 Large Bins 的空闲 chunk,用于跳表加速查找。 */structmalloc_chunk*fd_nextsize;structmalloc_chunk*bk_nextsize;};

Size 字段中的三个关键标志位 (低3位):

  • P (PREV_INUSE, bit 0):1 代表前一个物理相邻 chunk 正在使用;0 代表前一个空闲。这是合并(Coalescing)的关键。
  • M (IS_MMAPPED, bit 1):1 代表该 chunk 是直接通过mmap向 OS 申请的,不属于 heap。
  • A (NON_MAIN_ARENA, bit 2):1 代表属于非主 Arena(线程 Arena)。

1.2 Bins (空闲链表容器)

为了加速分配,glibc 将空闲的 chunk 按照大小分类放入不同的链表(Bins)中。

  1. Tcache (Thread Local Cache) [Glibc 2.26+]:
  • 最快路径。每个线程独有,不需要加锁。
  • 单向链表,LIFO(后进先出)。
  • 限制了大小和容量(默认 64 个 chunk)。
  1. Fastbins:
  • 小块内存(通常 < 80字节,取决于系统)。
  • 单向链表,LIFO。
  • 不会触发合并(为了保持分配速度)。
  1. Unsorted Bin:
  • 中转站。当用户释放内存(且不进 Tcache/Fastbin)时,或者分割大块内存后剩余的部分,首先进入这里。
  • 双向链表。
  • malloc时会遍历这里,如果有合适大小直接返回,否则将这里的 chunk 整理归类到 Small/Large Bins。
  1. Small Bins:
  • 大小 < 512 字节(64位系统通常 < 1024)。
  • 每个 bin 存储固定大小的 chunk(例如 32字节, 48字节…)。
  • 双向链表,FIFO。
  1. Large Bins:
  • 大于 Small Bins 的范围。
  • 每个 bin 存储一个范围大小的 chunk。
  • 双向链表 + 跳表(使用fd_nextsize排序)。

1.3 Arena (分配区)

为了解决多线程锁竞争,glibc 引入了 Arena。

  • Main Arena:使用sbrk扩展堆(Heap)。
  • Thread Arena:使用mmap分配堆内存。每个线程尝试获取一个 Arena,如果竞争激烈,会创建新的 Arena。

2. 当你调用malloc(size)时发生了什么?

malloc的核心逻辑在_int_malloc函数中。

详细步骤流程:

第一步:对齐与校验

用户申请size,glibc 首先将其对齐(Alignment)。

例如在 64 位系统申请 10 字节,实际需要:8字节(header) + 10字节(data) + 6字节(padding) = 24 字节。

第二步:检查 Tcache (极速路径)
  • 逻辑:检查对应大小的 Tcache list 是否有空闲 chunk。
  • 如果有:直接返回,不加锁。这是最快的情况。
  • 代码逻辑:
// tcache_getif(tcache!=NULL&&tcache->counts[tc_idx]>0){returntcache_get(tc_idx);}
第三步:检查 Fastbins (快速路径)
  • 如果请求大小在 Fastbin 范围内。
  • 加锁(获取 Arena 锁)。
  • 检查 Fastbin 链表。如果有,取出并返回。
第四步:检查 Small Bins (精确匹配)
  • 如果请求大小在 Small Bin 范围内。
  • 找到对应的 bin 索引。
  • 如果 bin 不为空,取出最后一个 chunk(FIFO)返回。
第五步:核心循环 - 处理 Unsorted Bin (整理阶段)

如果上述都在“缓存”里没找到,说明需要干重活了。glibc 开始遍历Unsorted Bin。这是最复杂的逻辑。

  • 遍历 Unsorted Bin 中的每一个 Chunk:
  1. 如果这个 Chunk 大小正好等于请求大小:
  • 直接分配给用户,返回。
  1. 如果这个 Chunk 大小属于 Small/Large Bin:
  • 把它从 Unsorted Bin 拿出来,放入对应的 Small/Large Bin 中(归位)。
  1. Last Remainder 机制:如果在遍历中,我们虽然没找到精确匹配,但找到了一个足够大的 chunk,我们可能会切分它,剩下的部分作为 Last Remainder 扔回 Unsorted Bin。
第六步:检查 Large Bins
  • 在整理完 Unsorted Bin 后(或者 Unsorted Bin 是空的),去 Large Bins 查找最佳匹配(Best Fit)。
  • 如果找到一个足够大的 chunk,切分 (Split)它。
  • 需要的空间 -> 返回给用户。
  • 剩余的空间 -> 放入 Unsorted Bin 或 Tcache。
第七步:使用 Top Chunk
  • 如果所有的 Bins 都是空的,或者找不到足够大的块。
  • 查看Top Chunk(堆顶的剩余空间)。
  • 如果 Top Chunk 大小 > 请求大小:
  • 切分 Top Chunk。
  • 调整 Top Chunk 指针。
第八步:系统调用 (Syscall)
  • 如果 Top Chunk 也不够大了:
  • 对于 Main Arena:调用sbrk()增加 Heap 大小。
  • 对于 Thread Arena:调用mmap()申请新的内存段。
  • 如果请求非常大(超过mmap_threshold,默认 128KB):直接调用mmap分配独立内存,不使用堆。

3. 当你调用free(ptr)时发生了什么?

free的核心逻辑在_int_free函数中。它的核心目标是回收内存尽可能减少碎片(通过合并)。

详细步骤流程:

第一步:基本校验
  • ptr是否为 NULL?(是则直接返回)。
  • 检查对齐、检查指针是否越界。
第二步:放入 Tcache (极速路径)
  • 检查该大小对应的 Tcache 是否满了(默认限制是 7 个)。
  • 如果没满:直接插入 Tcache 链表头部。不清除 P 标志位,不合并。返回。
第三步:直接 Mmap 释放
  • 检查IS_MMAPPED标志位。如果是mmap出来的(大内存),直接调用munmap系统调用归还给 OS。
第四步:放入 Fastbins
  • 加锁。
  • 如果大小在 Fastbin 范围内:
  • 检查 Double Free(看链表头是不是同一个指针)。
  • 插入 Fastbin 链表头部。
  • 注意:这里不修改相邻 chunk 的 P 标志位,不发生合并。
第五步:合并 (Coalesce) - 慢速路径

如果不是 Tcache 或 Fastbin,说明这是一个“标准”的释放,需要考虑碎片整理。

  1. 向后合并 (Merge with Next):
  • 检查物理地址上的下一个Chunk。
  • 如果下一个 Chunk 是 Top Chunk:这就意味着当前 Chunk 变成了新的 Top Chunk 的一部分。
  • 如果下一个 Chunk 是空闲的(不在使用中):将它从所在的 Bin 中摘除,与当前 Chunk 合并。
  1. 向前合并 (Merge with Prev):
  • 检查当前 Chunk 的prev_sizePREV_INUSE位。
  • 如果前一个 Chunk 是空闲的:利用prev_size找到前一个 Chunk 的头,将其摘除,与当前 Chunk 合并。
  1. 生成新的大 Chunk:
  • 合并完成后,我们得到一个更大的 Free Chunk。
第六步:放入 Unsorted Bin
  • 将合并后的这个大 Chunk 插入到Unsorted Bin的头部。
  • 为什么放这里?局部性原理。刚才释放的内存很可能马上又被申请,放这里比放回 Small/Large Bin 查找更快。
第七步:触发malloc_consolidate(特定条件)
  • 如果释放了一个很大的块,或者 Fastbins 积压太多,可能会触发整理,将 Fastbins 中的碎片强行合并并移入 Unsorted Bin。
第八步:归还给 OS (Trimming)
  • 如果合并后的 Chunk 与 Top Chunk 相邻,它们合并成一个新的、巨大的 Top Chunk。
  • 如果 Top Chunk 的大小超过了MMAP_THRESHOLD(通常是 128KB),glibc 会尝试调用sbrk(也就是malloc_trim) 收缩堆顶,把内存真正的还给操作系统。

4. 关键逻辑代码 (伪代码分析)

为了让你理解具体逻辑,这里提取了_int_malloc_int_free的关键骨架。

_int_malloc骨架

staticvoid*_int_malloc(mstate av,size_tbytes){size_tnb;// 对齐后的请求大小mchunkptr victim;// 找到的 chunk// 1. Convert request size to internal formchecked_request2size(bytes,nb);// 2. Tcache Check (省略,通常在 __libc_malloc 入口处理)// 3. Fastbin Checkif((unsignedlong)(nb)<=(unsignedlong)(get_max_fast())){// ... get from fastbin ...if(victim)returnchunk2mem(victim);}// 4. Small Bin Checkif(in_smallbin_range(nb)){// ... get from small bin ...// 如果找到精确匹配,直接返回}// 5. Large Bin / Unsorted Bin Loopfor(;;){// 遍历 Unsorted Binwhile((victim=unsorted_chunks(av)->bk)!=unsorted_chunks(av)){// 如果是精确匹配 (Exact Match)if(chunksize(victim)==nb){// 标记为在使用,返回set_inuse_bit_at_offset(victim,size);returnchunk2mem(victim);}// 否则,将 victim 放入 Small Bin 或 Large Bin// Process chunk...}// 6. Search Large Bins (Best Fit)// ... 寻找最佳匹配,切分 ...// 7. Use Top Chunkvictim=av->top;if(chunksize(victim)>=nb+MINSIZE){// 切分 Top Chunkvoid*p=chunk2mem(victim);av->top=chunk_at_offset(victim,nb);set_head(av->top,...);returnp;}// 8. Syscall (malloc_consolidate or sysmalloc)sysmalloc(...);}}

_int_free骨架

staticvoid_int_free(mstate av,mchunkptr p,inthave_lock){size_tsize=chunksize(p);mchunkptr nextchunk=chunk_at_offset(p,size);// 1. Tcache check (省略)// 2. Fastbin checkif((unsignedlong)(size)<=(unsignedlong)(get_max_fast())){// 放入 fastbin 头部,不修改标志位set_fastchunks(av);return;}// 3. Consolidate (合并)// Check backward (next chunk)if(!prev_inuse(p)){size+=prev_size(p);p=chunk_at_offset(p,-((long)prev_size(p)));unlink(av,p,bck,fwd);// 从链表移除}// Check forward (next chunk)if(nextchunk!=av->top){if(!inuse_bit_at_offset(nextchunk,nextsize)){size+=nextsize;unlink(av,nextchunk,bck,fwd);}}else{// 如果连接到 Top Chunk,直接合并进 Topsize+=nextsize;set_head(p,size|PREV_INUSE);av->top=p;// 可能触发 malloc_trimreturn;}// 4. Insert into Unsorted Bin// 将合并后的 p 放入 Unsorted Bin 头部bck=unsorted_chunks(av);fwd=bck->fd;p->bk=bck;p->fd=fwd;bck->fd=p;fwd->bk=p;// Set status bits (next chunk's PREV_INUSE = 0)set_head(p,size|PREV_INUSE);set_foot(p,size);}

5. 总结

glibc 的内存管理策略可以概括为:分级缓存 + 惰性合并

  1. 极速响应: 小内存优先走 Tcache 和 Fastbins,不合并,无锁或低锁,速度极快。
  2. 兜底整理: 当内存变大或缓存未命中,进入 Unsorted Bin,这是一个“整理中心”,在此处才进行分类放入 Small/Large Bins。
  3. 空间复用:free时,大块内存会积极合并(Coalescing),防止内存碎片化(External Fragmentation)。
  4. 系统交互: 只有在 Top Chunk 不够或显式缩减时,才与 OS 进行sbrk/mmap交互,减少昂贵的系统调用开销。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 10:20:33

Scala 变量

Scala 变量 概述 在Scala中,变量是用来存储数据的基本元素。变量可以存储任何类型的数据,例如数值、文本、布尔值等。Scala中的变量具有类型推断特性,这意味着变量在使用时不需要显式声明其类型。本文将详细介绍Scala变量的概念、特性、作用域以及如何声明和使用变量。 变…

作者头像 李华
网站建设 2026/4/18 11:07:41

细胞电生理仿真软件:GENESIS_(4).GENESIS的图形用户界面使用

GENESIS的图形用户界面使用 1. 图形用户界面概述 GENESIS&#xff08;GEneral NEural SImulation System&#xff09;是一款强大的细胞电生理仿真软件&#xff0c;支持多种仿真模型和实验设计。除了命令行操作&#xff0c;GENESIS还提供了一个图形用户界面&#xff08;GUI&am…

作者头像 李华
网站建设 2026/4/18 8:49:31

Graph-O1:基于蒙特卡洛树搜索与强化学习的文本属性图推理框架

摘要 本文介绍了Graph-O1&#xff0c;一种创新的智能体GraphRAG框架&#xff0c;通过结合蒙特卡洛树搜索&#xff08;MCTS&#xff09;与端到端强化学习&#xff0c;使大语言模型能够在文本属性图上进行逐步交互式推理。该方法有效解决了传统RAG方法在图结构数据上的局限性&am…

作者头像 李华
网站建设 2026/4/18 0:55:30

细胞电生理仿真软件:GENESIS_(7).细胞结构建模

细胞结构建模 在细胞电生理仿真软件中&#xff0c;细胞结构建模是基础且重要的一步。GENESIS&#xff08;General Neuronal Simulation System&#xff09;提供了多种方法来构建细胞结构模型&#xff0c;包括使用描述文件、脚本语言等。本节将详细介绍如何在GENESIS中进行细胞…

作者头像 李华
网站建设 2026/4/18 5:30:51

英伟达CEO黄仁勋:未来一切将通过虚拟孪生体来表现

在休斯顿举行的3DEXPERIENCE世界大会上&#xff0c;英伟达创始人兼CEO黄仁勋与达索系统CEO帕斯卡达洛兹共同描绘了基于物理"世界模型"的工业AI蓝图——这些系统旨在在产品、工厂甚至生物系统建造之前就进行仿真模拟。"人工智能将成为基础设施"&#xff0c;…

作者头像 李华
网站建设 2026/4/18 8:03:47

怎么查询联想笔记本型号

先进入系统设置--查看系统信息 关键硬件信息: 处理器:11th Gen Intel Core i7-1165G7 @ 2.80GHz 显卡:NVIDIA GeForce MX450 + Intel Iris Xe Graphics 内存:16GB RAM(15.8GB 可用) 存储:477GB SSD(三星 MZALQ512HALU-000L2,即 512GB NVMe SSD) 系统类型:64位操作系…

作者头像 李华