欢迎光临
我们一直在努力

4.2 hmm_range_fault() 逐行解析(上):遍历与 PFN 提取

4.1 篇我们从架构层面看了 HMM:驱动用 hmm_range 描述一段用户虚拟地址,调用 hmm_range_fault(),得到 hmm_pfns[],再把结果写进设备页表。本篇进入源码细节,重点分析 hmm_range_fault() 如何复用 walk_page_range() 遍历 CPU 页表,以及如何把正常 present PTE/PMD/PUD 转换成 HMM PFN 编码。

为了控制篇幅,本篇只讲“上半场”:遍历框架与 PFN 提取。也就是这些问题:

  • hmm_range_fault() 入口到底做了什么;
  • 为什么它必须在 mmap_lock 下运行;
  • walk_page_range() 如何把 VMA、PUD、PMD、PTE 分发给 HMM 回调;
  • 正常 present PTE 如何变成 HMM_PFN_VALID/HMM_PFN_WRITE | pfn;
  • THP、PUD leaf、HugeTLB 这类大页映射如何写入 map_order;
  • 为什么 HMM 输出的是 PFN 快照,而不是 pin 住的 struct page *。

下篇会接着讲“下半场”:swap、migration entry、device private entry、device exclusive entry、FAULT_FLAG_REMOTE 缺页、-EBUSY retry 和错误返回语义。


1 入口与遍历框架

本节从入口开始,依次解析调用栈、入口函数语义、-EBUSY 循环重试、回调表注册以及 VMA 过滤逻辑。

在这里插入图片描述

1.1 调用栈概览

HMM 的典型调用方式可以在 [lib/test_hmm.c] 里看到。dmirror_fault() 构造一个 hmm_range:

struct hmm_range range = {
.notifier = &dmirror->notifier,
.hmm_pfns = pfns,
.pfn_flags_mask = 0,
.default_flags =
HMM_PFN_REQ_FAULT | (write ? HMM_PFN_REQ_WRITE : 0),
.dev_private_owner = dmirror->mdevice,
};

然后按固定协议调用:

range->notifier_seq = mmu_interval_read_begin(range->notifier);
mmap_read_lock(mm);
ret = hmm_range_fault(range);
mmap_read_unlock(mm);

如果 hmm_range_fault() 成功,驱动还不能立刻相信结果。它要先拿自己的设备页表锁,再调用:

mmu_interval_read_retry(range->notifier, range->notifier_seq)

只有 retry 检查通过,hmm_pfns[] 才能被写进设备页表。

所以 hmm_range_fault() 只是整个协议中的中间步骤:它负责在 CPU 页表上做一次受保护的快照/缺页尝试;最终的“这个快照还能不能用”,由 mmu_interval_notifier 的序列号协议确认。

1.2 入口函数:短小但语义丰富

入口在 [mm/hmm.c]:

int hmm_range_fault(struct hmm_range *range)
{
struct hmm_vma_walk hmm_vma_walk = {
.range = range,
.last = range->start,
};
struct mm_struct *mm = range->notifier->mm;
int ret;

mmap_assert_locked(mm);

do {
if (mmu_interval_check_retry(range->notifier,
range->notifier_seq))
return EBUSY;
ret = walk_page_range(mm, hmm_vma_walk.last, range->end,
&hmm_walk_ops, &hmm_vma_walk);
} while (ret == EBUSY);
return ret;
}

这段代码可以拆成五个动作。

第一,构造内部 walker 状态:

struct hmm_vma_walk hmm_vma_walk = {
.range = range,
.last = range->start,
};

hmm_range 是公开 API 参数,hmm_vma_walk 是 HMM 私有遍历状态。当前它只有两个字段:

struct hmm_vma_walk {
struct hmm_range *range;
unsigned long last;
};

range 指回用户传入的请求;last 表示下一次 walk_page_range() 应该从哪里继续。这个字段在 fault、等待 migration entry、retry 时很关键:已经填好的 hmm_pfns[] 不必重填,尚未处理的地址从 last 继续。

第二,从 notifier 找到目标 mm:

struct mm_struct *mm = range->notifier->mm;

HMM 不让调用者额外传 mm,而是通过 mmu_interval_notifier 绑定的 mm 获得进程地址空间。这也强调了一点:hmm_range_fault() 不是裸页表 walker,它是 notifier 协议的一部分。

