欢迎光临
我们一直在努力

3.4 从 fake offset 到 CPU VA:drm_gem_mmap() 路径

本节看下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);

这里的几个参数分别对应不同含义:

参数在 GEM mmap 中的含义
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 已经知道该找谁来建立真实映射。

赞(0)
未经允许不得转载:171主机测评 » 3.4 从 fake offset 到 CPU VA:drm_gem_mmap() 路径
分享到: 更多 (0)

评论 抢沙发

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