HACDBS v2 硬件脏位清理深度解析 — 基于 Leo Bras patch v2
核心问题:HACDBS 里硬件自动识别 DBM+writable-dirty → writable-clean,但只在收到 HACDBSBR_EL2 触发后才执行,不是随时自动。本文档基于 Leo Bras 2026-06-29 提交的 patch v2(共 13 个 patch, 2186 行)的真实源码。
配套文档:《传统脏页 Bitmap 迁移路径深度解析(无 HACDBS)》
一、PTE.DBM 位的真实位置
1 |
关键位号:DBM 在 Stage-2 PTE 的 bit[51](属于高位段 KVM_PTE_LEAF_ATTR_HI)。不是 bit[7](bit[7] 是 S2AP[1])。这个位号是从 Leo Bras patch v2 的真实源码里查到的。
1 | Stage-2 PTE bits (4KB granule, with FEAT_HACDBS): |
DBM + writable-dirty 的语义:
DBM=0 + AP=RW:writable but NOT dirty(clean + writable) → guest 第一次写会 trap 到 EL2
DBM=1 + AP=RW:writable and dirty(硬件自动设的) → guest 写直接成功,不再 trap
硬件在 DBM 0→1 翻转的同时原子清 W 位 —— 即 "writable-dirty → writable-clean"。这是 ARM ARM DDI 0487M.b 白纸黑字的描述。
二、hw_entries 数据结构与编码
1 | hw_entries = kmalloc_objs(u64, entries_sz, GFP_KERNEL); |
1 | /* HDBSS entry field definitions */ |
hw_entries 的本质:
软件维护的 u64 数组(512 项 = 一个 page 大小)
物理连续内存(kmalloc,不是 vmalloc),硬件可以直接 DMA-style 读取
每个 entry 是 IPA 列表 + 寻路提示(TTWL),告诉硬件「去清这个 IPA 对应的 PTE 的 DBM 位」
所以 hw_entries 不是 bitmap(不是 1bit/page 的紧凑结构),而是显式的 IPA 数组,每项 8 字节。
为什么用显式 IPA 而不是 bitmap[i] 直接传给硬件?
原因:硬件只知道 IPA 物理地址,不知道 memslot 的 base_gfn,也不知道 bitmap 是 per-memslot 的。软件必须做算术换算:index i → memslot 内 gfn = base + i → gpa = gfn<<12 → IPA。把 IPA 编码进 entry [55:12],加上 TTWL 和 Valid,硬件才认得。
三、bitmap index → IPA:mask_to_hdbss 源码解析
1 | static inline int mask_to_hdbss(unsigned long *mask, u64 *hw_entries, |
这一段是你问的"index i 是否传给硬件"的答案:
不是直接传 index i,而是做算术换算 → IPA。三步走:
Step 1:
__ffs(*mask)在 mask 里找最低位的 1,得到局部偏移 j(mask 内 0~63)Step 2:
gfn_to_gpa(offset + j)—— offset 是 memslot 内的 gfn 起点,+j 是绝对 gfn,左移 12 位变成 GPA/IPAStep 3:把 IPA 编码进 entry 的 bit[55:12],加上 TTWL 和 Valid 位,硬件认得
这样硬件扫描 entry 列表时,每个 entry 就是一个完整的 IPA 寻址指令,不需要外部上下文。
TTWL(Translation Table Walk Level)的含义:
IPA 在 stage-2 里可能映射在 L0/L1/L2/L3 任意一级。TTWL 是硬件寻路提示,告诉 PE 从哪一级开始下钻:
TTWL=0 → L3(最终 4KB page descriptor)
TTWL=1 → L2(PMD 级 block descriptor)
TTWL=2 → L1(PUD 级 block descriptor)
TTWL=-4 → 终止哨兵(
HDBSS_ENTRY_TTWL_RESV),告诉硬件 hw_entries 数组结束了
在 patch 里固定用
HDBSS_ENTRY_TTWL(KVM_PGTABLE_LAST_LEVEL)(最后一级)作为起始值,意味着软件期望
PTE 都在最末级 —— 这通常是因为 dirty 跟踪只针对 4KB 页。
四、dirty_bit_clear:软件做的不是"清 dirty"
1 | static int dirty_bit_clear(struct kvm *kvm, u64 *hw_entries, int size) |
1 | static void hacdbs_start(u64 *hw_entries, int size) |
dirty_bit_clear 不是"清 dirty 的函数",是"配置 + 唤醒硬件的函数":
✅ 它做的:写 HCR_EL2.VM、加载 stage-2、写 HACDBSBR_EL2(BADDR+SZ+EN)、CPU WFI 等待、中断唤醒后清 EN
❌ 它没做:任何 page walk、任何 PTE 修改、任何 dirty 位翻转
真正"清 dirty"的代码是硬件跑的(PE 自动 stage-2 walk
命中 PTE,原子做 DBM 1→0 + W 0→1)。软件只是触发器。
WFI(Wait For Interrupt)的意义:
CPU 进入 idle,释放流水线资源给硬件。HACDBS sweep 完成后通过 HACDBS 中断唤醒 CPU:
1 | static irqreturn_t hacdbsirq_handler(int irq, void *pcpu) |
wfi() 退出时唤醒 CPU,进入
irq_handler,检查 HACDBSCONS_EL2
状态,决定成功还是回退。
五、硬件 sweep 工作流程
1 | 软件触发 (CPU) |
关键原子操作:PTE.DBM 1→0 + PTE.W 0→1
这是硬件自动完成的,用户态/内核态软件完全不需要介入(除了写 HACDBSBR_EL2)。这就是 HACDBS 名字的来源 —— Hardware Accelerated Clean Dirty Bit State。
结合 FEAT_HACDBS 的特性:
guest 第一次写 PTE 时,硬件就自动做 DBM 0→1 + W 1→0(无需 trap)
software 想 reset 整批 dirty 时,调 dirty_bit_clear,硬件批量做 DBM 1→0 + W 0→1
整个过程 hardware 维护 PTE,software 只读 PTE 状态就能知道哪些页 dirty(DBM=1)
关于 dbm track 时机(KVM_GET_DIRTY_LOG 路径):
实际上在 Leo Bras patch v2 里,dirty tracking 仍然依赖
stage-2 page fault(guest 写触发的 DABT 路径),由
mark_page_dirty_in_slot() 把脏位置入 memslot dirty
bitmap。HACDBS/HDBSS 这次 patch 引入的 dirty_bit_clear 是reverse
方向 —— 当 software 已经知道哪些页 dirty(bitmap 里
bit=1),想批量清除硬件记录的 dirty state 时,调用此函数让硬件做 DBM
复位。
所以三阶段分工:
Dirty 标记阶段:guest 写 → DABT trap →
mark_page_dirty_in_slot()→ 置 memslot dirty_bitmapDirty 取走阶段:QEMU
ioctl(KVM_GET_DIRTY_LOG)→ xchg bitmap → copy_to_userDirty 复位阶段:
dirty_bit_clear()→ HACDBSBR_EL2 → 硬件批量清 PTE.DBM + 恢复 W
六、HACDBS vs 软件 page walk 关键差异
| 维度 | 软件 walk(传统) | HACDBS(patch v2) |
|---|---|---|
| 触发入口 | kvm_arch_mmu_enable_log_dirty_pt_masked() |
__kvm_arch_dirty_log_clear() |
| 路径 | 软件逐 bit 软件 walk stage-2 | 硬件批量扫整张 stage-2 |
| 持锁 | mmu_lock 长时间持有 |
持锁时间短(清 DBM 期间硬件做,CPU WFI 中) |
| vCPU 并发 | 其他 vCPU 阻塞在 mmu_lock | 其他 vCPU 可以正常 schedule(preempt_disable 不影响) |
| 单次开销 | O(masks 数) × page walk depth(4 次内存读) | O(N entries) / hardware sweep speed |
| 适用规模 | 小范围 dirty 划算 | 大范围 dirty 更划算 |
| 错误处理 | 简单(软件自己 try again) | 复杂(HACDBSCONS_EL2.ERR_REASON: NOF / IPAHACF...) |
| WFI | 不用 | CPU 睡眠,硬件 async 干活 |
| PTE 修改 | 软件改 W 位 | 硬件原子做 DBM 1→0 + W 0→1 |
| hw_entries 内存 | 不用 | kmalloc 一页(512 项) |
| 中断 | 不用 | HACDBS 中断唤醒 WFI |
最关键的差异:软件 walk 是一次一页(per-bit 软件 page walk),HACDBS 是一次一批(per-batch 硬件 sweep)。两者触发时机不同,不是替代关系:
软件 walk在 KVM_GET_DIRTY_LOG 里立即生效(mask + enable_log_dirty_pt_masked)
HACDBS在 KVM_GET_DIRTY_LOG 后异步生效(dirty_bit_clear 写 HACDBSBR_EL2)
实际上 Leo Bras patch 里,__kvm_arch_dirty_log_clear()
是替换软件 walk 路径 —— 主框架代码不调用
kvm_arch_mmu_enable_log_dirty_pt_masked(),而是调用
arch-generic 的 kvm_arch_dirty_log_clear(),arm64
实现里就走 HACDBS 路径。
七、完整调用链(基于 patch v2)
1 | QEMU 迁移线程 |
三个 ★ 标记的关键节点:
★ 1(kvm_arch_dirty_log_clear):patch v2 新加的 arch-generic 入口,让 arm64 能用硬件加速
★ 2(mask_to_hdbss):把 bitmap 的 index i 换算成 IPA,编码进 hw_entries entry
★ 3(write_sysreg HACDBSBR_EL2):唯一的硬件触发点,硬件从这一行开始 sweep
整条链里 software 主导的只有 ★ 1 → ★ 2 → ★ 3,之后就是硬件干活、软件睡觉等中断。
八、三个核心问题的最终回答
问题 1:硬件会自动识别 DBM+writable-dirty → writable-clean 吗?
答:是的,但只在收到 HACDBSBR_EL2 触发后。
触发时机:
write_sysreg(SYS_HACDBSBR_EL2, BADDR | SZ | EN=1)之后硬件行为:对 hw_entries 里每个 entry,stage-2 walk 找到 PTE,原子操作做
PTE.DBM ← 0+PTE.W ← 1(即"writable-dirty → writable-clean")不是随时自动:硬件不会主动扫描 stage-2,必须 software 显式触发
反向自动(FEAT_HACDBS 的另一面):guest 第一次写 PTE 时,硬件自动做
DBM 0→1 + W 1→0(无需 trap)
问题 2:hw_entries 存的是什么?是否需要把 bitmap[i] 的 index i 传给硬件?
答:hw_entries 是显式的 IPA 数组,不直接传 index i。
存储内容:每个 u64 entry 编码
{IPA[55:12] + TTWL[3:1] + Valid[0]}为什么不用 bitmap[i]:硬件只知道 IPA,不知道 memslot 的 base_gfn。software 必须做算术换算:
index j → gfn = offset + j → IPA = gfn << 12换算函数:
mask_to_hdbss(),对每个 mask 里的 1 bit,gfn_to_gpa(offset + __ffs(mask))转 IPA,编码进 entry内存:kmalloc 一页(512 项),物理连续,硬件直接 DMA-style 读取
问题 3:dirty_bit_clear 函数有什么用?既然硬件可以自动识别和复位?
答:dirty_bit_clear 不是"清 dirty 的函数",是"配置 + 触发硬件的函数"。
它做的:写 HCR_EL2.VM、加载 stage-2、写 HACDBSBR_EL2(BADDR + SZ + EN=1)、CPU WFI 等待、中断唤醒后清 EN
它没做的:任何 page walk、任何 PTE 修改、任何 dirty 位翻转
真正的 dirty 清理由硬件做(PE 自动 stage-2 walk 命中 PTE,原子做 DBM 1→0 + W 0→1)
为什么需要这个软件函数:
硬件不知道哪些页 dirty,必须 software 提供 hw_entries IPA 列表
软件需要配置 HCR_EL2.VM + 加载 stage-2 TTBR 来建立硬件 sweep 的上下文
软件需要写 HACDBSBR_EL2.EN=1 来启动硬件 sweep
WFI + 中断机制是 software 和 hardware 的同步点
一句话总结:
dirty_bit_clear() 把 bitmap 索引 i
转换成 IPA 列表 hw_entries,写
HACDBSBR_EL2.EN=1 触发硬件;硬件做 stage-2 walk
→ PTE.DBM 1→0 + W 0→1(即 "writable-dirty →
writable-clean");完成后 触发 HACDBS 中断唤醒
CPU。
九、为什么 HACDBS 只做 dirty bit clean?—— 纯属性修改 vs 内存管理操作
核心结论:HACDBS 只处理 PTE 的属性 bit(DBM + W),不涉及 HPA 变化、内存分配、内存释放。这种纯属性修改操作最适合硬件 MMU 加速。其他涉及 HPA 的 PTE 操作(如 map / unmap / split / move)必须由软件做。
9.1 纯属性修改的特征
1 | 操作前 PTE: 0x0000_0000_0040_0E01 |
纯属性修改的关键特征:
✅ HPA(指向的物理页)完全不变
✅ 其他 bit 完全不变
❌ 只动了 1~2 个 bit(W 位 / DBM 位 / AF 位)
这种操作本质就是"位翻转",跟 HPA 是什么没关系,硬件完全能做。
9.2 硬件做"位翻转"的流程
1 | 硬件拿到 IPA(要改哪个 GFN) |
硬件做的四个优势:
Page walk 走 MMU 内部加速器(不走系统总线)
原子 RMW(软件做不到,硬件微码保证)
批量处理(多个 entry 一次性 sweep)
CPU 睡觉(WFI,其他 vCPU 不阻塞)
9.3 哪些 PTE 操作硬件做不了
| 操作 | 软件做 | 硬件做 | 谁做 |
|---|---|---|---|
| 改 PTE 的一个 bit(W / DBM / AF) | 慢但通用 | 快但受限 | 硬件 |
| 修改 PTE 指向的物理页(HPA) | 必须 | ❌ 不知道新 HPA | 软件 |
| 创建新的 PTE(map 新页) | 必须 | ❌ 需要新分配 HPA | 软件 |
| 拆大页(block → 4K × 512) | 必须 | ❌ 需要新表 | 软件 |
| 删 PTE(unmap) | 必须 | ❌ 需要 put_page | 软件 |
| 解决 TLB miss(自动 walk) | 不需要 | CPU 自动做 | 硬件 |
9.3.1 创建新 PTE(map)—— 必须软件做
1 | 初始状态:L2 table[5] = 0 (没有 PTE,是无效 entry) |
为什么硬件做不了:
硬件不知道新分配的 HPA 是多少(host 内核 alloc_page 出来的)
硬件不知道 KVM 想设什么权限(这是软件策略决定)
结论:必须软件做
9.3.2 拆大页(split block descriptor)—— 必须软件做
1 | 初始状态:L1 table[3] = 0x0000_0001_2340_0C01 ← Block descriptor (1GB 页) |
为什么硬件做不了:
拆大页要 kmalloc 一张新的 L2 table(软件内存分配)
要把原来 block descriptor 的属性展开到 512 个新 PTE(软件逻辑)
要考虑 4KB 对齐、contiguous bit 等复杂属性(软件策略)
结论:必须软件做
9.3.3 删除 PTE(unmap)—— 必须软件做
1 | 初始状态:L2 table[5] = 0x0000_0000_0040_0E01 ← PTE 指向某页 |
为什么硬件做不了:
释放物理页涉及 host 内核 refcount(软件语义)
需要通知 page cache、文件系统等(软件回调)
结论:必须软件做
9.3.4 修改 HPA(move page)—— 必须软件做
1 | 初始:L2 table[5] = 0x0000_0000_0040_0E01 ← HPA = 0x1234_5000 |
为什么硬件做不了:
HPA 由 host 内核分配,硬件不知道新 HPA
需要 copy 旧页内容到新页(软件 I/O)
结论:必须软件做
9.4 一句话本质
纯属性修改(HPA 不变,只动权限/状态 bit)→ 硬件做最划算
涉及 HPA / 内存分配 / 内存释放 → 必须软件做
9.5 对应到 HACDBS 的设计
Leo Bras patch v2 里,HACDBS 只用于 dirty bit clean,不是因为 dirty bit 简单,而是因为:
dirty bit 翻转是纯属性修改(DBM 1→0 + W 0→1)
PTE 指向的物理页不变
不需要新分配内存
不需要通知其他子系统
只动 2 个 bit(DBM + W)
| 扩展 | 操作 | PTE 改的 bit | HPA 变吗 |
|---|---|---|---|
| FEAT_HAFDBS | Access Flag 管理 | AF(1 bit) | 不变 |
| FEAT_HACDBS | Dirty Bit Modifier | DBM + W(2 bit) | 不变 |
| FEAT_HDBSS | Dirty 批量扫描 | DBM + W(2 bit) | 不变 |
| FEAT_BTI | Branch Target Identification | PTE.PXN/UXN 语义 | 不变 |
| FEAT_PAuth | Pointer Authentication | PTE.PAC 位 | 不变 |
共同点:全是纯属性修改。
这是 ARM 硬件扩展的通用模式:软件搞不定的"批量 + 一致性 + 原子性"操作,硬件下沉到 MMU 做。
HACDBS 不是"硬件替代软件做所有页表管理",而是"硬件接管软件做不划算的那一类操作(纯属性 bit 翻转)"。
十、hw_entries 与 HDBSS 的精确关系(基于 patch v2 真实数据流)
本章核心结论:hw_entries 就是 HDBSS(同一个东西的两个名字),二者不是"独立"也不是"无关"。HACDBS 引擎通过 HACDBSBR_EL2.BADDR 拿到这段内存的物理地址,按 HDBSS 协议消费里面的 entry。
10.1 命名直接对应
| 名称 | ARM 规范名 | patch v2 代码名 |
|---|---|---|
| 内存中的 8-byte entry 结构 | HDBSS (Hardware Dirty Bit State Structure) | u64 *hw_entries 数组 |
| entry 格式(IPA + TTWL + Valid) | HDBSS entry format | HDBSS_ENTRY_* 宏 |
| 基地址系统寄存器 | HACDBSBR_EL2 (HACDBS HDBSS Base Register) | SYS_HACDBSBR_EL2 |
| 硬件消费引擎 | FEAT_HACDBS mechanism | HACDBS 微架构单元 |
| 反向解析函数(hw_entries → bitmap) | (非规范) | hdbss_to_bitmap() |
| 正向填充函数(bitmap → hw_entries) | (非规范) | mask_to_hdbss() |
关键观察:代码里 hdbss_to_bitmap() 和
mask_to_hdbss() 这两个函数名直接用了 "hdbss",等同于在说
"处理 HDBSS 的函数"。
所以 hw_entries = HDBSS,是同一个内存结构在软件侧起的不同名字。
10.2 ARM ARM 规范里 HDBSS 的定义
ARM ARM 原文:
"The HDBSS is a structure in memory that consists of a sequence of 8-byte entries, each describing a translation table entry to be processed by the FEAT_HACDBS mechanism."
翻译:HDBSS 是内存中由 8 字节 entry 组成的结构,每个 entry 描述一个要由 FEAT_HACDBS 处理的 translation table entry。
→ u64 *hw_entries
数组完全符合这个定义,没有任何偏移。
10.3 HACDBSBR_EL2 名字里的"HDBSS"
1 | /* 全名 = HACDBS HDBSS Base Register */ |
注意:HACDBSBR_EL2
这个寄存器名字直接由 HACDBS + HDBSS 复合而成,全名 =
"HACDBS HDBSS Base Register"。
这说明在硬件实现层面,HACDBS 和 HDBSS 是绑定的,不是一个独立一个可选 —— 启动 HACDBS 的同时必须提供 HDBSS 的基地址。
10.4 hw_entries 的真实数据流(两条路径)
10.4.1 路径 A:从 dirty bitmap 出发(KVM_GET_DIRTY_LOG)
1 | memslot 的 dirty_bitmap (软件维护,1 bit/page, 紧凑) |
10.4.2 路径 B:从 dirty ring 出发(KVM_DIRTY_RING)
1 | dirty_gfns[] ring (软件/硬件维护的 GFN 列表) |
10.5 真实调用节奏:批量填充 + 满则触发
1 | hw_entries = kmalloc_objs(u64, 512, GFP_KERNEL); // 一次性分配 |
三个关键节奏:
批量填充:软件持续往 hw_entries 塞 IPA(mask_to_hdbss 内部 while)
满则触发:塞满 512 项才让硬件处理一次;如果总量是 2000 项,可能触发 4 次硬件 sweep(512+512+512+464)
循环复用:整段 hw_entries 不重新分配,触发后清零 idx 继续塞
三个常见误解:
❌ "软件一把填完再给硬件" —— 错。是循环复用,每满 512 项就触发一次
❌ "软件一条一条触发" —— 错。是批量,per-bit 触发开销太大
❌ "hw_entries 跟 HDBSS 没关系" —— 错。hw_entries 就是 HDBSS 在 patch v2 里的代码命名
10.6 一句话总结
hw_entries = HDBSS = 同一段物理内存,里面是 8-byte entry 数组,每个 entry 编码 {IPA + TTWL + Valid}。
软件的工作:从 dirty_bitmap 或 dirty_gfns ring 抽出要清 dirty 的 PTE 列表,循环复用地填进 HDBSS buffer,满 512 项就把 HDBSS 物理地址写进 HACDBSBR_EL2.BADDR 并置 EN=1,触发硬件 sweep。
硬件的工作:按 HDBSS 协议逐 entry 消费,对每个 entry 做 stage-2 walk + atomic 改 PTE.DBM + PTE.W,完成后触发 HACDBS 中断唤醒 CPU。
(注:内容由 AI 生成,请谨慎参考)