透明大页支持

本文档描述了透明大页 (THP) 支持的设计原则及其与内存管理系统其他部分的交互。

设计原则

  • “优雅回退”:不了解透明大页的 mm 组件会回退到将巨大的 pmd 映射分解为 pte 表,并在必要时拆分透明大页。因此,这些组件可以继续在常规页面或常规 pte 映射上工作。

  • 如果由于内存碎片导致大页分配失败,应优雅地分配常规页面,并在同一 vma 中混合使用,而不会出现任何故障或显著延迟,也不会引起用户空间的注意

  • 如果某些任务退出并且有更多大页可用(无论是立即在 buddy 中还是通过 VM),则由常规页面支持的客户机物理内存应自动(使用 khugepaged)重新定位到大页上

  • 它不需要内存预留,而是尽可能使用大页(此处唯一可能的预留是 kernelcore=,以避免不可移动的页面使所有内存碎片化,但这种调整并非透明大页支持特有,它是一个适用于内核中所有动态高阶分配的通用功能)

get_user_pages 和 pin_user_pages

如果 get_user_pages 和 pin_user_pages 在大页上运行,它们将像往常一样返回头页或尾页(就像在 hugetlbfs 上一样)。大多数 GUP 用户只关心页面的实际物理地址及其临时固定,以便在 I/O 完成后释放,因此他们永远不会注意到页面是巨大的。但是,如果任何驱动程序要处理尾页的页面结构(例如,检查 page->mapping 或其他与头页相关而不与尾页相关的位),则应更新为跳转以检查头页。对任何头/尾页进行引用将阻止任何人拆分该页面。

注意

这些不是对 GUP API 的新约束,它们与适用于 hugetlbfs 的约束相匹配,因此任何能够在 hugetlbfs 上处理 GUP 的驱动程序也可以在透明大页支持的映射上正常工作。

优雅回退

在页表中遍历代码,但不了解巨大的 pmd,可以简单地调用 split_huge_pmd(vma, pmd, addr),其中 pmd 是由 pmd_offset 返回的。通过 grep “pmd_offset” 并在 pmd_offset 返回 pmd 后在缺少的地方添加 split_huge_pmd,可以很容易地使代码感知透明大页。由于优雅的回退设计,只需一行代码的更改,您就可以避免编写成百上千行复杂的代码来使您的代码感知大页。

如果您不是在页表中遍历,而是遇到了无法在代码中本地处理的物理大页,则可以通过调用 split_huge_page(page) 来拆分它。例如,这就是 Linux VM 在尝试换出大页之前所做的。如果页面被固定,split_huge_page() 可能会失败,您必须正确处理这种情况。

使用一行代码更改使 mremap.c 感知透明大页的示例

diff --git a/mm/mremap.c b/mm/mremap.c
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -41,6 +41,7 @@ static pmd_t *get_old_pmd(struct mm_stru
                return NULL;

        pmd = pmd_offset(pud, addr);
+       split_huge_pmd(vma, pmd, addr);
        if (pmd_none_or_clear_bad(pmd))
                return NULL;

大页感知代码中的锁定

我们希望尽可能多的代码感知大页,因为调用 split_huge_page() 或 split_huge_pmd() 会有成本。

要使页表遍历感知巨大的 pmd,您只需在 pmd_offset 返回的 pmd 上调用 pmd_trans_huge()。您必须以读取(或写入)模式持有 mmap_lock,以确保 khugepaged 不会从您下方创建巨大的 pmd(khugepaged collapse_huge_page 除了 anon_vma 锁外,还以写入模式获取 mmap_lock)。如果 pmd_trans_huge 返回 false,您只需回退到旧的代码路径。如果 pmd_trans_huge 返回 true,您必须获取页表锁 (pmd_lock()) 并重新运行 pmd_trans_huge。获取页表锁将阻止巨大的 pmd 从您下方转换为常规 pmd(split_huge_pmd 可以与页表遍历并行运行)。如果第二个 pmd_trans_huge 返回 false,您应该只需释放页表锁并像以前一样回退到旧代码。否则,您可以继续本地处理巨大的 pmd 和大页。完成后,您可以释放页表锁。

引用计数和透明大页

THP 上的引用计数与在其他复合页面上的引用计数基本一致

  • get_page()/put_page() 和 GUP 在 folio->_refcount 上操作。

  • 尾页中的 ->_refcount 始终为零:get_page_unless_zero() 永远不会在尾页上成功。

  • 整个 THP 的 PMD 条目的映射/取消映射会增加/减少 folio->_entire_mapcount,增加/减少 folio->_large_mapcount,并且当 _entire_mapcount 从 -1 变为 0 或从 0 变为 -1 时,还会增加/减少 folio->_nr_pages_mapped ENTIRELY_MAPPED。

  • 具有 PTE 条目的各个页面的映射/取消映射会增加/减少 page->_mapcount,增加/减少 folio->_large_mapcount,并且当 page->_mapcount 从 -1 变为 0 或从 0 变为 -1 时,还会增加/减少 folio->_nr_pages_mapped,因为这会计算由 PTE 映射的页数。

split_huge_page 内部必须在从页面结构中清除所有 PG_head/tail 位之前,将头页中的引用计数分配给尾页。对于页表条目获取的引用计数,可以轻松完成此操作,但我们没有足够的信息来分配任何其他固定(即来自 get_user_pages)。 split_huge_page() 会使拆分固定的巨大页面的任何请求失败:它期望页面计数等于所有子页的 mapcount 之和加一(split_huge_page 调用者必须对头页进行引用)。

split_huge_page 使用迁移条目来稳定匿名页面的 page->_refcount 和 page->_mapcount。文件页面只是被取消映射。

我们对物理内存扫描器也是安全的:扫描器获取页面引用的唯一合法方法是 get_page_unless_zero()。

所有尾页的 ->_refcount 均为零,直到 atomic_add()。这会阻止扫描器在该点之前获取尾页的引用。在 atomic_add() 之后,我们不关心 ->_refcount 值。我们已经知道应该从头页取消多少引用。

对于头页,get_page_unless_zero() 将成功,我们不介意。很明显,拆分后引用应该放在哪里:它将保留在头页上。

请注意,split_huge_pmd() 对引用计数没有任何限制:pmd 可以在任何时候拆分,并且永远不会失败。

部分取消映射和 deferred_split_folio()

取消映射 THP 的一部分(使用 munmap() 或其他方式)不会立即释放内存。相反,我们会在 folio_remove_rmap_*() 中检测到 THP 的子页面未使用,并在内存压力出现时将 THP 排队进行拆分。拆分将释放未使用的子页面。

立即拆分页面不是一个选项,因为我们可以在检测到部分取消映射的位置进行锁定上下文。这可能适得其反,因为在许多情况下,如果 THP 跨越 VMA 边界,则会在 exit(2) 期间发生部分取消映射。

函数 deferred_split_folio() 用于将 folio 排队进行拆分。当我们通过收缩器接口获得内存压力时,拆分本身将会发生。