欢迎光临
我们一直在努力

3.1 用户态访问 BO 的 CPU VA的 fake offset 机制全流程解析

第二章已经把 BO 放进了多地址空间的整体框架里:同一个 BO,可以被 CPU 通过 CPU VA 访问,也可以被 GPU 通过 GPU VA 访问,还可能涉及 DMA address 和物理页。本章我们聚焦一个问题:用户态进程怎样拿到一个可以读写 BO 的 CPU 虚拟地址? 即用户态创建 BO 后,手里已经有一个 handle,怎么通过 handle ,让内核返回一个 CPU VA?

在linux驱动中,用户态访问 bo 用的是mmap接口,这里假设你已经熟悉该接口的工作原理。如果还没有清楚该接口的原理,请先搞清楚。

本节我想先把这条从 handle 获取VA的链路讲清楚:

GEM handle
-> ioctl 查询/创建 fake offset
-> mmap(drm_fd, fake_offset)
-> drm_gem_mmap() 根据 fake offset 找回 GEM object
-> 建立 VMA
-> page fault 时映射 BO 当前 backing
-> 用户态获得可访问的 CPU VA

理解这条链路,是阅读后面章节 drm_gem_object.vma_node、drm_vma_offset_manager、drm_mm 和 AMDGPU/TTM fault 路径的前提。

1. 用户态申请创建 BO 后到底拿到了什么

用户态创建一个 BO 后,返回值不是内核指针,也不是物理地址,而是一个 handle。

以 dumb buffer 为例,用户态调用:

struct drm_mode_create_dumb create = {
.width = width,
.height = height,
.bpp = 32,
};

ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);

成功后,用户态得到:

create.handle;
create.pitch;
create.size;

其中最关键的是 handle。它是用户态后续操作这个 BO 的入口,例如关闭、导出、查询 mmap offset 等。

但是 handle 的本质是:当前 DRM file 下的一个对象 ID。

它不是地址,也不是文件的 offset,更不是 PFN。它只在某个 drm_file 的 handle 表里有意义:

进程 A 的 drm_file
handle 1 -> GEM object X
handle 2 -> GEM object Y

进程 B 的 drm_file
handle 1 -> GEM object Z
handle 2 -> GEM object X

也就是说,同一个整数 handle,在不同进程里可以指向不同对象;同一个 GEM object,在不同进程里也可以有不同 handle。

所以 handle 解决的是:

用户态如何安全引用一个内核 GEM object。

它没有解决:

mmap() 所需的offset哪里来,如何从一个 offset 找到这个 GEM object。

这就是 fake offset 出场的原因。接下来我们先简单看下mmap() 需要的参数的含义和来源。

2. 为什么 handle 不能直接 mmap

Linux 的 mmap() 系统调用接口是:

void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);

对 DRM 来说,fd 通常是 /dev/dri/cardX 或 /dev/dri/renderDX 的设备文件描述符。问题是:一个 DRM fd 下面可以创建很多 BO,mmap() 必须知道这次要映射哪一个。

标准 mmap() 接口没有 handle 参数,只有 offset 参数。因此 DRM 需要把“要映射哪个 BO”编码进 offset。

直接把 handle 当 offset 有几个问题。

  • handle 是 per-file 的对象 ID,不是全局唯一地址。mmap() 的 offset 查找发生在 DRM 设备的 address_space 里,它需要一个能在地址空间(或者说设备文件)中定位对象的值,而不是某个进程 handle 表中的 ID。
  • handle 生命周期和 mmap offset 生命周期不同。handle 可以关闭,但 VMA 映射可能仍然存在;对象可能被 dma-buf、framebuffer、内核引用继续持有。把 handle 直接当 mmap 键,会让对象引用和地址映射生命周期纠缠在一起。
  • handle 是用户对象引用能力,mmap offset 是 VMA 路由能力。前者回答“这个进程是否持有对象”,后者回答“这个 mmap offset 对应哪个对象”。它们需要权限关联,但不应该是同一个值。

因此 GEM 选择了两步走:

handle
-> ioctl 验证 handle 合法性
-> 为对象分配或查询 fake offset
-> 用户态拿 fake offset 调 mmap

