深入剖析glibc malloc:从内存分配到多线程优化的底层原理
2026/7/17 4:57:32 网站建设 项目流程

1. 项目概述:从API到内核,malloc的冰山之下

如果你在Linux下写过C程序,那么malloc这个函数对你来说就像空气一样自然。我们用它申请内存,用free释放,似乎一切都是理所当然的。但你是否想过,当你调用malloc(1024)请求1KB内存时,操作系统和C库究竟为你做了什么?这背后是一场从用户态库函数到内核系统调用的精密协作,涉及内存池管理、多线程并发、性能优化和系统资源限制等一系列复杂机制。

我见过太多开发者,包括一些有多年经验的同行,对malloc的理解仅仅停留在“分配内存”的层面。当程序出现内存泄漏、性能瓶颈甚至诡异的崩溃时,他们往往束手无策,只能靠重启或增加内存来“解决”问题。实际上,深入理解malloc的实现细节,是诊断和解决这类问题的关键。这不仅关乎写出健壮的程序,更是在内存资源受限的嵌入式系统、追求极致性能的高并发服务器等场景下的必备技能。

今天,我们就来彻底拆解glibc中malloc的实现。我会带你走过从用户调用到内核响应的完整路径,剖析那些隐藏在简单API背后的复杂数据结构、算法权衡和工程实践。你会发现,一个看似简单的内存分配,背后竟有如此多的门道。

2. 内存分配器的核心职责与设计哲学

在深入代码之前,我们必须先搞清楚一个内存分配器(Memory Allocator)到底要解决哪些问题。这不仅仅是“给块内存”那么简单。

2.1 分配器面临的四大核心挑战

第一,速度要快。内存分配和释放可能是程序中最频繁的操作之一。一个缓慢的分配器会直接拖慢整个应用的性能。这里的“快”包含两层含义:一是单次分配/释放的延迟要低;二是在多线程环境下,分配器不能成为性能瓶颈。

第二,碎片要少。内存碎片分为内部碎片和外部碎片。内部碎片是指分配器给了你一块比实际请求稍大的内存,多出来的部分被浪费了。比如你申请13字节,分配器可能给你16字节,那3字节就是内部碎片。外部碎片则是指内存中散布着许多小的、不连续的空闲块,导致即使总空闲内存足够,也无法满足一个较大的连续内存请求。优秀的分配器需要在两者之间找到平衡。

第三,空间利用率要高。这看似与“碎片要少”相关,但侧重点不同。高空间利用率意味着分配器自身的管理开销(元数据)要尽可能小,并且要能有效地利用从操作系统获得的大块内存。

第四,可扩展性要强。在现代多核CPU上,程序往往是多线程的。如果所有线程都竞争同一把全局锁来分配内存,那么随着线程数增加,性能会急剧下降。分配器必须设计成能够支持并发分配,减少锁竞争。

glibc的malloc实现(通常称为ptmalloc2)的设计正是围绕解决这些挑战展开的。它的核心思想可以概括为:分级缓存、按需索取、线程隔离

2.2 glibc malloc的宏观架构:Arena与Heap

为了理解后续细节,我们需要先建立两个核心概念:Arena(分配区)和Heap(堆)。

你可以把Arena想象成一个独立的内存管理“小王国”。每个Arena管理着自己的一大片虚拟地址空间(即一个或多个Heap),并拥有独立的数据结构来跟踪哪些内存块是空闲的,哪些是已分配的。最关键的是,每个Arena有自己的锁。

在程序启动时,glibc会创建一个主Arenamain_arena),它管理着通过brk系统调用扩展的传统“堆”段。当多线程程序出现锁竞争时,glibc会动态创建新的Arena(称为thread arena)给新线程使用。这些thread arena使用mmap系统调用从操作系统映射大块内存(通常是1MB的整数倍)来作为自己的Heap

这样设计的好处显而易见:大部分时候,线程在自己的Arena上分配内存,无需与其他线程竞争锁,极大地提升了并发性能。只有当线程自己的Arena内存不足时,才可能需要向操作系统申请新的Heap,或者在某些情况下从其他Arena“偷”内存块,这时才需要加锁。

