驱动写出一个“能把数据给用户态”的接口并不难,难的是十年后仍能维护。内核与用户空间的边界本质上是一份 ABI 契约:什么数据稳定、谁拥有缓冲区、怎样处理版本演进、是否需要异步事件、错误和并发如何表达。先从语义出发选接口,比从“哪个 API 写起来快”出发可靠得多。
不同接口分别适合什么
sysfs 适合少量、单值、具有设备属性语义的配置或状态,例如开关、模式和只读统计;它不适合承载复杂协议或大量二进制数据。procfs 通常用于进程/系统状态展示。debugfs 明确是调试接口,不保证稳定 ABI,不能让生产程序依赖它。
ioctl 适合与特定文件描述符强绑定的控制命令,但命令号、结构体对齐、32/64 位兼容和版本扩展需要谨慎设计。Netlink 更适合结构化、双向、可多播的控制与事件消息;它的属性 TLV 模型便于渐进扩展。mmap 适合高频大数据共享,例如帧缓冲、DMA ring 或性能缓冲区,但必须清楚页面生命周期、一致性和权限边界。
一份接口契约至少写清四件事
第一,数据方向:用户写配置、内核发布状态,还是双方交换消息?第二,频率与大小:偶尔读一个状态值,还是每秒数千条数据?第三,生命周期:设备复位、热拔插或进程退出时,映射/订阅/阻塞读怎样结束?第四,版本策略:旧程序遇到新字段、新程序遇到旧内核时是忽略、降级还是失败?
以 Netlink 为例,属性解析应当允许未知的可选字段被安全忽略、拒绝缺失的必选字段,并为每个数值校验范围。以 ioctl 为例,结构体需要显式长度/版本或可扩展尾部,不能把内核指针和未初始化填充直接暴露给用户态。
1 | 消息/结构体 = version + required fields + optional extensible fields |
mmap 的高性能也带来高责任
将 ring buffer 映射给用户态可省掉频繁 copy,但用户和内核必须约定生产者/消费者索引的内存序、何时可重用槽位、设备退出后如何撤销映射。DMA 数据还涉及 cache coherency 和 IOMMU 权限。若没有完整所有权协议,零拷贝很容易变成读到旧帧、覆盖未消费数据或 use-after-free。
先从最小、稳定的接口开始。对控制面使用 Netlink/ioctl,对状态使用 sysfs/read,对大吞吐数据使用显式 ring + mmap;将调试便利留在 debugfs。接口一旦发布,最难的不是写第一版,而是让第二版不伤害第一版用户。
参考:sysfs rules · Netlink · mmap(2)