专栏导读:为什么需要从 MM 理解 HMM
一个真实的困境
假设你是一个 GPU 计算框架的开发者。用户写了这样一段代码:
float*data=malloc(1GB);// ... 填充数据 ...gpu_kernel<<<grid,block>>>(data);// 希望 GPU 直接访问 data在传统编程模型下,这不可能工作——GPU 有自己的显存(VRAM),CPU 的malloc返回的指针对 GPU 毫无意义。程序员必须手动管理数据搬移:
//h:host, d:device. 就是我们常说的 h2d,d2hfloat*h_data=malloc(1GB);// CPU 内存float*d_data=gpu_malloc(1GB);// GPU 显存memcpy_to_gpu(d_data,h_data,1GB);// 显式拷贝gpu_kernel<<<grid,block>>>(d_data);// 用 GPU 指针memcpy_from_gpu(h_data,d_data,1GB);// 拷贝回来这套"显式拷贝"模型有几个致命问题:
- 编程复杂度高— 程序员必须手动管理两套指针和数据一致性
- 无法处理指针追踪— 如果数据结构包含指针(链表、树),拷贝后指针全部失效
- 过度拷贝— 无法知道 GPU 实际会访问哪些页面,只能全量拷贝
- 与系统接口不兼容—
fork()、mmap()、信号处理等都可能修改地址空间,驱动无从得知
理想状态是:CPU 和 GPU 共享同一个虚拟地址空间,指针在两边通用,数据按需自动迁移。
这就是 HMM 要解决的问题。
什么是 HMM
HMM(Heterogeneous Memory Management)是 Linux 内核内存管理子系统的一组扩展,它让设备(GPU、FPGA、SmartNIC 等)能够:
- 镜像进程页表— 设备维护一份与 CPU 一致的地址映射,进程用同一个虚拟地址在 CPU 和设备间通信
- 感知页表变化— CPU 侧的
munmap、mremap、COW 等操作会自动通知设备更新映射 - 双向迁移页面— 页面可以在 CPU RAM 和设备内存之间按需迁移,对应用透明
- 让设备内存参与内核框架— 设备内存拥有
struct page,可以被内核的迁移、回收等框架管理
HMM不是一个独立的子系统,而是对现有 MM 机制的一系列精准扩展。它的代码量很小(核心仅 ~700 行),但它依赖的基础设施横跨整个 MM。
为什么必须从 MM 理解 HMM
很多开发者试图直接阅读mm/hmm.c,然后迅速迷失——因为 HMM 的每一行代码都在调用 MM 的底层接口:
| HMM 做的事 | 依赖的 MM 基础设施 |
|---|---|
| 遍历进程页表获取物理地址 | 五级页表结构、walk_page_range()框架 |
| 解码"页面在设备内存中" | 非驻留 PTE 编码(device private entry) |
| 保持设备映射与 CPU 一致 | MMU Notifier 序列号协议 |
| 迁移页面到设备内存 | migrate_vma*()三阶段迁移框架 |
| 让设备内存有 struct page | ZONE_DEVICE、dev_pagemap |
| 代替设备触发缺页 | handle_mm_fault()+FAULT_FLAG_REMOTE |
如果你不理解这些基础设施,HMM 的代码就是一堆无法解读的函数调用。反过来,如果你沿着 MM 的进化脉络学习,HMM 的每个设计决策都变得顺理成章。
MM 的进化脉络
Linux MM 并非一开始就具备管理设备内存的能力。它是随着硬件需求的变化,一步步进化而来的:
注意每一步进化都是在前一步的基础上扩展,而非推倒重来:
mmu_notifier最初是为 KVM 设计的,HMM 直接复用它来通知设备migrate_pages()最初是为 NUMA 均衡设计的,HMM 扩展出migrate_vma*()支持设备迁移swap entry编码最初只有 swap 和 migration 两种,HMM 新增了 device private/exclusive entry
HMM 的设计哲学就是"复用而非重造"。这也是为什么理解 MM 基础是掌握 HMM 的必经之路。
硬件背景:谁在用 HMM
GPU(主要消费者)
| 厂商 | 驱动 | HMM 使用方式 |
|---|---|---|
| AMD | amdgpu / KFD | hmm_range_fault()+migrate_vma*()实现 SVM(ROCm) |
| Intel | Xe | 通过drm_gpusvm框架使用 HMM |
| NVIDIA | Nouveau(开源) | nouveau_svm使用 HMM 做 SVM |
CXL 设备
CXL(Compute Express Link)设备提供 CPU 可直接访问的扩展内存。内核用DEVICE_COHERENT类型的 ZONE_DEVICE 管理,未来可能成为 HMM 最大的应用场景。
其他
- FPGA— 可通过 HMM 共享进程地址空间
- SmartNIC / DPU— RDMA + 设备内存管理
- 持久化内存(PMEM)— 虽然不用 HMM,但共享 ZONE_DEVICE 基础设施
本专栏的学习路径
我们把 HMM 的知识体系分为8 层,沿进化脉络从底向上:
每一层我们都会:
- 讲清经典 MM 是怎么做的— 建立基础心智模型
- 指出"不够"在哪里— 面对设备内存时的局限
- 展示如何扩展— 内核社区的解决方案
这样当你最终读到mm/hmm.c时,每一行代码都不再陌生。
前置知识
本专栏假设你具备:
- C 语言基础— 能读懂内核代码(指针、位操作、宏)
- 操作系统概念— 虚拟内存、页表、中断等基本概念
- 基本的内核阅读能力— 知道如何浏览内核源码树
不需要你已经精通 MM 或 GPU 驱动——这些正是本专栏要教的。
关键源码版本
本专栏基于Linux 6.x内核源码。HMM 相关代码在近几年持续演进,核心文件包括:
| 文件 | 内容 |
|---|---|
mm/hmm.c | HMM 核心实现(~700 行) |
include/linux/hmm.h | HMM 公共 API |
mm/migrate_device.c | 设备迁移框架 |
mm/memremap.c | ZONE_DEVICE 实现 |
lib/test_hmm.c | HMM 测试模块(最佳学习参考) |
下篇预告
第 1 篇:虚拟地址空间与页表——每个进程的私有世界
我们将从 MM 最基础的概念开始:进程如何拥有自己的虚拟地址空间?页表如何将虚拟地址翻译为物理地址?五级页表的结构是什么样的?
这些看似"老生常谈"的基础,恰恰是 HMMhmm_range_fault()遍历页表时的核心路径。打好这个基础,后面的一切才能事半功倍。