注意:虽然Arena减少了锁竞争,但它也带来了新的问题——内存可能无法在Arena之间有效流转。一个线程释放的内存,只能放回它所属Arena的空闲列表,其他线程无法直接使用。这可能导致某个线程大量分配又释放后,其Arena内有很多空闲内存,而另一个线程却因为自己的Arena内存不足而向操作系统申请新的映射,从而增加了进程的总内存占用(RSS)。这就是所谓的“内存碎片化”在多线程场景下的体现。

3. 核心数据结构拆解:Chunk是这一切的基石

malloc管理内存的基本单位不是字节,而是内存块(Chunk)。无论这块内存是已分配给你的,还是空闲待用的,在分配器内部,它们都被一个统一的malloc_chunk结构所描述。理解这个结构是理解所有分配算法的基础。

3.1 malloc_chunk结构:隐藏在返回指针之前的秘密

当你调用malloc(size)并获得一个指针p时,p指向的是这块内存中你可以使用的部分。但在p之前,还有一块隐藏的元数据区,这就是malloc_chunk。在64位系统上,它的定义通常如下(精简了调试信息):

struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* 前一个chunk的大小(如果前一个chunk空闲) */ INTERNAL_SIZE_T size; /* 本chunk的大小及状态位 */ struct malloc_chunk* fd; /* 仅空闲chunk使用:指向双向链表中的下一个chunk */ struct malloc_chunk* bk; /* 仅空闲chunk使用:指向双向链表中的前一个chunk */ };

这个结构有几个精妙之处:

  1. size字段的低三位是标志位:由于内存对齐要求(比如8字节或16字节对齐),chunk的大小总是对齐值的整数倍。这意味着size值的低几位永远是0。glibc巧妙地利用这最低三位来存储状态信息:

    • PREV_INUSE (P):最低位。如果为1,表示前一个物理相邻的chunk正在被使用。这是为了快速判断前一个chunk是否可以合并。
    • IS_MMAPPED (M):倒数第二位。如果为1,表示这个chunk是通过mmap直接映射的独立大块内存,不属于任何heap。释放时会直接用munmap还给操作系统。
    • NON_MAIN_ARENA (A):倒数第三位。如果为1,表示这个chunk属于一个thread arena,而非main_arena
  2. prev_size的复用:这是一个空间优化的经典案例。当一个chunk被分配出去时(即“在使用中”),它不需要fdbk指针(因为不属于任何空闲链表)。此时,从用户数据区开始的空间都归用户使用。而prev_size这个字段恰好位于用户数据区的开头!这意味着,对于正在使用中的chunk,它的prev_size空间被“借给”用户数据使用了。只有当这个chunk被释放、变成空闲状态时,prev_size字段才被重新用来存储前一个chunk的大小(如果前一个chunk也是空闲的,用于合并操作)。这种设计几乎做到了元数据零开销(对于已分配块,只有size字段是纯开销)。

  3. 双向链表(bins):对于空闲chunk,fdbk指针将它们组织成各种双向链表,这些链表被称为“bin”。这是分配器快速查找合适大小内存块的核心机制。

3.2 Bins:空闲内存的快速检索系统

glibc的malloc维护了一系列不同规格的“垃圾桶”(bins),用来分类存放空闲的chunk。根据chunk大小和查找策略,bins主要分为四类:

1. Fast bins:这是针对小内存、高频分配释放的优化。Fast bins维护着一些固定大小(通常是16到80字节左右,具体取决于配置)的单向链表(LIFO,后进先出)。当释放一个很小的chunk时,它会被放入对应的fast bin。注意,fast bin中的chunk的P位(PREV_INUSE)会被标记为1,这意味着它们不会被立即合并。这是为了下次分配同样大小的内存时,能直接从fast bin中取出,速度极快,避免了合并与分割的开销。fast bin的合并操作会被延迟,通常在特定时刻(如malloc_consolidate调用时)才统一处理。

2. Small bins:管理大小在512字节到1024字节(默认值,可调)之间的chunk。每个small bin管理一个固定大小的chunk,共有62个(32位系统)或63个(64位系统)small bin。它们组织成双向循环链表。分配时,采用FIFO(先进先出)策略,这有助于保持内存的局部性。

3. Large bins:管理大于1024字节的chunk。large bin的数量有限(比如6个),但每个bin管理的是一个大小范围内的chunk,而不是固定大小。同一个large bin里的chunk按大小排序。分配时,需要在这个bin里查找第一个大小足够且“最合适”(不一定最小,以减少碎片)的chunk。如果找到的chunk比请求大很多,还会将其分割,剩余部分作为新的空闲chunk放回bins。

