一、背景:为什么需要页面迁移?

在 NUMA 架构或内存管理需要均衡压力的场景下,页面迁移是核心手段。它能动态将内存页迁移到更合适的节点,以提升性能或回收资源。


二、整体流程概览

围绕4个核心函数展开:

  1. promote_active_list() ➔ 页面筛选
  2. isolate_lru_pages_promotion() ➔ 页面隔离
  3. promote_page_list() ➔ 迁移条件检查
  4. migrate_page_list() ➔ 真正迁移

最终实际迁移通过 migrate_pages() 实现。


三、主函数promote_active_list()

static unsigned long promote_active_list(unsigned long nr_to_scan,
	struct lruvec *lruvec, enum lru_list lru)
{
    LIST_HEAD(page_list); // 创建一个空链表 page_list,用于暂时存放被隔离出来的页面。
    pg_data_t *pgdat = lruvec_pgdat(lruvec); // 获取该lruvec所属的pgdat
    unsigned long nr_taken, nr_promoted;

    lru_add_drain(); // 确保per-CPU pagevec缓存刷回global LRU链表

    // 为什么需要这一步?
    // Linux为提高性能,引入了per-CPU pagevec缓存。避免每次页面状态变化都频繁改global LRU链表。
    // 但当我们要做扫描、迁移等操作时,必须同步这些缓存,否则会漏掉页面。

    spin_lock_irq(&lruvec->lru_lock); // 加锁保护LRU链表结构和统计信息,防止并发写。不是为读。
    nr_taken = isolate_lru_pages_promotion(nr_to_scan, lruvec, lru, &page_list, 0);
    __mod_node_page_state(pgdat, NR_ISOLATED_ANON, nr_taken); // 隔离页面的记账
    spin_unlock_irq(&lruvec->lru_lock);

    if (nr_taken == 0)
        return 0;

    nr_promoted = promote_page_list(&page_list, pgdat);

    spin_lock_irq(&lruvec->lru_lock);
    move_pages_to_lru(lruvec, &page_list);
    __mod_node_page_state(pgdat, NR_ISOLATED_ANON, -nr_taken); // 将隔离页面计数恢复,表示这些页面已经不再被隔离。
    spin_unlock_irq(&lruvec->lru_lock);

    mem_cgroup_uncharge_list(&page_list); // 对失败或不再需要的页面释放memcg资源

    // 因为迁移失败且没有被归还放入LRU中的页面(比如引用已经为0了),即将被释放这些页面 ➔ 必须解除charge避免内存组资源泄漏。

    free_unref_page_list(&page_list); // 释放引用计数为0的页面

    return nr_promoted;
}

NR_ISOLATED_ANON和NR_ISOLATED_FILE 是内核中的vmstat指标,记录当前有多少页面正在被隔离、尚未返回LRU。有的路径(如kswapd、direct reclaim)会根据这个计数器判断是否需要放慢扫描速度、或者停止隔离页面,避免OOM。

四、隔离页面isolate_lru_pages_promotion()

筛选并隔离候选页面的核心函数:

page = lru_to_page_head(src); // 获取链表头部页面
prefetchw_prev_lru_page_promotion(page, src, flags); // 提前对下一页调用write prefetch,预取到CPU缓存。
VM_WARN_ON(!PageLRU(page)); 

// 公共辅助函数
if (!__isolate_lru_page_prepare(page, 0)) {
	  list_move(&page->lru, src); // 如果检查失败 ➔ 页面重新放回 src ➔ 继续下一个。
	  continue;
}

// 确保页面引用计数有效,防止被其他线程释放。如果 page->_refcount == 0,说明可能已经被回收,不安全。!=0会对该页面的引用计数 +1
if (unlikely(!get_page_unless_zero(page))) { 
	  list_move(&page->lru, src);
	  continue;
}

// 清除PageLRU标志:标记这个页面现在不在LRU链表中了。
if (!TestClearPageLRU(page)) {
	  put_page(page); // 清除失败➔ 引用计数-1,如果引用计数泄漏 ➔ 页面永远不会被释放 ➔ 内存泄漏。
	  list_move(&page->lru, src);
	  continue;
}

__isolate_lru_page_prepare一般检查的内容包括:页面是否还在LRU链表(PageLRU);页面是否在被写回(dirty/writeback);页面是否处于被锁定等异常状态;是否适合进行隔离(如unevictable page)。

VM_WARN_ON防御性断言:确认page确实有PageLRU标志。理论上,从lru_to_page_head()拿到的页面必然是在LRU链表上的。但由于并发、race condition、或其他bug,某些极端情况下PageLRU可能被其他线程意外清掉(例如并发的回收、迁移、释放等)。

注意容易漏掉的点:为什么put_page()?(代码块里写了)

五、迁移准备promote_page_list()