第三,断言调用者已经拿了 mmap_lock:

mmap_assert_locked(mm);

walk_page_range() 要在 VMA 和页表层级上做遍历。VMA 生命周期、权限、maple tree 查找,都需要 mmap_lock 保护。HMM 这里不帮调用者加锁,是因为外层还要和 mmu_interval_read_begin/read_retry、驱动锁组合成一个更大的协议。

第四,每次进入页表遍历前先检查 invalidation:

if (mmu_interval_check_retry(range->notifier, range->notifier_seq))
return EBUSY;

如果在开始遍历前,CPU 页表已经发生过会影响这个 interval 的变化,HMM 直接返回 -EBUSY。调用者要重新 mmu_interval_read_begin(),重新开始。

第五,调用通用页表遍历框架:

ret = walk_page_range(mm, hmm_vma_walk.last, range->end,
&hmm_walk_ops, &hmm_vma_walk);

这就是 HMM 的设计风格:它没有自己写一套 PGD/P4D/PUD/PMD/PTE 遍历,而是复用 2.1 篇讲过的 walk_page_range()。HMM 只提供回调表,把“遇到 PTE/PMD/hole/VMA 时该怎么翻译成 hmm_pfns[]”这部分接进去。

1.3 为什么 -EBUSY 会在函数内部循环

入口函数有一个容易忽略的小循环:

do {
...
ret = walk_page_range(...);
} while (ret == EBUSY);

这里的 -EBUSY 有两层含义。

第一层是外部 invalidation 已经发生。这个由 mmu_interval_check_retry() 判断,入口会直接返回 -EBUSY 给调用者。因为这意味着本次 notifier_seq 已经过期,必须从外层协议重新开始。

第二层是遍历过程中遇到某些需要等待或 fault 的状态。例如 PTE 是 migration entry,HMM 可能等待迁移完成;或者某段需要 fault,HMM 调用 handle_mm_fault() 后要求重新 walk。此时内部回调会设置 hmm_vma_walk.last,返回 -EBUSY。入口函数随后用新的 last 再次调用 walk_page_range()。

源码注释把这个契约说得很清楚:

/*
* When -EBUSY is returned the loop restarts with
* hmm_vma_walk.last set to an address that has not been stored
* in pfns. All entries < last in the pfn array are set to their
* output, and all >= are still at their input values.
*/

这就是 last 存在的意义:它把输出数组划成两段。

range->start last range->end
|————————————-|—————————-|
已经转换成输出 PFN/flags 仍保留调用前输入请求 flags

这一点对驱动很重要。hmm_pfns[] 同时承载输入请求和输出结果,HMM 不能在失败重试时把还没处理的项误改掉。

1.4 回调表:HMM 如何插入 pagewalk

HMM 注册给 walk_page_range() 的回调表如下:

static const struct mm_walk_ops hmm_walk_ops = {
.pud_entry= hmm_vma_walk_pud,
.pmd_entry= hmm_vma_walk_pmd,
.pte_hole= hmm_vma_walk_hole,
.hugetlb_entry= hmm_vma_walk_hugetlb_entry,
.test_walk= hmm_vma_walk_test,
.walk_lock= PGWALK_RDLOCK,
};

这张表可以分成五类。

test_walk 先检查 VMA 是否适合 HMM。HMM 只处理有普通 struct page 语义、可读的用户映射。VM_IO、VM_PFNMAP 这类直接映射设备 MMIO 或没有普通 page backing 的 VMA,不适合交给设备当作普通内存镜像。

pte_hole 处理页表空洞。比如没有 VMA、没有页表页、PTE none。它会根据调用者是否请求 fault,决定填 0、填 HMM_PFN_ERROR,还是触发远程缺页。

pmd_entry 是本篇重点。普通 4K PTE 表、THP PMD、PMD migration entry、bad PMD,都会从这里分流。

pud_entry 只在架构支持 PUD 级透明大页时启用。它的语义和 PMD 大页类似:如果是 leaf,就一次填充一大段 PFN;如果不是 leaf,就让 pagewalk 继续下探。

hugetlb_entry 处理 HugeTLB。HugeTLB 有自己的锁和页表形式,所以单独一个回调。