4. Unsorted bin:这是一个“万能中转站”。当一个非fast大小的chunk被释放时,不会立刻放入small或large bin,而是先放入unsorted bin。当后续发生分配时,malloc会先遍历unsorted bin,尝试从中找到一块合适的内存。如果找到的chunk刚好满足请求,就直接分配;如果找到的chunk比请求大,就进行分割;如果找不到合适的,才会将unsorted bin中的chunk整理到对应的small或large bin中。这种设计是一种“缓存”策略,期望刚释放的内存很可能马上又被分配出去,从而提升效率。

此外,对于非常大的内存请求(默认超过128KB,即MMAP_THRESHOLD),malloc会绕过上述所有bins,直接使用mmap系统调用向操作系统申请一块独立的内存映射。释放时也直接用munmap归还。这种方式避免了在堆中管理大块内存造成的碎片,但mmap/munmap的系统调用开销比brk大。

4. malloc的完整工作流程:一次分配请求的漫游

现在,让我们跟随一次malloc(size)调用,看看它究竟经历了什么。假设我们请求分配一块size字节的内存。

4.1 第一步:请求转换与arena锁定

  1. 对齐与最小大小:首先,malloc会将用户请求的size转换为实际需要分配的chunk大小chunk_size。这包括加上malloc_chunk元数据中sizeprev_size字段的开销(至少8字节),并根据对齐要求(比如8或16字节对齐)进行上取整。同时,会确保chunk_size不小于MINSIZE(通常是16或32字节)。
  2. 获取arena:对于单线程程序或主线程,直接使用main_arena。对于多线程程序中的其他线程,会通过线程局部存储(TLS)获取其绑定的thread arena。如果线程还没有arena,可能会尝试复用已有的空闲arena,或者创建一个新的。
  3. 加锁:对获取到的arena加锁。这是保证该arena内部数据结构操作并发安全的关键。

4.2 第二步:尝试从fast bins中分配

如果转换后的chunk_size属于fast bin管理的范围,分配器会首先检查对应的fast bin链表是否为空。

  • 如果非空:直接从链表头部(LIFO)取出一个chunk。清除其P位(因为现在它被分配了,前一个chunk的P位需要更新,但fast chunk的P位本来就是1,这里主要是后续处理),将指向用户内存的指针返回。这是最快的路径。
  • 如果为空:进入下一步。

4.3 第三步:尝试从small bins中分配

如果chunk_size属于small bin范围,分配器会计算对应的small bin索引,并检查该bin。

  • 如果bin非空:从链表尾部取出一块chunk(FIFO)。因为small bin中所有chunk大小相同,所以取出的这块大小完全匹配,无需分割。直接返回指针。
  • 如果bin为空:进入下一步。

注意:在检查small bin之前或之后,分配器可能会先尝试处理unsorted bin。为了逻辑清晰,我们稍后描述。

4.4 第四步:处理unsorted bin与大内存分配

这是分配器最复杂的部分,可以看作一个循环:

  1. 遍历unsorted bin:分配器会遍历unsorted bin中的每一个chunk。

    • 精确匹配:如果遇到一个chunk,其大小正好等于chunk_size,则将其从unsorted bin中摘下,直接返回。
    • 分割:如果遇到一个chunk,其大小大于等于chunk_size + MINSIZE(即分割后剩下的部分还能形成一个有效的空闲chunk),则进行分割。将请求大小的部分作为分配块返回,剩余部分作为新的空闲chunk,重新放回unsorted bin(注意,是放回,而不是立即归类)。
    • 放入对应bin:如果遇到的chunk既不能直接分配,也不适合分割(比如太小),则根据其大小,将其从unsorted bin中移除,插入到对应的small bin或large bin中。
  2. 尝试large bins:如果unsorted bin遍历完毕仍未找到合适内存,且请求大小属于large bin范围,则开始在对应的large bin中查找。由于large bin中的chunk是按大小排序的,分配器会寻找第一个大小足够(且通常是最小的能满足需求的)的chunk。找到后,同样进行可能的分割,剩余部分作为新的空闲chunk,通常会放回unsorted bin。

  3. “Next Fit”搜索:如果对应的large bin也为空,或者没有足够大的chunk,分配器会使用“next fit”算法,从当前搜索位置开始,依次遍历所有后续的large bins,寻找任何一个能满足需求的chunk。

