难度 : 🟢🟡 入门到进阶
预计学习时间 : 1-2小时
前置知识: 操作系统基础、了解指针和内存概念
📋 概述
要深入理解AMDGPU SVM(Shared Virtual Memory)实现,我们需要掌握Linux内核的几个核心概念。本章将介绍虚拟内存管理、页表机制、MMU工作原理以及HMM框架等基础知识。这些知识是理解SVM如何在内核层面工作的关键。
不用担心,我们会用通俗的语言和大量图示来解释这些看似复杂的概念。
2.1 虚拟内存管理基础
为什么需要虚拟内存?
想象一下,如果程序直接使用物理地址会怎样:
问题1: 程序A使用地址0x1000,程序B也想使用 → 冲突!
问题2: 物理内存用完了怎么办?
问题3: 如何保护程序之间不互相干扰?
虚拟内存解决了这些问题。
虚拟地址空间
每个进程都有自己独立的虚拟地址空间:
进程A的视图: 进程B的视图:
+----------------+ +----------------+
| 0x7FFF_FFFF | | 0x7FFF_FFFF | ← 栈
| ... | | ... |
| 0x0040_0000 | | 0x0040_0000 | ← 代码段
+----------------+ +----------------+
↓ ↓
(通过页表转换) (通过页表转换)
↓ ↓
+----------------------------------------+
| 物理内存 (共享) |
| [进程A数据] [进程B数据] [内核] [空闲] |
+----------------------------------------+
Linux虚拟内存布局(x86-64)
0xFFFFFFFFFFFFFFFF ┌──────────────────┐
│ 内核空间 │ (内核代码和数据)
0xFFFF800000000000 ├──────────────────┤
│ 空洞 │ (不可访问)
0x00007FFFFFFFFFFF ├──────────────────┤
│ 栈 (向下增长) │ ← Stack
│ ↓ │
│ ... │
│ ↑ │
│ 堆 (向上增长) │ ← Heap (malloc)
├──────────────────┤
│ 数据段 (.data) │ ← 全局变量
├──────────────────┤
│ 代码段 (.text) │ ← 程序代码
0x0000000000400000 └──────────────────┘
关键概念
1. 虚拟地址 (Virtual Address)
程序使用的地址,不是真实的物理地址。
c
int *ptr = malloc(4); // ptr是虚拟地址,如0x7f8a2c001000
2. 物理地址 (Physical Address)
实际的RAM或设备内存地址。
物理地址: 0x8000_1000 → 对应DDR4的某个芯片的某个位置
3. 页 (Page)
内存管理的基本单位,通常是4KB(4096字节)。
虚拟内存空间 = N个页面
一个页面 = 4096字节
页面对齐: 地址的低12位为0
例如: 0x12345000 是页面起始地址
0x12345001 不是(在页面内的偏移1)
4. 页框 (Page Frame)
物理内存中的一个页大小的块。
2.2 页表和地址转换
页表的作用
页表是虚拟地址到物理地址的映射表:
虚拟地址 --[页表查询]--> 物理地址
多级页表结构(x86-64四级页表)
为了节省内存,现代系统使用多级页表:
虚拟地址 (64位):
┌──────┬──────┬──────┬──────┬──────┬──────────┐
│未使用 │ PGD │ PUD │ PMD │ PTE │ Offset │
│16 bit│ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │
└──────┴──────┴──────┴──────┴──────┴──────────┘
↓ ↓ ↓ ↓ ↓ ↓
页表索引 (每级9位) 页内偏移
地址转换过程:
1. CR3寄存器 → 指向PGD (Page Global Directory)
2. 用虚拟地址的PGD部分索引 → 找到PUD地址
3. 用虚拟地址的PUD部分索引 → 找到PMD地址
4. 用虚拟地址的PMD部分索引 → 找到PTE地址
5. 用虚拟地址的PTE部分索引 → 找到物理页框地址
6. 加上页内偏移 → 得到最终物理地址
图示:
虚拟地址: 0x00007F8A_2C001234
CR3 → PGD[127] → PUD[57] → PMD[88] → PTE[1] → 页框 + 0x234
↓
物理地址: 0x8000_1234
页表项(PTE)的结构
一个页表项不仅包含物理地址,还有很多标志位:
PTE (64位):
┌─────────────────────┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 物理页框号 (PFN) │X│D│A│U│W│P│... │
│ (52位) │N│ │ │/│/│ │ │
└─────────────────────┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│ │ │ │ │ └─ P: Present (是否在内存)
│ │ │ │ └─── R/W: 读/写权限
│ │ │ └───── U/S: 用户/内核
│ │ └─────── A: Accessed (已访问)
│ └───────── D: Dirty (已修改)
└─────────── XN: Execute Never
关键标志位:
- Present (P): 页面是否在物理内存中(如果不在,访问会触发缺页异常)
- Read/Write (R/W): 是否可写
- User/Supervisor (U/S): 用户态是否可访问
- Accessed (A): 是否被访问过(用于LRU算法)
- Dirty (D): 是否被修改过(用于回写)
SVM相关的页表
在SVM中,有两套页表:
CPU侧: GPU侧:
┌──────────────┐ ┌──────────────┐
│ CPU页表 │ │ GPU页表 │
│ (MMU管理) │ │ (IOMMU管理) │
└──────────────┘ └──────────────┘
↓ ↓
同一个虚拟地址 (0x7f8a2c001000)
↓ ↓
┌──────────────┐ ┌──────────────┐
│系统RAM物理地址 │ 或者 │ VRAM物理地址 │
└──────────────┘ └──────────────┘
挑战:如何保持两套页表的一致性?→ 这就是SVM驱动要解决的问题!
2.3 MMU和页面异常
MMU (Memory Management Unit)
MMU是CPU中负责地址转换的硬件单元:
CPU指令: load R1, [0x7f8a2c001234]
↓
虚拟地址
↓
┌──────────────────┐
│ MMU │
│ ┌────────────┐ │
│ │ TLB (缓存) │ │ ← 快速查找最近使用的转换
│ └────────────┘ │
│ ┌────────────┐ │
│ │页表遍历器 │ │ ← TLB miss时查询页表
│ └────────────┘ │
└──────────────────┘
↓
物理地址
↓
访问内存
TLB (Translation Lookaside Buffer)
TLB是页表的缓存,加速地址转换:
访问地址 → 查TLB
↓
TLB命中?
↙ ↘
Yes No
↓ ↓
使用缓存 遍历页表 → 填充TLB
↓ ↓
访问内存 ←───┘
性能影响:
- TLB命中:几个时钟周期
- TLB未命中:几十到上百个时钟周期
页面异常(Page Fault)
当MMU发现页表项的Present位为0时,触发页面异常:
CPU访问地址
↓
MMU查页表
↓
Present = 0?
↓ Yes
触发Page Fault异常
↓
陷入内核
↓
内核Page Fault处理程序
↓
分配物理页框 / 从磁盘加载 / 等
↓
更新页表: Present = 1
↓
返回用户态
↓
CPU重新执行访问 → 成功
页面异常的类型
1. Minor Page Fault(次要缺页)
页面在物理内存中,但页表未建立映射。
c
// 例如:malloc后首次访问
char *p = malloc(4096);
p[0] = 'A'; // 触发minor fault,建立映射
2. Major Page Fault(主要缺页)
页面不在物理内存中,需要从磁盘加载。
c
// 例如:访问被swap到磁盘的页面
// 或者mmap文件后首次访问
3. Segmentation Fault(段错误)
访问非法地址,权限不足等。
c
int *p = NULL;
*p = 42; // Segmentation Fault!
GPU的页面异常
GPU也有类似的机制(需要硬件支持,如AMD的XNACK):
GPU执行: store [0x7f8a2c001234], R1
↓
IOMMU查GPU页表
↓
Present = 0?
↓ Yes
触发GPU Page Fault
↓
发送中断到CPU
↓
驱动处理: svm_range_restore_pages()
↓
迁移页面 / 建立映射
↓
GPU重试访问 → 成功
关键差异:
- CPU页面异常处理在内核中(同步)
- GPU页面异常通过中断通知CPU(异步)
2.4 DMA和IOMMU
DMA (Direct Memory Access)
DMA允许设备直接访问内存,无需CPU参与,CPU不必逐字节搬运数据,提高效率。
传统方式: DMA方式:
┌────┐ ┌────┐
│CPU │ ← 读 ← 设备 │CPU │ (做其他事)
└────┘ └────┘
↓ ↑(完成后告知CPU)
写 → 内存 设备 ──DMA──> 内存
IOMMU (Input/Output Memory Management Unit)
IOMMU是设备的MMU,提供地址转换和保护,具有如下作用:
-
地址转换:设备使用虚拟地址
-
内存保护:设备只能访问授权的内存
-
支持SVM:设备可以使用进程的虚拟地址空间
没有IOMMU:
设备 → 使用物理地址 → 直接访问内存
问题: 设备可以访问任何物理地址,不安全!有IOMMU:
设备 → 使用IOVA → IOMMU转换 → 物理地址
↑
(I/O Virtual Address)
IOMMU在SVM中的角色
进程虚拟地址: 0x7f8a2c001000
↓
CPU访问: MMU转换
↓
GPU访问: IOMMU转换 (使用PASID识别进程)
↓
可能指向不同的物理位置:
- 系统RAM: 通过PCIe访问
- GPU VRAM: 直接访问
PASID (Process Address Space ID)
PASID标识进程的地址空间:
GPU发起访问时携带PASID:
┌──────────────────────────┐
│ PASID=123 | VA=0x7f8a... │
└──────────────────────────┘
↓
IOMMU根据PASID选择页表
↓
使用进程123的页表进行转换
作用:让GPU可以同时处理多个进程的任务,每个使用各自的地址空间。
2.5 HMM框架介绍
什么是HMM?
HMM (Heterogeneous Memory Management) 是Linux内核提供的框架,用于支持异构设备(如GPU)访问系统内存。
引入背景:
- 多种设备(GPU、FPGA、DSP等)需要SVM功能
- 避免每个驱动重复实现相同的功能
- 提供统一的接口和抽象
HMM的核心功能
┌────────────────────────────────┐
│ HMM框架 │
├────────────────────────────────┤
│ • 镜像CPU页表到设备 │
│ • 迁移页面 (RAM ↔ 设备内存) │
│ • 处理页面异常 │
│ • MMU Notifier集成 │
└────────────────────────────────┘
↓ ↓
┌────────┐ ┌────────┐
│GPU驱动 │ │FPGA驱动│ ...
└────────┘ └────────┘
HMM关键数据结构
1. hmm_range
表示一个地址范围:
c
struct hmm_range {
unsigned long start; // 起始虚拟地址
unsigned long end; // 结束虚拟地址
unsigned long *pfns; // 输出:页框号数组
...
};
2. hmm_range_fault()
核心函数,用于查询CPU页表并迁移页面。
c
// AMDGPU SVM中的使用示例
ret = hmm_range_fault(&range);
// 返回后,pfns数组包含每个页面的状态和物理地址
HMM的工作流程
1. 驱动调用hmm_range_fault()
↓
2. HMM遍历CPU页表
↓
3. 对于每个页面:
- 如果不在内存 → 触发minor fault
- 如果只读但需要写 → 触发写时复制
- 记录页面状态到pfns数组
↓
4. (可选) 迁移页面到设备内存
↓
5. 驱动建立设备页表映射
Device Private Memory
HMM支持将设备内存(如GPU VRAM)整合到Linux内存管理中:
c
// 在内核中,VRAM页面也有struct page
struct page *vram_page = pfn_to_page(vram_pfn);
// 但标记为设备私有
page->zone_device_data = svm_bo; // 指向SVM BO
好处:
- 系统可以统一管理所有内存
- 支持页面在RAM和VRAM间迁移
- 可以使用标准的内存管理API
MMU Notifier
HMM内部使用MMU Notifier监听CPU页表变化:
c
// 在kfd_svm.c中
static const struct mmu_interval_notifier_ops svm_range_mn_ops = {
.invalidate = svm_range_cpu_invalidate_pagetables,
};
// 注册监听
mmu_interval_notifier_insert(&prange->notifier, mm,
start, length,
&svm_range_mn_ops);
工作原理:
CPU修改页表 (如munmap, mprotect)
↓
内核调用mmu_notifier_invalidate_range()
↓
HMM通知所有注册的notifier
↓
AMDGPU驱动的回调: svm_range_cpu_invalidate_pagetables()
↓
驱动使GPU页表失效,保持一致性
💡 重点提示
-
页表是核心:理解页表结构是理解SVM的基础。CPU页表和GPU页表需要保持一致。
-
页面异常是关键机制:无论是CPU还是GPU,页面异常都是延迟分配和按需迁移的基础。
-
IOMMU使SVM成为可能:没有IOMMU,设备只能使用物理地址,无法共享虚拟地址空间。
-
HMM简化了实现:使用HMM框架,驱动不需要从头实现所有功能。
-
异步是挑战:GPU页面异常是异步的,需要复杂的同步机制。
⚠️ 常见误区
❌ 误区1:"虚拟地址转换很慢"
- ✅ 正确理解:有TLB缓存,大多数情况下转换很快(几个周期)。
❌ 误区2:"IOMMU就是设备的MMU"
- ✅ 正确理解:功能类似,但IOMMU在系统芯片组中,不在设备内。
❌ 误区3:"HMM是AMD专有的"
- ✅ 正确理解:HMM是Linux内核通用框架,Nvidia、Intel等也使用。
❌ 误区4:"页表遍历需要4次内存访问"
- ✅ 正确理解:现代CPU有页表缓存(page walk cache),通常更快。
📝 实践练习
-
地址转换练习 :
给定虚拟地址
0x00007f8a_2c001234,画出四级页表查询过程。 -
思考题:
- 为什么需要多级页表而不是单级?
- TLB的命中率对性能有多大影响?
- GPU页表可以和CPU页表完全相同吗?
-
代码阅读:
bash# 查看HMM相关代码 ls mm/hmm.c grep -n "hmm_range_fault" mm/hmm.c -
实验(需要root):
bash# 查看进程的内存映射 cat /proc/self/maps # 查看页表统计 cat /proc/meminfo | grep -i pagetable
📚 本章小结
- 虚拟内存:每个进程有独立的虚拟地址空间,通过页表映射到物理内存
- 页表:多级结构,每个PTE包含物理地址和标志位
- MMU/IOMMU:CPU和设备的地址转换单元,支持虚拟地址
- 页面异常:访问未映射页面时触发,是按需分配的基础
- HMM框架:Linux内核提供的异构内存管理框架,简化SVM实现
这些基础知识是理解AMDGPU SVM实现的必备前提。
📖 扩展阅读
- Linux 页表机制详解(x86_64 架构)
- AMDGPU页表机制
- Linux HMM(Heterogeneous Memory Management)原理与实现详解
- MMU Notifier详细分析
➡️ 下一步
掌握了Linux内核基础后,我们将在下一章探讨AMDGPU驱动的整体架构,了解SVM在驱动中的位置以及与其他组件的关系。
🔗 导航
- 上一章:01 - 什么是SVM
- 下一章: 03 - AMDGPU驱动架构概览
- 返回目录: AMD ROCm-SVM技术的实现与应用深度分析目录