3.1 异构计算的挑战:为什么 MM 需要进化
本篇目标:从 GPU、DSP、FPGA、SmartNIC、CXL 这类异构设备的编程模型出发,理解传统 Linux MM 为什么不够用。我们会先分析 split address space 和显式拷贝的痛点,再看 PCIe/一致性/带宽/延迟带来的硬件约束,最后引出 HMM 的两条主线:地址空间镜像和设备内存纳入
struct page/ZONE_DEVICE。
1. 从“CPU 的内存”到“进程的内存”
前 9 篇里,我们一直站在 CPU MM 的视角看内存管理:
- VMA 描述进程虚拟地址空间
- CPU 页表把虚拟地址翻译成 PFN
- 缺页处理按需分配或读入页面
- 页面回收和 swap 让内存可以腾挪
- page migration 让页面可以从一个 NUMA 节点搬到另一个节点
- pagewalk、MMU notifier、interval notifier 让其他子系统可以读取或订阅 CPU 页表
这些机制共同构成一个很强的抽象:
对 CPU 来说,一个进程拥有一个连续的虚拟地址空间。只要地址有效,CPU 就能通过页表找到背后的物理页。
但异构计算提出了新的问题:如果 GPU、FPGA、SmartNIC 也要代表这个进程访问数据,它们看到的是同一个地址空间吗?
传统答案通常是:不是。
CPU 有 CPU 的地址空间,GPU 有 GPU 的显存地址空间,RDMA 设备有自己的 DMA 映射,设备驱动有自己的 buffer 管理 API。于是一个程序里的数据被撕成两份:
- CPU 侧:malloc / mmap / anonymous / file-backed memory
- GPU 侧:cudaMalloc / driver BO / device memory allocation
这就是 HMM 文档里说的 split address space。
2. split address space:显式拷贝为什么会痛
早期 GPU 编程模型通常要求程序员显式管理数据位置:
float *host = malloc(size);
float *dev;
cudaMalloc(&dev, size);
cudaMemcpy(dev, host, size, cudaMemcpyHostToDevice);
kernel<<<grid, block>>>(dev);
cudaMemcpy(host, dev, size, cudaMemcpyDeviceToHost);
cudaFree(dev);
free(host);
对于简单数组,这还可以接受。程序员知道数据是一个连续 buffer,拷贝进去、算完、拷贝回来。
但真实程序的数据结构往往不是平坦数组:
struct node {
struct node *left;
struct node *right;
void *payload;
};
如果要把一棵树从 CPU 内存复制到 GPU 内存,问题就来了:
- 每个节点都要复制
- 节点之间的指针关系要重新映射
payload可能指向另一个库分配的对象- 有些对象可能是 file-backed mmap,有些可能是匿名页
- CPU 和 GPU 两份副本还要保持一致
HMM 文档把这个问题讲得很直接:复制复杂数据结构时,需要重新映射元素之间的 pointer relations,这很容易出错,也很难调试。
2.1 库生态会被迫分裂
split address space 还会污染软件接口。
一个库原本只需要接收普通指针:
void process(float *data, size_t n);
一旦设备内存成为另一种世界,库可能被迫提供多套入口:
void process_cpu(float *data, size_t n);
void process_cuda(cuda_ptr data, size_t n);
void process_rocm(hip_ptr data, size_t n);
void process_fpga(fpga_buf data, size_t n);
如果每个库都为每种设备 allocator 复制一套 API,组合爆炸很快就会出现。
更糟的是,现代编译器和运行时希望自动识别并 offload 一些计算模式。编译器看到的是普通 C/C++ 指针。如果设备不能直接理解这些指针,自动 offload 就会被显式拷贝和设备专用 allocator 卡住。