对上一轮确认的页面做迁移前最后一次检查

  • 必须 trylock_page() ➔ 保证页面迁移过程独占。
  • 添加业务的实际需求比如是否支持大页面迁移、SetPageActive等 ➔ 确保迁移合理性。
  • 部分isolate检查在__isolate_lru_page_prepare函数也做过,是否重复检查要考虑的点是:isolate状态下可能有其他线程修改了页面状态?可能被修改的状态再检查一遍是必须的。
// 关键步骤:
    while (!list_empty(page_list)) {
		struct page *page;

		page = lru_to_page(page_list);
		list_del(&page->lru);
	
		if (!trylock_page(page)){ // 加锁,SetPageActive()、迁移等操作必须持有 page lock
			// __isolate_lru_page_prepare 并没有 lock,只是快速检查,因为不迁移
			goto __keep;
		}
		if (unlikely(!page_evictable(page))){ 
			goto __keep_locked;
		}
		if (PageWriteback(page)){
			goto __keep_locked;
		}
		if (PageTransHuge(page) && !thp_migration_supported()){
			goto __keep_locked;
		}

		SetPageActive(page);
		list_add(&page->lru, &promote_pages);
		unlock_page(page);
		continue;
__keep_locked:
		unlock_page(page);
__keep:
		list_add(&page->lru, &ret_pages);
    }
条件 isolate (__isolate_lru_page_prepare) promote (promote_page_list)
是否在LRU链表 不检查
是否Unevictable
是否Writeback
是否已锁定 不检查 必须 trylock_page()
是否THP 不关心 迁移操作支持

六、执行迁移migrate_page_list()

static unsigned long migrate_page_list(struct list_head *migrate_list,
	pg_data_t *pgdat, bool promotion)
{
    // ……

    if (list_empty(migrate_list) || target_nid == NUMA_NO_NODE)
        return 0;

    migrate_pages(migrate_list, alloc_migrate_page, NULL,
        target_nid, MIGRATE_ASYNC, MR_NUMA_MISPLACED, &nr_succeeded);

    // 有需要的话,可以监控迁移成功次数。需要额外代码完善/proc/vmstat接口。
    count_vm_events(NR_PROMOTED, nr_succeeded);

    return nr_succeeded;
}
参数 含义
migrate_list 待迁移的页面链表
alloc_migrate_page 回调函数,负责在迁移时分配目标Node的新页面
NULL 回调上下文参数(未使用)
target_nid 目标Node ID
MIGRATE_ASYNC 异步迁移,尽量不阻塞
MR_NUMA_MISPLACED migrate reason,表示是NUMA失配迁移
&nr_succeeded 输出成功迁移的页面数

七、复杂的migrate_pages()

  • 分配目标Node新页面。调用 alloc_pages_node() 等接口,在目标Node上申请一个相同order、相同GFP的页面。支持Hugepage、Anonpage、Filepage等多种情况。当目标Node内存不足 ➔ 分配失败 ➔ 页面迁移失败 ➔ fallback.
  • 拷贝旧页面数据到新页面。通常调用 migrate_page_copy()。对于Anon Page ➔ 拷贝Page内容(匿名内存);对于File Page ➔ 特别注意PageCache、Dirty bit等状态同步;Compound page的拷贝又不一样。
  • 更新映射关系(page table remap)。page table remap后,旧页才能真正释放。①修改VMA的页表项(PTE)。危险性极高 必须持有pte lock. ②让虚拟地址开始指向新分配的页面 ➔ MMU刷新TLB. 依赖页表正确, 因为刷新TLB只是“同步通知CPU”(使用flush_tlb_page()),并不会主动修复问题。
  • 释放旧页面、LRU链表更新。虚拟地址只有一个 ➔ VMA/PTE只能指向一个物理页;但物理页可能临时有两个 ➔ 旧页 + 新页。页表切换后旧页面被put_page() ➔ 如果引用计数为0 ➔ 释放;但是如果 refcount > 0 ➔ 是因为其他地方(swap、writeback、page cache等)还暂时需要这个页,但VMA映射已经指向了新页。等其他引用也释放后,旧页才真正释放。

并发的page fault:如果在迁移过程中,另一个CPU线程可能访问同一个虚拟地址 ➔ 触发缺页 ➔ 触发页表更新。如果没有同步好Page fault 线程看到的是旧页 ➔ 但是迁移线程可能已经换成新页了 ➔ 造成UAF或数据丢失。甚至可能两边互相覆盖页表 ➔ 系统崩溃。解决方案:PageLock加锁使得读写序列化;页表更新是原子的 ➔ pte lock保护 ➔ SMP-safe.


文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
 上一篇
CPU架构的差异 CPU架构的差异
组合lscpu等命令输出的信息,判断底层隔离技术等其他和CPU紧密相关的设计是用的哪一种。记录遇到的被困扰过的差异点。
2025-05-11
下一篇 
内存压力 内存压力
lmbench套件以及其他程序用于构造内存压力,便于做一些和内存有关的基准测试
  目录