本节看下mmap的驱动实现部分,讲清楚VMA与BO是如何建立关联的,为后续访问时的fault做准备。
3.1 讲了为什么需要 fake offset,3.2 讲了 fake offset 保存在 drm_gem_object.vma_node 中,3.3 讲了 drm_vma_offset_manager 如何依赖 drm_mm 分配和查找 fake offset 区间。
现在用户态已经拿到了 fake offset,下一步就是:
cpu = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, drm_fd, fake_offset);
这一节就沿着 mmap(fd, fake_offset) 往下走,看 DRM core 如何从 fake offset 找回 BO,并为用户态建立一个 VMA。
核心路径是:
用户态 mmap(fd, fake_offset)
-> drm_gem_mmap()
-> drm_vma_offset_exact_lookup_locked()
-> fake offset 找到 vma_node
-> container_of(vma_node) 找到 drm_gem_object
-> drm_vma_node_is_allowed() 权限检查
-> drm_gem_mmap_obj()
-> vma->vm_private_data = obj
-> vma->vm_ops = obj->funcs->vm_ops
-> obj->funcs->mmap() 或默认 GEM mmap 设置
1. 用户态 mmap(fd, offset)
用户态拿到 fake offset 后,会调用标准 Linux mmap():
void *cpu = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
drm_fd,
fake_offset);
这里的几个参数分别对应不同含义:
| drm_fd | DRM 设备文件描述符,例如 /dev/dri/renderD128 |
| fake_offset | 前面 ioctl 返回的 mmap fake offset |
| size | 用户态希望映射的长度,不能超过 vma_node 允许的范围 |
| PROT_READ/WRITE | CPU 侧读写权限请求 |
| MAP_SHARED | 设备映射通常要求共享映射,不走普通 COW 语义 |
对内核来说,fake_offset 最终会进入 VMA 的 vm_pgoff:
fake_offset(byte) -> vma->vm_pgoff(page)
也就是说,drm_gem_mmap() 看到的不是字节 offset,而是 page-based offset:
vma->vm_pgoff = fake_offset >> PAGE_SHIFT
这正好和前面 drm_vma_node_offset_addr() 的方向相反:
创建 offset 时:
node->vm_node.start << PAGE_SHIFT -> fake_offset
mmap 进入内核时:
fake_offset >> PAGE_SHIFT -> vma->vm_pgoff
用户态这一步只表达一个请求:
请把 drm_fd 这个设备文件中,从 fake_offset 开始的一段区域映射到我的进程地址空间。
但用户态并不知道这个 offset 背后对应哪个 BO,也不知道 BO 当前在 VRAM、GTT 还是 system memory。这些都由 DRM/GEM/驱动在内核里处理。
在 DRM 驱动中,设备文件的 file_operations.mmap 通常会走到 drm_gem_mmap()。以 GEM 默认 file operations 为例:
#define DRM_GEM_FOPS \\
... \\
.mmap = drm_gem_mmap, \\
...
因此,用户态的 mmap(drm_fd, fake_offset) 进入 DRM core 后,核心入口就是:
int drm_gem_mmap(struct file *filp, struct vm_area_struct *vma)
这一层开始,fake offset 才真正发挥作用。
2. drm_gem_mmap() 根据 offset 查找 vma_node
drm_gem_mmap() 的第一件事,是根据 vma->vm_pgoff 查找 vma_node。
源码核心逻辑如下:
int drm_gem_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct drm_file *priv = filp->private_data;
struct drm_device *dev = priv->minor->dev;
struct drm_gem_object *obj = NULL;
struct drm_vma_offset_node *node;
int ret;
if (drm_dev_is_unplugged(dev))
return –ENODEV;
drm_vma_offset_lock_lookup(dev->vma_offset_manager);
node = drm_vma_offset_exact_lookup_locked(dev->vma_offset_manager,
vma->vm_pgoff,
vma_pages(vma));
if (likely(node)) {
obj = container_of(node, struct drm_gem_object, vma_node);
if (!kref_get_unless_zero(&obj->refcount))
obj = NULL;
}
drm_vma_offset_unlock_lookup(dev->vma_offset_manager);
if (!obj)
return –EINVAL;
...
}
这段代码里有几个关键动作。
第一,取出当前 DRM file 和 DRM device:
struct drm_file *priv = filp->private_data;
struct drm_device *dev = priv->minor->dev;
priv 代表当前打开的 DRM 文件上下文。后面权限检查时会用它判断当前进程是否被允许 mmap 这个 BO。
第二,用 vma->vm_pgoff 精确查找 vma_node:
node = drm_vma_offset_exact_lookup_locked(dev->vma_offset_manager,
vma->vm_pgoff,
vma_pages(vma));
这里的 vma->vm_pgoff 就是用户态传入的 fake offset 转换成的页号。vma_pages(vma) 是这次 mmap 请求的页数。
为什么用 exact_lookup?因为 mmap() 入口希望用户态传入的是对象 fake offset 的起点,而不是对象内部某个偏移位置。drm_vma_offset_exact_lookup_locked() 会检查:
node = drm_vma_offset_lookup_locked(mgr, start, pages);
return (node && node->vm_node.start == start) ? node : NULL;
也就是说,即使某个 offset 落在某个 BO 的 fake offset 区间内部,只要它不是 node 起点,也会被拒绝。这让 mmap 入口更严格。
第三,通过 container_of() 从 vma_node 找回 drm_gem_object:
obj = container_of(node, struct drm_gem_object, vma_node);
这是整个 fake offset 机制的核心反向映射:
fake offset
-> drm_vma_offset_manager lookup
-> drm_vma_offset_node
-> container_of(vma_node)
-> drm_gem_object
第四,用 kref_get_unless_zero() 防止对象正在销毁:
if (!kref_get_unless_zero(&obj->refcount))
obj = NULL;
查找过程持有 VMA offset manager 的 lookup lock,但对象可能正在最后释放流程中。如果对象引用计数已经归零,说明对象正在销毁,drm_gem_mmap() 会把它视为无效并返回 -EINVAL。
这一节可以总结为一句话:
drm_gem_mmap() 不认识用户态 handle,它只根据 vma->vm_pgoff 这个 fake offset,在 drm_vma_offset_manager 中找到 vma_node,再通过 container_of() 找回 GEM object。
3. 权限检查:drm_vma_node_allow / revoke
找到 drm_gem_object 后,drm_gem_mmap() 还不会立刻建立映射。它先检查当前 drm_file 是否被授权访问这个 vma_node:
if (!drm_vma_node_is_allowed(node, priv)) {
drm_gem_object_put(obj);
return –EACCES;
}
这里的 priv 是当前打开 DRM 设备文件的 struct drm_file。权限检查的含义是:
当前这个 DRM file 是否被允许 mmap 这个 fake offset 对应的 GEM object?
权限记录不在 drm_mm 中,而在 drm_vma_offset_node.vm_files 中。相关结构是:
struct drm_vma_offset_node {
rwlock_t vm_lock;
struct drm_mm_node vm_node;
struct rb_root vm_files;
void *driver_private;
};
其中:
vm_node -> fake offset 区间
vm_files -> 哪些 drm_file 可以 mmap 这个 node
授权通过 drm_vma_node_allow() 完成:
int drm_vma_node_allow(struct drm_vma_offset_node *node,
struct drm_file *tag)
{
return vma_node_allow(node, tag, true);
}
撤销授权通过 drm_vma_node_revoke() 完成:
void drm_vma_node_revoke(struct drm_vma_offset_node *node,
struct drm_file *tag)
检查授权则通过:
bool drm_vma_node_is_allowed(struct drm_vma_offset_node *node,
struct drm_file *tag)
这个设计很关键,因为 fake offset 本身不是安全凭证。用户态即使知道某个 fake offset,也必须满足:
offset 能查到 vma_node
+ 当前 drm_file 在 node->vm_files 里
+ 对象/驱动允许 CPU mmap
+ 后续驱动能建立有效映射
否则 mmap() 会失败。
在常见 GEM handle 创建路径中,驱动或 GEM core 会在对象发布给某个 drm_file 时建立相应授权。后续 handle 关闭或对象释放时,再撤销对应授权。这样一来,fake offset 虽然存在于设备文件 address_space 中,但不是任意进程都能 mmap。
因此本节的关键结论是:
drm_mm 解决“offset 区间不重叠并可查找”,drm_vma_node_allow/revoke/is_allowed 解决“哪个 drm_file 有权 mmap”。二者共同构成 fake offset 机制的安全边界。
4. drm_gem_mmap_obj() 建立 VMA
通过 offset 查找到对象,并通过权限检查后,drm_gem_mmap() 调用:
ret = drm_gem_mmap_obj(obj,
drm_vma_node_size(node) << PAGE_SHIFT,
vma);
这里第二个参数是允许映射的对象大小:
drm_vma_node_size(node) << PAGE_SHIFT
因为 drm_vma_node_size() 返回的是 page-based 大小,所以要左移 PAGE_SHIFT 转成 byte。
drm_gem_mmap_obj() 的职责是:
1. 检查 VMA 请求大小是否超过对象允许映射大小
2. 为这次 VMA 映射增加 GEM object 引用
3. 把 GEM object 绑定到 vma->vm_private_data
4. 设置 vma->vm_ops
5. 调用驱动的 obj->funcs->mmap(),或使用 GEM 默认 mmap 设置
源码如下:
int drm_gem_mmap_obj(struct drm_gem_object *obj, unsigned long obj_size,
struct vm_area_struct *vma)
{
int ret;
if (obj_size < vma->vm_end – vma->vm_start)
return –EINVAL;
drm_gem_object_get(obj);
vma->vm_private_data = obj;
vma->vm_ops = obj->funcs->vm_ops;
if (obj->funcs->mmap) {
ret = obj->funcs->mmap(obj, vma);
if (ret)
goto err_drm_gem_object_put;
WARN_ON(!(vma->vm_flags & VM_DONTEXPAND));
} else {
if (!vma->vm_ops) {
ret = –EINVAL;
goto err_drm_gem_object_put;
}
vm_flags_set(vma, VM_IO | VM_PFNMAP |
VM_DONTEXPAND | VM_DONTDUMP);
vma->vm_page_prot = pgprot_writecombine(
vm_get_page_prot(vma->vm_flags));
vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
}
return 0;
err_drm_gem_object_put:
drm_gem_object_put(obj);
return ret;
}
先看第一步大小检查:
if (obj_size < vma->vm_end – vma->vm_start)
return –EINVAL;
这确保用户态不能映射超过 fake offset node 允许的范围。也就是说,用户态不能拿一个合法 fake offset,然后故意传入更大的 length 越界映射后面的对象。
再看引用计数:
drm_gem_object_get(obj);
这是给当前 VMA 持有一份对象引用。只要这个 VMA 还存在,fault handler 后续就可以安全使用 vma->vm_private_data 里的对象指针。对应释放通常在 vm_ops->close 路径里完成。
接着设置:
vma->vm_private_data = obj;
vma->vm_ops = obj->funcs->vm_ops;
这一步让 VMA 和 GEM object 关联起来。后续 page fault 时,fault handler 可以通过 vmf->vma->vm_private_data 找回对象。
最后分两种情况。
如果驱动提供了 obj->funcs->mmap:
ret = obj->funcs->mmap(obj, vma);
则 GEM core 把具体 VMA 设置交给驱动。复杂驱动通常会走这条路。AMDGPU 设置了:
const struct drm_gem_object_funcs amdgpu_gem_object_funcs = {
...
.mmap = amdgpu_gem_object_mmap,
.vm_ops = &amdgpu_gem_vm_ops,
};
AMDGPU 的 mmap 回调会做一些驱动检查,然后转到 TTM:
static int amdgpu_gem_object_mmap(struct drm_gem_object *obj,
struct vm_area_struct *vma)
{
struct amdgpu_bo *bo = gem_to_amdgpu_bo(obj);
if (amdgpu_ttm_tt_get_usermm(bo->tbo.ttm))
return –EPERM;
if (bo->flags & AMDGPU_GEM_CREATE_NO_CPU_ACCESS)
return –EPERM;
return drm_gem_ttm_mmap(obj, vma);
}
drm_gem_ttm_mmap() 继续调用:
ttm_bo_mmap_obj(vma, bo);
对于没有提供 .mmap 回调、但提供了 vm_ops 的简单 GEM 驱动,GEM core 会走默认设置:
vm_flags_set(vma, VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP);
vma->vm_page_prot = pgprot_writecombine(vm_get_page_prot(vma->vm_flags));
vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
这一步只是设置 VMA 属性和 fault 操作入口,并不一定立刻建立所有 PTE。对于 TTM/AMDGPU 这类驱动,真正把 CPU VA 映射到 backing 的动作通常发生在后续 page fault 中。
到这里,fake offset 的任务已经完成了。它把 mmap(fd, offset) 请求导向了正确的 BO,并让 VMA 绑定到了后续 fault handler 能理解的对象指针。
5. 总结
现在把把整个绑定过程串起来:
用户态 mmap(fd, fake_offset)
-> drm_gem_mmap()
fake_offset -> vma_node -> drm_gem_object
-> drm_gem_mmap_obj()
vma->vm_private_data = drm_gem_object
vma->vm_ops = obj->funcs->vm_ops
-> amdgpu_gem_object_mmap()
-> drm_gem_ttm_mmap()
-> ttm_bo_mmap_obj()
vma->vm_private_data = ttm_buffer_object
vma->vm_ops = amdgpu_gem_vm_ops / TTM vm_ops
用户态看到的是一个 CPU VA;内核 VMA 里保存的是后续 fault 所需的对象指针。之后 CPU 访问这个 CPU VA 时,page fault 才会真正处理 backing:如果是 system/GTT,就映射对应 page;如果是 visible VRAM,就映射 BAR aperture 对应 PFN;如果是 invisible VRAM,AMDGPU 可能尝试迁移到 visible VRAM 或 GTT。
因此,本节的终点不是“CPU 已经访问到了物理内存”,而是:
fake offset 已经被解析成 BO,VMA 已经绑定到 BO,后续 fault handler 已经知道该找谁来建立真实映射。