Lazy loaded image
Words 0Read Time 1 min
Invalid Date
⚙️
核心判断
在真正进入 mmap 零拷贝之前,最该先验证的是:单纯在内核态里,这条 DMA 生命周期能不能完整闭环。 如果连 prepare、submit、complete、reclaim 这条链都没跑顺,就别急着把复杂度再拉进用户态。

这一章解决什么问题

这一章的目标很明确:
先在内核态跑通一条完整 DMA 收发闭环。
 
先把这四件事钉死:
  1. 数据如何进入可提交状态
  1. 设备如何开始处理
  1. 完成后软件如何接手
  1. 结果如何被安全回收
 
只有这条闭环成立,后面谈零拷贝、用户态协议、性能优化才有资格。

一条最小闭环长什么样

这张图对应的核心问题只有一个:
这块 buffer 在每一步到底归谁。

从发送路径看闭环:CPU 准备 → 设备读取

典型场景

  • 内核准备一块要发给设备的数据
  • 驱动把它交给设备 DMA 读取
  • 设备处理完成后返回完成事件

典型时序

一个很典型的 TX 骨架

发送路径的关键点

  • CPU 写完后,不能立刻假设设备就能看到最新版本
  • descriptor 写完后,不能让设备过早看到半成品
  • 设备完成后,slot 才能回到可复用状态
 
很多所谓“偶发发错数据”,本质上都是提交和回收边界不清。

从接收路径看闭环:设备写入 → CPU 消费

典型场景

  • 驱动预先准备空 buffer
  • 设备把结果 DMA 写进去
  • 完成中断到来后,CPU 再读取结果

典型时序

接收路径的关键点

  • completion 到来不等于“所有人都能立刻安全读”
  • 在 CPU 真正接手前,必须先把可见性问题处理好
  • consume 不是装饰动作,而是回收语义的边界点

如果你的数据不是天然连续:SG 闭环怎么理解

当单块 buffer 不适合真实场景时,就会进入 SG:
  • 多段内存被映射成一条 DMA 事务
  • 完成后统一回收 / unmap
 
最关键的不是 API 数量变多,而是:
生命周期从“单 slot”扩展成“一个 SG 列表也有完整 owner 与回收边界”。

一个最小 SG 思维骨架

这里真正重要的不是会不会写 for_each_sg,而是你要始终记住:
  • map 的对象是一整组分段内存,不是某一个幸运 buffer
  • 回收时必须按 SG 事务整体 unmap,而不是只盯某一段
  • descriptor 链只是硬件视角的遍历结构,生命周期边界仍然由驱动来定义

再看两类最常见的实战骨架

1. coherent ring 骨架更适合作为第一版闭环

很多初版驱动最稳的起点,其实不是一上来就 streaming,而是先把:
  • ring metadata
  • descriptor
  • 状态页
  • producer / consumer 管理结构
放进 coherent 内存。
 
这类骨架的价值在于:
  • 一致性语义简单
  • 更适合先把生命周期和状态机跑通
  • 方便先验证硬件接口、寄存器和完成路径
 
典型心智模型就是:
这类写法最适合回答一个问题:
我能不能先别急着追极限性能,而先把闭环秩序跑通?

2. streaming TX / RX 骨架更像性能版闭环

一旦进入高吞吐路径,常见形态就是:
  • 提交前 dma_map_single()
  • 完成后 dma_unmap_single()
  • 中间 descriptor / irq / reclaim 把一整条生命周期串起来
 
比如 TX 路径你真正该盯的是:
  • 映射是否成功
  • skb / buffer 的生命周期是否和 DMA 生命周期绑定
  • completion 后是否立即 unmap + free
 
这类骨架比 coherent 更接近真实网卡、块设备、采集链路的数据面。

代码层面最小应该拆成哪些函数

建议先把核心路径拆成下面几块:
这套拆法的价值在于:
  • prepare 管分配与前置状态
  • submit 管交接给设备
  • irq/wait 管完成通知
  • consume/reclaim 管回收
 
