引言:为什么需要反向映射

在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)
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部