第二章已经把 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 的管理者的实现