4.5 第五步:向操作系统申请更多内存

如果以上所有bins都无法满足请求,说明arena管理的现有堆内存中已经没有足够大的连续空闲空间了。这时,分配器必须向操作系统“要”更多内存。

  1. 扩展堆(对于main_arena):对于main_arena,它会通过sbrkbrk系统调用,增加程序“break”指针的位置,从而扩展传统堆段的大小。
  2. 映射新堆(对于thread arena):对于thread arena,它会通过mmap系统调用,映射一块新的、较大的内存区域(例如1MB)作为新的heap
  3. 从新内存中分配:从新获得的大块内存中切割出满足需求的chunk。如果新内存远大于请求,剩余部分会被组织成一个大的空闲chunk,加入到unsorted bin中,供后续分配使用。
  4. 内存合并(malloc_consolidate):在向操作系统申请内存之前,分配器可能会先尝试进行一次“内存整理”,即调用malloc_consolidate函数。这个函数会做两件重要的事:
    • 遍历所有fast bins,将其中的chunk取出,清除P位,并尝试与相邻的空闲chunk合并,然后将合并后的chunk放入unsorted bin。
    • 尝试合并arena中top chunk(每个heap最顶端的特殊空闲chunk)相邻的空闲内存。 这个整理过程可能会释放出足够的连续内存来满足当前请求,从而避免昂贵的系统调用。

4.6 第六步:返回指针与清理

一旦找到了合适的chunk(无论是从bins中找到的,还是从新内存中切割的),分配器会:

  1. 如果需要分割,设置好分割后两个chunk的sizeprev_size字段。
  2. 将分配出去的chunk的P位(对于它后面的chunk而言)设置为1,表示“前一个chunk在使用中”。
  3. 更新arena的各种统计信息(如已分配内存总量)。
  4. 释放arena锁。
  5. 最后,将指向用户数据区的指针(即chunk地址 + 2*sizeof(size_t))返回给调用者。

5. free的逆向旅程:内存如何被回收

理解了分配,释放就相对容易了。free(p)的核心任务是将chunk放回合适的地方,并尝试合并相邻的空闲chunk以减少碎片。

5.1 安全检查与合并前置操作

  1. 指针检查:free首先会检查传入的指针p是否为空(NULL),如果是,直接返回(标准规定free(NULL)无害)。
  2. 获取chunk指针:通过p向前偏移,找到该内存块对应的malloc_chunk结构体起始地址。
  3. 基础检查:进行一些基本安全检查,例如检查size字段是否对齐、是否过小等(在开启某些调试选项如MALLOC_CHECK_时会更严格)。
  4. 判断mmapped chunk:检查size字段的M位。如果为1,说明这是通过mmap直接分配的大内存块,则直接调用munmap释放整个映射区域,然后返回。

5.2 合并相邻空闲chunk

这是free算法的精髓,目的是减少外部碎片。

  1. 向后合并(与上一个chunk合并):检查当前chunk的P位。如果P位为0,说明物理上在前一个的chunk是空闲的。通过当前chunk的prev_size字段,可以找到前一个空闲chunk的起始地址。将前一个chunk从其所在的bin中解除链接,然后与当前chunk合并成一个更大的空闲chunk。
  2. 向前合并(与下一个chunk合并):通过当前chunk的size字段,可以计算出下一个chunk的起始地址。检查下一个chunk的size字段的P位(注意,下一个ch头的P位表示的是“前一个chunk是否在使用”,而它的前一个正是当前要释放的chunk)。在free时,下一个chunk头的P位如果为0是不正常的(因为当前chunk正在使用),但这里主要是看下一个chunk本身是否空闲(通过检查下下个chunk的P位,或者查看下一个chunk是否在bins中)。更常见的做法是:直接检查下一个chunk是否处于空闲状态(通过其size字段或是否在bins中)。如果下一个chunk空闲,则将其从其所在的bin中解除链接,与当前chunk合并。

关键点:合并操作必须将相邻的空闲chunk从它们原来所在的bin中移除(unlink),这是一个需要小心处理的操作,也是许多堆溢出漏洞利用的关键点。

5.3 放入合适的bin

