页面迁移

页面迁移允许在进程运行时,在 NUMA 系统中的节点之间移动页面的物理位置。这意味着进程看到的虚拟地址不会改变。然而,系统会重新排列这些页面的物理位置。

另请参阅异构内存管理 (HMM),了解将页面迁移到设备私有内存或从中迁移。

页面迁移的主要目的是通过将页面移动到正在访问该内存的进程正在运行的处理器附近,来减少内存访问的延迟。

页面迁移允许进程通过 mbind() 设置新的内存策略时,使用 MF_MOVE 和 MF_MOVE_ALL 选项手动重新定位其页面所在的节点。进程的页面也可以使用 sys_migrate_pages() 函数调用从另一个进程重新定位。migrate_pages() 函数调用接收两组节点,并将进程的位于 from 节点上的页面移动到目标节点。页面迁移功能由 Andi Kleen 的 numactl 包提供(需要 0.9.3 以上的版本。从 https://github.com/numactl/numactl.git 获取)。numactl 提供了 libnuma,它为页面迁移提供了类似于其他 NUMA 功能的接口。cat /proc/<pid>/numa_maps 可以方便地查看进程的页面所在位置。另请参阅 proc(5) 手册页中的 numa_maps 文档。

如果例如调度器已将进程重新定位到远程节点上的处理器,则手动迁移很有用。批处理调度器或管理员可能会检测到这种情况,并将进程的页面移动到更靠近新处理器的位置。内核本身仅提供手动页面迁移支持。自动页面迁移可以通过移动页面的用户空间进程来实现。特殊的函数调用“move_pages”允许移动进程内的单个页面。例如,NUMA 分析器可能会获得显示频繁的节点外访问的日志,并可以使用该结果将页面移动到更有利的位置。

较大的安装通常使用 cpuset 将系统分区为节点部分。Paul Jackson 为 cpuset 配备了在任务移动到另一个 cpuset 时移动页面的能力(请参阅 CPUSETS)。Cpusets 允许进程位置的自动化。如果将任务移动到新的 cpuset,则也会将其所有页面一起移动,以便进程的性能不会急剧下降。此外,如果更改了 cpuset 的允许内存节点,则 cpuset 中进程的页面也会被移动。

页面迁移允许在所有迁移技术中,保留一组节点内页面相对位置,这将保留即使在迁移进程后生成的特定内存分配模式。为了保持内存延迟,这是必要的。迁移后,进程将以类似的性能运行。

页面迁移分几个步骤进行。首先是对那些尝试从内核使用 migrate_pages() 的人进行高层描述(对于用户空间的使用,请参阅上面提到的 Andi Kleen 的 numactl 包),然后是关于底层细节如何工作的低层描述。

内核中使用 migrate_pages()

  1. 从 LRU 中删除页面。

    要迁移的页面列表是通过扫描页面并将其移动到列表中生成的。这是通过调用 folio_isolate_lru() 来完成的。调用 folio_isolate_lru() 会增加对页面的引用,以便在页面迁移发生时它不会消失。它还阻止了交换器或其他扫描程序遇到页面。

  2. 我们需要一个类型为 new_folio_t 的函数,该函数可以传递给 migrate_pages()。此函数应找出如何在给定旧页面的情况下分配正确的新页面。

  3. 调用 migrate_pages() 函数,它会尝试执行迁移。它将调用该函数为考虑移动的每个页面分配新的页面。

migrate_pages() 的工作原理

migrate_pages() 会在其页面列表中执行多次传递。如果此时对页面的所有引用都可以删除,则会移动该页面。该页面已通过 folio_isolate_lru() 从 LRU 中删除,并且引用计数已增加,因此在页面迁移发生时,该页面不会被释放。

步骤

  1. 锁定要迁移的页面。

  2. 确保写回已完成。

  3. 锁定我们要移动到的新页面。锁定它是为了使对此(尚未更新)页面的访问在移动进行时立即被阻止。

  4. 页面表对该页面的所有引用都转换为迁移条目。这会减少页面的 mapcount。如果生成的 mapcount 不为零,则我们不迁移该页面。现在,所有尝试访问该页面的用户空间进程都将在页面锁上等待,或等待迁移页表条目被删除。

  5. 获取 i_pages 锁。这将导致所有尝试通过映射访问该页面的进程在自旋锁上阻塞。

  6. 检查页面的引用计数,如果引用仍然存在,则退出。否则,我们知道我们是唯一引用此页面的人。

  7. 检查基数树,如果它不包含指向此页面的指针,则我们退出,因为其他人修改了基数树。

  8. 使用旧页面中的一些设置预处理新页面,以便访问新页面将发现具有正确设置的页面。

  9. 更改基数树以指向新页面。

  10. 删除旧页面的引用计数,因为地址空间引用已消失。建立对新页面的引用,因为地址空间引用了新页面。

  11. 释放 i_pages 锁。这样,就可以再次在映射中进行查找。进程将从在锁上自旋移动到在锁定的新页面上休眠。

  12. 页面内容复制到新页面。

  13. 其余的页面标志复制到新页面。

  14. 清除旧页面标志,以表明该页面不再提供任何信息。

  15. 触发新页面上排队的写回。

  16. 如果迁移条目插入到页表中,则将其替换为真实的 pte。这样做将为尚未在页面锁上等待的用户空间进程启用访问权限。

  17. 从旧页面和新页面中删除页面锁。在页面锁上等待的进程将重做其页面错误,并将到达新页面。

  18. 新页面移动到 LRU,并且可以再次被交换器等扫描。

非 LRU 页面迁移

尽管迁移最初旨在减少 NUMA 的内存访问延迟,但压缩也使用迁移来创建高阶页面。出于压缩目的,移动非 LRU 页面(如 zsmalloc 和 virtio-balloon 页面)也很有用。

如果驱动程序希望使其页面可移动,则应定义一个 struct movable_operations。然后,它需要在每个可能移动的页面上调用 __SetPageMovable()。这使用 page->mapping 字段,因此驱动程序不能将此字段用于其他目的。

监控迁移

以下事件(计数器)可用于监控页面迁移。

  1. PGMIGRATE_SUCCESS:正常页面迁移成功。每个计数表示已迁移了一个页面。如果该页面是非 THP 和非 hugetlb 页面,则此计数器将增加 1。如果该页面是 THP 或 hugetlb,则此计数器将增加 THP 或 hugetlb 子页面的数量。例如,迁移具有 4KB 大小基本页面(子页面)的单个 2MB THP 将导致此计数器增加 512。

  2. PGMIGRATE_FAIL:正常页面迁移失败。与上面的 PGMIGRATE_SUCCESS 相同的计数规则:如果它是 THP 或 hugetlb,则它将增加子页面的数量。

  3. THP_MIGRATION_SUCCESS:已迁移一个 THP,且没有被拆分。

  4. THP_MIGRATION_FAIL:无法迁移 THP,也无法拆分。

  5. THP_MIGRATION_SPLIT:已迁移一个 THP,但不是以这种方式:首先,必须拆分 THP。拆分后,其子页面使用了迁移重试。

THP_MIGRATION_* 事件还会更新相应的 PGMIGRATE_SUCCESS 或 PGMIGRATE_FAIL 事件。例如,THP 迁移失败将导致 THP_MIGRATION_FAIL 和 PGMIGRATE_FAIL 都增加。

Christoph Lameter,2006 年 5 月 8 日。Minchan Kim,2016 年 3 月 28 日。

struct movable_operations

驱动程序页面迁移

定义:

struct movable_operations {
    bool (*isolate_page)(struct page *, isolate_mode_t);
    int (*migrate_page)(struct page *dst, struct page *src, enum migrate_mode);
    void (*putback_page)(struct page *);
};

成员

isolate_page

VM 调用此函数来准备要移动的页面。该页面已锁定,驱动程序不应解锁它。如果该页面可移动,则驱动程序应返回 true,如果当前不可移动,则应返回 false。此函数返回后,VM 将使用 page->lru 字段,因此驱动程序必须保留通常存储在此处的任何信息。

migrate_page

隔离后,虚拟机使用隔离的src页面调用此函数。驱动程序应将src页面的内容复制到dst页面,并设置dst页面的字段。两个页面都被锁定。如果页面迁移成功,驱动程序应调用__ClearPageMovable(src)并返回MIGRATEPAGE_SUCCESS。如果驱动程序当前无法迁移页面,它可以返回-EAGAIN。虚拟机将其解释为临时的迁移失败,稍后会重试。任何其他错误值都是永久的迁移失败,并且不会重试迁移。驱动程序在migrate_page()函数中不应触及src->lru字段。它可以写入dst->lru

putback_page

如果隔离页面上的迁移失败,虚拟机将通过调用此函数通知驱动程序该页面不再是迁移的候选页面。驱动程序应将隔离的页面放回其自己的数据结构中。