kernel: 5.10
Arch: aarch64
页面迁移
页面迁移可以指定一个进程的页面至其指定的内存节点上。它的设计初衷是为了:通过将页面移动到该进程所处的NUMA节点上来减少内存访问的延迟。后来内存规整和内存热插拔等场景都使用了此功能。
migrate_pages()
是页面迁移的主要接口
int migrate_pages(struct list_head *from, new_page_t get_new_page,
free_page_t put_new_page, unsigned long private,
enum migrate_mode mode, int reason)
from: 迁移页面的链表。
get_new_page:申请新内存的页面的函数指针
put_new_page: 迁移失败时释放目标页面的函数指针
private: 传递给get_new_page的参数。
mode: 迁移模式。
MIGRATE_ASYNC | 异步迁移,过程中不会发生阻塞, |
MIGRATE_SYNC_LIGHT | 轻度同步迁移,允许大部分的阻塞操作,唯独不允许脏页的回写操作 |
MIGRATE_SYNC | 同步迁移,迁移过程会发生阻塞,若需要迁移的某个page正在writeback或被locked会等待它完成 |
MIGRATE_SYNC_NO_COPY | 同步迁移,但不等待页面的拷贝过程。页面的拷贝通过回调migratepage(),过程可能会涉及DMA |
reason: 迁移因素
MR_COMPACTION | 内存规整导致的迁移 |
MR_MEMORY_FAILURE | 当内存出现硬件问题(ECC校验失败等)时触发的页面迁移(memory–failure.c) |
MR_MEMORY_HOTPLUG | 内存热插拔导致的迁移 |
MR_SYSCALL | 应用层主动调用migrate_pages()或move_pages()触发的迁移。 |
MR_MEMPOLICY_MBIND | 调用mbind系统调用设置memory policy时触发的迁移 |
MR_NUMA_MISPLACED | numa balance触发的页面迁移 |
MR_CONTIG_RANGE | 调用alloc_contig_range()为CMA或HugeTLB分配连续内存时触发的迁移(和compact相关)。 |
从migrate reasons可以看出, 内存规整、NUMA balance、内存热插拔、CMA、HugeTLB等是应用页面迁移的主要场景。
内核实现
migrate_pages()
函数的主要流程如下:
LRU页面的迁移:
- 分配new page
- 获取old page的页面锁
PG_locked
- 若是正在writeback的页面,则根据迁移模式判断是否等待页面wirteback(
MIGRATE_SYNC_LIGHT
和MIGRATE_ASYNC
不等待) - 获取new page的页面锁
PG_locked
- 调用
try_to_unmap_page
解除old page的页表映射 - 调用
move_to_new_page
拷贝old page的内容和struct page元数据到new page - 调用
remove_migration_ptes
迁移页表,通过反向映射机制RMAP来建立new page的映射关系。
可以发现, 页面迁移不是简单的把一个page从A位置移动到B位置, 它的本质是一个分配新页面, 将旧页面的内容拷贝至新页面, 解除旧页面的映射关系,并将映射关系映射到新页面,最后释放旧页面的过程。
为什么页面迁移的过程中都需要获取old page和 new page的锁呢?
试想一下, 我们将old page A的数据拷贝到B, 取消了A的映射, 如果此时B的映射还没有建立, 这个时候产生地址访问会发生什么?
理论上虚拟地址没有对应物理地址的映射,应该产生一个page fault, 在page fault 处理流程中, 也会尝试获取PG_locked, 如果获取不到,就会重新retry 缺页的流程。
/*
* lock_page_or_retry - Lock the page, unless this would block and the
* caller indicated that it can handle a retry.
*
* Return value and mmap_lock implications depend on flags; see
* __lock_page_or_retry().
*/
static inline int lock_page_or_retry(struct page *page, struct mm_struct *mm,
unsigned int flags)
{
might_sleep();
return trylock_page(page) || __lock_page_or_retry(page, mm, flags);
}
所以migrate_pages流程通过try_lockpage, 能够让page的移动看上去是无缝连接的过程。
非LRU页面的迁移:
bda807d44(“mm: migrate: support non-lru movable page migration”), 该补丁使得驱动中用到的页面也可以支持迁移。
从该补丁可知, 如果一个驱动想要支持页面迁移, 那么它必须实现struct address_space_operations
数据结构中的三个方法:
struct address_space_operations {
..
int (*migratepage) (struct address_space *,
struct page *, struct page *, enum migrate_mode);
bool (*isolate_page)(struct page *, isolate_mode_t);
void (*putback_page)(struct page *);
..
}
-
bool (*isolate_page)();
在页面迁移的某些场景中,如memory hotplug
,memory compaction
会调用isolate_memory_page()
函数来分离页面, 当页面分离之后,这些页面会被标记成PG_isolated, 这样其他CPU在并发分离页面时会忽略这个页面。 -
int (*migratepage)();
页面成功完成分离后, 内存管理的页面迁移就会调用migratepage
来迁移页面, 如上述流程图中所示。migratepage的作用是将旧页面的内容移动到新页面,并设置new page的字段。 在完成页面迁移的动作后,驱动还需要`__ClearPageMovable(page), 来指old page不再可移动。 -
void (*putback_page)( );
在页面迁移失败时, 驱动需要把分离的页面返回到自己的数据结构中。
此外驱动还需要支持两个标志位。
- PG_movable
通过__SetPageMovable()
函数可以设置改标志位, 该函数会位page->mapping的低bit设置PAGE_MAPPING_MOVABLE, 可以用于判断是否为movable的non-lru页面。 - PG_isolated
该标志位是page数据结构的新增标志位,主要是为了防止多个CPU同时分离同1个页面。如果驱动发现某一个页面是PG_isolated, 说明页面迁移机制已经分离了该页面, 驱动程序就不能在使用这个页面的page中的lru成员。
Numa balance中的页面迁移
现在我们结合Numa balance进行分析, 看看页面迁移的实际应用。
内核会周期性的扫描task的地址空间:
task_tick_fair //周期性更新
task_tick_numa
init_task_work(work, task_numa_work);
task_work_add(curr, work, true);
numa balance会在周期性的tick更新中穿插一个task, 该task会在task_work_run()
执行时被调用。
task_numa_work()
的核心函数是change_prot_numa()
。
unsigned long change_prot_numa(struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
int nr_updated;
nr_updated = change_protection(vma, addr, end, PAGE_NONE, MM_CP_PROT_NUMA);
if (nr_updated)
count_vm_numa_events(NUMA_PTE_UPDATES, nr_updated);
return nr_updated;
}
change_protection()
会将所有映射到VMA的PTE页表项该为PAGE_NONE,使得下次进程访问页表的时候产生缺页中断,
handle_pte_fault()
函数会由于缺页中断的机会调用到do_numa_page()
,
tatic int handle_pte_fault(struct vm_fault *vmf)
{
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf);
do_numa_page()
选择更好的node并进行迁移
static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
//修改页表为原始权限,在下次扫描到该页时访问不再发生异常
pte = pte_modify(old_pte, vma->vm_page_prot);
last_cpupid = page_cpupid_last(page);
page_nid = page_to_nid(page);
// 需要迁移目的node id。如果等于-1,则表示不用迁移
target_nid = numa_migrate_prep(page, vma, vmf->address, page_nid,
&flags);
pte_unmap_unlock(vmf->pte, vmf->ptl);
if (target_nid == NUMA_NO_NODE) {
put_page(page);
goto out;
}
/* Migrate to the requested node */
migrated = migrate_misplaced_page(page, vma, target_nid);
}
numa_migrate_prep()
会根据mbind设置的策略决定目标node,在可迁移的情况下还会根据numa balance当前缺页统计来决定是否迁移。
主要通过group_faults_cpu()
来对比本节点和target节点的page faults数量;migrate_misplaced_page()
会调用migate_pages
进行实际的页面迁移工作
我们可以通过 cat /proc/vmstat | grep numa
来观察一些页面迁移的指标
numa_hint_faults //page fault数
numa_hint_faults_local //page fault发生在本地的数
numa_pages_migrated //迁移页的数
如何避免页面迁移?
假设进程申请了某个page A, 然后将该地址通过参数的方式传递给设备使用, 如果在设备使用前, page A被迁移到page B, 那么设备继续使用之前的地址就会出问题;
这种情况在使用用户态DMA的时候比较常见;
那么该如何避免页面迁移呢?
我们回到之前的介绍, 调用move_to_new_page
时会拷贝old page的内容和struct page元数据到new page
move_to_new_page() -> migrate_page() -> migrate_page_move_mapping()
int migrate_page_move_mapping(struct address_space *mapping,
struct page *newpage, struct page *page, int extra_count)
{
XA_STATE(xas, &mapping->i_pages, page_index(page));
struct zone *oldzone, *newzone;
int dirty;
int expected_count = expected_page_refs(mapping, page) + extra_count;
int nr = thp_nr_pages(page);
if (!mapping) {
/* Anonymous page without mapping */
if (page_count(page) != expected_count) ------- (1)
return -EAGAIN;
/* No turning back from here */
newpage->index = page->index;
newpage->mapping = page->mapping;
if (PageSwapBacked(page))
__SetPageSwapBacked(newpage);
return MIGRATEPAGE_SUCCESS;
}
oldzone = page_zone(page);
newzone = page_zone(newpage);
xas_lock_irq(&xas);
if (page_count(page) != expected_count || xas_load(&xas) != page) { ------ (2)
xas_unlock_irq(&xas);
return -EAGAIN;
}
if (!page_ref_freeze(page, expected_count)) {
xas_unlock_irq(&xas);
return -EAGAIN;
}
从(1)和 (2) 可以看出, 在做页面迁移时, 会先check page的refcnt是否符合预期;
所以我们可以通过类似pin_user_pages()
这样的实现, “不合理”的增大page的引用计数, 来实现避免页面迁移的操作。
pin_user_pages()-> __gup_longterm_locked() -> __get_user_pages() -> try_grab_page()
GUP_PIN_COUNTING_BIAS的值是1024;
所以在做用户态DMA时, 可以通过GUP来pin住memory, 防止DMA的地址被swap、迁移或释放。
参考资料
- https://www.kernel.org/doc/html/latest/vm/page_migration.html
- Linux内存管理:页面迁移
- Documentation/vm/page_migration
- 论Linux的页迁移(Page Migration)完整版