3. shared address space:理想目标是什么
异构计算想要的是 shared address space:CPU 上有效的进程虚拟地址,对设备也应该是有效地址。
也就是:
struct graph *g = build_graph();
// CPU 可以访问 g
cpu_process(g);
// GPU 也可以拿同一个 g 做计算
launch_gpu_kernel(g);
理想状态下,设备不需要程序员手动复制整套对象图,也不需要另一套设备专用指针。它应该能沿着进程指针访问数据。
但这句话背后有几个非常重的内核问题:
| 问题 | 内核必须解决什么 |
|---|---|
| 地址翻译 | 设备如何知道进程 VA 对应哪个 PFN? |
| 页表同步 | CPU 页表 COW/unmap/migrate 后,设备页表如何同步? |
| 缺页处理 | 设备访问一个尚未驻留的 VA,谁来 fault? |
| 权限一致 | CPU PTE 只读,设备能否写?写了如何触发 COW? |
| 页面位置 | 页面在系统内存、显存、CXL 内存之间如何迁移? |
| 生命周期 | 进程退出、VMA 释放、页面回收时设备怎么办? |
前 6-9 篇其实已经为这些问题埋了伏笔:
walk_page_range()可以读取 CPU 页表mmu_interval_notifier可以订阅某段 VA 的页表变化- page migration 可以在不改变 VA 的前提下改变背后的 PFN
- migration entry / swap entry 可以表达“虚拟地址暂时不指向普通 RAM 页”
HMM 就是把这些能力组合起来,让设备参与进程地址空间。
4. 硬件现实:设备内存不是普通内存
如果所有设备都和 CPU 完全 cache coherent、支持所有原子操作、访问延迟和带宽都接近本地内存,那么 MM 的扩展会简单很多。
现实不是这样。
HMM 文档以 PCIe 为例,列出几个限制:
- 设备访问系统内存通常要经过 IOMMU
- cache coherency 可能存在,也可能有限
- 设备对系统内存支持的原子操作有限
- CPU 访问设备内存更受限制,通常只能访问 BAR 映射的一小段
- CPU 对设备内存往往不能做完整原子操作
- PCIe 带宽远低于 GPU 本地显存带宽
- 设备访问系统内存的延迟远高于访问本地显存
文档中提到,PCIe 4.0 x16 大约 32GB/s,而高端 GPU 显存带宽可以到 1TB/s 量级。这不是小差距,而是数量级差距。
所以 shared address space 不能简单理解为“设备永远直接访问系统内存”。那样虽然编程模型统一了,但性能可能很差。
真正需要的是:
地址空间共享,数据位置可迁移。
设备可以用同一个进程 VA 访问数据;内核和驱动可以根据访问模式,把热数据搬到更合适的位置。
5. 两种设备内存:private 与 coherent
从内核角度看,设备内存至少要区分两类。
5.1 MEMORY_DEVICE_PRIVATE
include/linux/memremap.h 中这样描述:
MEMORY_DEVICE_PRIVATE:
Device memory that is not directly addressable by the CPU: CPU can neither
read nor write private memory.
这类内存典型对应 GPU 私有显存。设备可以高带宽访问它,但 CPU 不能像访问普通 RAM 那样直接读写。
如果一个进程的某个 VA 背后的页面被迁移到了 private device memory,CPU 再访问这个 VA 时应该发生缺页,然后把页面迁回系统内存。
这听起来很像 swap:
CPU 视角:
VA 当前不指向普通 RAM 页
PTE 中放一个特殊 entry
CPU 访问时 fault
fault handler 把内容搬回 RAM
后面第 13 篇会讲 device private entry,它正是把“页面在设备私有内存里”编码进非驻留 PTE。
5.2 MEMORY_DEVICE_COHERENT
MEMORY_DEVICE_COHERENT 表示 CPU 和设备都能 cache coherent 访问的设备内存,常见于更先进的一致性互连,比如 CAPI/CXL 一类平台。
它更接近普通内存,但仍然不是完全无差别的 ZONE_NORMAL:
- 它由设备驱动 hotplug 进来
- 它通常仍然需要可迁移,不能被长期 pin 住
- 内核需要知道这是设备管理的物理地址范围
这也是第 27 篇会继续展开的方向:一致性设备内存让 CPU 直接访问成为可能,但 MM 仍然需要管理“这页属于哪类内存、能否迁移、能否 pin、失败如何处理”。
6. HMM 的两个支柱
HMM 文档把设计概括成两个主要机制。
6.1 地址空间镜像:把 CPU 页表复制到设备页表
第一件事是 shared virtual memory:设备可以使用进程虚拟地址。
HMM 不可能替所有 GPU/FPGA/IOMMU 写设备页表,因为每个硬件的页表格式、命令提交、TLB flush 都不同。
所以 HMM 做的是公共部分:
- 用
mmu_interval_notifier_insert()订阅某段进程 VA - 用
hmm_range_fault()读取 CPU 页表,必要时触发缺页 - 输出
hmm_pfns,告诉驱动每个 VA 对应的 PFN 和权限 - 用
mmu_interval_read_retry()保证读取期间没有并发失效 - CPU 页表变化时,通过 notifier 让驱动清理设备页表
硬件相关部分仍由驱动完成:
- 分配命令 buffer
- 写 GPU/IOMMU 页表更新命令
- invalidate 设备 TLB
- 提交命令并等待完成
这符合内核一贯的边界划分:核心 MM 提供语义和同步协议,设备驱动实现硬件细节。
6.2 设备内存表示:给设备内存创建 struct page
第二件事是把设备内存接入内核 page 模型。
最早的设计尝试过用设备私有数据结构记录迁移到设备的页面。但这样会遇到一个问题:Linux 里太多代码路径都围绕 struct page 工作。
如果设备内存没有 struct page,内核需要在很多地方写特殊分支:
- rmap 如何看它?
- migration 如何处理它?
- GUP 如何识别它?
- refcount/lifecycle 谁维护?
- page fault 如何把它迁回 RAM?
HMM 后来选择了更自然的路线:
为设备内存也创建特殊的
struct page,让大多数 MM 代码仍然看到“这是一个 page”。
这就是 ZONE_DEVICE 和 dev_pagemap 的意义。
7. ZONE_DEVICE:设备内存进入 memmap
Documentation/mm/memory-model.rst 对 ZONE_DEVICE 的描述很关键:
ZONE_DEVICEprovidesstruct pagemem_mapservices for device driver identified physical address ranges.
也就是说,设备驱动拿到一段设备物理地址范围后,可以通过 memremap_pages() / devm_memremap_pages() 给它建立 vmemmap,让内核能够:
pfn_to_page(device_pfn)
page_to_pfn(device_page)
这不是把设备内存变成普通 buddy allocator 里的空闲页。ZONE_DEVICE 页面通常不会 online 成系统可分配内存,也不会像 ZONE_NORMAL 那样进 buddy。
它提供的是“page 元数据服务”:
设备物理地址范围
↓ memremap_pages()
ZONE_DEVICE
↓ vmemmap
struct page / folio
↓
MM 可以用 page 模型引用、迁移、fault、回收相关状态
include/linux/memremap.h 里的 struct dev_pagemap 是控制面:
struct dev_pagemap {
enum memory_type type;
const struct dev_pagemap_ops *ops;
void *owner;
int nr_range;
union {
struct range range;
DECLARE_FLEX_ARRAY(struct range, ranges);
};
};
dev_pagemap_ops 中有后续 HMM 章节会反复出现的回调:
struct dev_pagemap_ops {
void (*folio_free)(struct folio *folio);
vm_fault_t (*migrate_to_ram)(struct vm_fault *vmf);
int (*memory_failure)(struct dev_pagemap *pgmap,
unsigned long pfn,
unsigned long nr_pages,
int mf_flags);
void (*folio_split)(struct folio *head, struct folio *tail);
};
其中 migrate_to_ram() 专门用于 device private memory:CPU 访问设备私有页时,fault handler 要把页面迁回 CPU 可访问内存。
8. test_hmm:把概念串成真实 API
lib/test_hmm.c 是内核自测模块,也是理解 HMM API 的最短路径之一。
8.1 注册设备内存
test_hmm 会构造 dev_pagemap:
devmem->pagemap.range.start = res->start;
devmem->pagemap.range.end = res->end;
devmem->pagemap.type = MEMORY_DEVICE_PRIVATE;
devmem->pagemap.nr_range = 1;
devmem->pagemap.ops = &dmirror_devmem_ops;
devmem->pagemap.owner = mdevice;
ptr = memremap_pages(&devmem->pagemap, numa_node_id());
如果测试的是 coherent 类型,也会设置:
devmem->pagemap.type = MEMORY_DEVICE_COHERENT;
这段代码就是第 11、12 篇的入口:
- 第 11 篇讲
ZONE_DEVICE如何给设备 PFN 创建struct page - 第 12 篇讲
dev_pagemap如何描述设备内存范围、类型、回调和 owner
8.2 镜像 CPU 页表
第 9 篇已经看过 test_hmm 的典型流程:
range->notifier_seq = mmu_interval_read_begin(range->notifier);
mmap_read_lock(mm);
ret = hmm_range_fault(range);
mmap_read_unlock(mm);
mutex_lock(&dmirror->mutex);
if (mmu_interval_read_retry(range->notifier,
range->notifier_seq)) {
mutex_unlock(&dmirror->mutex);
continue;
}
这段代码解决的是“设备想访问某段 VA,如何获得当前 PFN 和权限”。
8.3 在系统内存和设备内存之间迁移
test_hmm 还使用 migrate_vma_*():
ret = migrate_vma_setup(&args);
// driver 分配目标页并复制内容
migrate_vma_pages(&args);
migrate_vma_finalize(&args);
这段代码解决的是“这段 VA 背后的页面能否搬到设备内存,或者从设备内存搬回系统内存”。
这就是 HMM 的完整闭环:
设备内存注册:memremap_pages() / dev_pagemap
地址空间镜像:mmu_interval_notifier + hmm_range_fault()
数据位置迁移:migrate_vma_setup/pages/finalize
CPU 回访处理:device private entry + migrate_to_ram()
9. 为什么不是简单 pin 住用户页
很多设备驱动最早解决用户内存访问的方式是 pin 用户页:
get_user_pages()
DMA map pinned pages
设备访问这些 pages
短期看这很直接,但它不是 HMM 想要的长期模型。
长期 pin 会带来几个问题:
- 页面不能迁移,NUMA balancing、compaction、memory hotplug 都会受影响
- 文件页 truncate、hole punch、COW 等语义变复杂
- 内存回收无法自由回收这些页
- 设备访问的是固定系统内存,无法透明迁到高带宽设备内存
- 大量长期 pin 会破坏 MM 的“页面可移动”假设
HMM 的目标不是“把页面钉死给设备用”,而是:
设备可以访问进程地址空间,但 MM 仍然保有迁移、回收、权限变化和生命周期管理能力。
这就是为什么前两篇 MMU Notifier 很重要。设备页表不是一次建好永久有效,而是必须跟随 CPU MM 变化。
10. 为什么不是把显存直接当普通内存
另一个直觉方案是:既然设备内存很大,为什么不直接把 GPU 显存 online 成普通内存?
对 MEMORY_DEVICE_PRIVATE 来说,这行不通,因为 CPU 不能直接访问它。
普通 Linux 内存必须满足很多前提:
- CPU 可以 load/store
- CPU 可以处理普通缺页后直接访问
- 内核可以在需要时复制、清零、checksum、原子操作
- 页面可以进入普通 allocator 和 LRU 流程
GPU 私有显存不满足这些条件。CPU 访问它需要 fault,然后由驱动把内容迁回 RAM。
对 MEMORY_DEVICE_COHERENT 来说,情况更接近普通内存,但仍然需要保留“这是设备管理的内存”这一事实。因为它可能有不同的 pin 规则、迁移策略、错误处理和性能特征。
所以 HMM 不是把设备内存伪装成完全普通的 ZONE_NORMAL,而是建立一个中间层:
不是普通 RAM
也不是驱动私有黑盒
而是带 struct page 的 ZONE_DEVICE
这正是“让设备内存成为一等公民”的含义:一等公民不是没有差异,而是差异能被 MM 明确建模。
11. HMM 与前 9 篇的关系图
到这一篇为止,前面的机制都开始汇合:
| 前置机制 | HMM 如何使用 |
|---|---|
VMA / mm_struct |
设备订阅和镜像的是进程地址空间 |
struct page / PFN |
HMM 用 PFN/struct page 表达系统页和设备页 |
| 页表 / PTE | 设备页表镜像 CPU 页表状态 |
| 缺页处理 | 设备访问缺页或 CPU 访问 device private 页都要回到 fault 语义 |
| 页面回收 / swap entry | device private entry 复用“非驻留 PTE”的思想 |
walk_page_range() |
hmm_range_fault() 用它遍历 CPU 页表 |
| page migration | migrate_vma 把页面搬到设备或搬回 RAM |
| MMU notifier | CPU 页表变化时通知设备页表失效 |
| interval notifier | 设备按 VA 区间订阅,并用序列号 retry 处理并发 |
所以 HMM 不是在 MM 旁边另起炉灶,而是把已有 MM 能力重新组合,用于异构设备。