walk_lock = PGWALK_RDLOCK 表示 pagewalk 以读锁语义遍历 VMA。这和 HMM 的目标一致:尽量读取页表快照,而不是主动改变页表。只有调用者明确要求 fault 时,HMM 才会进入 handle_mm_fault()。

1.5 VMA 过滤:hmm_vma_walk_test()

每个 VMA 被真正遍历前,walk_page_range() 会调用 test_walk:

static int hmm_vma_walk_test(unsigned long start, unsigned long end,
struct mm_walk *walk)
{
struct hmm_vma_walk *hmm_vma_walk = walk->private;
struct hmm_range *range = hmm_vma_walk->range;
struct vm_area_struct *vma = walk->vma;

if (!(vma->vm_flags & (VM_IO | VM_PFNMAP)) &&
vma->vm_flags & VM_READ)
return 0;
...
hmm_pfns_fill(start, end, range, HMM_PFN_ERROR);
return 1;
}

第一段判断是白名单:

!(vma->vm_flags & (VM_IO | VM_PFNMAP)) &&
vma->vm_flags & VM_READ

也就是说,VMA 不能是直接 I/O/PFNMAP 映射,而且至少要可读。HMM 后面会把 PTE 转成 PFN,再让设备访问这些 PFN。如果 VMA 本身没有普通 page 语义,或者用户态都不能读,那就不能当作可镜像内存。

如果 VMA 不合适,HMM 会先看调用者有没有要求 fault:

if (hmm_range_need_fault(..., 0))
return EFAULT;

如果调用者只是做快照,没有强制要求有效页,那么 HMM 把这段输出成 HMM_PFN_ERROR,并返回 1 告诉 pagewalk 跳过这个 VMA,继续处理下一个 VMA。

这里体现了 HMM API 的两个模式:

  • 快照模式:不能镜像的地方标成 error,调用者自己决定怎么处理;
  • fault 模式:调用者要求必须可访问,不能满足就失败。

2. PMD 回调与分流

hmm_vma_walk_pmd() 是普通页表路径的中心。它根据 PMD 类型分流:none、migration entry、not present、trans huge、bad、以及普通 PMD 指向 PTE 表。

在这里插入图片描述

hmm_vma_walk_pmd() 的函数签名:

static int hmm_vma_walk_pmd(pmd_t *pmdp,
unsigned long start,
unsigned long end,
struct mm_walk *walk)

它先计算本段对应的 hmm_pfns[] 起点和页数:

unsigned long *hmm_pfns =
&range->hmm_pfns[(start range->start) >> PAGE_SHIFT];
unsigned long npages = (end start) >> PAGE_SHIFT;
unsigned long addr = start;

这个索引公式贯穿 HMM:

i = (addr – range->start) >> PAGE_SHIFT

也就是说,hmm_pfns[0] 对应 range->start,hmm_pfns[1] 对应下一页,以此类推。HMM 不保存额外映射表,虚拟地址和数组下标直接按页大小换算。

然后它 lockless 读取 PMD:

again:
pmd = pmdp_get_lockless(pmdp);

这里先不拿 PMD lock。原因是 HMM 读取的是“在 notifier 协议下可重试的快照”。如果并发线程正在拆 THP 或修改页表,HMM 依赖 page table rules 和 MMU notifier 来发现变化,而不是把所有路径都锁成串行。

接下来按 PMD 类型分流。

2.1 PMD none:交给 hole 路径

if (pmd_none(pmd))
return hmm_vma_walk_hole(start, end, 1, walk);

PMD none 表示这一段没有下级页表。HMM 不在 PMD 函数里直接填结果,而是交给统一 hole 处理。因为 hole 可能有 VMA,也可能没有 VMA;可能需要 fault,也可能只是快照。统一处理能避免重复逻辑。

2.2 PMD migration entry:等待或填空

if (thp_migration_supported() && pmd_is_migration_entry(pmd)) {
if (hmm_range_need_fault(hmm_vma_walk, hmm_pfns, npages, 0)) {
hmm_vma_walk->last = addr;
pmd_migration_entry_wait(walk->mm, pmdp);
return EBUSY;
}
return hmm_pfns_fill(start, end, range, 0);
}

这是非驻留路径的一部分,本篇先看接口形态:如果调用者要求 fault,HMM 等待 PMD migration 完成,然后返回 -EBUSY 让入口从 last 重新 walk;如果只是快照,就把这段填 0。第篇会专门解释 migration entry 为什么这样处理。

