本节讲清楚用户态看到的offset存储在哪里,是有谁管理以及怎么生成的。本节会涉及到amdgpu bo的创建流程的上半部分—初始化gem部分,可以用来理解前两章的kgd_mem的层级关系。
3.1 已经把用户态访问 BO 的 CPU VA 链路搭起来了:
handle -> fake offset -> mmap -> CPU VA -> fault -> BO backing
其中 fake offset 是 mmap() 找回 GEM object 的关键。现在问题往前推进一步:
- 它保存在 BO 的哪个字段里?
- fake offset 什么时候创建?
- fake offset 什么时候销毁?
本节围绕 AMDGPU 的 BO 创建路径展开,但主题仍然限定在 CPU mmap 这条线:
amdgpu_gem_create_ioctl()
-> amdgpu_gem_object_create()
-> amdgpu_bo_create()
-> drm_gem_private_object_init()
-> drm_vma_node_reset(&obj->vma_node)
-> ttm_bo_init_reserved()
-> drm_vma_offset_add(&bo->base.vma_node)
先说结论:fake offset 存储在vma_node字段中。
- vma_node 是 drm_gem_object 的公共字段,不是 AMDGPU 私有字段;
- 对 AMDGPU/TTM 的普通用户态 BO 来说,fake offset 通常在 BO 初始化阶段就被加入 VMA offset manager,而不是等到真正 mmap() 时才临时创建。
1. vma_node 在哪里
vma_node 位于 GEM 对象基类 struct drm_gem_object 中:
struct drm_gem_object {
...
struct drm_vma_offset_node vma_node;
...
};
它的注释已经把职责说得很清楚:
/**
* @vma_node:
*
* Mapping info for this object to support mmap. Drivers are supposed to
* allocate the mmap offset using drm_gem_create_mmap_offset(). The
* offset itself can be retrieved using drm_vma_node_offset_addr().
*
* Memory mapping itself is handled by drm_gem_mmap(), which also checks
* that userspace is allowed to access the object.
*/
struct drm_vma_offset_node vma_node;
这里有三个关键点。
第一,vma_node 是为了 support mmap,不是为了描述 BO 的真实物理位置。
第二,mmap offset 可以通过 drm_vma_node_offset_addr() 取出。
第三,真正的 mmap 建立由 drm_gem_mmap() 处理,它会检查用户态是否有权限访问该对象。
所以 vma_node 的职责可以概括为:
给一个 GEM object 挂上一段 fake offset 区间,使用户态后续可以通过 mmap(fd, offset) 找回这个对象。
2. vma_node 内部是什么
vma_node 的类型是 struct 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;
};
它内部有两个核心部分。
2.1 vm_node:fake offset 区间
struct drm_mm_node vm_node;
这个 drm_mm_node 记录的是对象在 fake offset 空间中的区间:
vm_node.start -> fake offset 的页号起点
vm_node.size -> 这个对象占用多少页 offset 空间
注意这里的 start 是 page-based,不是 byte-based。返回给用户态 mmap 的 byte offset 时,需要左移 PAGE_SHIFT:
static inline __u64 drm_vma_node_offset_addr(struct drm_vma_offset_node *node)
{
return ((__u64)node->vm_node.start) << PAGE_SHIFT;
}
面纱终于揭开,所以用户态看到的 fake offset 就是来自:
obj->vma_node.vm_node.start << PAGE_SHIFT
2.2 vm_files:访问权限
这里提一下这个vm_files,虽然不是这里的重点。
struct rb_root vm_files;
这个红黑树记录哪些 drm_file 被允许 mmap 这个 node。也就是说,fake offset 不是裸奔的全局入口;即使用户态猜到了某个 offset,drm_gem_mmap() 还会检查当前 file 是否被授权。
权限相关 API 包括:
int drm_vma_node_allow(struct drm_vma_offset_node *node,
struct drm_file *tag);
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);
所以 vma_node 同时承担两个任务:
vma_node
-> vm_node : fake offset 区间
-> vm_files : 哪些 drm_file 可以 mmap
3. AMDGPU BO 的继承关系
AMDGPU 的 BO 不是直接继承 drm_gem_object,而是通过 TTM 多包了一层:
struct amdgpu_bo
-> struct ttm_buffer_object tbo
-> struct drm_gem_object base
-> struct drm_vma_offset_node vma_node
因此在 AMDGPU 中访问 vma_node 的路径通常是:
bo->tbo.base.vma_node
而不是:
bo->vma_node
这点很重要。因为它说明 vma_node 属于 GEM 公共对象层,而不是 AMDGPU VRAM/GTT resource 层。
AMDGPU 提供了一个小封装来返回 mmap offset:
static inline u64 amdgpu_bo_mmap_offset(struct amdgpu_bo *bo)
{
return drm_vma_node_offset_addr(&bo->tbo.base.vma_node);
}
这里也能看出:AMDGPU 返回给用户态的 offset,就是 GEM 基类中 vma_node 的 offset。
4. vma_node 的初始化与赋值流程
4.1 初始化 GEM 基类并 reset vma_node
AMDGPU BO 创建时,会先分配 struct amdgpu_bo,然后初始化里面的 GEM 基类:
bo = kvzalloc(bp->bo_ptr_size, GFP_KERNEL);
if (bo == NULL)
return –ENOMEM;
drm_gem_private_object_init(adev_to_drm(adev), &bo->tbo.base, size);
bo->tbo.base.funcs = &amdgpu_gem_object_funcs;
drm_gem_private_object_init() 内部会做基础字段初始化,其中包括:
drm_vma_node_reset(&obj->vma_node);
drm_vma_node_reset() 的作用是把 node 清成初始状态:
static inline void drm_vma_node_reset(struct drm_vma_offset_node *node)
{
memset(node, 0, sizeof(*node));
node->vm_files = RB_ROOT;
rwlock_init(&node->vm_lock);
}
这一步只表示:
vma_node 这个字段可以安全使用了。
它还不等于:
这个 BO 已经有了 fake offset。
reset 之后,vma_node.vm_node.start 和 vma_node.vm_node.size 还没有被分配到 drm_vma_offset_manager 的 offset 空间里。真正把它加入 manager 的动作发生在后面的 TTM 初始化中。
4.2 TTM 为 device BO 分配 fake offset
AMDGPU 随后调用:
r = ttm_bo_init_reserved(&adev->mman.bdev, &bo->tbo, bp->type,
&bo->placement, page_align, &ctx, NULL,
bp->resv, bp->destroy);
ttm_bo_init_reserved() 会根据 BO type 决定是否给它分配 VMA offset:
if (bo->type == ttm_bo_type_device || bo->type == ttm_bo_type_sg) {
ret = drm_vma_offset_add(bdev->vma_manager,
&bo->base.vma_node,
PFN_UP(bo->base.size));
if (ret)
goto err_put;
}
这里就是 AMDGPU 普通用户态 BO 获得 fake offset 的关键点。
对 ttm_bo_type_device 来说,TTM 注释是:
/**
* @ttm_bo_type_device: These are 'normal' buffers that can
* be mmapped by user space. Each of these bos occupy a slot in the
* device address space, that can be used for normal vm operations.
*/
也就是说,普通用户态 BO 会占用 DRM 设备 address space 中的一段 slot。这个 slot 就是后续用户态 mmap 使用的 fake offset 区间。
AMDGPU 用户态 GEM 创建路径传入的正是 ttm_bo_type_device:
r = amdgpu_gem_object_create(adev, size, args->in.alignment,
initial_domain,
flags, ttm_bo_type_device,
resv, &gobj, fpriv->xcp_id + 1);
dumb buffer 创建路径也是一样:
r = amdgpu_gem_object_create(adev, args->size, 0, domain, flags,
ttm_bo_type_device, NULL, &gobj,
fpriv->xcp_id + 1);
所以,对于 AMDGPU 用户态可 mmap 的普通 BO,fake offset 的创建可以理解为:
drm_gem_private_object_init()
-> reset vma_node
ttm_bo_init_reserved(type = ttm_bo_type_device)
-> drm_vma_offset_add(bdev->vma_manager,
&bo->base.vma_node,
PFN_UP(bo->base.size))
-> vma_node 获得 fake offset 区间
5. 哪些 BO 不会分配用户态 mmap offset
这个认知很重要,并不是所有 AMDGPU BO 都会被用户态 mmap。因为有些bo不需要被用户态访问。从TTM框架的视角,TTM 的 BO type 至少有三类:
enum ttm_bo_type {
ttm_bo_type_device,
ttm_bo_type_kernel,
ttm_bo_type_sg
};
其中:
- ttm_bo_type_device:普通用户态可 mmap BO,会分配 fake offset;
- ttm_bo_type_sg:导入/共享类 BO,也会分配 fake offset;
- ttm_bo_type_kernel:内核专用 BO,不面向用户态 mmap,不会在 ttm_bo_init_reserved() 中调用 drm_vma_offset_add()。
所以要区分两件事:
drm_vma_node_reset()
-> 初始化 vma_node 字段
drm_vma_offset_add()
-> 真正把 vma_node 加入 fake offset 空间
内核 BO 也会因为 GEM 基类初始化而 reset vma_node,但通常不会获得用户态 mmap offset。
6. 返回 fake offset 给用户态
AMDGPU 中查询 mmap offset 的路径可以通过 dumb mmap 或 AMDGPU_GEM_MMAP 走到同一个 helper:
int amdgpu_mode_dumb_mmap(struct drm_file *filp,
struct drm_device *dev,
uint32_t handle, uint64_t *offset_p)
{
struct drm_gem_object *gobj;
struct amdgpu_bo *robj;
gobj = drm_gem_object_lookup(filp, handle);
if (!gobj)
return –ENOENT;
robj = gem_to_amdgpu_bo(gobj);
if (amdgpu_ttm_tt_get_usermm(robj->tbo.ttm) ||
(robj->flags & AMDGPU_GEM_CREATE_NO_CPU_ACCESS)) {
drm_gem_object_put(gobj);
return –EPERM;
}
*offset_p = amdgpu_bo_mmap_offset(robj);
drm_gem_object_put(gobj);
return 0;
}
这里的逻辑很清晰:
用户态传入 handle
-> drm_gem_object_lookup(filp, handle)
-> 得到 drm_gem_object
-> 转成 amdgpu_bo
-> 检查是否允许 CPU mmap
-> amdgpu_bo_mmap_offset(robj)
-> drm_vma_node_offset_addr(&bo->tbo.base.vma_node)
-> 返回 fake offset
这个过程只返回 offset,不建立 CPU 页表,也不决定 BO 当前在 VRAM 还是 GTT。
换句话说:
AMDGPU_GEM_MMAP / dumb map offset ioctl 返回的是“下一次 mmap 用的路由键”,不是“CPU 已经可以访问的地址”。
用户态还必须继续调用:
cpu = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, fake_offset);
mmap() 成功后返回的 cpu 才是 CPU VA。
7. fake offset 的释放
fake offset 既然是分配出来的,也必须释放。
对 TTM BO 来说,释放路径中会调用:
drm_vma_offset_remove(bdev->vma_manager, &bo->base.vma_node);
它和创建时的:
drm_vma_offset_add(bdev->vma_manager, &bo->base.vma_node,
PFN_UP(bo->base.size));
构成一对。
生命周期可以概括为:
BO 创建
-> drm_vma_node_reset()
-> drm_vma_offset_add()
-> fake offset 可被查询并用于 mmap
BO 最后一个引用释放
-> drm_vma_offset_remove()
-> fake offset 区间归还给 manager
这也说明 fake offset 的生命周期跟 BO 内核对象绑定,而不是跟某个 handle 完全绑定。handle 关闭后,BO 如果仍被其他引用持有,VMA offset 不一定立即消失;BO 真正释放时,offset 才会被移除。
8. vma_node 与真实 backing store 的边界
到这里需要再次把边界划清楚。
vma_node 管的是:
用户态 mmap offset 空间
它不管:
BO 当前在 VRAM、GTT 还是 system memory
AMDGPU BO 创建时,ttm_bo_init_reserved() 除了分配 VMA offset,还会调用 ttm_bo_validate(),根据 placement 给 BO 分配真实 resource。这个 resource 可能是:
- TTM_PL_VRAM:AMDGPU VRAM manager 管理,当前实现主要使用 drm_buddy;
- TTM_PL_TT:GTT/system memory 相关路径,GTT address 管理中可见 drm_mm;
- TTM_PL_SYSTEM 或其他驱动私有 placement。
但是这些属于 TTM resource manager / AMDGPU memory manager 的职责,不属于 vma_node 的职责。
可以把两条线并排看:
CPU mmap 路由线:
drm_gem_object.vma_node
-> drm_vma_offset_manager
-> fake offset
-> drm_gem_mmap() 找回 BO
BO backing 分配线:
ttm_bo_validate()
-> ttm_resource_manager.alloc()
-> VRAM / GTT / SYSTEM resource
-> fault 时映射真实 backing
两条线在同一个 BO 上汇合,但它们管理的是两个不同地址空间。
因此,一个 BO 即使当前是 VRAM BO,也仍然可以拥有 vma_node 和 fake offset。fake offset 只是让 mmap() 找到它;CPU 是否能真正访问,还要看 fault 时 BO backing 是否 CPU-visible,必要时是否能迁移到 visible VRAM 或 GTT。
9. 本篇小结
本节回答了 fake offset 是如何在 BO 创建路径中出现的。
核心链路是:
amdgpu_gem_object_create()
-> amdgpu_bo_create()
-> drm_gem_private_object_init()
-> drm_vma_node_reset(&obj->vma_node)
-> ttm_bo_init_reserved(type = ttm_bo_type_device)
-> drm_vma_offset_add(&bo->base.vma_node)
-> vma_node 获得 fake offset 区间
几个关键结论:
- vma_node 是 drm_gem_object 的字段,AMDGPU 通过 bo->tbo.base.vma_node 使用它;
- drm_vma_node_reset() 只是初始化字段,drm_vma_offset_add() 才是真正分配 fake offset;
- AMDGPU 用户态普通 BO 是 ttm_bo_type_device,因此会在 ttm_bo_init_reserved() 中分配 fake offset;
- amdgpu_bo_mmap_offset() 返回的是 drm_vma_node_offset_addr(&bo->tbo.base.vma_node);
- fake offset 的释放由 drm_vma_offset_remove() 完成;
- vma_node 管的是 mmap fake offset,不管 VRAM/GTT 物理资源分配。
接下来 3.3 我们继续下钻:fake offst 空间的大小如何给定;drm_vma_offset_add() 内部如何依赖 drm_mm 给 vma_node 分配一段不重叠的 fake offset 区间。