跳转至

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