2.3 PMD not present:交给 absent PMD

if (!pmd_present(pmd))
return hmm_vma_handle_absent_pmd(walk, start, end, hmm_pfns, pmd);

not present PMD 可能是 device private THP migration 相关 entry,也可能是其他无法转换的状态。它不属于本篇的正常 PFN 提取主线,也放到下篇。

2.4 PMD trans huge:一次填一整个 PMD

if (pmd_trans_huge(pmd)) {
pmd = pmdp_get_lockless(pmdp);
if (!pmd_trans_huge(pmd))
goto again;

return hmm_vma_handle_pmd(walk, addr, end, hmm_pfns, pmd);
}

如果 PMD 是透明大页,HMM 不会拆成 PTE 表处理,而是调用 hmm_vma_handle_pmd()。如果第二次读取发现它已经不再是 THP,说明并发拆分发生了,就回到 again 重新判断。

2.5 PMD bad:不能 fault 就标 error

if (pmd_bad(pmd)) {
if (hmm_range_need_fault(hmm_vma_walk, hmm_pfns, npages, 0))
return EFAULT;
return hmm_pfns_fill(start, end, range, HMM_PFN_ERROR);
}

bad PMD 不是正常用户映射。如果调用者要求必须得到有效页,返回 -EFAULT;否则把这段输出成 error。

2.6 普通 PMD:进入 PTE 表

剩下的就是普通 PMD 指向 PTE page table:

ptep = pte_offset_map(pmdp, addr);
if (!ptep)
goto again;
for (; addr < end; addr += PAGE_SIZE, ptep++, hmm_pfns++) {
int r;

r = hmm_vma_handle_pte(walk, addr, end, pmdp, ptep, hmm_pfns);
if (r) {
/* hmm_vma_handle_pte() did pte_unmap() */
return r;
}
}
pte_unmap(ptep 1);
return 0;

这里开始进入本篇最核心的 4K PTE 提取路径:每走一个 PTE,就把对应的 hmm_pfns 指针向前移动一格。

注意错误路径上的注释:如果 hmm_vma_handle_pte() 返回错误,它自己已经 pte_unmap()。这避免调用者重复 unmap。


3. PTE 处理与 PFN 提取

本节深入 PTE 层面:先看权限转换 helper,再看 PTE 主函数如何分流处理,最后通过一个完整示例展示 PFN 编码过程。

3.1 权限转换:pte_to_hmm_pfn_flags()

在进入完整 PTE 处理前,先看一个小 helper:

static inline unsigned long pte_to_hmm_pfn_flags(struct hmm_range *range,
pte_t pte)
{
if (pte_none(pte) || !pte_present(pte) || pte_protnone(pte))
return 0;
return pte_write(pte) ? (HMM_PFN_VALID | HMM_PFN_WRITE) : HMM_PFN_VALID;
}

它只回答一个问题:从 CPU PTE 权限看,这个页对设备来说是不是有效、是不是可写?

  • pte_none():没有映射,返回 0;
  • !pte_present():非驻留 entry,不在这里解码,返回 0;
  • pte_protnone():NUMA balancing 或保护型 none,不能当作有效映射,返回 0;
  • pte_write():有效且可写,返回 HMM_PFN_VALID | HMM_PFN_WRITE;
  • 否则:有效但只读,返回 HMM_PFN_VALID。

这个 helper 不提取 PFN,只生成 flags。真正的 PFN 来自 pte_pfn(pte)。

3.2 PTE 主函数:hmm_vma_handle_pte()

hmm_vma_handle_pte() 的参数很直接:

static int hmm_vma_handle_pte(struct mm_walk *walk, unsigned long addr,
unsigned long end, pmd_t *pmdp, pte_t *ptep,
unsigned long *hmm_pfn)

addr 是当前虚拟地址,ptep 是当前 PTE,hmm_pfn 是输出数组中对应的那一项。

函数开头保存当前 PTE 和输入请求 flags:

pte_t pte = ptep_get(ptep);
uint64_t pfn_req_flags = *hmm_pfn;
uint64_t new_pfn_flags = 0;

这里很关键:调用前的 *hmm_pfn 可能不是空的,它可能带有单页请求位,例如 HMM_PFN_REQ_FAULT、HMM_PFN_REQ_WRITE,也可能带有 sticky flags,例如 HMM_PFN_DMA_MAPPED。所以函数先把旧值读出来,后面再决定输出。

