mmap、brk 与 malloc 到底是什么关系
理解了堆、虚拟地址空间和缺页之后,一个很自然的问题就是:
程序里写的
malloc,到底是怎么和操作系统内存管理接上的?
这背后最常见的一组概念就是 malloc、brk 和 mmap。
一、先说结论
最重要的一句话是:
malloc是用户态库函数;brk和mmap是向操作系统申请虚拟地址空间的底层机制。
也就是说,三者不是同一层级:
malloc是程序员直接调用的接口brk/mmap是内存分配器向操作系统要空间的方式
二、malloc 是什么
malloc 是 C 标准库提供的动态内存分配接口。
它的职责不是“直接向内核申请一块物理内存”,而是:
- 先看看自己手里有没有合适的空闲块
- 有的话直接切一块给你
- 不够的话再向操作系统申请更多虚拟内存
所以 malloc 更像是:
用户态内存分配器对外暴露的接口。
三、brk 是什么
brk / sbrk 可以理解成:
通过移动程序 break(堆顶边界)来扩展或缩小传统堆区。
它主要服务的是传统 heap 这块连续增长的区域。
你可以把它想成:
- 堆原来在这里结束
- 现在把堆顶继续往高地址推
- 于是堆可用空间变大
所以 brk 更适合:
- 小块、频繁的堆分配
- 由分配器在连续 heap 中切分复用
四、mmap 是什么
mmap 是一种更通用的内存映射机制。
它可以用来:
- 映射文件
- 创建匿名映射
- 申请大块虚拟内存
- 建立共享内存区域
- 加载动态库
它的本质不是“扩 heap”,而是:
在用户空间中单独建立一块新的映射区域。
五、malloc、brk、mmap 的关系
可以分三层理解:
第 1 层:程序员
写的是:
malloccallocrealloc
第 2 层:用户态分配器
分配器负责:
- 从已有空闲块中切空间
- 复用已经释放的空间
- 不够时再继续向 OS 申请
第 3 层:向 OS 要更多地址空间
分配器通常会选择:
- 用
brk扩大传统堆区 - 或者用
mmap单独映射一块新区域
所以:
malloc不是系统调用,它常常是通过brk或mmap间接拿到更多可分配空间。
六、为什么小块分配更常走 brk
传统 heap 区适合做“小块池化管理”。
如果每个小对象都单独走 mmap,会带来:
- 映射管理开销大
- 系统调用次数多
- 地址空间更零散
- 页级管理成本更高
所以小块对象更适合先从 heap 这块“大池子”里切分。
七、为什么大块分配更倾向走 mmap
大块内存如果也都混在 heap 里,会带来几个问题:
- heap 更容易膨胀
- 释放后更容易在堆中间留下大洞
- 碎片问题更明显
- 不容易直接归还给操作系统
而 mmap 的优势在于:
- 独立映射
- 独立释放
- 更容易通过
munmap直接还给 OS - 不容易把传统 heap 搅乱
所以很多现代分配器会选择:
小块申请更多走 heap /
brk,大块申请更倾向走mmap。
八、为什么 heap 里的大块 free 了,也不一定容易立刻还给 OS
因为 brk 管的是一段连续增长的堆区。
如果释放的是堆中间的一块大内存,即使它已经空闲,分配器也通常只能把它留在堆里等待后续复用,而不容易直接把它归还给操作系统。
只有当释放的是:
- heap 顶端末尾那一段连续空间
才更容易真正收缩 brk,把空间还给 OS。
这也是为什么 mmap 更适合独立的大块分配。
九、malloc 成功返回后,为什么第一次访问仍然可能缺页
因为 malloc 返回的是:
对当前进程来说合法的虚拟地址空间。
但这不等于:
- 这块空间已经立刻有了物理页框
- 对应页已经全部驻留在内存中
很多情况下仍然会采用按需分配:
- 地址先给你
- 真正第一次访问某一页时
- 才分配物理页框或触发缺页建立映射
所以 malloc 成功并不等于物理内存立刻全部准备好。
十、文件映射和匿名映射的区别
文件映射
虚拟地址区间映射到某个文件内容。
适用于:
- 映射普通文件
- 加载程序代码
- 映射动态库
这类页如果以后不在内存里,往往可以再从文件读回来。
匿名映射
没有对应文件,常用于:
- 大块内存分配
- 私有匿名页
- 共享匿名内存
这类页如果被换出,更依赖 swap。
十一、总结
malloc是用户态库函数,是程序动态分配内存最常见的接口;brk和mmap则是内存分配器向操作系统申请虚拟地址空间的底层方式。一般来说,小块分配更常在传统 heap 中通过brk扩展并复用空闲块,大块分配、文件映射和共享内存等场景更常使用mmap。需要注意的是,malloc返回的是合法虚拟地址,并不代表对应页已经立刻分配好了物理内存,很多情况下仍然要在第一次访问时按需分配页框。