12. 本篇小结:MM 为什么必须进化
传统 MM 假设 CPU 是地址空间的主要消费者。设备最多通过 DMA 访问一段被驱动准备好的 buffer。
异构计算改变了这个前提:
- 设备希望直接使用进程指针
- 数据结构越来越复杂,显式拷贝不可维护
- 高层语言/编译器希望自动 offload
- 设备本地内存带宽远高于系统内存
- 但设备内存又不等同于普通 RAM
- 页面仍然需要迁移、回收、权限控制和生命周期管理
于是 MM 必须从“管理 CPU 可访问的系统内存”进化成:
管理一个进程地址空间中可能位于 CPU RAM、设备私有内存、一致性设备内存之间的页面,并让 CPU 与设备的页表保持同步。
HMM 的价值就在这里:它不是替代 Linux MM,而是让设备内存和设备页表进入 Linux MM 已有的语义框架。
13. 本篇关键代码路径
| 文件 | 核心内容 |
|---|---|
Documentation/mm/hmm.rst |
HMM 动机、split address space、shared address space、设备内存和迁移概览 |
include/linux/hmm.h |
hmm_range、hmm_pfn_flags、hmm_range_fault() 接口 |
include/linux/memremap.h |
memory_type、dev_pagemap、dev_pagemap_ops、MEMORY_DEVICE_PRIVATE/COHERENT |
Documentation/mm/memory-model.rst |
ZONE_DEVICE 的内存模型说明 |
Documentation/mm/physical_memory.rst |
物理内存 zone 分类和 ZONE_DEVICE 定位 |
lib/test_hmm.c |
memremap_pages()、mmu_interval_notifier、hmm_range_fault()、migrate_vma_*() 的自测样例 |
14. 下篇预告
3.2:ZONE_DEVICE:为设备内存创建 struct page
本篇解释了为什么设备内存不能只是驱动私有黑盒,也不能简单冒充普通 RAM。下一篇我们会进入 ZONE_DEVICE:它如何基于 SPARSEMEM_VMEMMAP 给设备 PFN 建立 struct page,memremap_pages() 做了哪些内存热插拔动作,为什么 ZONE_DEVICE 页面不进入普通 buddy,以及 MEMORY_DEVICE_PRIVATE、MEMORY_DEVICE_COHERENT、DAX、P2PDMA 这些类型分别解决什么问题。
15. 思考题
-
对复杂对象图来说,为什么显式 CPU↔GPU 拷贝比简单数组更难维护?
-
shared address space 是否意味着设备应该永远直接访问系统内存?为什么设备本地内存仍然重要?
-
MEMORY_DEVICE_PRIVATE为什么不能被 CPU 当作普通内存直接访问?CPU 访问它时应该发生什么? -
为什么 HMM 选择给设备内存创建特殊
struct page,而不是让驱动维护完全私有的数据结构? -
长期 pin 用户页为什么会破坏 MM 的迁移、回收和内存热插拔能力?
📚 关联阅读
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)