mmap 内存管理
是什么 / 解决什么问题
mmap 用于内存分配时,侧重于匿名映射和内存管理语义(与文件映射篇互补)。包括大块内存分配、内存释放与回收策略、大页等。
使用模式
匿名内存分配
// 分配大块内存(malloc 对 >= 128KB 的请求默认使用 mmap)
void *mem = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (mem == MAP_FAILED) { perror("mmap"); }
// 释放
munmap(mem, size); // 立即归还给系统(与 free 不同)
madvise — 告知内核内存使用意图
// MADV_DONTNEED: 不再需要这些页,内核可以回收
madvise(addr, length, MADV_DONTNEED);
// 匿名映射:页被释放,再次访问得到零页(相当于"重置")
// 文件映射:页被释放,再次访问重新从文件读取
// MADV_FREE: 懒惰释放(4.5+)
madvise(addr, length, MADV_FREE);
// 内核在内存压力时才回收。如果回收前再次写入,则保留不回收
// 比 DONTNEED 更快(不立即清零)
// MADV_WILLNEED: 即将访问,预先加载
madvise(addr, length, MADV_WILLNEED);
// MADV_HUGEPAGE: 尝试使用透明大页
madvise(addr, length, MADV_HUGEPAGE);
// MADV_NOHUGEPAGE: 禁用透明大页
madvise(addr, length, MADV_NOHUGEPAGE);
// MADV_POPULATE_READ/WRITE: 预先 fault 所有页面 (5.14+)
madvise(addr, length, MADV_POPULATE_WRITE);
mremap — 调整映射大小
// 等价于 realloc 但针对 mmap
void *new_addr = mremap(old_addr, old_size, new_size, MREMAP_MAYMOVE);
// MREMAP_MAYMOVE: 允许移动到新地址(如果原地扩展不了)
// 返回新地址(可能与 old_addr 不同)
// 优势:不需要复制数据(只调整页表)
内存池模式
// 预分配大块内存,自己管理分配
void *pool = mmap(NULL, POOL_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// "释放"时用 MADV_DONTNEED 重置页面(不 munmap)
// 避免频繁 mmap/munmap 的 VMA 操作开销
madvise(block_addr, block_size, MADV_DONTNEED);
// 重新使用时直接写入(触发缺页中断分配新零页)
行为与语义
MADV_DONTNEED vs MADV_FREE
|
MADV_DONTNEED |
MADV_FREE |
| 回收时机 |
立即 |
内存压力时才回收 |
| 再次访问 |
零页(保证) |
可能是旧数据(未被回收时) |
| RSS 变化 |
立即下降 |
延迟下降 |
| 适用 |
需要确定性释放 |
缓存型使用(jemalloc) |
匿名映射的物理内存生命周期
mmap(MAP_ANONYMOUS)
→ 仅分配虚拟地址空间,RSS = 0
首次写入某页
→ page fault → 分配零物理页,RSS += 4KB
madvise(MADV_DONTNEED)
→ 物理页释放,RSS -= 4KB,虚拟地址保留
munmap
→ 虚拟地址 + 物理页全部释放
MAP_POPULATE — 预 fault
// 映射时立即分配所有物理页(避免运行时 page fault)
void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,
-1, 0);
// 返回后所有页面已在物理内存中
// 代价:mmap 调用变慢(需要分配 size/4KB 个物理页)
MAP_NORESERVE
// 不为映射预留 swap 空间(允许更大的 overcommit)
void *mem = mmap(NULL, huge_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
-1, 0);
// 用于稀疏使用的大映射(实际只使用一小部分)
性能考量
mmap vs malloc (brk)
|
mmap(匿名) |
malloc(小块 via brk) |
| 分配开销 |
VMA 创建(~几μs) |
用户态链表操作(~几十ns) |
| 释放行为 |
munmap 立即归还 OS |
free 通常不归还(缓存复用) |
| 碎片 |
无外部碎片 |
可能有外部碎片 |
| 适合 |
大块(>128KB) |
小块频繁分配 |
减少 VMA 操作开销
// 频繁 mmap/munmap 导致 VMA 红黑树操作开销
// 改用大块预分配 + madvise 管理
// 避免:
for (...) {
void *p = mmap(NULL, 4096, ...); // 每次创建 VMA
// use p
munmap(p, 4096); // 每次删除 VMA
}
// 改为:
void *pool = mmap(NULL, LARGE_SIZE, ...); // 一次 VMA
for (...) {
void *p = pool + offset;
// use p
madvise(p, 4096, MADV_DONTNEED); // 释放物理页,保留 VMA
}
透明大页 (THP) 的利弊
优点:
- 减少 TLB miss(一个 TLB 条目覆盖 2MB)
- 减少页表层级遍历
- 对大内存应用提升显著(数据库、JVM)
缺点:
- 内存碎片导致分配延迟(内核需要找到连续 2MB 物理页)
- 内存浪费(部分使用的 2MB 页不能分割释放)
- khugepaged 后台合并线程消耗 CPU
- 可能增加延迟抖动(compaction)
# THP 控制
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
# madvise: 只对 MADV_HUGEPAGE 的区域启用(推荐)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
常见陷阱与 FAQ
1. munmap 部分区域
// 可以 munmap 映射的任意子区域
void *mem = mmap(NULL, 4 * PAGE_SIZE, ...);
munmap(mem + PAGE_SIZE, PAGE_SIZE); // 只释放中间一页
// 原始映射被分裂成两个 VMA
2. fork 后匿名映射的行为
// MAP_PRIVATE + MAP_ANONYMOUS:
// fork 后父子各有独立副本(COW)
// MAP_SHARED + MAP_ANONYMOUS:
// fork 后父子共享同一物理页(进程间通信)
3. ENOMEM 的真正含义
void *p = mmap(NULL, size, ...);
if (p == MAP_FAILED && errno == ENOMEM) {
// overcommit_memory=0: 内核认为无法满足(启发式判断)
// overcommit_memory=2: 确实超过了 commit 限制
// 不一定是物理内存不足!可能是地址空间用完或 VMA 数量达到上限
}
// VMA 数量限制
cat /proc/sys/vm/max_map_count // 默认 65530
观测与调试
# 查看匿名内存使用
cat /proc/<pid>/status | grep -E "VmRSS|RssAnon|RssFile"
# 查看 madvise 效果
watch -n1 'cat /proc/<pid>/status | grep VmRSS'
# 查看 THP 使用
cat /proc/<pid>/smaps_rollup | grep -i huge
grep AnonHugePages /proc/meminfo
# 追踪 mmap/munmap 调用
strace -e trace=mmap,munmap,madvise,mremap -p <pid>
# 查看内存碎片(大页可用性)
cat /proc/buddyinfo
# 每列数字表示对应 order 的空闲块数量
# order 9 (2MB) 的数量 > 0 才能分配大页
延伸阅读
man 2 mmap / man 2 madvise / man 2 mremap
man 5 proc — /proc/[pid]/smaps 详细内存信息
- jemalloc / tcmalloc 文档 — 现代 allocator 如何使用 mmap