不要把这几步揉成一个大函数。揉在一起时,表面上省事,实际是在把状态机埋进 spaghetti code。

一个最小 slot 状态定义

然后配套约束:
  • prepare 只能从 FREE 进入 CPU_OWNED
  • submit 只能从 CPU_OWNED 进入 DEVICE_OWNED
  • irq complete 只能从 DEVICE_OWNED 进入 READY_FOR_CPU
  • consume 只能从 READY_FOR_CPU 回到 FREE
 
如果这些约束没落到代码里,后面任何“规范文档”都只是许愿。

prepare 阶段要做什么

  • 找到空闲 slot
  • 标记为 CPU_OWNED
  • 返回 buffer 位置或上下文
 
对 TX 路径来说,此后 CPU/内核可以写数据;
对 RX 路径来说,此后驱动可以把这个 slot 交给硬件准备接收。

submit 阶段要做什么

  • 检查 slot 当前状态是否合法
  • sync_for_device
  • 填写 descriptor
  • 保证 descriptor 与 doorbell 顺序正确
  • 通知设备开始处理
  • 状态切到 DEVICE_OWNED
 
submit 的本质不是“调一个 API”,而是:
把控制权正式从软件交给设备。

completion 阶段要做什么

在中断里至少要完成这些动作

  1. 确认是哪一个 slot 完成
  1. DEVICE_OWNED 迁移到下一阶段
  1. 若设备写了数据,则执行 sync_for_cpu
  1. 把结果放入可消费队列或唤醒等待者

一条建议

别在中断里写太多业务逻辑,但也别让中断只打印一句 “DMA done”。
 
最低限度也要推进 slot 状态,否则完成事件到了也等于没到。

wait / consume / reclaim 分别干嘛

wait

  • 阻塞或轮询,直到有完成 slot
  • 获取当前可消费结果

consume

  • 表示“上层已读完这份结果”
  • 是逻辑上的确认动作

reclaim

  • 把 slot 重新放回空闲池
  • 恢复到下一轮可复用状态
 
很多代码最大的问题是把 consume 和 reclaim 混成一件事,甚至干脆省略 consume。结果就是:
  • 上层是否真正用完数据,无人知道
  • buffer 可能被过早复用

最小接收闭环伪代码

这段伪代码真正表达的是:
一个 slot 不只是“用一下”,而是完整经历一次受控生命周期。

timeout 和 reset 必须怎么接进闭环

timeout

如果等太久没完成,至少要决定:
  • 当前 slot 标记成 ERROR 还是继续等待
  • 是否触发设备重置
  • 是否通知上层这次事务失败

reset

reset 后必须重新同步软件和硬件世界:
  • 清理 in-flight 状态
  • 作废旧 descriptor/completion
  • 重建可提交队列
 
别把 reset 想成“再试一次”。对 DMA 路径而言,reset 本质上是在宣布旧生命周期结束并建立新世界。

一组最重要的调试日志

建议优先打印状态迁移,而不是只打印函数名:
这比打印:
有用得多。后者只是热闹,前者才有证据链。

本章结论

  1. 在进入用户态零拷贝前,必须先在内核态跑通完整 DMA 闭环。
  1. prepare、submit、complete、consume、reclaim 这五步最好显式拆开。
  1. TX、RX、SG 这些路径虽然形态不同,但本质都在回答同一个问题:这次 DMA 生命周期现在归谁。
  1. slot 状态迁移必须落到代码约束里,而不是只留在脑子里。
  1. timeout 和 reset 不是边角料,而是闭环的一部分。

配套阅读

这一章最适合和下面几章一起看:

下一章要干什么

下一章开始把内核闭环扩展到用户态:
  • mmap 数据面
  • ioctl/poll 控制面
  • 用户态 submit / wait / consume 协议
 
因为一旦把用户态拉进来,系统就从“两方交接”升级成“三方协作”。复杂度会立刻上台阶。
上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...