无标题
在 Linux 用户态程序中,malloc 和 mmap 都可以用来获得一段可访问的虚拟内存,但它们并不是同一层次的接口。
简单来说:
malloc是 C 标准库提供的动态内存分配接口;mmap是操作系统提供的虚拟内存映射接口;malloc在实现过程中,可能通过brk扩展堆,也可能通过匿名mmap向内核申请内存。
因此,二者既有区别,也存在实现上的联系。
一、malloc 是什么
malloc 的函数原型如下:
1 |
|
调用成功时,malloc 返回一块至少为 size 字节的内存;失败时返回 NULL。使用结束后,应当通过 free 释放:
1 | int *array = malloc(100 * sizeof(int)); |
malloc 属于用户态内存分配器。它不仅负责向操作系统申请内存,还需要完成以下工作:
- 管理已经取得的内存区域;
- 从空闲内存块中寻找合适的块;
- 对内存块进行切分和合并;
- 记录每个内存块的大小、状态等元数据;
- 尽量减少内存碎片;
- 在多线程环境下保证分配过程的正确性。
所以,每次调用 malloc 并不一定都会进入内核。很多时候,内存分配器只是在已经持有的内存池中取出一个空闲块。
二、mmap 是什么
mmap 的函数原型如下:
1 |
|
mmap 的本质是在当前进程的虚拟地址空间中建立一段新的映射。它既可以映射文件,也可以创建匿名映射。
1. 文件映射
下面的代码把文件的一部分映射到进程地址空间:
1 | void *addr = mmap(NULL, length, |
此时,对映射区域的访问会与文件内容建立联系。根据 MAP_SHARED 或 MAP_PRIVATE 的不同,修改可能写回文件,也可能通过写时复制形成进程私有页面。
2. 匿名映射
如果使用 MAP_ANONYMOUS,映射不与任何普通文件关联:
1 | void *addr = mmap(NULL, 1024 * 1024, |
匿名映射常用于申请大块内存、创建线程栈,以及在父子进程之间建立共享内存。
三、malloc 与 mmap 的根本区别
1. 所处层次不同
malloc 是 C 标准库接口,主要目标是为程序提供任意大小、便于使用的内存块。
mmap 是系统调用接口,主要目标是在进程虚拟地址空间中建立映射。它关心的是虚拟内存区域、页面权限、文件映射和共享关系。
也就是说,malloc 面向“内存块”,而 mmap 面向“虚拟内存区域”。
2. 分配粒度不同
程序可以调用 malloc(13) 申请 13 字节。虽然分配器通常会进行内存对齐,并额外保存管理信息,但它仍然适合处理小块内存。
mmap 由内核按页面管理。即使只映射很少的字节,内核也必须以页为基本单位建立和维护映射。在常见系统中,一页通常为 4 KiB。
因此,频繁使用 mmap 分配大量小对象,会产生较高的系统调用和虚拟内存区域管理开销。
3. 是否一定进入内核不同
mmap 是系统调用,执行时需要进入内核。
malloc 不一定进入内核。如果用户态内存池中存在合适的空闲块,malloc 可以直接完成分配。只有现有内存不足,或者需要处理较大的分配请求时,它才可能通过 brk 或 mmap 向内核申请更多地址空间。
4. 释放方式不同
malloc 对应 free:
1 | void *ptr = malloc(size); |
mmap 对应 munmap:
1 | void *addr = mmap(/* ... */); |
调用 free 后,内存通常只是回到用户态分配器的空闲链表或其他数据结构中,不一定立即归还给操作系统。分配器可能保留这块内存,供后续的 malloc 重复使用。
调用 munmap 后,对应映射会直接从进程地址空间中撤销。再次访问该地址通常会触发段错误。
5. 使用目的不同
malloc 主要用于:
- 创建动态数组;
- 创建链表、树等动态数据结构;
- 分配生命周期不固定的对象;
- 为普通用户态程序申请工作内存。
mmap 主要用于:
- 把文件映射到内存;
- 建立匿名内存映射;
- 创建进程间共享内存;
- 控制页面的读、写、执行权限;
- 在指定或合适的虚拟地址范围建立映射;
- 为内存分配器提供底层的大块内存。
四、malloc 与 brk、mmap 的联系
传统 Unix 进程的内存布局中存在“堆”区域。内核可以通过调整 program break,也就是堆的末端位置,扩大或缩小堆。
在 Linux 中,相关系统调用是 brk。C 库还提供了较少直接使用的 sbrk 接口。
一个典型的用户态内存分配器可以采用以下策略:
1 | 较小的 malloc 请求 |
因此,不能简单地说“malloc 使用堆,而 mmap 不使用堆”,更准确的说法是:
malloc向调用者提供堆式动态分配语义,但其底层获得内存的方式可以是扩展传统堆,也可以是创建匿名映射。
以 glibc 的 malloc 实现为例,大块分配通常倾向于使用 mmap。具体阈值并不是 C 标准的一部分,也不应当被程序当作固定常量依赖。分配器可以根据运行状态、配置和版本动态调整策略。
五、为什么大块内存适合使用 mmap
假设程序通过扩展堆取得一块大内存,这块内存位于其他堆对象之间。当它被释放时,如果它不是堆顶的连续空闲区域,分配器往往不能直接降低 program break,只能暂时把它保留在空闲内存池中。
而通过 mmap 创建的大块分配通常拥有独立的虚拟内存区域。释放时可以直接调用 munmap 撤销整个映射,更容易把虚拟地址空间及相关物理页资源归还给内核。
不过,mmap 也有额外成本:
- 每次映射和解除映射都需要系统调用;
- 内核需要维护虚拟内存区域;
- 大量映射可能增加页表和 VMA 管理压力;
- 频繁映射、解除映射可能导致地址空间更零散;
- 首次访问页面时仍可能发生缺页异常。
所以,内存分配器一般只在大块分配中使用 mmap,小块分配则尽量在用户态内存池中完成。
六、申请虚拟内存不等于立即获得物理内存
无论是扩大堆还是建立匿名映射,成功返回通常只说明进程获得了相应的虚拟地址范围,并不代表内核已经立刻为所有页面分配了物理内存。
例如:
1 | char *p = malloc(1024UL * 1024 * 1024); |
即使一次申请 1 GiB 成功,在程序尚未访问这些页面时,实际驻留在物理内存中的页面可能很少。
当程序第一次读写某个尚未建立物理映射的页面时,CPU 会触发缺页异常。内核随后检查这次访问是否合法,并在需要时分配物理页、清零页面、读取文件数据或建立页表项。
这就是按需分页。它也是理解 malloc、mmap 与物理内存关系的关键:
1 | malloc 或 mmap 成功 |
当然,具体行为还会受到内存过量承诺、页面是否已存在、映射类型以及系统配置等因素影响。
七、内存碎片方面的区别
1. malloc 的碎片
malloc 需要在用户态管理不同大小的内存块,因此可能产生:
- 内部碎片:实际分配的块比用户请求的空间更大;
- 外部碎片:空闲空间总量足够,但被分散成多个不连续的小块。
成熟的内存分配器会通过大小分类、空闲链表、合并相邻空闲块、线程缓存等方式改善性能和碎片问题。
2. mmap 的碎片
mmap 不需要像 malloc 一样切分字节级小块,但它以页为单位管理映射。如果只使用映射页中的一小部分,其余空间仍会造成浪费。
此外,大量独立映射也会使进程的虚拟地址空间被切分成很多区域,增加内核管理成本。
八、mmap 能否代替 malloc
理论上,可以基于匿名 mmap 自己实现一个内存分配器,但不适合每次申请对象都直接调用一次 mmap。
例如,为一个 24 字节的结构体单独创建一个页面映射,至少存在以下问题:
- 页内空间浪费严重;
- 系统调用开销较大;
- 映射数量会快速增加;
- 释放时需要额外保存映射长度;
- 难以获得成熟分配器已有的缓存和并发优化。
更合理的自定义分配器做法是:通过 mmap 一次取得较大的内存区域,再在用户态把它切分成许多小块。这其实再次体现了二者的层次关系——mmap 可以作为底层原料来源,malloc 式分配器负责上层的细粒度管理。
九、如何观察 malloc 是否使用了 mmap
可以使用 strace 观察程序执行的系统调用:
1 | strace -e trace=brk,mmap,munmap ./a.out |
测试程序如下:
1 |
|
运行结果中可能同时出现 brk、mmap 和 munmap。不过需要注意:
- 程序启动和动态链接器本身也会使用
mmap; - 不同 C 库和不同版本的分配策略可能不同;
- 分配器会缓存内存,系统调用与每次
malloc不一定一一对应; - 大块分配阈值可能动态变化。
因此,应结合地址、长度以及调用发生的时间分析结果,而不能只要看到 mmap 就认定它一定来自某次 malloc。
十、对比总结
| 对比项 | malloc | mmap |
|---|---|---|
| 接口层次 | C 标准库接口 | 操作系统虚拟内存接口 |
| 主要对象 | 任意大小的内存块 | 以页面为基础的虚拟内存区域 |
| 是否一定进入内核 | 不一定 | 是 |
| 底层来源 | 内存池、brk、匿名 mmap 等 |
内核建立的文件或匿名映射 |
| 释放接口 | free |
munmap |
| 释放后是否立即撤销映射 | 通常不一定 | 是 |
| 是否支持文件映射 | 不直接支持 | 支持 |
| 是否方便共享内存 | 不直接支持 | 支持 |
| 典型用途 | 普通动态对象和数据结构 | 文件映射、大块匿名内存、共享内存 |
| 管理粒度 | 可处理很小的对象 | 以页为基本管理单位 |
十一、结论
malloc 和 mmap 最大的区别在于抽象层次:
malloc是面向应用程序的动态内存分配器;mmap是面向进程地址空间的虚拟内存映射机制。
二者的联系是,mmap 可以成为 malloc 的底层实现手段之一。小块内存通常由分配器在用户态内存池中快速处理,大块内存则可能通过匿名 mmap 创建独立映射。
理解二者时,还要牢记一点:申请到虚拟地址不等于相同大小的物理内存已经立即就位。真正的物理页往往是在程序访问页面并触发缺页异常后,才由内核按需建立。
所以,从用户程序到物理内存,完整的关系可以概括为:
1 | 应用程序 |
