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 中的具体区间。这种设计让应用可以一次性注册大块内存,再灵活地按任务切分使用,减少重复注册开销。

DOCA Context 应用实例根对象 · 资源归属与生命周期边界 DOCA Device 硬件功能单元抽象 · 能力查询与队列创建 打开 / 引用 DOCA Memory 一段设备可见内存区域 属性:地址 · 长度 · 访问权限 · 导出类型 DOCA Buffer Memory 上的逻辑数据单元 属性:偏移 · 长度 · 关联 Memory 创建 创建 引用区间 Progress Engine 轮询 / 事件驱动 · 推进异步任务完成 提交任务 · 收割完成事件 驱动 DOCA Event 任务完成通知 成功 / 失败 / 部分完成 产生 异步任务 提交 → 执行 → 完成 状态由 Event 反馈 提交 生命周期顺序:Context → Device → Memory → Buffer → 任务提交 → Event 回收 → 逆序销毁 销毁必须逆序:先释放 Buffer 与任务,再释放 Memory,最后关闭 Device 与 Context

二、异步任务模型与 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 后都应处理并释放。

建议在应用初始化阶段就规划好对象的创建顺序,并在退出路径上使用统一的清理函数,按“任务 → Buffer → Memory → Device → Context”的逆序释放,避免遗漏。

五、对象关系速查

对象职责创建依赖销毁时机
Context应用根对象,资源归属边界无最后销毁
Device硬件功能单元抽象ContextContext 之前
Memory设备可见内存区域Context / DeviceBuffer 之后
BufferMemory 上的逻辑数据单元Memory任务完成后、Memory 之前
Event任务完成通知与结果Progress Engine获取后立即释放
Progress Engine推进异步任务、收割完成事件ContextContext 停止后

把这六个对象的关系记牢,后续无论是做网络数据面、存储加速还是安全卸载,代码结构都会围绕同一套骨架展开:Context 管边界,Device 管能力,Memory 管空间,Buffer 管数据,Progress Engine 管推进,Event 管结果。掌握这套模型,就掌握了 DOCA 应用的基本节奏。