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

简单来说:

  • 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_SHAREDMAP_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 可以直接完成分配。只有现有内存不足,或者需要处理较大的分配请求时,它才可能通过 brkmmap 向内核申请更多地址空间。

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 会触发缺页异常。内核随后检查这次访问是否合法,并在需要时分配物理页、清零页面、读取文件数据或建立页表项。

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

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;
}

运行结果中可能同时出现 brkmmapmunmap。不过需要注意:

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

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

十、对比总结

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

十一、结论

mallocmmap 最大的区别在于抽象层次:

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

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

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

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

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