none 与 UFFD WP marker

if (pte_none(pte) || pte_is_uffd_wp_marker(pte)) {
required_fault =
hmm_pte_need_fault(hmm_vma_walk, pfn_req_flags, 0);
if (required_fault)
goto fault;
goto out;
}

这段属于“没有 present PFN,但可能可以 fault”的路径。没有 fault 请求时,new_pfn_flags 仍然是 0,最后输出就会保留 sticky flags 加 0。需要 fault 时跳到 fault 标签,由 hmm_vma_fault() 处理。细节放到第 17 篇。

非 present PTE:下篇重点

if (!pte_present(pte)) {
const softleaf_t entry = softleaf_from_pte(pte);
...
}

这里会处理 device private、swap、device exclusive、migration entry 等非驻留 PTE。它和第 13、14 篇关系很深,也是第 17 篇的主角。本篇只需要记住:正常 present PTE 会跳过这一大段,继续往下走。

3.3 present PTE:先算 CPU 权限 flags

正常页会进入这里:

cpu_flags = pte_to_hmm_pfn_flags(range, pte);
required_fault =
hmm_pte_need_fault(hmm_vma_walk, pfn_req_flags, cpu_flags);
if (required_fault)
goto fault;

cpu_flags 可能是:

0
HMM_PFN_VALID
HMM_PFN_VALID | HMM_PFN_WRITE

如果调用者要求可读/可写,而当前 CPU PTE 不满足,hmm_pte_need_fault() 会返回需要 fault 的类型。比如调用者要求写,但 PTE 只有只读权限,HMM 会走 write fault。

如果不需要 fault,就继续做 PFN 提取。

3.4 special mapping:不是普通 page 就报 error

HMM 不是所有 present PTE 都能给设备用。它会检查:

if (!vm_normal_page(walk->vma, addr, pte) &&
!is_zero_pfn(pte_pfn(pte))) {
if (hmm_pte_need_fault(hmm_vma_walk, pfn_req_flags, 0)) {
pte_unmap(ptep);
return EFAULT;
}
new_pfn_flags = HMM_PFN_ERROR;
goto out;
}

vm_normal_page() 用来判断这个 PTE 是否对应普通内存页。某些特殊映射虽然 present,但没有普通 struct page 语义,不能安全交给设备镜像。

唯一放行的特殊情况是 zero page:

is_zero_pfn(pte_pfn(pte))

源码注释解释了原因:架构会为 zero page 定义 struct page,所以 HMM 可以把它当普通页处理。

如果调用者强制要求 fault,而这个 special mapping 又不能变成普通页,就返回 -EFAULT。如果只是快照,则输出 HMM_PFN_ERROR。

3.5 正常输出:PFN OR flags

在这里插入图片描述

真正的正常路径只有一行:

new_pfn_flags = pte_pfn(pte) | cpu_flags;

然后统一写回:

out:
*hmm_pfn = (*hmm_pfn & HMM_PFN_INOUT_FLAGS) | new_pfn_flags;
return 0;

这个写回公式值得单独记:

输出 = 旧值里的 sticky flags | 新提取的 PFN/权限/error

HMM_PFN_INOUT_FLAGS 当前包括:

HMM_PFN_DMA_MAPPED | HMM_PFN_P2PDMA | HMM_PFN_P2PDMA_BUS

这些 flags 是 input-to-output carried flags。比如某个 PFN 已经被 DMA map 过,HMM 重新转换 CPU 页表时不应该无故丢掉这个状态。相反,REQ_FAULT/REQ_WRITE 是输入请求语义,输出时会被新的结果覆盖。


3.6 PFN 编码示例:一个只读匿名页

假设 range->start = 0x10000000,当前处理地址 addr = 0x10002000,那么输出下标是:

i = (0x10002000 – 0x10000000) >> PAGE_SHIFT = 2

假设 PTE present,只读,PFN 是 0x12345:

cpu_flags = HMM_PFN_VALID;
new_pfn_flags = 0x12345 | HMM_PFN_VALID;
range->hmm_pfns[2] = sticky_flags | new_pfn_flags;

驱动看到这一项时,判断:

if (hmm_pfn & HMM_PFN_VALID)
page = hmm_pfn_to_page(hmm_pfn);
if (hmm_pfn & HMM_PFN_WRITE)
/* device mapping can be writable */