这一步把 handle 机制和 mmap 机制解耦了。接下来就是我们的主角了:fake offset。

3. fake offset 是什么

fake offset 是 DRM/GEM 为 mmap 创造出来的一段伪文件偏移。

它看起来像普通文件 offset:

void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, drm_fd, fake_offset);

但它不是普通文件里的真实字节偏移,也不是显存物理地址。它的作用更像一个“路由键”:

fake offset
-> drm_vma_offset_manager
-> drm_vma_offset_node
-> drm_gem_object

在内核中,每个支持 mmap 的 GEM object 都可以拥有一个 vma_node:

struct drm_gem_object {
...
struct drm_vma_offset_node vma_node;
...
};

vma_node 会被插入到 DRM 设备的 drm_vma_offset_manager 中。这个 manager 管理一段专门为 GEM mmap 准备的 fake offset 空间。用户态拿到的 offset,本质上就是 vma_node 在这段空间中的起点。

可以把它想成这样:

DRM fake mmap offset space

0x100000000 ───────────────┐
│ GEM object A 的 vma_node
0x100010000 ───────────────┘

0x100020000 ───────────────┐
│ GEM object B 的 vma_node
0x100040000 ───────────────┘

用户态并不知道 vma_node 的存在,只知道驱动 ioctl 返回了一个 offset。随后它把这个 offset 传给 mmap(),DRM core 再通过 offset 反查到对应的 GEM object。

所以 fake offset 的准确含义是:

DRM 设备文件 address_space 中的一段伪 offset,用来把用户态 mmap(fd, offset) 请求路由到某个 GEM object。

这里有一个重要的点要澄清:fake offset 不是物理地址。这是最容易误解的地方。

fake offset 不是 VRAM 地址,不是 GTT 地址,也不是 DMA address。它只存在于 CPU mmap 建立映射之前的对象查找阶段。

对比一下几类地址:

名称属于哪一层作用
handle GEM 用户引用层 当前进程引用 BO 的 ID
fake offset GEM mmap 路由层 让 mmap() 找到 GEM object
CPU VA 用户进程虚拟地址空间 mmap() 返回给用户态的可访问指针
resource start TTM/驱动资源层 BO 当前 backing 在 VRAM/GTT/system 中的位置
GPU VA GPU 页表层 GPU shader/SDMA 访问 BO 时使用的虚拟地址

用户态 mmap 的前半段只处理前三者:

handle -> fake offset -> CPU VA

BO 当前真正位于哪里,是后半段 fault 或驱动 mmap 回调要处理的问题:

CPU VA fault -> BO resource -> PFN / iomem / page

因此,当我们说 drm_vma_offset_node 内部有 drm_mm_node 时,不能把它理解成“这个 BO 有一个 drm_mm_node 表示显存物理区间”。在 fake offset 机制里,这个 drm_mm_node 表示的是 mmap offset 空间中的一段区间。

这也是为什么一个 VRAM BO 仍然可以有 vma_node:

  • vma_node 负责 mmap offset;
  • VRAM/GTT resource 负责真实 backing;
  • 两者不是同一个地址空间。

到这里我觉得fake offset的概念已经非常清晰了,接下来让我们从用户态和内核两个视角看下fake offset的生命周期。

4. fake offset 的典型用户态流程

以 dumb buffer 为例,用户态通常分三步获得 CPU VA。

4.1 创建 BO,获得 handle

struct drm_mode_create_dumb create = {
.width = 1920,
.height = 1080,
.bpp = 32,
};

ret = ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
if (ret)
return ret;

uint32_t handle = create.handle;
uint64_t size = create.size;

此时用户态只有 handle,还没有 CPU VA。

4.2 通过 handle 查询 mmap fake offset

struct drm_mode_map_dumb map = {
.handle = handle,
};

ret = ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);
if (ret)
return ret;

uint64_t fake_offset = map.offset;

这一步的内核语义是:

handle
-> drm_gem_object_lookup()
-> 找到 GEM object
-> 确保 object 有 vma_node offset
-> 返回 drm_vma_node_offset_addr(&obj->vma_node)

4.3 使用 fake offset 调用 mmap,获得 CPU VA

void *cpu = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
fake_offset);

if (cpu == MAP_FAILED)
return errno;

