1. 概述
drm_gpusvm_pages 是 drm_gpusvm 框架中管理 物理页面 DMA 映射状态 的核心数据结构。它封装了一段虚拟地址范围对应的 DMA 地址数组、设备页面映射信息以及有效性状态标志,是 GPU 访问 CPU 内存(或设备内存)的"物理层数据载体"。
设计定位 :如果说 drm_gpusvm_range 负责管理"哪段虚拟地址被 GPU 追踪"(VA 范围管理),那么 drm_gpusvm_pages 负责管理"这些页面的 DMA 地址是什么、是否有效"(物理映射管理)。
务必理解该概述!!!
2. 数据结构定义
这里有两个核心的数据结构:drm_gpusvm_pages和drm_gpusvm_pages_flags。drm_gpusvm_pages_flags表示page的状态。
2.1 drm_gpusvm_pages
c
struct drm_gpusvm_pages {
struct drm_pagemap_addr *dma_addr; // DMA 地址数组(每个元素带地址+类型+order)
struct drm_pagemap *dpagemap; // 设备页面的 drm_pagemap(仅 devmem 页面时非 NULL)
unsigned long notifier_seq; // MMU notifier 序列号(用于失效检测)
struct drm_gpusvm_pages_flags flags; // 状态标志
};
各字段详解:
dma_addr --- DMA 地址数组:
- 类型是
drm_pagemap_addr,不是简单的dma_addr_t。每个元素编码了:addr:实际的 DMA 地址proto:互联类型(DRM_INTERCONNECT_SYSTEM或设备互联)order:大页 order(支持 HugePage,一个条目可以覆盖 2^order 个页面)dir:DMA 方向(DMA_TO_DEVICE/DMA_BIDIRECTIONAL)
- 通过
kvmalloc_array(npages, sizeof(*dma_addr))分配 - 注意:数组索引
j和页面索引i不一定相同,因为大页条目可跨多个页面
dpagemap --- 设备页面映射:
- 当页面全部来自同一个设备内存(VRAM)时指向对应的
drm_pagemap - 约束:一个 pages 内只允许一个
drm_pagemap(即所有设备页面必须来自同一设备) - 对于纯系统内存页面,此字段为 NULL
notifier_seq --- 通知器序列号:
- 记录最后一次成功
get_pages时的 MMU notifier 序列号 - 初始值为
LONG_MAX(表示从未获取过页面) - 用于
mmu_interval_read_retry()检测 CPU 页表是否在获取后发生了变化
这个字段的理解,离不开SVM机制的一个核心技术:mmu notifier。还没有理解该技术的请学习:Linux MMU Notifier 机制与应用系列。
flags --- 原子状态标志:
- 大多数标志的设置/清除需要在
notifier_lock保护下进行。具体flags参见2.2。
2.2 drm_gpusvm_pages_flags
c
// include/drm/drm_gpusvm.h
struct drm_gpusvm_pages_flags {
union {
struct {
/* 创建时设置的标志 */
u16 migrate_devmem : 1; // 是否允许迁移到设备内存
/* 以下标志在 notifier_lock 保护下设置/清除 */
u16 unmapped : 1; // 页面已被 MMU notifier 标记为失效
u16 partial_unmap : 1; // 仅部分页面被 unmap(VMA 部分重叠)
u16 has_devmem_pages : 1; // 包含设备私有页面(VRAM)
u16 has_dma_mapping : 1; // DMA 映射已建立
};
u16 __flags; // 用于 WRITE_ONCE/READ_ONCE 的原子字段
};
};
标志的分类:
| 标志 | 设置时机 | 含义 |
|---|---|---|
migrate_devmem |
创建时(不可变) | 该 pages 是否支持设备内存迁移 |
unmapped |
invalidate 回调中 | MMU notifier 通知 CPU 页表已失效 |
partial_unmap |
invalidate 回调中 | VMA 只部分覆盖了 range,需要后续拆分 |
has_devmem_pages |
get_pages 成功后 | 页面来自 ZONE_DEVICE(设备私有内存) |
has_dma_mapping |
get_pages 成功后 | 已完成 DMA 映射,dma_addr 数组有效 |
3. 操作接口
drm_gpusvm_pages 提供了 两层 API:底层独立 API 和高层 Range 包装 API。
3.1 底层独立 API(Standalone Pages API)
这组接口直接操作 drm_gpusvm_pages *,不依赖 drm_gpusvm_range:
3.1.1 drm_gpusvm_get_pages() --- 获取并 DMA 映射页面
c
int drm_gpusvm_get_pages(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages,
struct mm_struct *mm,
struct mmu_interval_notifier *notifier,
unsigned long pages_start,
unsigned long pages_end,
const struct drm_gpusvm_ctx *ctx);
功能:这是整个页面管理的核心函数。执行 HMM range fault 获取 CPU PFN,然后建立 DMA 映射。
执行流程:
drm_gpusvm_get_pages()
│
├─ 1. 乐观检查:如果 pages 已有效,直接跳到 set_seqno
│ drm_gpusvm_pages_valid_unlocked(svm_pages)
│
├─ 2. 分配 HMM PFN 数组
│ pfns = kvmalloc_array(npages, sizeof(*pfns))
│
├─ 3. HMM Range Fault(获取 CPU 物理页面)
│ hmm_range_fault(&hmm_range) ← 在 mmap_read_lock 下执行
│ // 如果 -EBUSY 则重试(带超时)
│
├─ 4. 在 notifier_lock 下执行 DMA 映射
│ drm_gpusvm_notifier_lock(gpusvm)
│ │
│ ├─ 检查 unmapped 标志 → 如果已 unmap 则 -EFAULT
│ ├─ 检查 mmu_interval_read_retry → 如果失效则释放锁重试
│ ├─ 分配 dma_addr 数组(如果尚未分配)
│ │
│ ├─ 遍历每个 HMM PFN:
│ │ ├─ device_private/coherent page:
│ │ │ dpagemap->ops->device_map(page, order, dir)
│ │ │ → 编码为设备互联类型的 drm_pagemap_addr
│ │ │
│ │ └─ 普通系统页面:
│ │ dma_map_page(dev, page, 0, size, dir)
│ │ → 编码为 DRM_INTERCONNECT_SYSTEM 的 drm_pagemap_addr
│ │
│ ├─ 设置 flags: has_dma_mapping = true
│ ├─ 如果有设备页面: has_devmem_pages = true, dpagemap = ...
│ ├─ WRITE_ONCE(flags.__flags)
│ │
│ drm_gpusvm_notifier_unlock(gpusvm)
│
└─ 5. 记录 notifier_seq
svm_pages->notifier_seq = hmm_range.notifier_seq
关键设计点:
- DMA 映射在 notifier_lock 下执行:防止在映射过程中页面被释放。notifier 回调会阻塞在 notifier_lock 上,或先 unmap DMA
- 混合页面约束 :同一 pages 中不允许混合不同
drm_pagemap的设备页面,也不允许设备页面和系统页面混合(代码中对此返回-EOPNOTSUPP) - 大页支持 :通过
drm_gpusvm_hmm_pfn_to_order()检测 CPU 大页映射,单个dma_addr条目可覆盖多个页面 - 乐观短路 :如果 pages 已有效(
valid_unlocked),直接更新notifier_seq返回
3.1.2 drm_gpusvm_unmap_pages() --- 撤销 DMA 映射
c
void drm_gpusvm_unmap_pages(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages,
unsigned long npages,
const struct drm_gpusvm_ctx *ctx);
功能 :撤销 DMA 映射但保留 dma_addr 数组(可重新 get_pages 复用)。
内部实现 (__drm_gpusvm_unmap_pages):
对每个 dma_addr[j]:
├─ DRM_INTERCONNECT_SYSTEM → dma_unmap_page(dev, addr, size, dir)
└─ 设备页面 → dpagemap->ops->device_unmap(dpagemap, dev, addr)
清除标志: has_devmem_pages = false, has_dma_mapping = false
置空: svm_pages->dpagemap = NULL
锁定策略:
- 如果
ctx->in_notifier = true:假设已持有notifier_lock写锁 - 如果
ctx->in_notifier = false:自动获取notifier_lock读锁
3.1.3 drm_gpusvm_free_pages() --- 释放 DMA 映射 + 数组
c
void drm_gpusvm_free_pages(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages,
unsigned long npages);
功能 :unmap_pages + 释放 dma_addr 数组。这是完全清理。
实现:
c
void drm_gpusvm_free_pages(...)
{
drm_gpusvm_notifier_lock(gpusvm);
__drm_gpusvm_unmap_pages(gpusvm, svm_pages, npages);
__drm_gpusvm_free_pages(gpusvm, svm_pages); // kvfree(dma_addr); dma_addr = NULL
drm_gpusvm_notifier_unlock(gpusvm);
}
可以看到在获取page的DMA地址时,使用了dpagemap->ops的device_map和device_unmap的操作。这是与drm_pagemap发生关联的地方。关于dpagemap的分析请移步:drm_pagemap 框架设计分析。
3.2 Range 包装 API
这组接口操作 drm_gpusvm_range *,内部委托给底层 API 操作 range->pages:
3.2.1 drm_gpusvm_range_get_pages()
c
int drm_gpusvm_range_get_pages(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_range *range,
const struct drm_gpusvm_ctx *ctx);
实现(纯转发):
c
return drm_gpusvm_get_pages(gpusvm, &range->pages, gpusvm->mm,
&range->notifier->notifier,
drm_gpusvm_range_start(range),
drm_gpusvm_range_end(range), ctx);
自动从 range 提取 mm、notifier、start/end 参数。
3.2.2 drm_gpusvm_range_unmap_pages()
c
void drm_gpusvm_range_unmap_pages(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_range *range,
const struct drm_gpusvm_ctx *ctx);
自动计算 npages 并委托给 drm_gpusvm_unmap_pages(&range->pages, npages, ctx)。
3.2.3 drm_gpusvm_range_pages_valid()
c
bool drm_gpusvm_range_pages_valid(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_range *range);
直接调用 drm_gpusvm_pages_valid(gpusvm, &range->pages)。
3.3 辅助/内部接口
drm_gpusvm_pages_valid() --- 有效性检查
c
static bool drm_gpusvm_pages_valid(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages)
{
lockdep_assert_held(&gpusvm->notifier_lock);
return svm_pages->flags.has_devmem_pages || svm_pages->flags.has_dma_mapping;
}
必须在 notifier_lock 下调用。如果既没有设备页面也没有 DMA 映射,则视为无效。
drm_gpusvm_pages_valid_unlocked() --- 无锁有效性检查
c
static bool drm_gpusvm_pages_valid_unlocked(struct drm_gpusvm *gpusvm,
struct drm_gpusvm_pages *svm_pages)
{
if (!svm_pages->dma_addr)
return false;
drm_gpusvm_notifier_lock(gpusvm);
pages_valid = drm_gpusvm_pages_valid(gpusvm, svm_pages);
if (!pages_valid)
__drm_gpusvm_free_pages(gpusvm, svm_pages); // 顺便清理
drm_gpusvm_notifier_unlock(gpusvm);
return pages_valid;
}
先做无锁的快速路径检查(dma_addr == NULL?),然后短暂获取锁检查标志。如果无效则顺便释放 dma_addr 数组。
drm_gpusvm_range_set_unmapped() --- 标记为已 unmap
c
void drm_gpusvm_range_set_unmapped(struct drm_gpusvm_range *range,
const struct mmu_notifier_range *mmu_range)
{
lockdep_assert_held_write(&range->gpusvm->notifier_lock);
range->pages.flags.unmapped = true;
if (drm_gpusvm_range_start(range) < mmu_range->start ||
drm_gpusvm_range_end(range) > mmu_range->end)
range->pages.flags.partial_unmap = true;
}
在 MMU notifier 的 invalidate 回调中调用,标记该 range 的页面已失效。
4. 生命周期状态机
┌─────────────────────────────────────────────────────────────┐
│ drm_gpusvm_pages 生命周期 │
└─────────────────────────────────────────────────────────────┘
[初始化]
notifier_seq = LONG_MAX
dma_addr = NULL
flags = { migrate_devmem = X } ← 只有 migrate_devmem 被设置
│
▼
┌───────────┐ drm_gpusvm_get_pages()
│ EMPTY │ ──────────────────────────────────► ┌──────────────┐
│ │ HMM fault + DMA map │ MAPPED │
│ dma_addr │ │ │
│ = NULL │ │ dma_addr = ✓ │
│ has_dma │ │ has_dma = 1 │
│ = 0 │ ◄────────────────────────────────── │ notifier_seq │
└───────────┘ drm_gpusvm_free_pages() │ = valid_seq │
▲ (unmap + kvfree dma_addr) └──────┬───────┘
│ │
│ │ MMU notifier
│ │ invalidate
│ ▼
│ ┌──────────────┐
│ drm_gpusvm_free_pages() │ INVALIDATED │
│◄────────────────────────────────────────────│ │
│ │ unmapped = 1 │
│ │ has_dma = 0 │
│ │ (after unmap)│
│ └──────────────┘
│ │
│ │ 重新
│ │ get_pages()
│ ▼
│ ┌──────────────┐
│ │ RE-MAPPED │
│◄────────────────────────────────────────────│ │
drm_gpusvm_free_pages() │ 新的 dma_addr│
│ has_dma = 1 │
└──────────────┘
关键状态转换:
- EMPTY → MAPPED :
drm_gpusvm_get_pages()成功后 - MAPPED → INVALIDATED :MMU notifier 触发 → driver
invalidate()回调 →drm_gpusvm_range_set_unmapped()+drm_gpusvm_range_unmap_pages() - INVALIDATED → MAPPED :重新
drm_gpusvm_get_pages()--- 但对于unmapped=true的 pages,get_pages()会返回-EFAULT - 任意状态 → EMPTY :
drm_gpusvm_free_pages()完全清理
注意:当
unmapped = true时,get_pages()内部检测到会返回-EFAULT,意味着这个 pages 已经不能重用了。对于 range 场景,驱动需要 remove 旧 range 并创建新 range。
5. 与 drm_gpusvm_range 的关系
5.1 组合关系
c
struct drm_gpusvm_range {
struct drm_gpusvm *gpusvm;
struct drm_gpusvm_notifier *notifier;
struct kref refcount;
struct interval_tree_node itree; // ← VA 范围管理
struct list_head entry;
struct drm_gpusvm_pages pages; // ← 物理页面管理(内嵌)
};
drm_gpusvm_range 和 drm_gpusvm_pages 是 组合(composition) 关系:
- Range 负责:VA 范围追踪(interval tree)、notifier 关联、引用计数、生命周期管理
- Pages 负责:DMA 地址数组、页面有效性标志、notifier 序列号
- Range 包含 Pages 作为内部状态成员
5.2 职责分离示意
┌─────────────────────────────────────────────────────────┐
│ drm_gpusvm_range │
│ │
│ ┌─ VA 管理层 ──────────────────────────────────────┐ │
│ │ itree.start / itree.last (VA 范围) │ │
│ │ notifier (MMU 通知器) │ │
│ │ refcount (引用计数) │ │
│ │ entry (链表遍历) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ 物理映射层 (pages) ─────────────────────────────┐ │
│ │ dma_addr[] (DMA 地址数组) │ │
│ │ dpagemap (设备 pagemap) │ │
│ │ notifier_seq (失效检测序列号) │ │
│ │ flags (状态标志) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
5.3 两层 API 对应关系
Range API (高层) Pages API (底层)
┌──────────────────────┐ ┌──────────────────────┐
│ range_get_pages() │ ────────► │ get_pages() │
│ 自动提取 mm, │ │ 需要显式传入 mm, │
│ notifier, start, │ │ notifier, start, │
│ end from range │ │ end │
├──────────────────────┤ ├──────────────────────┤
│ range_unmap_pages() │ ────────► │ unmap_pages() │
│ 自动计算 npages │ │ 需要传入 npages │
├──────────────────────┤ ├──────────────────────┤
│ range_pages_valid() │ ────────► │ pages_valid() │
│ │ │ (static) │
├──────────────────────┤ ├──────────────────────┤
│ range_remove() │ │ free_pages() │
│ 内部调用 unmap + │ │ unmap + kvfree │
│ free_pages + │ │ dma_addr │
│ remove from tree │ │ │
└──────────────────────┘ └──────────────────────┘
6. 独立使用场景:Xe userptr
drm_gpusvm_pages 的独立 API 的一个重要用例是 Xe 驱动的 userptr:
c
// drivers/gpu/drm/xe/xe_userptr.h
struct xe_userptr {
struct list_head invalidate_link;
struct list_head repin_link;
struct drm_gpusvm_pages pages; // ← 独立使用,不在 range 内
struct mmu_interval_notifier notifier; // ← 自己管理 notifier
bool initial_bind;
};
为什么 userptr 不用 range?
- userptr 有自己的 VMA 管理(
xe_userptr_vma),不参与 SVM 的 interval tree - userptr 有自己的
mmu_interval_notifier,不需要 drm_gpusvm 的 notifier 层 - 只需要复用 DMA 映射逻辑(HMM fault → DMA map → validity tracking)
用法示例:
c
// Pin pages(获取 DMA 映射)
int xe_vma_userptr_pin_pages(struct xe_userptr_vma *uvma)
{
return drm_gpusvm_get_pages(&vm->svm.gpusvm, &uvma->userptr.pages,
uvma->userptr.notifier.mm,
&uvma->userptr.notifier,
xe_vma_userptr(vma),
xe_vma_userptr(vma) + xe_vma_size(vma),
&ctx);
}
// Invalidate 时撤销映射
drm_gpusvm_unmap_pages(&vm->svm.gpusvm, &uvma->userptr.pages,
xe_vma_size(vma) >> PAGE_SHIFT, &ctx);
// 销毁时完全释放
drm_gpusvm_free_pages(&vm->svm.gpusvm, &uvma->userptr.pages,
xe_vma_size(vma) >> PAGE_SHIFT);
这种设计使得 drm_gpusvm 可以在 非完整 SVM 模式 下使用:
c
// drm_gpusvm_init() 注释:
// "If only using the simple drm_gpusvm_pages API (get/unmap/free),
// then only @gpusvm, @name, and @drm are expected."
即只需初始化 gpusvm 的 name 和 drm(不需要 mm、ops、notifiers、ranges),就可以使用 pages API。
7. dma_addr 数组的编码格式
每个 dma_addr[j] 是 struct drm_pagemap_addr,编码方式:
系统内存页面
c
svm_pages->dma_addr[j] = drm_pagemap_addr_encode(
dma_addr, // dma_map_page() 返回的地址
DRM_INTERCONNECT_SYSTEM, // 互联类型
order, // CPU 大页 order
dma_dir // DMA 方向
);
设备私有页面
c
svm_pages->dma_addr[j] = dpagemap->ops->device_map(
dpagemap, dev, page, order, dma_dir
);
// 返回的 drm_pagemap_addr 的 proto 不是 DRM_INTERCONNECT_SYSTEM
8. 设计总结
| 设计维度 | 描述 |
|---|---|
| 职责 | 管理一段虚拟地址范围的 DMA 映射状态 |
| 核心数据 | dma_addr[] 数组 + 有效性标志 |
| 双模使用 | 可嵌入 range 使用(SVM),也可独立使用(userptr) |
| 并发安全 | notifier_lock 保护写入,WRITE_ONCE/READ_ONCE 支持乐观读 |
| 大页支持 | drm_pagemap_addr.order 字段支持合并连续大页 |
| 混合约束 | 同一 pages 内不允许混合不同设备/不同 pagemap 的页面 |
| 复用机制 | unmap_pages 只撤销映射不释放数组,允许重新 get_pages |
drm_gpusvm_pages 是 drm_gpusvm 框架的 物理层抽象。通过将 DMA 映射管理从 VA 范围管理中解耦出来,实现了:
- SVM range 和 userptr 共享同一套 DMA 映射代码
- 独立的有效性追踪,支持细粒度的 per-range 失效检测
- 灵活的生命周期管理(unmap 后可 re-map,不必重建整个 range)