hmm_pfn_to_page() 会屏蔽高位 flags:

return pfn_to_page(hmm_pfn & ~HMM_PFN_FLAGS);

所以 hmm_pfns[] 的低位是 PFN,高位是 HMM flags。这和 migrate_vma 的 src/dst 编码类似,都是“数组项 = 状态位 + PFN”。


4. 大页映射的统一处理

THP PMD、PUD leaf、HugeTLB 虽然底层页表结构不同,但 HMM 的输出模式一致:按 base page 填充 hmm_pfns[],同时通过 map_order 记录实际映射粒度。

4.1 THP PMD:不拆页,但逐页填 PFN

如果 PMD 是 transparent huge page,HMM 走 hmm_vma_handle_pmd():

static int hmm_vma_handle_pmd(struct mm_walk *walk, unsigned long addr,
unsigned long end, unsigned long hmm_pfns[],
pmd_t pmd)

先计算这段有多少 4K 页:

npages = (end addr) >> PAGE_SHIFT;

然后把 PMD 权限转成 HMM flags:

cpu_flags = pmd_to_hmm_pfn_flags(range, pmd);

pmd_to_hmm_pfn_flags() 比 PTE 版本多了一项 map_order:

return (pmd_write(pmd) ? (HMM_PFN_VALID | HMM_PFN_WRITE) :
HMM_PFN_VALID) |
hmm_pfn_flags_order(PMD_SHIFT PAGE_SHIFT);

PMD_SHIFT – PAGE_SHIFT 表示这个映射覆盖多少个 base page 的 order。比如 2MB THP 在 4K base page 上就是 order 9。

如果不需要 fault,就计算起始 PFN:

pfn = pmd_pfn(pmd) + ((addr & ~PMD_MASK) >> PAGE_SHIFT);

这里的偏移很重要。walk_page_range() 传进来的 addr/end 不一定覆盖整个 PMD,可能只是用户请求范围和 PMD 的交集。因此 HMM 要用 addr 在 PMD 内的偏移修正起始 PFN。

最后逐页填充:

for (i = 0; addr < end; addr += PAGE_SIZE, i++, pfn++) {
hmm_pfns[i] &= HMM_PFN_INOUT_FLAGS;
hmm_pfns[i] |= pfn | cpu_flags;
}

注意:即使 CPU 映射是一个 THP,HMM 仍然把 hmm_pfns[] 按 base page 填满。这样老驱动可以按页消费数组;新驱动则可以通过 hmm_pfn_to_map_order() 看到这些项同属一个大映射,从而优化设备页表更新。

4.2 PUD leaf:更大的同构逻辑

如果架构支持 PUD 级透明大页,HMM 会启用 hmm_vma_walk_pud()。

PUD leaf 的正常输出逻辑和 THP PMD 高度相似:

cpu_flags = pud_to_hmm_pfn_flags(range, pud);
required_fault = hmm_range_need_fault(..., cpu_flags);
...
pfn = pud_pfn(pud) + ((addr & ~PUD_MASK) >> PAGE_SHIFT);
for (i = 0; i < npages; ++i, ++pfn) {
hmm_pfns[i] &= HMM_PFN_INOUT_FLAGS;
hmm_pfns[i] |= pfn | cpu_flags;
}

差别主要在锁和 pagewalk 控制:

spinlock_t *ptl = pud_trans_huge_lock(pudp, walk->vma);
...
walk->action = ACTION_CONTINUE;
...
walk->action = ACTION_SUBTREE;

如果 PUD 是 leaf,HMM 填 PFN 并让 pagewalk 继续。如果不是 leaf,则设置 ACTION_SUBTREE,让 pagewalk 下探到 PMD/PTE 层。

所以从 HMM 输出语义看,PUD leaf、PMD THP、PTE 的差异不在最终数组形态,而在“PFN 来源”和“map_order”。最终驱动拿到的仍然是一页一个 hmm_pfn。

4.3 HugeTLB:单独回调,同样输出 PFN 数组

HugeTLB 不走普通 THP 路径,而是有专门的 hmm_vma_walk_hugetlb_entry()。它先拿 HugeTLB 的 PTE lock:

ptl = huge_pte_lock(hstate_vma(vma), walk->mm, pte);
entry = huge_ptep_get(walk->mm, addr, pte);

