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 三阶段详解
