在 Linux 用户态程序中,malloc 和 mmap 都可以用来获得一段可访问的虚拟内存,但它们并不是同一层次的接口。

简单来说:

  • malloc 是 C 标准库提供的动态内存分配接口;
  • mmap 是操作系统提供的虚拟内存映射接口;
  • malloc 在实现过程中,可能通过 brk 扩展堆,也可能通过匿名 mmap 向内核申请内存。

因此,二者既有区别,也存在实现上的联系。

一、malloc 是什么

malloc 的函数原型如下:

1
2
3
#include <stdlib.h>

void *malloc(size_t size);

调用成功时,malloc 返回一块至少为 size 字节的内存;失败时返回 NULL。使用结束后,应当通过 free 释放:

1
2
3
4
5
6
7
8
9
int *array = malloc(100 * sizeof(int));
if (array == NULL) {
perror("malloc");
return 1;
}

/* 使用 array */

free(array);

malloc 属于用户态内存分配器。它不仅负责向操作系统申请内存,还需要完成以下工作:

  1. 管理已经取得的内存区域;
  2. 从空闲内存块中寻找合适的块;
  3. 对内存块进行切分和合并;
  4. 记录每个内存块的大小、状态等元数据;
  5. 尽量减少内存碎片;
  6. 在多线程环境下保证分配过程的正确性。

所以,每次调用 malloc 并不一定都会进入内核。很多时候,内存分配器只是在已经持有的内存池中取出一个空闲块。

二、mmap 是什么

mmap 的函数原型如下:

1
2
3
4
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot,
int flags, int fd, off_t offset);

mmap 的本质是在当前进程的虚拟地址空间中建立一段新的映射。它既可以映射文件,也可以创建匿名映射。

1. 文件映射

下面的代码把文件的一部分映射到进程地址空间:

1
2
3
4
5
6
7
8
9
10
11
12
void *addr = mmap(NULL, length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);

if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}

/* 可以像访问普通内存一样访问 addr */

munmap(addr, length);

此时,对映射区域的访问会与文件内容建立联系。根据 MAP_SHARED 或 MAP_PRIVATE 的不同,修改可能写回文件,也可能通过写时复制形成进程私有页面。

2. 匿名映射

如果使用 MAP_ANONYMOUS,映射不与任何普通文件关联:

1
2
3
4
5
6
7
8
9
10
11
void *addr = mmap(NULL, 1024 * 1024,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);

if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}

