每个进程对内核说”我有哪些地址段”,靠
mm_struct 这个核心数据结构。里面挂着所有
VMA(Virtual Memory Area),每个 VMA 描述一段连续的 VA →
同一保护属性 + 同一 backing。
一、先看图
flowchart TD
TASK[task_struct] --> MM[mm_struct]
MM --> MT[maple tree<br/>VMA 索引]
MT --> V1[VMA: text<br/>0x400000-0x600000<br/>rx, file]
MT --> V2[VMA: heap<br/>0x600000-0x900000<br/>rw, anon]
MT --> V3[VMA: stack<br/>0x7ffc...-0x7fff...<br/>rw, anon, growsdown]
MT --> V4[VMA: mmap<br/>0x7f0000...<br/>rx, file .so]
MM --> PGD[pgd → 多级页表]
classDef meta fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef vma fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class TASK,MM,PGD meta
class V1,V2,V3,V4 vma
二、mm_struct 核心字段
struct mm_struct {
struct maple_tree mm_mt; // VMA 索引(6.1+ 替代老 rbtree)
pgd_t *pgd; // 顶级页表
atomic_t mm_users; // 用户态引用计数(>0 才有活进程)
atomic_t mm_count; // 内核引用(lazy tlb 也算一份)
unsigned long task_size; // 用户空间上界
unsigned long start_code, end_code;
unsigned long start_data, end_data;
unsigned long start_brk, brk;
unsigned long start_stack;
unsigned long arg_start, arg_end;
unsigned long env_start, env_end;
struct rw_semaphore mmap_lock;
...
};关键:
- mm_users > 0 才有活线程;=0 但 mm_count > 0 可能还有 lazy TLB CPU 引用
- mmap_lock:VMA 操作的大锁;争抢剧烈是多线程 mmap/munmap 热点
三、VMA(vm_area_struct)
struct vm_area_struct {
unsigned long vm_start, vm_end;
pgoff_t vm_pgoff; // 文件偏移
struct file *vm_file; // NULL=匿名
vm_flags_t vm_flags; // VM_READ|VM_WRITE|VM_EXEC|VM_SHARED...
const struct vm_operations_struct *vm_ops;
struct anon_vma *anon_vma;
...
};VMA 是”按段管理”思路——连续地址 + 相同属性 = 一个 VMA。mprotect 只改 vm_flags(可能拆 VMA)。
观察:
cat /proc/$$/maps
# 地址范围 权限 offset dev inode pathname四、从红黑树到 maple tree
4.1 老方案:红黑树 + 链表
5.x 及以前:VMA 同时挂红黑树(按地址索引)和链表(遍历)。
问题:
- 两套结构、双倍指针
- 每次 mmap/munmap 要同时维护两套
- RCU 读很难做
4.2 maple tree(6.1 合入)
Liam Howlett 的改造:用 B-tree 变种(maple tree)替代红黑树 + 链表。
好处:
- 单一数据结构
- RCU 读友好(
mmap_read_lock可以用 per-VMA lock 替代 mmap_lock) - 内存局部性好(B-tree 扇出高、层级浅)
- VMA 数量从几百到几万都高效
4.3 per-VMA lock(6.4+)
mmap_lock 改成读端走 per-VMA
lock,减少全局争抢。mm->per_vma_lock_seq
做无锁版本号检测。
效果:多线程 mmap 扩展性提升显著。
五、anon_vma 与 rmap
5.1 问题
内核要回收物理页,需要知道哪些 PTE 引用了它。对匿名页来说——fork 后父子共享同一物理页,谁的 PTE?
5.2 anon_vma
anon_vma
└── anon_vma_chain → VMA1 (parent)
└── anon_vma_chain → VMA2 (child)
每个匿名 VMA 有 anon_vma +
anon_vma_chain(链接所有共享同 anon 页的
VMA)。page_referenced() /
try_to_unmap() 通过 rmap 遍历所有映射者。
5.3 file rmap
文件页通过
address_space->i_mmap(interval tree)做
rmap。
六、mmap_lock 争抢与诊断
# 看 mmap_lock 争抢
perf lock contention -t -b -s 5
# 或
bpftrace -e 'tracepoint:lock:contention_begin /args->lock_addr == kaddr("mmap_lock")/ { @[comm]=count(); }'典型触发者:
- 大量短连接的 Go/Java 服务(goroutine 或 thread 频繁 mmap)
- JIT 编译器频繁创建 executable mmap
malloc竞争(glibc arena 可缓解)
七、VMA 合并与拆分
内核尽量合并相邻同属性 VMA(减少 VMA 数量)。mprotect/mremap 可能拆分一个 VMA。
过多 VMA(> 65530 ≈
vm.max_map_count)会导致 mmap 失败:
# Elasticsearch 启动常踩
sysctl vm.max_map_count=262144
八、特殊 VMA
- [vdso]:内核虚拟 DSO,提供 gettimeofday 等快速 syscall
- [vsyscall]:x86 遗留,已废弃
- [heap]:brk 区域
- [stack]:主线程栈
- guard page:PROT_NONE VMA 做栈溢出保护
九、mm_struct 生命周期
fork → dup_mm → 复制 mm_struct + 所有 VMA(COW 页表)
exec → exec_mmap → 丢弃旧 mm,创建新 mm
exit → mmput → mm_users-- → =0 时 exit_mmap → 释放所有 VMA + 页表
mmget / mmput
是引用计数接口;mmgrab / mmdrop 管
mm_count(lazy TLB 用)。
十、观察工具
# VMA 数量
wc -l /proc/$$/maps
# 详细 VMA 信息
cat /proc/$$/smaps_rollup
# mm_struct 内核信息
cat /proc/$$/status | grep -E 'Vm|Rss|Swap'
# maple tree 统计(debugfs)
cat /sys/kernel/debug/maple_tree_allocations 2>/dev/null十一、小结
- mm_struct 是进程地址空间在内核的总表;VMA 是每段描述
- maple tree 替代红黑树 + 链表是 6.1 最大 mm 改造
- per-VMA lock 把 mmap_lock 从全局锁变读端分散
- anon_vma / rmap 让回收能反向找到所有映射者
- VMA 数量 > max_map_count 是常见生产问题
参考文献
- Gorman, M. “Understanding the Linux Virtual Memory Manager.” §4
Documentation/mm/maple_tree.rst- Liam Howlett, “Maple tree introduction.” LPC 2022
- Suren Baghdasaryan, “per-VMA locks.” LWN.net 2023
include/linux/mm_types.h、mm/mmap.c
工具
/proc/$$/maps、/proc/$$/smapspmap -xperf lock contentionbpftrace+ mmap_lock tracing
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】task_struct 解剖
Linux 里「一个进程/线程」对应的内核数据结构是 struct task_struct,8KB 左右,几百个字段。本文把它切成 PID/凭据/内存/文件/信号/调度/namespace/追踪 八个区域,讲清楚 current 宏、thread_info、per_task 栈与 task_struct 的布局关系,以及字段变化背后的十年演进。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。