成功后,cpu 才是用户态能读写的 CPU VA。

此时仍要注意:mmap() 返回 CPU VA,并不意味着所有真实物理页都已经立刻映射好了。很多 TTM/GEM 驱动会在 page fault 时再根据 BO 当前 backing 插入 PTE。这是本专栏后续章节一个重要内容。

5. 内核侧的两段式路径

上面是从用户态视角看到的流程,从内核角度看,fake offset 机制也可以分成两段。

第一段:创建或查询 fake offset。

用户态 ioctl(handle)
-> handle 表查找 GEM object
-> drm_gem_create_mmap_offset() 或驱动等价逻辑
-> drm_vma_offset_add()
-> obj->vma_node 获得 fake offset 区间
-> 返回 offset 给用户态

第二段:mmap 时使用 fake offset 找回对象。

用户态 mmap(fd, fake_offset)
-> drm_gem_mmap()
-> drm_vma_offset_exact_lookup_locked()
-> fake offset 找到 drm_vma_offset_node
-> 权限检查 drm_vma_node_is_allowed()
-> 找到 drm_gem_object
-> drm_gem_mmap_obj()
-> 设置 VMA / vm_ops

这两段路径共同完成一件事:把一个没有独立文件描述符的 GEM object,安全地接入 Linux 标准 mmap() 模型。

6. 与 drm_mm 的关系

该章后面会讲drm_vma_offset_manager和 drm_mm,但这里先提一下。更详细的分析见接下来的章节,或者阅读"关联阅读"中给出的材料。

drm_vma_offset_manager 内部有:

struct drm_vma_offset_manager {
rwlock_t vm_lock;
struct drm_mm vm_addr_space_mm;
};

每个 drm_vma_offset_node 内部又有:

struct drm_vma_offset_node {
rwlock_t vm_lock;
struct drm_mm_node vm_node;
struct rb_root vm_files;
void *driver_private;
};

这里的 drm_mm 管理的是 fake offset 空间。它负责:

  • 找一段未被占用的 offset 区间;
  • 保证不同 BO 的 fake offset 不重叠;
  • 在 BO 销毁时释放 offset 区间;
  • 支持后续根据 offset 查找对应 node。

drm_mm 作为 drm_vma_offset_manager 的底层区间分配器,用于管理 GEM mmap fake offset 空间。

这里多提一嘴,drm_mm 在AMD的驱动中还有另外一个应用:AMDGPU GTT manager 中使用 drm_mm管理GTT空间的分配。

从这两个drm_mm 的应用,可以理解下drm_mm 通用的区间管理器这个说法的含义。drm_mm管理的空间到底是什么地址空间,有什么用途,要看它的使用者的业务。

7. 本篇小结

用户态访问 BO 的 CPU VA,不是从 handle 一步跳过去的,而是经过了一条清晰的中间路径:

handle -> fake offset -> mmap -> CPU VA -> fault -> BO backing

其中:

  • handle 是当前进程引用 GEM object 的 ID;
  • fake offset 是 DRM mmap 机制用来反查 GEM object 的伪 offset;
  • CPU VA 是 mmap() 返回给用户态的虚拟地址;
  • BO 的真实 backing 位于 system memory、GTT、visible VRAM 或其他驱动资源中;
  • fake offset 不等于物理地址,也不等于 VRAM/GTT resource start。

接下来:

  • 3.2 会进入 drm_gem_object 和 vma_node,解释 fake offset 在对象结构中如何保存;
  • 3.3 会看 drm_mm 如何分配 fake offset 区间;
  • 3.4 再沿着 drm_gem_mmap() 路径,看用户态 mmap() 如何从 fake offset 找回 BO 并建立 VMA。

最后的最后,给张总结图。 在这里插入图片描述


📚 关联阅读

  • linux IDR 机制及相关API 介绍:介绍了 handle 的内核实现原理
  • linux mmap 的匿名映射和设备映射:介绍了 mmap 的一个用法
  • drm_vma_offset_manager VMA管理器: 分析了 fake offset 的管理者的实现
赞(0)
未经允许不得转载:171主机测评 » 3.1 用户态访问 BO 的 CPU VA的 fake offset 机制全流程解析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址