AMDGPU SVM 属性设置流程:从用户态 ioctl 到 attr_set_ctx 的完整信息收集
AMD 正在使用 drm svm框架重构SVM的实现看来drm svm框架要进入大范围应用了。下面是在kernel社区上由AMD的开发人员提交的POC 验证版本的patches的技术方案实现。这里快速总结了实现以飨读者。因是POC版本设计可能会变动读者们慎重使用。本文仅用来跟踪前沿驱动技术的迭代发展现状。1. 概述本文档描述 AMDGPU SVMShared Virtual Memory子系统中用户态通过 DRM ioctl 设置 SVM 属性的完整内核态处理流程。重点分析从 ioctl 入口到属性变更上下文struct attr_set_ctx收集完毕的全过程。核心设计思想attr 层作为「属性管理器」负责维护虚拟地址空间上的属性区间树interval tree并在属性变更时收集差异信息封装为attr_set_ctx传递给 range 层执行实际的 GPU 映射/迁移操作。2. 数据结构定义2.1 UAPI 层用户态/内核态接口/* include/uapi/drm/amdgpu_drm.h */structdrm_amdgpu_gem_svm{__u64 start_addr;/* 目标虚拟地址页对齐 */__u64 size;/* 区间大小字节页对齐 */__u32 operation;/* 操作码AMDGPU_SVM_OP_SET_ATTR (0) / GET_ATTR (1) */__u32 nattr;/* 属性数组长度 */__u64 attrs_ptr;/* 指向用户态 drm_amdgpu_svm_attribute[] 的指针 */};structdrm_amdgpu_svm_attribute{__u32 type;/* 属性类型 */__u32 value;/* 属性值 */};属性类型枚举类型常量值含义AMDGPU_SVM_ATTR_PREFERRED_LOC0首选内存位置AMDGPU_SVM_ATTR_PREFETCH_LOC1预取目标位置AMDGPU_SVM_ATTR_ACCESS2启用 GPU 访问AMDGPU_SVM_ATTR_ACCESS_IN_PLACE3启用 GPU 原地访问AMDGPU_SVM_ATTR_NO_ACCESS4禁用 GPU 访问AMDGPU_SVM_ATTR_SET_FLAGS5按位设置标志AMDGPU_SVM_ATTR_CLR_FLAGS6按位清除标志AMDGPU_SVM_ATTR_GRANULARITY7页粒度标志位定义标志值分类AMDGPU_SVM_FLAG_HOST_ACCESS0x01MAPPINGAMDGPU_SVM_FLAG_COHERENT0x02PTEAMDGPU_SVM_FLAG_HIVE_LOCAL0x04MAPPINGAMDGPU_SVM_FLAG_GPU_RO0x08PTEAMDGPU_SVM_FLAG_GPU_EXEC0x10PTEAMDGPU_SVM_FLAG_GPU_READ_MOSTLY0x20MAPPINGAMDGPU_SVM_FLAG_GPU_ALWAYS_MAPPED0x40MAPPINGAMDGPU_SVM_FLAG_EXT_COHERENT0x80PTE标志位被分为两组掩码对应不同的触发类型/* amdgpu_svm_attr.h */#defineAMDGPU_SVM_PTE_FLAG_MASK\(AMDGPU_SVM_FLAG_COHERENT|AMDGPU_SVM_FLAG_EXT_COHERENT|\AMDGPU_SVM_FLAG_GPU_RO|AMDGPU_SVM_FLAG_GPU_EXEC)#defineAMDGPU_SVM_MAPPING_FLAG_MASK\(AMDGPU_SVM_FLAG_HOST_ACCESS|AMDGPU_SVM_FLAG_HIVE_LOCAL|\AMDGPU_SVM_FLAG_GPU_READ_MOSTLY|AMDGPU_SVM_FLAG_GPU_ALWAYS_MAPPED)2.2 内核态属性结构/* amdgpu_svm_attr.h */enumamdgpu_svm_attr_access{AMDGPU_SVM_ACCESS_NONE0,/* GPU 无访问权限 */AMDGPU_SVM_ACCESS_ENABLE1,/* GPU 可访问允许迁移 */AMDGPU_SVM_ACCESS_IN_PLACE2,/* GPU 原地访问不迁移 */};structamdgpu_svm_attrs{int32_tpreferred_loc;/* 首选内存位置 */int32_tprefetch_loc;/* 预取目标位置 */uint32_tflags;/* 标志位集合 */uint32_tgranularity;/* 页粒度 */enumamdgpu_svm_attr_accessaccess;/* 访问模式 */};2.3 属性区间树structamdgpu_svm_attr_range{structinterval_tree_nodeit_node;/* 区间树节点 [start_page, last_page] */structlist_headlist;/* 链表节点按地址有序 */structamdgpu_svm_attrsattrs;/* 该区间的属性值 */};structamdgpu_svm_attr_tree{structmutexlock;/* 保护区间树的互斥锁 */structrb_root_cachedtree;/* 红黑树根区间树底层实现 */structlist_headrange_list;/* 所有 attr_range 的有序链表 */structamdgpu_svm*svm;/* 回指 SVM 上下文 */};2.4 变更上下文attr 层与 range 层的桥梁/* amdgpu_svm_attr.c - 文件作用域仅在 attr 层内部使用 */structattr_set_ctx{unsignedlongstart;/* 受影响的起始页 */unsignedlonglast;/* 受影响的结束页 */uint32_ttrigger;/* 变更触发位掩码 */structamdgpu_svm_attrsprev_attrs;/* 变更前属性 */structamdgpu_svm_attrsnew_attrs;/* 变更后属性 */};触发位定义enumamdgpu_svm_attr_change_trigger{AMDGPU_SVM_ATTR_TRIGGER_ACCESS_CHANGE(1U0),/* 访问权限变更 */AMDGPU_SVM_ATTR_TRIGGER_PTE_FLAG_CHANGE(1U1),/* PTE 级标志变更 */AMDGPU_SVM_ATTR_TRIGGER_MAPPING_FLAG_CHANGE(1U2),/* 映射策略标志变更 */AMDGPU_SVM_ATTR_TRIGGER_LOCATION_CHANGE(1U3),/* 预取位置变更 */AMDGPU_SVM_ATTR_TRIGGER_GRANULARITY_CHANGE(1U4),/* 页粒度变更 */AMDGPU_SVM_ATTR_TRIGGER_ATTR_ONLY(1U5),/* 仅属性记录变更无需 range 层操作 */};3. 完整调用链用户态: ioctl(fd, DRM_IOCTL_AMDGPU_GEM_SVM, args) │ ▼ ┌─────────────────────────────────────────────────────┐ │ DRM 子系统分发 │ │ amdgpu_drv.c: amdgpu_ioctls_kms[] 注册表 │ │ DRM_IOCTL_DEF_DRV(AMDGPU_GEM_SVM, │ │ amdgpu_gem_svm_ioctl, │ │ DRM_AUTH|DRM_RENDER_ALLOW) │ └─────────────────────┬───────────────────────────────┘ │ DRM 框架自动 copy_from_user │ 将 drm_amdgpu_gem_svm 复制到内核栈 ▼ amdgpu_gem_svm_ioctl() [amdgpu_svm.c] │ ├── ① 参数校验 ├── ② amdgpu_svm_copy_attrs() → 从用户态复制属性数组 └── ③ switch(operation) │ SET_ATTR │ ▼ amdgpu_svm_set_attr() [amdgpu_svm.c] │ ├── amdgpu_svm_range_sync_work() → 刷新 range 工作队列 └── amdgpu_svm_attr_set() → 进入 attr 层 │ ├── ④ 逐属性校验 ├── ⑤ VMA 范围校验 ├── ⑥ 初始化默认属性 └── ⑦ amdgpu_svm_attr_set_range() → 按段迭代 ┌────────┴────────┐ 命中已有区间 落入空洞 │ │ ▼ ▼ amdgpu_svm_attr_ amdgpu_svm_attr_ set_existing() set_hole() │ │ └────────┬─────────┘ ▼ attr_set_ctx 收集完毕 ▼ amdgpu_svm_attr_apply_change() ▼ amdgpu_svm_range_apply_attr_change() range 层消费4. 各阶段详细分析4.1 阶段一DRM ioctl 入口与参数复制函数amdgpu_gem_svm_ioctl()DRM 框架在分发 ioctl 之前已经根据DRM_IOWR宏的定义将drm_amdgpu_gem_svm结构体从用户空间copy_from_user到内核栈上的data指针。ioctl handler 直接将data强转为struct drm_amdgpu_gem_svm *。校验逻辑1. SVM 是否已启用vm-svm ! NULL → -EOPNOTSUPP 2. start_addr 和 size 是否页对齐 → -EINVAL 3. start_addr 和 size 是否非零 → -EINVAL4.2 阶段二用户态属性数组复制函数amdgpu_svm_copy_attrs()staticintamdgpu_svm_copy_attrs(conststructdrm_amdgpu_gem_svm*args,structdrm_amdgpu_svm_attribute**attrs,size_t*size){if(!args-nattr||args-nattrAMDGPU_SVM_MAX_ATTRS)/* 上限 64 */return-EINVAL;if(!args-attrs_ptr)return-EINVAL;*sizeargs-nattr*sizeof(**attrs);*attrsmemdup_user(u64_to_user_ptr(args-attrs_ptr),*size);returnPTR_ERR_OR_ZERO(*attrs);}关键点属性数量上限为64AMDGPU_SVM_MAX_ATTRS防止用户态传入过大数组导致内存耗尽。memdup_user()原子地完成内存分配 copy_from_user返回内核堆上的副本。u64_to_user_ptr()将__u64安全转换为用户态指针处理 32/64 位兼容性。至此内核拥有用户请求的完整副本目标地址范围属性数组。4.3 阶段三中间分发层函数amdgpu_svm_set_attr()staticintamdgpu_svm_set_attr(structamdgpu_vm*vm,...){structamdgpu_svm*svmvm-svm;amdgpu_svm_range_sync_work(svm);/* 刷新 range 工作队列 */returnamdgpu_svm_attr_set(svm-attr_tree,start,size,nattr,attrs);}amdgpu_svm_range_sync_work()在进入 attr 层之前刷新 range 层的异步工作队列减少后续操作因 mmap 锁竞争而失败的概率。从vm到svm-attr_tree的转换完成了从 DRM/VM 抽象到 SVM 子系统内部结构的过渡。4.4 阶段四属性校验函数amdgpu_svm_attr_set()→amdgpu_svm_attr_set_validate()逐条遍历用户传入的属性数组按类型执行合法性检查属性类型校验规则PREFERRED_LOC接受SYSMEM和UNDEFINEDGPU ID 0 隐式接受单 GPU 架构PREFETCH_LOC接受SYSMEM和 GPU ID 0拒绝UNDEFINEDACCESS/ACCESS_IN_PLACE/NO_ACCESSvalue 不能为 0 或UNDEFINEDSET_FLAGS/CLR_FLAGSvalue 不能包含AMDGPU_SVM_VALID_FLAG_MASK之外的位GRANULARITY无限制由attr_apply时截断到 0x3f任何一条属性校验失败整个 ioctl 立即返回-EINVAL。4.5 阶段五VMA 范围校验函数amdgpu_svm_attr_validate_range_vma()在持有mmap_read_lock的情况下遍历目标页范围[start_page, last_page]对应的所有 VMA确保每一页都被有效 VMA 覆盖不存在空洞。VMA 不带有VM_IO | VM_PFNMAP | VM_MIXEDMAP标志排除设备映射区域。不满足条件则返回-EFAULT。这保证了后续操作不会作用在不合法的虚拟地址范围上。4.6 阶段六默认属性初始化函数attr_set_default()staticvoidattr_set_default(structamdgpu_svm*svm,structamdgpu_svm_attrs*attrs){attrs-preferred_locAMDGPU_SVM_LOCATION_UNDEFINED;attrs-prefetch_locAMDGPU_SVM_LOCATION_UNDEFINED;attrs-granularitysvm-default_granularity;attrs-flagsAMDGPU_SVM_FLAG_HOST_ACCESS|AMDGPU_SVM_FLAG_COHERENT;attrs-accesssvm-xnack_enabled?AMDGPU_SVM_ACCESS_ENABLE:AMDGPU_SVM_ACCESS_NONE;}默认属性用于 interval tree 中不存在属性区间空洞的地址段。这意味着 attr 层采用稀疏存储策略只有与默认值不同的区间才会分配amdgpu_svm_attr_range节点。4.7 阶段七按段迭代 ——amdgpu_svm_attr_set_range()这是信息收集的主循环。用户请求的地址范围[start_page, last_page]可能跨越多个已有属性区间和空洞因此需要逐段处理。用户请求范围: [] 区间树现状: [range_A] [range_B] [range_C] ↑ ↑ ↑ ↑ 空洞 空洞 空洞 空洞主循环以cursor指针从start_page开始每次处理一个段while(cursorlast){mutex_lock(attr_tree-lock);nodeinterval_tree_iter_first(attr_tree-tree,cursor,cursor);if(node){/* 命中已有区间 → amdgpu_svm_attr_set_existing() */seg_lastmin(last,attr_last_page(range));}else{/* 落入空洞 → amdgpu_svm_attr_set_hole() */nextinterval_tree_iter_first(...,cursor1,ULONG_MAX);seg_lastmin(last,attr_start_page(next_range)-1);}/* 填充 attr_set_ctx ... */mutex_unlock(attr_tree-lock);/* 持 svm_lock 消费 change ... */cursorseg_last1;}锁序每段处理中先持attr_tree-lockmutex操作区间树释放后再持svm-svm_lockrwsem write传递给 range 层。两把锁不嵌套。4.8 路径 A空洞处理 ——amdgpu_svm_attr_set_hole()当cursor所在位置没有任何属性区间时该段地址继承默认属性。输入: default_attrs 用户属性数组 处理: 1. new_attrs default_attrs 2. amdgpu_svm_attr_apply(new_attrs, nattr, attrs) // 叠加用户请求 3. if (new_attrs default_attrs) → return 0 // 无变化跳过 4. 分配新的 amdgpu_svm_attr_range插入区间树 5. trigger attr_change_ctx_trigger(default_attrs, new_attrs) 6. 填充 attr_set_ctx{start, last, trigger, default_attrs, new_attrs}优化如果叠加后属性仍等于默认值则不分配节点、不产生 change保持稀疏存储。4.9 路径 B已有区间处理 ——amdgpu_svm_attr_set_existing()当cursor命中已有的amdgpu_svm_attr_range时流程更为复杂。4.9.1 属性叠加old_attrsrange-attrs;new_attrsold_attrs;amdgpu_svm_attr_apply(new_attrs,nattr,attrs);amdgpu_svm_attr_apply()遍历用户属性数组按类型修改new_attrs操作语义PREFERRED_LOCnew.preferred_loc valuePREFETCH_LOCnew.prefetch_loc valueACCESSnew.access ENABLEACCESS_IN_PLACEnew.access IN_PLACENO_ACCESSnew.access NONESET_FLAGSnew.flagsCLR_FLAGSnew.flags ~valueGRANULARITYnew.granularity min(value, 0x3f)注意用户可以在一次 ioctl 中传入多条属性它们按数组顺序依次叠加。后出现的同类型属性覆盖先前的。4.9.2 无变化快速路径if(attr_same_attrs(range,nattr,attrs)){if(!force_trigger)return0;// 完全无变化跳过// force_trigger 场景: 见 4.9.3}4.9.3 Force Trigger 机制两种情况下即使属性值未变也必须产生触发xnack 关闭时的 ACCESS 设置attr 层不知道 range 层的gpu_mapped状态range 层必须重新检查并按需建立映射。PREFETCH_LOC 重复设置预取是一次性命令one-shot不是持久状态。即使prefetch_loc值相同页可能已被驱逐回 RAM必须重新触发迁移。force_trigger(!attr_tree-svm-xnack_enabledattr_has_access(nattr,attrs))||attr_has_prefetch_loc(nattr,attrs);4.9.4 触发类型计算staticuint32_tattr_change_ctx_trigger(conststructamdgpu_svm_attrs*prev,conststructamdgpu_svm_attrs*new){uint32_ttrigger0;uint32_tchanged_flagsprev-flags^new-flags;if(prev-access!new-access)trigger|TRIGGER_ACCESS_CHANGE;if(changed_flagsAMDGPU_SVM_PTE_FLAG_MASK)trigger|TRIGGER_PTE_FLAG_CHANGE;// COHERENT/EXT_COHERENT/GPU_RO/GPU_EXECif(changed_flagsAMDGPU_SVM_MAPPING_FLAG_MASK)trigger|TRIGGER_MAPPING_FLAG_CHANGE;// HOST_ACCESS/HIVE_LOCAL/READ_MOSTLY/ALWAYS_MAPPEDif(prev-prefetch_loc!new-prefetch_loc)trigger|TRIGGER_LOCATION_CHANGE;if(prev-granularity!new-granularity)trigger|TRIGGER_GRANULARITY_CHANGE;if(!trigger)triggerTRIGGER_ATTR_ONLY;// 属性值没变或变化不影响硬件returntrigger;}trigger 位掩码的设计使得 range 层可以精确知道需要执行哪些操作ACCESS_CHANGE→ 重新评估 GPU 映射/解映射PTE_FLAG_CHANGE→ 需要更新 GPU 页表项的标志位MAPPING_FLAG_CHANGE→ 需要重新评估映射策略LOCATION_CHANGE→ 触发数据迁移GRANULARITY_CHANGE→ 影响后续 range 分割粒度ATTR_ONLY→ 仅属性变更range 层无需操作4.9.5 区间树分裂当用户请求范围不完整覆盖已有区间时需要将其分裂已有区间: [----------- range -----------] 用户范围: [] 分裂后: [left] [updated] [ right ]if(startrange_start)leftattr_alloc_range(range_start,start-1,old_attrs);// 保留旧属性if(lastrange_last)rightattr_alloc_range(last1,range_last,old_attrs);// 保留旧属性attr_remove_range_locked(attr_tree,range,false);// 从树中移除原区间不释放if(left)attr_insert_range_locked(attr_tree,left);attr_set_interval(range,start,last);// 复用原节点调整区间range-attrsnew_attrs;// 更新为新属性attr_insert_range_locked(attr_tree,range);if(right)attr_insert_range_locked(attr_tree,right);注意中间段复用原range对象避免额外分配两端新分配节点继承旧属性。4.9.6 打包 attr_set_ctx无论走哪条路径最终都调用amdgpu_svm_attr_change_ctx_set(change,start,last,trigger,prev_attrs,new_attrs);将本段的变更信息完整封装到栈上的attr_set_ctx变量中。5. attr_set_ctx 消费主循环中每段attr_set_ctx收集完毕后释放attr_tree-lock随即消费mutex_unlock(attr_tree-lock);down_write(svm-svm_lock);retamdgpu_svm_attr_apply_change(svm,change);up_write(svm-svm_lock);amdgpu_svm_attr_apply_change()是消费端的入口staticintamdgpu_svm_attr_apply_change(structamdgpu_svm*svm,conststructattr_set_ctx*change){if(!change-trigger||change-triggerAMDGPU_SVM_ATTR_TRIGGER_ATTR_ONLY)return0;/* 无硬件影响直接跳过 */returnamdgpu_svm_range_apply_attr_change(svm,change-start,change-last,change-trigger,change-prev_attrs,change-new_attrs);}过滤条件trigger 0不可能但防御性编程和ATTR_ONLY属性记录变了但不影响硬件直接跳过不进入 range 层。若 range 层返回-EAGAIN主循环将标记need_retry在整个范围处理完后由上层amdgpu_svm_attr_set()的 retry 循环重试。6. 关键设计要点稀疏存储attr 层只为与默认值不同的地址段创建attr_range节点空洞隐式继承默认属性。段式处理用户请求范围被区间树自然地分割为多个段每段独立收集attr_set_ctx、独立消费。这意味着一次 ioctl 可能产生多个到 range 层的调用。锁分离attr_tree-lockmutex, attr 层和svm-svm_lockrwsem, range 层不嵌套每段处理先释放 attr 锁再获取 range 锁减少锁持有时间。Force Triggerattr 层引入force_trigger机制弥补自身信息不足——它不知道 range 层的gpu_mapped状态也无法判断页面是否已被驱逐因此在特定场景下即使属性值未变也生成触发。重试机制range 层可能因无法获取 mmap 锁而返回-EAGAIN上层通过flush cond_resched goto retry实现退让重试。