页面迁移¶
页面迁移允许在进程运行时,在 NUMA 系统中的节点之间移动页面的物理位置。 这意味着进程看到的虚拟地址不会改变。 但是,系统会重新排列这些页面的物理位置。
另请参阅 异构内存管理 (HMM) 以将页面迁移到或从设备专用内存迁移。
页面迁移的主要目的是通过将页面移动到访问该内存的进程正在运行的处理器附近,来减少内存访问的延迟。
通过在通过 mbind() 设置新的内存策略时使用 MF_MOVE 和 MF_MOVE_ALL 选项,页面迁移允许进程手动重新定位其页面所在的节点。 进程的页面也可以使用 sys_migrate_pages() 函数调用从另一个进程重新定位。 migrate_pages() 函数调用接受两组节点,并将进程的页面从源节点移动到目标节点。 页面迁移函数由 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 分析器可以获取显示频繁的异地节点访问的日志,并且可以使用结果将页面移动到更有利的位置。
较大的安装通常使用 cpusets 将系统划分为节点 sections。 Paul Jackson 为 cpusets 配备了在任务移动到另一个 cpuset 时移动页面的能力(参见 CPUSETS)。 Cpusets 允许进程本地化的自动化。 如果任务移动到新的 cpuset,那么它的所有页面也会随之移动,以便进程的性能不会急剧下降。 此外,如果更改了 cpuset 的允许内存节点,则 cpuset 中进程的页面也会被移动。
页面迁移允许为所有迁移技术保留节点组中页面的相对位置,即使在迁移进程之后,这些技术也将保留特定的内存分配模式。 这对于保持内存延迟是必要的。 迁移后,进程将以相似的性能运行。
页面迁移分几个步骤进行。 首先,是为那些试图从内核使用 migrate_pages() 的人提供的高级描述(对于用户空间用法,请参阅上面提到的 Andi Kleen 的 numactl 软件包),然后是关于底层细节如何工作的低级描述。
在内核中使用 migrate_pages()¶
从 LRU 中删除页框。
要迁移的页框列表是通过扫描页框并将它们移动到列表中生成的。 这是通过调用
folio_isolate_lru()
完成的。 调用folio_isolate_lru()
会增加对页框的引用,以便在页框迁移发生时它不会消失。 它还可以防止交换器或其他扫描遇到页框。我们需要一个 new_folio_t 类型的函数,它可以传递给 migrate_pages()。 此函数应确定如何为旧页框分配正确的新页框。
调用 migrate_pages() 函数,它尝试执行迁移。 它将调用该函数来为每个被考虑移动的页框分配新的页框。
migrate_pages() 的工作原理¶
migrate_pages() 在其页框列表上执行多次传递。 如果在某个时间点可以删除对页框的所有引用,则会移动页框。 页框已经通过 folio_isolate_lru()
从 LRU 中删除,并且引用计数已增加,因此在页框迁移发生时无法释放页框。
步骤
锁定要迁移的页面。
确保回写已完成。
锁定我们要移动到的新页面。 锁定它的目的是为了在移动过程中,立即阻止对这个(尚未更新)页面的访问。
对页面的所有页表引用都将转换为迁移条目。 这会减少页面的 mapcount。 如果生成的 mapcount 不为零,那么我们不迁移该页面。 所有尝试访问该页面的用户空间进程现在将等待页面锁,或者等待迁移页表条目被删除。
获取 i_pages 锁。 这将导致所有尝试通过映射访问页面的进程在自旋锁上阻塞。
检查页面的引用计数,如果仍然存在引用,则返回。 否则,我们知道我们是唯一引用此页面的人。
检查 radix 树,如果它不包含指向此页面的指针,那么我们返回,因为其他人修改了 radix 树。
使用旧页面中的一些设置准备新页面,以便对新页面的访问将发现一个具有正确设置的页面。
更改 radix 树以指向新页面。
旧页面的引用计数被删除,因为地址空间引用已消失。 建立对新页面的引用,因为地址空间引用了新页面。
释放 i_pages 锁。 这样,就可以再次在映射中进行查找。 进程将从在锁上自旋转移到在新锁定的页面上休眠。
页面内容被复制到新页面。
剩余的页面标志被复制到新页面。
旧页面标志被清除,以表明该页面不再提供任何信息。
触发新页面上的排队回写。
如果迁移条目已插入到页表中,则用实际的 pte 替换它们。 这样做将允许用户空间进程访问,而不是已经等待页面锁的进程。
从旧页面和新页面中删除页面锁。 等待页面锁的进程将重做它们的页面错误,并将到达新页面。
新页面被移动到 LRU,并且可以再次被交换器等扫描。
非 LRU 页面迁移¶
虽然迁移最初旨在减少 NUMA 的内存访问延迟,但压缩也使用迁移来创建高阶页面。 出于压缩的目的,能够移动非 LRU 页面(例如 zsmalloc 和 virtio-balloon 页面)也很有用。
如果驱动程序想要使其页面可移动,它应该定义一个 struct movable_operations
。 然后,它需要在它可能能够移动的每个页面上调用 __SetPageMovable()。 这使用 page->mapping
字段,因此该字段不适用于驱动程序将其用于其他目的。
监视迁移¶
以下事件(计数器)可用于监视页面迁移。
PGMIGRATE_SUCCESS:正常的页面迁移成功。 每个计数意味着一个页面被迁移。 如果该页面是非 THP 和非 hugetlb 页面,则此计数器增加一。 如果该页面是 THP 或 hugetlb,则此计数器增加 THP 或 hugetlb 子页面的数量。 例如,迁移具有 4KB 大小基本页面(子页面)的单个 2MB THP 将导致此计数器增加 512。
PGMIGRATE_FAIL:正常的页面迁移失败。 与上面的 PGMIGRATE_SUCCESS 相同的计数规则:如果它是 THP 或 hugetlb,这将增加子页面的数量。
THP_MIGRATION_SUCCESS:THP 已被迁移,而没有被拆分。
THP_MIGRATION_FAIL:THP 无法迁移,也无法拆分。
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
隔离后,VM 使用隔离的 src 页面调用此函数。 驱动程序应将 src 页面的内容复制到 dst 页面,并设置 dst 页面的字段。 这两个页面都被锁定。 如果页面迁移成功,驱动程序应调用 __ClearPageMovable(src) 并返回 MIGRATEPAGE_SUCCESS。 如果驱动程序此时无法迁移页面,它可以返回 -EAGAIN。 VM 将此解释为临时迁移失败,并将在以后重试。 任何其他错误值都是永久迁移失败,并且不会重试迁移。 在 migrate_page() 函数中,驱动程序不应触摸 src->lru 字段。 它可以写入 dst->lru。
putback_page
如果隔离页面上的迁移失败,VM 通过调用此函数通知驱动程序该页面不再是迁移的候选页面。 驱动程序应将隔离的页面放回其自己的数据结构中。