合并完成后,我们得到了一个更大的空闲chunk。现在需要把它放到合适的地方,供后续malloc使用。

  1. 放入fast bins(如果是小内存):如果合并后的chunk大小属于fast bin范围,则将其插入对应fast bin链表的头部。此时,该chunk的P位(对于它后面的chunk而言)被设置为1,以防止它立即与后面的空闲chunk合并。这就是fast bin“延迟合并”的特性。
  2. 放入unsorted bin(如果是普通内存):对于不属于fast bin大小的chunk,合并后会被放入unsorted bin的头部。放入unsorted bin前,会设置好合并后chunk的size,并清除其P位(表示它现在是空闲的),同时设置它后面chunk的prev_size字段(如果后面chunk不是top chunk)。

5.4 与top chunk合并及内存收缩

如果被释放的chunk恰好与arena的top chunk(每个heap顶端那个可以无限生长的特殊空闲chunk)相邻,那么free会直接将其合并到top chunk中,而不是放入任何bin。

在某些情况下,如果top chunk变得非常大(超过一个阈值),free可能会调用brkmunmap来将一部分内存真正归还给操作系统,减少进程的内存占用。这是一个相对昂贵的操作,不会频繁发生。

最后,释放arena锁,free操作完成。

6. 多线程环境下的挑战与优化:Arena与锁

单线程下的内存管理已经足够复杂,多线程则引入了锁竞争这个性能杀手。glibc的ptmalloc2采用了一种“线程本地缓存”与“全局池”结合的策略来应对。

6.1 Thread Local Cache:tcache的引入

在较新版本的glibc(2.26之后)中,引入了一个更为激进的优化:线程本地缓存(tcache)。每个线程都有一个私有的tcache,它是一个包含多个单链表的数组,每个链表缓存一种特定大小的空闲chunk(通常是small size范围内的)。

  • 分配时:malloc会首先检查对应大小的tcache链表。如果有,直接从中取出,完全无锁,速度极快。
  • 释放时:free会首先尝试将chunk放入对应大小的tcache链表。每个链表有容量限制(默认7个)。如果链表已满,则走正常的释放流程(尝试合并,放入fast/unsorted bin等)。

tcache极大地提升了小内存分配/释放的性能,因为它将最常见的操作完全本地化了。但它也加剧了“内存不能在线程间流转”的问题,因为被tcache缓存的内存,其他线程根本看不到。

6.2 Arena数量的动态管理

如前所述,glibc会动态创建thread arena。其数量并非无限,通常与CPU核心数相关(例如,32位系统上是2倍核心数,64位系统上是8倍核心数)。当线程数超过arena数量时,多个线程将共享一个arena,此时锁竞争会加剧。

你可以通过环境变量MALLOC_ARENA_MAX来设置最大arena数量。在内存紧张或线程数极多的服务端应用中,适当调低这个值可以控制虚拟内存地址空间(VMA)的数量,但可能会增加锁竞争。

6.3 锁的粒度与性能权衡

ptmalloc2的主要锁是arena锁。每个arena一把锁。在引入tcache后,大部分小内存操作无需碰触arena锁。但对于大内存分配、从操作系统申请新内存、以及tcache缓存满后的释放操作,仍然需要获取arena锁。

这种设计是一种权衡:它不像全局锁那样粗暴,也不像完全无锁分配器那样复杂。它在保证正确性的前提下,为大多数常见场景提供了不错的性能。

7. 高级话题与调优实践

了解了基本原理,我们来看看如何利用这些知识解决实际问题。

7.1 常见内存问题诊断

  1. 内存泄漏:程序持续运行后,RSS(常驻内存集)或heap段不断增长。可以使用valgrind --leak-check=full来定位未释放的内存块。理解mallocfree的机制能帮你看懂valgrind的输出,比如“definitely lost”、“indirectly lost”等报告的含义。
  2. 内存碎片:程序运行一段时间后,虽然总空闲内存很多,但申请较大块内存时失败(ENOMEM)。可以通过malloc_info()函数或mallinfo()(已废弃)来查看arena内部bins的分布情况。如果发现大量小的、分散的空闲chunk,就是外部碎片。调大M_MMAP_THRESHOLD让大块内存走mmap,或者使用malloc_trim(0)尝试向系统归还空闲内存,可能有所缓解,但治标不治本。根本解决可能需要优化内存分配模式,或使用更抗碎片的分配器(如jemalloc,tcmalloc)。
  3. 性能瓶颈:多线程程序在高并发下性能不佳。使用perfSystemTap等工具观察mallocfree的耗时,以及锁竞争情况。如果锁竞争严重,可以考虑:
    • 调整MALLOC_ARENA_MAX增加arena数量。
    • 使用线程本地缓存(tcache)是默认开启的,确保它生效。
    • 对于特定场景,可以考虑使用对象池或自己实现一个简单的、无锁的每线程内存池,完全绕过malloc

