一、背景:为什么需要页面迁移?
在 NUMA 架构或内存管理需要均衡压力的场景下,页面迁移是核心手段。它能动态将内存页迁移到更合适的节点,以提升性能或回收资源。在看下面的代码前,最好了解一下strcut page结构体,更好的理解页面迁移时的一些判断。
二、整体流程概览
围绕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链表更新。
flowchart TD A[释放旧页面、LRU链表更新] --> B{虚拟地址只有一个} B -->|VMA/PTE只能指向一个物理页| C[物理页可能临时有两个] C --> D[旧页 + 新页] D --> E[页表切换后旧页面被put_page] E --> F{引用计数为0?} F -->|是| G[释放旧页] F -->|否 refcount > 0| H[swap/writeback/page cache等暂时需要] H --> I[VMA映射已指向新页] I --> J{其他引用释放?} J -->|是| G
并发的page fault风险:
flowchart TD
subgraph 迁移过程
A[迁移线程] -->|页表更新| B[新页]
end
subgraph 并发访问
C[另一个CPU线程] -->|访问同一虚拟地址| D[触发缺页异常]
D --> E[触发页表更新]
end
E -.->|无同步| F[看到旧页]
B -.->|迁移线程已换新页| F
F --> G[UAF或数据丢失]
E -.->|竞争条件| H[两边互相覆盖页表]
H --> I[系统崩溃]
style G fill:#ff6b6b
style I fill:#ff6b6b
解决方案:
flowchart LR
A[PageLock加锁] -->|读写序列化| B[解决并发访问]
C[pte lock保护] -->|页表更新原子性| D[SMP-safe]
style B fill:#51cf66
style D fill:#51cf66