8.1 勾勒了amdgpu_cs_ioctl的九步全景,指出整条路径始于对用户态提交请求的解析,终于向调度器入队并返回 fence。本节聚焦最前端的第 ①—③ 阶段:内核如何把用户递交的一堆「chunk」翻译为内核可执行的amdgpu_job,并把每一段间接缓冲(IB)的描述填充就位。这一步不涉及任何 BO 加锁与驻留(那是 8.3 的主题),但它决定了后续所有阶段将要操作的作业骨架——有几个 job、每个 job 挂几段 IB、领队是谁、user-fence 写到哪里。
一次提交从用户态看只是一个drm_amdgpu_cs结构,其中携带若干 chunk;从内核态看,则要经历「准备上下文 → 清点 → 备料 → 装配」四个动作。对应到源码,就是amdgpu_cs_ioctl顶部依次调用的三个函数:
r=amdgpu_cs_parser_init(&parser,adev,filp,data);/* ① 准备上下文 */...r=amdgpu_cs_pass1(&parser,data);/* ② 清点 + 备料(分配 job) */...r=amdgpu_cs_pass2(&parser);/* ③ 装配 IB */...r=amdgpu_cs_parser_bos(&parser,data);/* ④ 交给 8.3 */1. 入口与 parser_init:为整次提交铺开一张「状态桌」
贯穿命令提交全程的核心数据结构是struct amdgpu_cs_parser(下称 parser)。它像一张摊开的工作台,从解析到入队的每个阶段都往上面添置或取用零件:解析出的 chunk 数组、分配出的 job、待锁定的 BO 集合、收集到的依赖 fence,全都挂在 parser 上。amdgpu_cs_parser_init负责把这张桌子清空并摆好最初的几样工具:
staticintamdgpu_cs_parser_init(structamdgpu_cs_parser*p,...){if(cs->in.num_chunks==0)return-EINVAL;memset(p,0,sizeof(*p));p->adev=adev;p->filp=filp;p->ctx=amdgpu_ctx_get(fpriv,cs->in.ctx_id);/* 取出提交所属的 ctx */if(!p->ctx)return-EINVAL;amdgpu_sync_create(&p->sync);/* 依赖 fence 的容器(供 8.4 收集用) */drm_exec_init(&p->exec,DRM_EXEC_INTERRUPTIBLE_WAIT|DRM_EXEC_IGNORE_DUPLICATES,0);/* BO 加锁上下文(供 8.3 使用) */return0;}这里有三件事值得留意:
amdgpu_ctx_get依据用户传入的ctx_id取出提交上下文(amdgpu_ctx)。ctx 承载了调度实体(entity)与「代际(generation)」信息——后者用于识别 GPU 复位后上下文是否已失效,pass1 末尾会用它做一致性校验。amdgpu_sync_create与drm_exec_init在此刻只是初始化容器:p->sync要到 8.4 才装入依赖 fence,p->exec要到 8.3 才真正锁定 BO。提前初始化是为了让后续阶段无需再关心「有没有准备好」。- 除
p->ctx与两个容器外,parser 的其余字段被memset清零,意味着 gang 规模、job 数组、bo_list 等都从「空」开始,由 pass1 逐步填入。
可跳过的实现细节:若你只想把握主干,记住一句话即可——
parser_init只是「开桌铺料」,取出 ctx 并初始化好后续要用的两个空容器。真正有内容的工作从 pass1 开始。
2. pass1:先「清点」,再「备料」
amdgpu_cs_pass1的职责可以概括为一句话:把用户态的 chunk 数组安全地拷进内核,逐个分类清点,最后据此分配出恰好数量的 job。之所以要分成「先清点、后分配」两步,是因为分配一个amdgpu_job时必须提前知道它将挂载几段 IB——而这个数目只有把所有 IB chunk 数过一遍才能确定。
2.1 把 chunk 拷进内核
提交请求里的 chunk 以「指针数组」的形式存放在用户空间,pass1 首先把这层指针数组整体拷入,再逐个把每个 chunk 的数据体拷入:
chunk_array=memdup_array_user(u64_to_user_ptr(cs->in.chunks),cs->in.num_chunks,sizeof(uint64_t));...for(i=0;i<p->nchunks;i++){copy_from_user(&user_chunk,chunk_ptr,sizeof(structdrm_amdgpu_cs_chunk));p->chunks[i].chunk_id=user_chunk.chunk_id;p->chunks[i].length_dw=user_chunk.length_dw;p->chunks[i].kdata=vmemdup_array_user(...,size,sizeof(uint32_t));...}每个 chunk 都带有一个chunk_id标明其类型,pass1 用一个switch分门别类,并对每种类型先做长度下限校验(if (size < sizeof(...)) goto free_partial_kdata;),确保用户传入的数据体足以容纳对应结构,防止后续解析越界。
2.2 IB chunk:只清点,不装配
对AMDGPU_CHUNK_ID_IB类型,pass1 调用amdgpu_cs_p1_ib——注意它此刻只统计数量,不填充内容:
staticintamdgpu_cs_p1_ib(structamdgpu_cs_parser*p,structdrm_amdgpu_cs_chunk_ib*chunk_ib,unsignedint*num_ibs){intr=amdgpu_cs_job_idx(p,chunk_ib);/* 该 IB 应归入哪个 job? */if(r<0)returnr;if(num_ibs[r]>=amdgpu_ring_max_ibs(chunk_ib->ip_type))return-EINVAL;++(num_ibs[r]);/* 仅仅计数 */p->gang_leader_idx=r;return0;}要理解这里的num_ibs[r],得先引入gang 提交(gang submit)的概念。一次amdgpu_cs可以同时向多个不同的引擎队列投放 IB(例如同一提交里既有 GFX 又有 Compute),这些并列的 job 组成一个「gang」,被要求作为一个整体一起上场。amdgpu_cs_job_idx正是把每段 IB 归类到它所属的队列:
staticintamdgpu_cs_job_idx(structamdgpu_cs_parser*p,structdrm_amdgpu_cs_chunk_ib*chunk_ib){amdgpu_ctx_get_entity(p->ctx,chunk_ib->ip_type,chunk_ib->ip_instance,chunk_ib->ring,&entity);/* (引擎类型,实例,ring) → 调度实体 */for(i=0;i<p->gang_size;++i)/* 已经见过这个实体? */if(p->entities[i]==entity)returni;/* 复用同一个 gang 槽位 */if(i==AMDGPU_CS_GANG_SIZE)return-EINVAL;p->entities[i]=entity;/* 新实体:扩大 gang */p->gang_size=i+1;returni;}于是同一队列的多段 IB 会落到同一个 gang 槽位(num_ibs[r]累加),不同队列的 IB 则各占一个槽位。pass1 结束时,gang_size就是本次提交要创建的 job 数,num_ibs[i]则是第 i 个 job 要挂载的 IB 段数。gang_leader_idx记录最后一段 IB 所属的槽位,作为整个 gang 的「领队」——领队 job 承载 user-fence 地址等 gang 级别的信息。
2.3 其余 chunk:user-fence 与 bo_list
pass1 在switch中还处理另外两类需要提前解析的 chunk:
| chunk 类型 | 处理函数 | 动作 |
|---|---|---|
AMDGPU_CHUNK_ID_IB | amdgpu_cs_p1_ib | 归类到 gang 槽位并计数num_ibs[] |
AMDGPU_CHUNK_ID_FENCE | amdgpu_cs_p1_user_fence | 查出 user-fence BO(p->uf_bo)并记录写入偏移 |
AMDGPU_CHUNK_ID_BO_HANDLES | amdgpu_cs_p1_bo_handles | 依据用户句柄数组建立p->bo_list |
各类DEPENDENCIES/SYNCOBJ_* | —(此阶段跳过) | 留待 pass2 与 8.4 处理 |
amdgpu_cs_p1_user_fence通过drm_gem_object_lookup找到那块用于回写完成信号的 BO,校验其大小(必须为一页且偏移合法、且不是 userptr),并以p->uf_bo引用持有。amdgpu_cs_p1_bo_handles把用户递交的 BO 句柄数组转换为内核的amdgpu_bo_list,即本次提交引用的完整 BO 集合——这正是 8.3 要逐个加锁与驻留的对象清单。
而所有依赖类 chunk(DEPENDENCIES、SYNCOBJ_IN/OUT、timeline 等)在 pass1 里只是break掠过:它们不影响 job 的形状,因而无需在「清点」阶段介入,留到 pass2/8.4 再解析。
3. amdgpu_job 的分配:把清点结果变成实体
清点完毕,pass1 末尾据此为每个 gang 槽位分配一个真正的amdgpu_job:
for(i=0;i<p->gang_size;++i){ret=amdgpu_job_alloc(p->adev,vm,p->entities[i],vm,num_ibs[i],p->filp->client_id,GFP_KERNEL,&p->jobs[i]);.../* 按 enforce_isolation 策略设置隔离/清理着色器标志 */}p->gang_leader=p->jobs[p->gang_leader_idx];if(p->ctx->generation!=p->gang_leader->generation){ret=-ECANCELED;/* ctx 已因 GPU 复位而失效 */gotofree_all_kdata;}if(p->uf_bo)p->gang_leader->uf_addr=uf_offset;/* user-fence 写入地址落到领队 */amdgpu_vm_set_task_info(vm);/* 记录任务信息,便于故障诊断 */几处要点:
amdgpu_job_alloc传入num_ibs[i]:这正是前面「先清点」的意义——分配 job 时就按精确的 IB 段数备好job->ibs[]数组,pass2 只需往里填即可,无需再动态扩容。- 代际一致性校验:
p->ctx->generation != gang_leader->generation返回-ECANCELED。当 GPU 发生复位(TDR)后,旧 ctx 的代际会被推进,此检查确保不会把作业提交到一个已经「失效」的上下文中。 - user-fence 落到领队:若提交请求了 user-fence,其写入地址
uf_addr只记录在 gang_leader 上——整个 gang 完成后由领队统一回写完成信号。
至此,本次提交的作业骨架已经成型:几个空的 job 各自知道自己属于哪个引擎、要挂几段 IB、谁是领队、user-fence 写往何处。缺的只是每段 IB 的具体描述——这是 pass2 的活。
4. pass2:把每段 IB 的描述填进 job
amdgpu_cs_pass2再次遍历所有 chunk,但这一次是真正处理而非清点。它的switch分支覆盖 IB 与各类依赖 chunk;其中依赖类分支(p2_dependencies、p2_syncobj_*)属于「依赖与同步」的主题,将在 8.4 展开。本节只关注 IB 分支amdgpu_cs_p2_ib:
staticintamdgpu_cs_p2_ib(structamdgpu_cs_parser*p,structamdgpu_cs_chunk*chunk,...){structdrm_amdgpu_cs_chunk_ib*chunk_ib=chunk->kdata;intr=amdgpu_cs_job_idx(p,chunk_ib);/* 与 pass1 相同的归类,定位到对应 job */job=p->jobs[r];ib=&job->ibs[job->num_ibs++];/* 取出 job 中下一个空的 IB 槽 *//* 一系列合法性检查 */if(ring->no_user_submission)return-EINVAL;/* 内核队列禁止用户提交 */if(p->uf_bo&&ring->funcs->no_user_fence)return-EINVAL;/* MM 引擎不支持 user-fence */.../* CE CS 拦截、GFX 抢占 IB 的 CE/DE 计数上限校验 */r=amdgpu_ib_get(p->adev,vm,...,AMDGPU_IB_POOL_DELAYED,ib);/* 分配 IB 管理结构 */ib->gpu_addr=chunk_ib->va_start;/* IB 在 GPU 地址空间中的起始 VA */ib->length_dw=chunk_ib->ib_bytes/4;/* IB 长度(以 dword 计) */ib->flags=chunk_ib->flags;return0;}核心动作只有两步:取槽与填料。
- 取槽:
ib = &job->ibs[job->num_ibs++]。由于 pass1 已按精确数量分配好ibs[]数组,这里的自增不会越界;job->num_ibs从 0 开始随填充递增,最终等于 pass1 清点出的段数。 - 填料:
amdgpu_ib_get为该 IB 备好底层管理结构,随后把用户描述里的三项关键信息落位——gpu_addr(IB 数据在 GPU 虚拟地址空间中的位置)、length_dw(长度)、flags(如 PREAMBLE、PREEMPT 等)。注意gpu_addr只是记录了一个 GPU VA,此刻并不校验它是否真的映射有效;那要等 8.3 完成 BO 驻留、8.5 完成页表回填之后才成立。
填充过程中的若干检查体现了引擎能力约束:内核内部队列不接受用户提交、多媒体引擎不支持 user-fence、GFX 抢占场景下 CE/DE 各自最多一段可抢占 IB 等。这些检查一旦失败即中止整次提交。
5. 形象化小结:一次「按单备餐」的流程
若把命令提交比作后厨接单出餐,第 ①—③ 阶段恰好对应「接单—清点—开工单—备料」四步:
| 后厨动作 | CS 阶段 | 关键产物 |
|---|---|---|
| 铺开操作台、拿出订单夹与料盒 | ①parser_init | 空的 parser:ctx、sync、exec容器 |
| 逐条读订单、数清每个灶台要几道菜 | ②pass1清点 | gang_size、num_ibs[]、bo_list、uf_bo |
| 按灶台开出恰好数量的工单 | ②pass1分配 | 各amdgpu_job(含 gang_leader) |
| 把每道菜的用料填进对应工单 | ③pass2 | 填好的job->ibs[](gpu_addr / 长度 / flags) |
贯穿其间的设计要点是**「先清点、后分配」**:只有先把所有 IB chunk 数过一遍,才能一次性为每个 job 分配尺寸精确的 IB 数组,让 pass2 的填充零动态扩容、零越界。而依赖 chunk 在这三步里始终「靠边站」——它们不改变作业的形状,因而被推迟到 8.4 统一收集。
可跳过的实现细节:若你只关心接口与主干,记住三点即可——(1) pass1 清点出「几个 job、每个挂几段 IB」并据此分配 job;(2) pass2 把每段 IB 的
gpu_addr/长度/flags填进对应 job;(3) 这一整章都还没碰 BO 锁与驻留。gang、CE/DE 抢占、user-fence 落位等细节可在需要时再回看。
作业骨架已经就绪,但此刻它引用的所有 BO 还散落在各处、既未锁定也未必驻留在 GPU 可访问的内存域。下一节 8.3 将进入第 ④ 阶段amdgpu_cs_parser_bos,讲解drm_exec如何无死锁地一次性锁定全部 BO、ttm_bo_validate如何把它们拉回合法内存域,为后续的依赖收集与入队铺平道路。