4. DOCA核心编程模型
DOCA 程序不是从“打开网卡”开始,而是从建立一套受控的运行时上下文开始。理解 DOCA 编程模型的关键,是把 Context、Device、Memory、Buffer 四类抽象看成一条自上而下的资源归属链:Context 是进程级入口,Device 是硬件能力句柄,Memory 是跨主机与设备可见的地址空间,Buffer 则是 Memory 上的一段可被硬件直接读写的窗口。程序生命周期与错误处理,本质上就是这条链的建立、使用与逆序释放。
一、四类核心抽象的分工
DOCA Context 是进程与 DOCA 库之间的会话。它保存日志级别、设备枚举结果、内部线程与资源池等状态。一个进程通常只创建一个 Context,所有 Device、Memory、Buffer 都挂在它下面。Context 创建失败,后续任何 API 都无意义,因此它必须最先创建、最后销毁。
DOCA Device 代表一块可被编程的 DPU 或网卡功能单元。通过 doca_devinfo 枚举后,用 doca_dev_open 打开得到设备句柄。Device 不是“整块卡”,而是某个具体能力平面,例如网络卸载平面、RDMA 平面或存储平面。同一块物理 DPU 上可以打开多个 Device 句柄,分别对应不同功能。
DOCA Memory 描述一段对主机和设备同时可见的内存区域。普通 malloc 得到的主机内存,DPU 无法直接 DMA 访问;必须通过 DOCA 内存 API 注册,得到设备可用的地址映射。注册时需要给出长度、对齐和访问权限,返回的是一段“设备虚拟地址 + 主机虚拟地址”的配对信息。
DOCA Buffer 是 Memory 上的切片视图,通常由 doca_buf 表示,携带数据指针、长度、当前读写偏移。网络收发、加解密、压缩等操作都以 Buffer 为单位提交给硬件。Buffer 本身不拥有内存,它只是引用 Memory,因此释放 Buffer 不会释放底层内存。
二、程序生命周期的标准骨架
一个最小可用的 DOCA 程序,其生命周期可以概括为“创建—打开—注册—构造—提交—回收—释放”七个阶段。下面给出一个网络卸载场景的骨架示例,参数为教学假设值,实际以设备能力查询结果为准。
/* 1. 创建 Context */
doca_error_t r = doca_init();
struct doca_ctx *ctx = doca_ctx_create();
doca_ctx_set_log_level(ctx, DOCA_LOG_LEVEL_INFO);
/* 2. 枚举并打开 Device */
struct doca_devinfo *info = doca_devinfo_get_first(ctx);
struct doca_dev *dev = NULL;
r = doca_dev_open(info, &dev); /* 失败则 goto cleanup */
/* 3. 注册 Memory:2 MB,4 KB 对齐,读写权限 */
void *host_addr = aligned_alloc(4096, 2 * 1024 * 1024);
struct doca_mem *mem = NULL;
r = doca_mem_reg(dev, host_addr, 2 * 1024 * 1024,
DOCA_MEM_ACCESS_RW, &mem);
/* 4. 构造 Buffer:引用 mem 的前 1500 字节 */
struct doca_buf *buf = NULL;
r = doca_buf_create(mem, 0, 1500, &buf);
/* 5. 提交任务(伪代码,具体 API 依平面而定) */
r = doca_task_submit(ctx, buf);
/* 6. 回收 Buffer 与 Memory */
doca_buf_destroy(buf);
doca_mem_unreg(mem);
free(host_addr);
/* 7. 关闭 Device 与 Context */
doca_dev_close(dev);
doca_ctx_destroy(ctx);
注意释放顺序必须与创建顺序严格相反:Buffer 先于 Memory,Memory 先于 Device,Device 先于 Context。若先销毁 Context,其下挂的 Device 句柄会变成悬空引用,后续 doca_dev_close 将返回 DOCA_ERROR_INVALID_VALUE 或直接触发断言。
三、错误处理机制
DOCA 所有 API 返回 doca_error_t 枚举。工程上不应只判断“是否等于 0”,而应区分三类错误:
- 参数类错误:
DOCA_ERROR_INVALID_VALUE、DOCA_ERROR_NOT_SUPPORTED。通常由调用方传参不当或设备不支持该能力引起,应在开发阶段暴露,不应重试。 - 资源类错误:
DOCA_ERROR_NO_MEMORY、DOCA_ERROR_BAD_STATE。多因内存不足或状态机顺序错误,需检查资源配额与调用时序。 - 瞬时类错误:
DOCA_ERROR_AGAIN、DOCA_ERROR_IO_FAILED。表示当前不可立即完成或底层 I/O 抖动,可结合事件循环重试。
check_doca(r, "doca_mem_reg"),在失败时打印函数名、错误码与 doca_error_get_string(r),并跳转到 cleanup 标签,避免在多层嵌套中遗漏释放。四、一个可复用的错误处理骨架
下面给出带 cleanup 链的写法,所有资源初始化为 NULL,释放函数对 NULL 安全,从而保证任意阶段失败都能安全退出。
struct doca_ctx *ctx = NULL;
struct doca_dev *dev = NULL;
struct doca_mem *mem = NULL;
struct doca_buf *buf = NULL;
void *host = NULL;
doca_error_t r;
r = doca_ctx_create(&ctx);
if (r != DOCA_SUCCESS) goto cleanup;
r = doca_dev_open(doca_devinfo_get_first(ctx), &dev);
if (r != DOCA_SUCCESS) goto cleanup;
host = aligned_alloc(4096, 2 * 1024 * 1024);
r = doca_mem_reg(dev, host, 2 * 1024 * 1024, DOCA_MEM_ACCESS_RW, &mem);
if (r != DOCA_SUCCESS) goto cleanup;
r = doca_buf_create(mem, 0, 1500, &buf);
if (r != DOCA_SUCCESS) goto cleanup;
/* 正常业务处理 ... */
cleanup:
if (buf) doca_buf_destroy(buf);
if (mem) doca_mem_unreg(mem);
if (host) free(host);
if (dev) doca_dev_close(dev);
if (ctx) doca_ctx_destroy(ctx);
return r;
五、常见陷阱与判断依据
| 现象 | 可能原因 | 判断依据 |
|---|---|---|
| doca_dev_open 返回 NOT_SUPPORTED | 该 Device 不具备目标能力平面 | 用 doca_devinfo_get_cap 查询能力位 |
| doca_mem_reg 返回 INVALID_VALUE | 地址未按 4 KB 对齐或长度越界 | 检查 aligned_alloc 与长度参数 |
| 提交任务后无完成事件 | Buffer 未绑定到已启动的 Context | 确认 ctx 已 start,buf 归属正确 |
| 释放时崩溃 | 释放顺序与创建顺序不一致 | 对照 cleanup 链逐项核对 |