drm_gpusvm_pages — svm range物理页面映射状态管理者的实现详细分析

1. 概述

drm_gpusvm_pages 是 drm_gpusvm 框架中管理 物理页面 DMA 映射状态 的核心数据结构。它封装了一段虚拟地址范围对应的 DMA 地址数组、设备页面映射信息以及有效性状态标志,是 GPU 访问 CPU 内存(或设备内存)的"物理层数据载体"。

设计定位 :如果说 drm_gpusvm_range 负责管理"哪段虚拟地址被 GPU 追踪"(VA 范围管理),那么 drm_gpusvm_pages 负责管理"这些页面的 DMA 地址是什么、是否有效"(物理映射管理)。

务必理解该概述!!!

2. 数据结构定义

这里有两个核心的数据结构:drm_gpusvm_pagesdrm_gpusvm_pages_flagsdrm_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 提取 mmnotifierstart/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  │
                                                     └──────────────┘

关键状态转换

  1. EMPTY → MAPPEDdrm_gpusvm_get_pages() 成功后
  2. MAPPED → INVALIDATED :MMU notifier 触发 → driver invalidate() 回调 → drm_gpusvm_range_set_unmapped() + drm_gpusvm_range_unmap_pages()
  3. INVALIDATED → MAPPED :重新 drm_gpusvm_get_pages() --- 但对于 unmapped=true 的 pages,get_pages() 会返回 -EFAULT
  4. 任意状态 → EMPTYdrm_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_rangedrm_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."

即只需初始化 gpusvmnamedrm(不需要 mmopsnotifiersranges),就可以使用 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 范围管理中解耦出来,实现了:

  1. SVM range 和 userptr 共享同一套 DMA 映射代码
  2. 独立的有效性追踪,支持细粒度的 per-range 失效检测
  3. 灵活的生命周期管理(unmap 后可 re-map,不必重建整个 range)