4. DOCA Core编程模型

DOCA Core 是整套 DOCA 框架的“地基”,它把 DPU 上的硬件资源(PCIe 设备、HugePage、队列、事件通道、DMA 引擎等)抽象成一组统一的软件对象,让上层应用不必直接操作寄存器或驱动 ioctl,就能以面向对象的方式完成设备发现、内存注册、任务提交与事件回收。本节围绕四个核心机制展开:设备抽象、内存管理、事件驱动模型,以及异步任务与线程安全。

一、设备抽象:从 PCIe 设备到 doca_dev

DOCA 用 doca_dev 表示一个可被应用操作的设备。应用启动后先枚举设备,再按属性筛选。典型流程是:调用 doca_devinfo_list_create() 创建设备信息列表,遍历列表并用 doca_devinfo_get_pci_addr()、doca_devinfo_get_iface_name() 等接口读取属性,选中目标设备后调用 doca_dev_open() 打开,得到 doca_dev* 句柄。这个句柄是所有后续上下文(context)的父对象。

设备抽象的关键价值在于“能力查询”。不同 DPU 型号、不同固件版本支持的加速能力不同,应用不应硬编码假设,而应通过 doca_devinfo_get_caps() 或各模块的 *_cap_* 接口查询。例如判断某设备是否支持 RDMA 或加密,再决定是否创建对应 context。这样同一份代码可以在 BlueField-2、BlueField-3 等不同平台上自适应运行。

二、内存管理:HugePage、dmabuf 与内存注册

DPU 加速的本质是让硬件直接读写主机内存,因此内存必须满足“物理连续、可被设备访问、地址可翻译”三个条件。DOCA 的内存管理围绕 doca_mmap 展开:应用先分配一块内存(通常来自 HugePage,如 2MB 或 1GB 页),再创建 mmap 对象并把这段内存注册进去,硬件才能通过 DMA 访问。

注册时需要指定访问权限(本地读、本地写、远端读、远端写)和内存类型(如 DOCA_ACCESS_FLAG_PCI_READ_WRITE)。注册成功后,应用可通过 doca_mmap_get_mkey() 获取内存密钥(mkey),用于 RDMA 操作;通过 doca_mmap_export_dmabuf() 导出 dmabuf 文件描述符,实现跨进程或与 GPU 共享内存。一个常见的教学示例是:分配 64MB HugePage,注册为 mmap,导出 dmabuf 后交给另一个进程,两个进程即可零拷贝地共享同一块缓冲区。

内存注册是“一次性开销”,应尽量在初始化阶段完成,避免在数据路径上频繁创建/销毁 mmap,否则会引入显著延迟。

三、事件驱动模型:PE、事件通道与轮询

DOCA 采用事件驱动而非阻塞调用。核心对象是 doca_pe(Progress Engine,进度引擎)和 doca_event。应用把多个 context 关联到同一个 PE,PE 负责从硬件事件通道(event channel)收集完成事件,并回调应用注册的处理函数。

典型循环是:调用 doca_pe_progress() 推进一次,它会检查所有关联的 event channel,若有完成事件则触发对应回调;若无事件则立即返回,应用可继续做其他工作或短暂休眠。这种“非阻塞轮询 + 回调”的模式,既避免了线程阻塞,又能让单线程高效管理成百上千个并发任务。

事件通道的创建通常与 context 绑定,例如 doca_rdma_create_event_channel()。应用需要把 event channel 与 PE 关联:doca_pe_connect_event_channel()。此后 PE 就能感知该通道上的完成事件。理解“context → event channel → PE → 回调”这条链路,是掌握 DOCA 异步编程的关键。

四、异步任务与线程安全

DOCA 的任务提交接口(如 doca_rdma_task_write())都是异步的:调用后立即返回一个 task 句柄,任务在硬件中执行,完成后通过 PE 回调通知。应用不能假设提交即完成,必须通过回调或状态查询确认结果。任务对象通常需要应用自行管理生命周期,回调触发前不能释放。

线程安全方面,DOCA 的基本约定是:一个 PE 及其关联的 context、task 不应被多个线程同时操作。如果应用需要多线程,推荐“每线程一个 PE”的模型:每个线程独立创建 PE、独立处理自己的 context 和任务,线程之间通过无锁队列或共享 mmap 传递数据。这样既避免了锁竞争,也符合 DOCA 对象“单线程亲和”的设计哲学。

下面这张图概括了 DOCA Core 四个机制之间的对象关系与数据流:设备抽象提供父句柄,内存管理提供可被 DMA 访问的缓冲区,事件驱动模型负责回收完成事件,异步任务则贯穿提交与完成两端。

设备抽象 doca_devinfo_list doca_dev_open → doca_dev* 内存管理 HugePage → doca_mmap mkey / dmabuf 导出 事件驱动模型 doca_pe + doca_event event channel → 回调 父句柄 注册到PE 异步任务提交与完成 doca_rdma_task_write() → 立即返回 task 句柄 → 硬件执行 → PE 回调通知 线程安全约定 每线程独立 PE · context/task 单线程亲和 · 跨线程用无锁队列或共享 mmap 数据流:应用提交任务 → 硬件 DMA 读写已注册内存 → 完成事件进入 event channel → PE 轮询触发回调 → 应用处理结果 对象生命周期:doca_dev → doca_mmap / doca_pe → doca_ctx → doca_task

五、一个最小可运行骨架

把上述机制串起来,一个 DOCA Core 应用的最小骨架大致如下(伪代码,省略错误处理):

// 1. 枚举并打开设备
doca_devinfo_list_create(&list);
doca_devinfo_list_get_devinfo(list, 0, &info);
doca_dev_open(info, &dev);

// 2. 分配并注册内存
void *buf = mmap_hugepage(64 * 1024 * 1024);
doca_mmap_create(&mmap);
doca_mmap_set_memrange(mmap, buf, 64 * 1024 * 1024);
doca_mmap_set_permissions(mmap, DOCA_ACCESS_FLAG_PCI_READ_WRITE);
doca_mmap_start(mmap);

// 3. 创建 PE 并关联事件通道
doca_pe_create(&pe);
doca_rdma_create_event_channel(&evch);
doca_pe_connect_event_channel(pe, evch);

// 4. 提交异步任务
doca_rdma_task_write(ctx, buf, len, remote_addr, &task);

// 5. 推进事件循环
while (!done) {
    doca_pe_progress(pe);   // 非阻塞,触发回调
}

这段骨架体现了 DOCA Core 的核心节奏:初始化阶段完成设备、内存、PE 的创建与关联;运行阶段只做任务提交和 doca_pe_progress() 推进。回调函数中处理完成事件、释放 task、决定是否提交下一个任务。掌握这个节奏,再叠加 RDMA、DMA、压缩等具体模块,就能快速构建出高性能的 DPU 加速应用。

常见误区:在回调中执行耗时操作(如文件 IO、大块内存拷贝)会阻塞 PE 推进,导致其他任务完成事件延迟处理。正确做法是把耗时工作交给工作线程,回调中只做状态标记和轻量转发。