一、背景:为什么需要页面迁移?
在 NUMA 架构或内存管理需要均衡压力的场景下,页面迁移是核心手段。它能动态将内存页迁移到更合适的节点,以提升性能或回收资源。
二、整体流程概览
围绕4个核心函数展开:
promote_active_list()
➔ 页面筛选isolate_lru_pages_promotion()
➔ 页面隔离promote_page_list()
➔ 迁移条件检查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.