4. DOCA Core编程模型
DOCA Core 是整套 DOCA 应用的地基。它并不直接提供某个具体网络或存储功能,而是把“如何访问硬件、如何申请内存、如何提交任务、如何等待完成”抽象成一组统一对象。理解这些对象之间的从属关系与生命周期,是后续编写 DOCA 应用、避免资源泄漏和竞态问题的前提。本节围绕 Context、Device、Memory、Buffer、Event、Progress Engine 以及异步任务模型展开,重点说明它们各自承担什么职责、如何组合,以及一个任务从提交到完成经历了哪些状态。
一、核心对象的层级与职责
DOCA Core 的对象不是平铺的,而是有明确的从属关系。最上层是 DOCA Context,它代表一个 DOCA 应用实例,是所有其他对象的根。Context 之下挂载 DOCA Device,Device 是对一块可被 DOCA 操作的硬件设备(例如 BlueField DPU 上的某个功能单元)的抽象。应用通过 Context 打开 Device,再基于 Device 创建 Memory 和 Buffer,最后通过 Progress Engine 驱动异步任务。
需要区分 Memory 与 Buffer:Memory 描述一段可被设备访问的内存区域,关注的是“这块内存在哪里、多大、是否对设备可见”;Buffer 则是在 Memory 之上按需切分出的逻辑数据单元,关注的是“这次任务要处理哪一段数据”。一个 Memory 可以派生出多个 Buffer,Buffer 通过偏移和长度引用 Memory 中的具体区间。这种设计让应用可以一次性注册大块内存,再灵活地按任务切分使用,减少重复注册开销。
二、异步任务模型与 Progress Engine
DOCA 的任务是异步的。应用不会在提交任务后阻塞等待,而是把任务交给 Progress Engine,由它负责推进。Progress Engine 有两种典型工作模式:轮询模式和事件驱动模式。轮询模式下,应用在一个循环中反复调用推进函数,检查是否有任务完成;事件驱动模式下,Progress Engine 关联一个事件通道,任务完成时通过 Event 通知应用,应用可以在等待事件时让出 CPU。
无论哪种模式,任务的完成都以 DOCA Event 的形式反馈。Event 携带任务的结果状态:成功、失败或部分完成,并可能附带实际处理的数据量。应用必须显式地“收割”这些 Event,否则完成队列会堆积,最终导致无法提交新任务。这是初学者最容易忽略的一点:提交任务只是开始,回收 Event 才是闭环。
三、一个完整的任务生命周期
下面以一个假想的“内存拷贝任务”为例,说明从初始化到销毁的完整过程。示例参数为教学假设,不代表真实硬件实测值。
// 1. 创建 Context
doca_ctx = doca_ctx_create();
// 2. 打开 Device 并绑定到 Context
doca_dev = doca_dev_open("mlx5_0");
doca_ctx_set_dev(doca_ctx, doca_dev);
// 3. 创建 Memory(假设 1 MB,设备可读写)
doca_mem = doca_mem_create(doca_ctx, 1024 * 1024,
DOCA_ACCESS_FLAG_READ | DOCA_ACCESS_FLAG_WRITE);
// 4. 在 Memory 上切分 Buffer(偏移 0,长度 4096)
doca_buf = doca_buf_create(doca_mem, 0, 4096);
// 5. 启动 Context,进入可提交状态
doca_ctx_start(doca_ctx);
// 6. 提交异步任务
task = doca_task_submit(doca_ctx, doca_buf, DOCA_TASK_COPY);
// 7. 推进并收割 Event
while (!done) {
doca_pe_progress(doca_pe); // 轮询推进
event = doca_pe_get_event(doca_pe); // 获取完成事件
if (event) {
status = doca_event_get_status(event);
doca_event_free(event); // 必须释放
done = 1;
}
}
// 8. 逆序销毁
doca_ctx_stop(doca_ctx);
doca_buf_destroy(doca_buf);
doca_mem_destroy(doca_mem);
doca_dev_close(doca_dev);
doca_ctx_destroy(doca_ctx);
这段代码的关键不在语法,而在顺序。Context 必须先启动才能提交任务;Buffer 必须在 Memory 之后创建、在 Memory 之前销毁;Event 必须被释放。任何一步顺序错误,都可能导致提交失败、内存泄漏或设备无法再次打开。
四、生命周期管理的常见陷阱
第一,忘记停止 Context 就销毁。Context 处于运行状态时直接销毁,可能留下未完成的任务和未回收的 Event。正确做法是先停止 Context,等待所有任务完成,再销毁子对象。
第二,Buffer 生命周期长于 Memory。如果先销毁 Memory 再销毁 Buffer,Buffer 会引用已释放的内存,造成悬空引用。销毁顺序必须与创建顺序相反。
第三,Event 未释放导致队列满。Progress Engine 的完成队列容量有限,长期不回收 Event 会让队列耗尽,后续任务无法完成。每次获取 Event 后都应处理并释放。
五、对象关系速查
| 对象 | 职责 | 创建依赖 | 销毁时机 |
|---|---|---|---|
| Context | 应用根对象,资源归属边界 | 无 | 最后销毁 |
| Device | 硬件功能单元抽象 | Context | Context 之前 |
| Memory | 设备可见内存区域 | Context / Device | Buffer 之后 |
| Buffer | Memory 上的逻辑数据单元 | Memory | 任务完成后、Memory 之前 |
| Event | 任务完成通知与结果 | Progress Engine | 获取后立即释放 |
| Progress Engine | 推进异步任务、收割完成事件 | Context | Context 停止后 |
把这六个对象的关系记牢,后续无论是做网络数据面、存储加速还是安全卸载,代码结构都会围绕同一套骨架展开:Context 管边界,Device 管能力,Memory 管空间,Buffer 管数据,Progress Engine 管推进,Event 管结果。掌握这套模型,就掌握了 DOCA 应用的基本节奏。