本文是第三章的导读:承接 3.1 GEM 概述,在进入 3.2 drm_gem_object 的字段详解之前,用 Linux 文件的直觉把设备、drm_file、gem bo 对象、handle、mmap 与回调表串成一条线。
阅读提示:本文只建立直觉,涉及的机制都在后续小节展开。遇到暂不理解的术语可先跳过,读完后续内容再回来看这篇。
初学者看drm_gem_object常被它的一堆字段和回调劝退。但如果你用过 Linux 的open()/mmap(),其实已经有了理解它所需的直觉:把一块 GPU(drm_device)看作一个大文件。第二章已经讲过,这在 VFS 层并不只是比喻,DRM 就是这样实现的。
1. drm_device 就是"一个大文件"
用户态访问 GPU,要先打开一个设备节点。本专栏走 render node/dev/dri/renderD128(见 2.1 ),同一块 GPU 通常还有 primary 节点/dev/dri/card0。节点有多个,"文件"却只有一个。2.2 讲过,drm_open()会执行:
filp->f_mapping=dev->anon_inode->i_mapping;同一设备所有节点、所有 open 实例的f_mapping都指向设备的同一个address_space。从 mmap 的角度看,它们打开的是同一个文件。也正因如此,对象迁移或设备拔出时,内核可以在这一个address_space上撤销所有进程对该对象的 CPU 映射。
这个文件的偏移空间中,有一段窗口由设备的dev->vma_offset_manager管理,专门分配给对象:在 64 位系统上从 4 GiB 处开始,大小约 1 TiB(见 3.3 §1)。
2. drm_file 就是对这个文件的一次 open()
每次open()设备节点,内核创建一个drm_file,它与 VFS 的struct file(open file description)一一对应,挂在filp->private_data上。和普通文件一样(见 2.1.3 §1):
- 隔离的边界是open 实例,不是进程:同一进程 open 两次,得到两个独立的
drm_file; dup()、fork()产生的 fd 共享同一个drm_file;libdrm_amdgpu 的设备去重也会让同一进程内的多个组件共享一个drm_file(见 2.2 §9)。
drm_file不"拥有"对象,它只持有一张句柄表object_idr,把 per-file 的 32 位 handle 映射到共享的drm_gem_object。两个drm_file里的 handle 1 可以指向不同的对象。
3. drm_gem_object 就是文件里的一段区间
需要 CPU 映射的对象,会在设备的偏移空间里分配一段按页对齐、互不重叠的区间,记录在对象的vma_node中。这个偏移不对应任何物理地址,只是"大文件"里的一个位置,因此称为伪偏移(fake offset)。
于是:一个drm_gem_object≈ 设备这个大文件里的一段区间;handle 是某个 open 实例给它起的名字。
区间是对象的属性,一个对象只有一段;handle 是 open 实例的属性,同一个对象可以在多个drm_file中各有 handle。图中的 objB 就被两个drm_file同时引用。
4. mmap:按偏移找到对象,再交给对象自己
一次 mmap 的完整步骤见下图:①② 在用户态,③④⑤ 由设备唯一的 file 级.mmap入口drm_gem_mmap()完成。
结合文件类比,有两点值得强调:
- 偏移不需要保密,权限靠 handle(④)。建 handle 时
drm_vma_node_allow()把该drm_file加入对象的允许列表,其他drm_file即使拿到偏移值也会被拒绝。handle 决定谁能看见对象,偏移决定它在文件里的位置,mmap 两者都要。 - 入口只有一个,做法由对象决定(⑤)。未实现
funcs->mmap时,由 core 按默认方式设置 VMA,缺页交给vm_ops->fault。
5. 为什么是 funcs:file 接口细化到每个对象
第 4 节的步骤 ⑤ 揭示了drm_gem_object_funcs的本质。对象的后备存储千差万别:shmem 页(drm_gem_shmem_object)、物理连续内存(drm_gem_dma_object)、VRAM 与 GTT(ttm_buffer_object),但 core 只有一套流程。解决办法是给每个对象挂一张函数指针表funcs,让同一段 core 代码对不同对象表现出不同行为,相当于 C++ 的虚函数表,是对象级的多态。
其中一部分回调,正是把 file 级操作下放到对象:
| 回调 | 对应的 file 级操作 |
|---|---|
mmap/vm_ops | 设备 fd 的mmap()(fops->mmap) |
open/close | 在某个drm_file中建立、释放一个 handle |
export | PRIME_HANDLE_TO_FD:对象被包进 dma-buf 自己的匿名struct file |
status/rss | 读取/proc/<pid>/fdinfo/<fd>(fops->show_fdinfo,见 2.1.3 §6);amdgpu 用自己的统计实现,不经过这两个回调 |
其余回调由内核内部事件触发,不对应 file 级入口:free在refcount归零时调用,pin/get_sg_table由 dma-buf 导入方触发,vmap由需要 CPU 地址的内核代码触发,evict由驱动的回收路径触发。完整分组见 3.2.1 drm_gem_object_funcs:回调的分组、功能与调用时机。
6. 类比的边界
"对象 = 文件里的一段区间"在偏移空间这一层成立,但 GEM 对象不是磁盘文件里的静态字节:
| 维度 | 磁盘文件的一段内容 | GEM 对象 |
|---|---|---|
| 内容落点 | 固定在文件里 | 后备页会迁移、被驱逐、缺页时按需填入:偏移稳定,物理落点动态 |
| 归属 | 属于这个文件 | 可被多个drm_file引用,经 dma-buf 还可跨进程、跨设备共享 |
| 寿命 | 随文件 | 由refcount决定,独立于任何drm_file |
另有四点要摆正:
- "大文件"是
drm_device,不是某个设备节点,也不是某个drm_file。各节点只是它的入口,drm_file只是对它的一次 open。 - 有两种"文件"。设备这个大文件提供偏移空间,用来定位对象;对象的后备存储可能是另一个文件,shmem 对象的
obj->filp就是一个真实的 shmem 文件,页从那里来。前者回答"在哪里找到它",后者回答"内容存在哪里"。 - handle 与偏移是两层名字。handle 是 per-file 的,ioctl 用它指称对象;偏移是全设备的,mmap 用它定位对象。两者经 mmap 的权限检查连在一起(第 4 节的步骤 ④)。
- 类比只覆盖身份与 CPU 视图。按 3.1 §4 的划分,对象还有 GPU 视图(
gpuva,见 3.4)与设备间视图(dma_buf/import_attach,见 3.5),这两类访问不经过设备的偏移空间,不在本类比的适用范围内。
7. 小结
把drm_device看作一个大文件:
- 它的偏移空间被切成"每对象一段",每段就是一个
drm_gem_object; drm_file是对它的一次open(),用 per-file 的 handle 引用这些共享对象;- 设备唯一的
.mmap入口按偏移找到对象、确认本 open 实例持有 handle,再交给对象的funcs完成映射,这就是对象级多态。
有了这个模型,再读 3.2 drm_gem_object 的字段详解,vma_node(它在文件里的位置)、handle_count(它有几个名字)、refcount(它的寿命)、funcs(它的虚函数表)、filp(shmem 后备存储)就都各归其位了。