引言:为什么需要反向映射
在Linux内核内存管理中,页面回收是一个关键决策:当内存紧张时,内核必须决定哪些页面可以被换出或释放。然而,内核需要回答一个看似简单却极其复杂的问题:"谁在使用这个页面?"
这就是反向映射(Reverse Mapping, rmap)机制要解决的问题。正向映射是从虚拟地址找到物理页框(PFN),而反向映射则是从物理页框反向找到所有映射了它的PTE(页表项)。
一、反向映射的核心数据结构
1.1 anon_vma 与 anon_vma_chain
对于匿名页面(进程堆、栈、mmap匿名映射等),Linux使用anon_vma结构体来跟踪所有映射了同一物理页面的进程:
// include/linux/rmap.h
struct anon_vma {
struct anon_vma *root; // 指向根anon_vma(父子进程共享)
rwlock_t rwsem; // 读写锁
atomic_t refcount; // 引用计数
unsigned degree; // 在anon_vma树中的深度
struct rb_root_cached rb_root; // 关联的AVC红黑树根
};
struct anon_vma_chain {
struct vm_area_struct *vma; // 映射该页面的VMA
struct anon_vma *anon_vma; // 所属的anon_vma
struct list_head same_vma; // 同一VMA中的链表
struct rb_node rb; // AV红黑树节点
unsigned long cached_stable_id;
};
关键洞察:anon_vma_chain让每个进程可以记录"我这个进程的哪个VMA映射了这个页面",而anon_vma则汇聚了所有共享该页面的进程信息。
1.2 父子进程的AV继承
fork()时子进程继承父进程的AV关系。为了优化,anon_vma采用了链式结构(通过root指针)。当页面需要反向映射遍历时,从任意进程出发通过root指针到达根节点,然后遍历根节点的anon_vma_chain链表即可找到所有映射者。
二、文件映射页面的反向映射
2.1 address_space 与 i_mmap
文件映射页面(file-backed page)的反向映射通过address_space中的i_mmap(间隔树/interval tree)实现。每个vm_area_struct都在i_mmap树中注册了自己的映射范围。
// include/linux/fs.h
struct address_space {
struct inode *host; // 关联的inode
struct xarray i_pages; // 页面缓存映射(xarray)
struct rb_root_cached i_mmap; // 文件映射VMA的间隔树
};
文件页面反向映射的优势:无需遍历PTE,直接通过i_mmap间隔树查找所有映射了该文件区域的VMA。
2.2 try_to_unmap 的双重路径
页面回收时,try_to_unmap()会调用两种不同的反向映射策略:
// mm/rmap.c
int try_to_unmap(struct page *page, enum ttu_flags flags) {
struct rmap_walk_control rwc = {
.rmap_one = try_to_unmap_one, // 遍历每个PTE
.arg = (void *)flags,
.done = page_not_mapped,
.anon_lock = page_lock_anon_vma_read,
};
if (PageAnon(page))
// 匿名页面: 通过anon_vma遍历进程PTE
rmap_walk_anon(page, &rwc, false);
else
// 文件页面: 通过i_mmap遍历VMA
rmap_walk_file(page, &rwc, false);
if (!page_mapped(page) && (flags & TTU_UNMAP))
return SWAP_SUCCESS;
return SWAP_AGAIN;
}
三、COW(Copy-on-Write)与反向映射的协同
3.1 fork时的COW静默标记
fork时父子进程共享页面,但都标记为只读。此时它们共享同一个anon_vma,这意味着会同步进入对方的反向映射链中。
3.2 do_wp_page:缺页触发复制
当写操作触发写保护缺页时,内核判断是否可以"重新共享"还是必须"复制":
// mm/memory.c (简化)
static vm_fault_t do_wp_page(struct vm_fault *vmf) {
struct page *old_page = vmf->page;
// 路径1: 只有一个映射者 → 无需复制,直接标记可写
if (page_count(old_page) == 1 && !swapcache) {
// exclusive页面:可能是KSM解耦后的独立副本
// 直接标记为dirty+writable
pte = pte_mkwrite(pte_mkdirty(pte));
return VM_FAULT_WRITE | VM_FAULT_WRITE_BIT;
}
// 路径2: 多个映射者 → 必须复制新页面
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vmf->vm_mm, vmf->address);
copy_user_highpage(new_page, old_page, vmf->address, vma);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, mk_pte(new_page, vma->vm_page_prot));
page_dup_rmap(new_page, false);
}
3.3 KSM:Kernel Samepage Merging
是COW的"逆向"机制——让相同内容的页面合并为一个只读页面。KSM使用稳定树(stable tree)和不稳定树(unstable tree)两层结构:
- 稳定树存储已合并的只读页面,通过
node节点关联到mm_slot结构 - 不稳定树存储尚未确认稳定的页面候选,每次扫描时通过crypto hash校验确认
四、kswapd:页面回收的决策引擎
4.1 水位线(watermark)机制
每个内存Zone定义了三个水位线:WMARK_MIN、WMARK_LOW、WMARK_HIGH。当空闲页面低于low时唤醒kswapd,低于min时触发同步回收(direct reclaim)。
// mm/page_alloc.c
struct zone {
unsigned long _watermark[NR_WMARK]; // [min, low, high]
unsigned long watermark_boost; // 水位线提升量
};
// 简化的kswapd唤醒逻辑
void wakeup_kswapd(struct zone *zone, enum zone_idx classzone_idx) {
if (zone->free_pages <= zone->_watermark[WMARK_LOW])
wake_up_interruptible(&zone_pgdat(zone)->kswapd_wait);
}
4.2 LRU双链与页面老化
Linux使用双链LRU(Active/Inactive)实现近似LRU策略。页面的活跃程度通过PG_active标志和链表移动来判断:
// mm/vmscan.c
enum LRU_LISTS {
LRU_INACTIVE_ANON = 0,
LRU_ACTIVE_ANON = 1,
LRU_INACTIVE_FILE = 2,
LRU_ACTIVE_FILE = 3,
NR_LRU_LISTS
};
// 简化的shrink_lruvec()逻辑
unsigned long shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc) {
// 决定回收比例(swappiness: 0-200)
unsigned long anon_cost = get_lru_size(anon) * swappiness;
unsigned long file_cost = get_lru_size(file) * (200 - swappiness);
// 优先回收inactive列表尾部的页面
list_for_each_entry_safe(page, next, &lruvec->lists[LRU_INACTIVE_ANON], lru) {
if (referenced_recently(page)) {
// 页面最近被访问,移动到active链表(第二次机会)
activate_page(page, lruvec);
} else {
// 真正不活跃:回收
shrink_page_list_one(page, lruvec);
}
}
}
4.3 回收压力传播
当kswapd无法快速补充空闲页面时,回收压力通过"分配优先级降级"传播:GFP_HIGHUSER → GFP_HIGH → GFP_USER → GFP_KERNEL。最极端情况下触发OOM killer强制杀进程。
五、反向映射的生产环境挑战
5.1 rmap遍历的性能代价
大内存服务器上,单个物理页可能被数千个PTE映射。try_to_unmap_one()在每个进程地址空间执行TLB无效化,这是一次重要的IPI(处理器间中断)风暴。
5.2 THP(透明大页)与rmap的冲突
2MB大页拆分回4KB基础页时,分裂路径需要重建整个rmap结构,这是THP争议的核心原因之一。5.4内核后引入deferred_split_huge_page()作延迟化处理。
六、总结:Linux内存回收设计哲学
Linux内存管理的设计核心是延迟决策:
- 分配页面时不立即决定谁可以回收(先分配,后追溯)
- 回收时才通过反向映射机制寻找所有引用者
- LRU第二机会算法给页面"缓刑期",避免误杀热页面
- 水位线分级响应避免了回收的"全有或全无"震荡
反向映射虽然带来运行时开销,但它让内核在复杂的共享内存场景下仍能保持正确的回收语义——这是简单引用计数无法企及的灵活性。
参考资料
- Love, Robert. "Linux Kernel Development." 第3版, Chapter 15
- Bovet, Daniel P. "Understanding the Linux Kernel." 第3版
- 内核源码: mm/rmap.c, mm/vmscan.c, mm/ksm.c, mm/memory.c (linux-6.6)

发表评论 取消回复