7.2 环境变量与mallopt调优

glibc的malloc行为可以通过环境变量和mallopt函数进行调优:

  • MALLOC_ARENA_MAX:设置最大arena数量。
  • MALLOC_MMAP_THRESHOLD_:设置使用mmap分配的阈值。对于频繁分配释放的大内存,设置为一个较大的值(如1MB)可以避免频繁的mmap/munmap系统调用开销。但可能会增加堆碎片。
  • MALLOC_TRIM_THRESHOLD_:设置free操作中触发malloc_trim(向系统归还内存)的阈值。默认值较大,对于内存敏感的应用可以调小,但会增加系统调用开销。
  • mallopt函数:在程序内动态设置上述参数。例如:
    #include <malloc.h> mallopt(M_MMAP_THRESHOLD, 1024*1024); // 设置mmap阈值为1MB

7.3 替代分配器简介

ptmalloc2无法满足你的需求时,可以考虑其他内存分配器:

  • jemalloc(Facebook):最初为FreeBSD开发,后被Facebook等公司广泛使用。它在多线程场景下的扩展性极佳,碎片控制也更好。常用于高性能服务器(如Redis、RocksDB默认使用它)。
  • tcmalloc(Google):Google的线程缓存malloc。其tcache设计比glibc的更早更激进,每个线程有完全独立的小对象缓存,对于小对象分配极快。也提供了出色的堆剖析工具。
  • mimalloc(Microsoft):较新的分配器,注重安全和性能。声称比jemalloctcmalloc更快,碎片更少。

更换分配器通常只需要在链接时替换库文件(如-ljemalloc),并可能需设置一些环境变量。但在生产环境更换前,务必进行充分的测试和性能评估。

8. 从原理到实践:一个简单的调试示例

理论说了这么多,我们来看点实际的。如何直观地看到这些bins和chunk呢?虽然不能直接查看glibc内部数据结构,但我们可以通过写一个小程序,并利用gdb和glibc提供的钩子函数(hook)或调试工具来观察。

下面是一个极其简单的程序,我们用它来引发一些特定的分配行为:

#include <stdlib.h> #include <stdio.h> #include <unistd.h> int main() { // 1. 分配一个fast bin大小的内存 void *p1 = malloc(16); // 实际chunk大小可能为32或40字节 printf("p1 = %p\n", p1); // 2. 分配一个small bin大小的内存 void *p2 = malloc(512); // 实际chunk大小可能为528字节 printf("p2 = %p\n", p2); // 3. 分配一个large bin大小的内存 void *p3 = malloc(2048); // 实际chunk大小可能为2064字节 printf("p3 = %p\n", p3); // 4. 分配一个超大内存,触发mmap void *p4 = malloc(256*1024); // 超过默认128KB阈值 printf("p4 = %p\n", p4); getchar(); // 暂停,方便观察内存状态 free(p1); free(p2); free(p3); free(p4); getchar(); // 再次暂停,观察free后的状态 return 0; }

编译并运行这个程序:gcc -g test_malloc.c -o test_malloc && ./test_malloc。程序会打印地址并等待。

在另一个终端,用gdb附加到这个进程(gdb -p <pid>)。在glibc中,一些内部符号是导出的,我们可以尝试打印main_arena(注意,多线程下可能不是这个)。更通用的方法是使用malloc_info函数将状态输出到XML,或者使用像pwndbggef这样的增强型GDB插件,它们提供了类似heapbins这样的命令来可视化堆状态。

例如,在pwndbg中,在第一个getchar处中断后,可以输入heap查看堆布局,输入bins查看各个bins的状态。你会看到p1p2p3对应的chunk位于堆上,而p4的地址通常离堆段很远,因为它来自mmap映射的区域。

执行完free后再次查看bins,你应该能看到p1对应的chunk出现在fast bin中,p2p3对应的chunk出现在unsorted bin中(或者根据大小进入small/large bin)。而p4对应的内存,因为是通过mmap分配的,会直接从进程的地址空间中被移除。

这个简单的实验能让你对理论有一个直观的印证。在实际调试复杂的内存问题时,这种观察内部状态的能力至关重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询