munmap(addr, 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
2
void *ptr = malloc(size);
free(ptr);

mmap 对应 munmap:

1
2
void *addr = mmap(/* ... */);
munmap(addr, length);

调用 free 后,内存通常只是回到用户态分配器的空闲链表或其他数据结构中,不一定立即归还给操作系统。分配器可能保留这块内存,供后续的 malloc 重复使用。

调用 munmap 后,对应映射会直接从进程地址空间中撤销。再次访问该地址通常会触发段错误。

5. 使用目的不同

malloc 主要用于:

  • 创建动态数组;
  • 创建链表、树等动态数据结构;
  • 分配生命周期不固定的对象;
  • 为普通用户态程序申请工作内存。

mmap 主要用于:

  • 把文件映射到内存;
  • 建立匿名内存映射;
  • 创建进程间共享内存;
  • 控制页面的读、写、执行权限;
  • 在指定或合适的虚拟地址范围建立映射;
  • 为内存分配器提供底层的大块内存。

四、malloc 与 brk、mmap 的联系

传统 Unix 进程的内存布局中存在“堆”区域。内核可以通过调整 program break,也就是堆的末端位置,扩大或缩小堆。

在 Linux 中,相关系统调用是 brk。C 库还提供了较少直接使用的 sbrk 接口。

一个典型的用户态内存分配器可以采用以下策略:

1
2
3
4
5
6
7
8
9
较小的 malloc 请求
↓
从已有空闲块中分配
↓ 空间不足
通过 brk 扩展堆

较大的 malloc 请求
↓
通过匿名 mmap 建立独立映射

因此,不能简单地说“malloc 使用堆,而 mmap 不使用堆”,更准确的说法是:

malloc 向调用者提供堆式动态分配语义,但其底层获得内存的方式可以是扩展传统堆,也可以是创建匿名映射。

以 glibc 的 malloc 实现为例,大块分配通常倾向于使用 mmap。具体阈值并不是 C 标准的一部分,也不应当被程序当作固定常量依赖。分配器可以根据运行状态、配置和版本动态调整策略。

五、为什么大块内存适合使用 mmap

假设程序通过扩展堆取得一块大内存,这块内存位于其他堆对象之间。当它被释放时,如果它不是堆顶的连续空闲区域,分配器往往不能直接降低 program break,只能暂时把它保留在空闲内存池中。

而通过 mmap 创建的大块分配通常拥有独立的虚拟内存区域。释放时可以直接调用 munmap 撤销整个映射,更容易把虚拟地址空间及相关物理页资源归还给内核。

不过,mmap 也有额外成本:

  • 每次映射和解除映射都需要系统调用;
  • 内核需要维护虚拟内存区域;
  • 大量映射可能增加页表和 VMA 管理压力;
  • 频繁映射、解除映射可能导致地址空间更零散;
  • 首次访问页面时仍可能发生缺页异常。

所以,内存分配器一般只在大块分配中使用 mmap,小块分配则尽量在用户态内存池中完成。

六、申请虚拟内存不等于立即获得物理内存

无论是扩大堆还是建立匿名映射,成功返回通常只说明进程获得了相应的虚拟地址范围,并不代表内核已经立刻为所有页面分配了物理内存。

例如:

1
2
3
4
char *p = malloc(1024UL * 1024 * 1024);
if (p == NULL) {
return 1;
}

即使一次申请 1 GiB 成功,在程序尚未访问这些页面时,实际驻留在物理内存中的页面可能很少。

当程序第一次读写某个尚未建立物理映射的页面时,CPU 会触发缺页异常。内核随后检查这次访问是否合法,并在需要时分配物理页、清零页面、读取文件数据或建立页表项。

这就是按需分页。它也是理解 malloc、mmap 与物理内存关系的关键:

1
2
3
4
5
6
7
8
9
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
void *small = malloc(32);
void *large = malloc(64 * 1024 * 1024);

if (small == NULL || large == NULL) {
free(small);
free(large);
return 1;
}

printf("small = %p\n", small);
printf("large = %p\n", large);

free(small);
free(large);
return 0;
}

运行结果中可能同时出现 brk、mmap 和 munmap。不过需要注意:

  1. 程序启动和动态链接器本身也会使用 mmap;
  2. 不同 C 库和不同版本的分配策略可能不同;
  3. 分配器会缓存内存,系统调用与每次 malloc 不一定一一对应;
  4. 大块分配阈值可能动态变化。

因此,应结合地址、长度以及调用发生的时间分析结果,而不能只要看到 mmap 就认定它一定来自某次 malloc。

十、对比总结

对比项 malloc mmap
接口层次 C 标准库接口 操作系统虚拟内存接口
主要对象 任意大小的内存块 以页面为基础的虚拟内存区域
是否一定进入内核 不一定 是
底层来源 内存池、brk、匿名 mmap 等 内核建立的文件或匿名映射
释放接口 free munmap
释放后是否立即撤销映射 通常不一定 是
是否支持文件映射 不直接支持 支持
是否方便共享内存 不直接支持 支持
典型用途 普通动态对象和数据结构 文件映射、大块匿名内存、共享内存
管理粒度 可处理很小的对象 以页为基本管理单位

十一、结论

malloc 和 mmap 最大的区别在于抽象层次:

  • malloc 是面向应用程序的动态内存分配器;
  • mmap 是面向进程地址空间的虚拟内存映射机制。

二者的联系是,mmap 可以成为 malloc 的底层实现手段之一。小块内存通常由分配器在用户态内存池中快速处理,大块内存则可能通过匿名 mmap 创建独立映射。

理解二者时,还要牢记一点:申请到虚拟地址不等于相同大小的物理内存已经立即就位。真正的物理页往往是在程序访问页面并触发缺页异常后,才由内核按需建立。

所以,从用户程序到物理内存,完整的关系可以概括为:

1
2
3
4
5
6
7
应用程序
↓ malloc/free
用户态内存分配器
↓ brk 或 mmap
内核虚拟内存管理
↓ 缺页异常与按需分页
物理内存