然后生成 flags:

cpu_flags = pte_to_hmm_pfn_flags(range, entry) |
hmm_pfn_flags_order(huge_page_order(hstate_vma(vma)));

再逐页填入输出数组:

pfn = pte_pfn(entry) + ((start & ~hmask) >> PAGE_SHIFT);
for (; addr < end; addr += PAGE_SIZE, i++, pfn++) {
range->hmm_pfns[i] &= HMM_PFN_INOUT_FLAGS;
range->hmm_pfns[i] |= pfn | cpu_flags;
}

从驱动视角,HugeTLB 和 THP 一样:每个 base page 都有一项,但每项都携带相同的 map_order。区别在于内核内部锁、页表格式和 fault 路径不同。

5 fault 判断与快照语义

本节解释两个关键问题:hmm_range_need_fault() 如何决定是否触发 fault,以及为什么 HMM 返回的是可验证快照而非 pin 住的页面。

5.1 hmm_range_need_fault() 的作用

虽然 fault 细节放到第 17 篇,但本篇需要理解 hmm_range_need_fault() 为什么贯穿所有路径。

它的入口是:

hmm_range_need_fault(hmm_vma_walk, hmm_pfns, npages, cpu_flags)

意思是:给定这一段页当前从 CPU 页表看到的 flags,看看调用者的请求是否已经满足。

函数先做快速判断:

if (!((range->default_flags | range->pfn_flags_mask) &
HMM_PFN_REQ_FAULT))
return 0;

如果整段默认策略和单页 mask 都不允许 fault,那就一定不需要 fault。HMM 只做快照。

如果允许 fault,它会逐页检查:

required_fault |= hmm_pte_need_fault(hmm_vma_walk, hmm_pfns[i], cpu_flags);

hmm_pte_need_fault() 会把单页请求和默认请求合并:

pfn_req_flags &= range->pfn_flags_mask;
pfn_req_flags |= range->default_flags;

然后判断三件事:

if (!(pfn_req_flags & HMM_PFN_REQ_FAULT))
return 0;

if ((pfn_req_flags & HMM_PFN_REQ_WRITE) &&
!(cpu_flags & HMM_PFN_WRITE))
return HMM_NEED_FAULT | HMM_NEED_WRITE_FAULT;

if (!(cpu_flags & HMM_PFN_VALID))
return HMM_NEED_FAULT;

这解释了为什么 present PTE 路径也可能进入 fault:PTE present 但只读,而调用者要求写,就需要 write fault;PTE protnone 或当前 flags 无效,而调用者要求有效,就需要 read fault。

本篇关注的是“不需要 fault 时如何提取 PFN”。需要 fault 时会跳到 hmm_vma_fault(),下篇展开。

5.2 HMM 不 pin page,只提供可验证快照

读完 PFN 提取路径后,容易产生一个误解:hmm_range_fault() 返回 PFN,是不是就等价于 GUP pin 住这些页?答案是否定的。

源码注释说它类似 get_user_pages(),但有一个关键区别:

This is similar to get_user_pages(), except that it can read the page tables
without mutating them (ie causing faults).

更准确地说,HMM 返回的是“在 notifier 序列号保护下的页表快照”。它没有给每个 page 增加长期 pin,也不阻止 CPU 后续 unmap、mprotect、migration 或 reclaim。设备驱动必须依赖 mmu_interval_notifier 来维护设备页表:

hmm_range_fault() 填出 PFN
-> driver 准备更新设备页表
-> mmu_interval_read_retry() 确认期间没有 invalidation
-> 设备页表可使用
-> 后续 CPU 页表变化触发 invalidate
-> 驱动清理设备页表镜像

这也是 HMM 和 GUP 的设计差异。GUP pin 更像“拿住页面”;HMM mirror 更像“建立一个会被 notifier 失效的镜像”。

对 GPU/SVM 来说,这个模型更合适。设备页表可以长期存在,但它的有效性由 MMU notifier 管,而不是靠长期 pin 阻止内存管理动作。


6 总结与连接

本节把正常路径串联起来,并说明本篇与前后章节的关系。

6.1 正常路径串联

现在可以把本篇讲过的正常 present PTE 路径串成一条线:

驱动准备 hmm_range
-> mmu_interval_read_begin()
-> mmap_read_lock(mm)
-> hmm_range_fault(range)
-> mmu_interval_check_retry()
-> walk_page_range(…, hmm_walk_ops, hmm_vma_walk)
-> hmm_vma_walk_test(): 过滤 VMA
-> hmm_vma_walk_pmd(): 分流 PMD 类型
-> 普通 PMD: pte_offset_map()
-> hmm_vma_handle_pte()
-> ptep_get()
-> pte_to_hmm_pfn_flags()
-> hmm_pte_need_fault()
-> vm_normal_page()/zero page 检查
-> hmm_pfns[i] = sticky | pte_pfn(pte) | flags
-> THP PMD: hmm_vma_handle_pmd()
-> pmd_pfn() + offset
-> map_order = PMD_SHIFT – PAGE_SHIFT
-> 逐页填 hmm_pfns[]
-> HugeTLB/PUD leaf: 类似填 PFN + map_order
-> mmap_read_unlock(mm)
-> mmu_interval_read_retry()
-> driver 消费 hmm_pfns[]

最终每个输出项有几种典型形态:

输出项含义
0 当前没有有效 PFN,但未来 fault 可能成功
HMM_PFN_ERROR 不能给设备访问,驱动应失败或跳过
HMM_PFN_VALID | pfn 有效只读映射
HMM_PFN_VALID | HMM_PFN_WRITE | pfn 有效可写映射
… | map_order | pfn 来自大页映射,可按 order 优化
sticky_flags | … 保留 DMA/P2PDMA 等输入输出贯穿状态

6.2 与前面章节的连接

这一篇其实是第 6 篇页表遍历框架在 HMM 中的真实应用。

经典 MM 里,walk_page_range() 是一个通用遍历工具:调用者提供 mm_walk_ops,框架负责沿着 VMA 和页表层级走。HMM 没有重造 walker,而是把自己的语义塞进回调:

  • VMA 过滤:只接受普通可读用户映射;
  • hole 处理:根据 fault 策略填 0/error 或触发 fault;
  • PMD/PTE 处理:把 CPU 页表项转成 hmm_pfns[];
  • 大页处理:保留 map_order,但仍按 base page 输出;
  • retry 处理:通过 last 和 -EBUSY 局部重走。

它也是 2.4 篇 mmu_interval_notifier 的直接消费点。hmm_range_fault() 的输出不是独立真理,而是必须放在 read_begin/read_retry 之间才有意义。

同时,它为第 3.4、 3.5 篇留下了接口位置:当 PTE 不是 present,而是 device private、device exclusive 或 migration entry 时,hmm_vma_handle_pte() 会进入另一套分支。那套分支决定设备私有页如何被原 owner 识别,迁移中的页如何等待,swap entry 如何触发远程 fault。

这就是下一篇要讲的内容。

6.3 小结

hmm_range_fault() 的上半场可以概括为一句话:在 mmap_lock 和 mmu_interval_notifier 序列号保护下,复用 walk_page_range() 读取 CPU 页表,把每个用户虚拟页转换成设备驱动可消费的 hmm_pfn。

它的设计有几个鲜明特点:

  • 复用通用 pagewalk,而不是自建页表遍历;
  • 输出数组按 base page 组织,即使底层是 THP/PUD leaf/HugeTLB;
  • 用高位 flags + 低位 PFN 表达权限、错误和大页 order;
  • 保留 sticky flags,覆盖输入请求 flags;
  • 不 pin page,而是依靠 notifier 维护设备页表镜像有效性;
  • 遇到并发变化或需要 fault 时,通过 last 和 -EBUSY 重走未完成部分。

下一篇我们继续进入 hmm_vma_handle_pte() 的另一半:非驻留 PTE 与 fault 策略。那里会把第 3.4 的 device private/exclusive entry、 migration entry,以及 FAULT_FLAG_REMOTE 远程缺页全部串起来。


📚 关联阅读

  • 3.1 异构计算的挑战:为什么 MM 需要进化
  • 3.3 dev_pagemap:设备内存区域的控制面
  • 3.4 非驻留 PTE 的新成员:Device Private & Exclusive Entry
  • 3.5 设备页面迁移:migrate_vma 三阶段详解
赞(0)
未经允许不得转载:171主机测评 » 4.2 hmm_range_fault() 逐行解析(上):遍历与 PFN 提取
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址