技术知识库

KFIFO 如何用环形缓冲区管理字符设备数据

KFIFO 如何用环形缓冲区管理字符设备数据

摘要

KFIFO 是 Linux 内核提供的先进先出环形缓冲区。写入的数据追加到队尾,读取从队头取出并消费;读写位置由 KFIFO 内部维护,因此字符设备不再需要用 *ppos 和额外长度变量手工管理普通数组。它解决了排队、边界计算和已读空间复用,但不会自动决定 FIFO 为空或已满时应该等待还是报错。

问题/背景

普通 misc_mailbox 使用数组、有效长度和文件偏移:

char mailbox_buf[256];
size_t mailbox_len;

驱动必须自行计算剩余数据、限制复制长度并推进 *ppos。同一文件描述符先写后读时,写操作推进的文件偏移还可能使后续读取直接得到 EOF。实验 6-4 用 KFIFO 替换这种线性缓冲区管理。

核心机制

先进先出与环形空间

写入 A、B、C:  队头 → A B C ← 队尾
读取一次:      返回 A,队头移动到 B
继续写入 D:    队头 → B C D ← 队尾

KFIFO 内部维护读索引和写索引。读出的空间可以在环形数组中重新利用,不需要移动尚未读取的数据。

定义一个静态 KFIFO

#include <linux/kfifo.h>

DEFINE_KFIFO(kfifo_mailbox_fifo, char, 256);
  • kfifo_mailbox_fifo 是对象名称。
  • char 是元素类型。
  • 256 是元素数量;静态 KFIFO 的容量应为 2 的整数次幂。

从用户空间写入

unsigned int copied;
int ret;

ret = kfifo_from_user(&kfifo_mailbox_fifo,
			      buf, count, &copied);
if (ret)
	return ret;

return copied;

KFIFO 会根据剩余空间决定实际接收长度,把数据复制到队尾,并推进内部写索引。驱动必须返回 copied,因为 FIFO 空间不足时它可能小于 count。

复制到用户空间

unsigned int copied;
int ret;

ret = kfifo_to_user(&kfifo_mailbox_fifo,
			    buf, count, &copied);
if (ret)
	return ret;

return copied;

KFIFO 只返回当前已有的数据,并推进内部读索引。已经读出的内容不再出现在下一次读取中。loff_t *ppos 仍保留在 file_operations 规定的函数签名里,但这个 FIFO 设备不再依靠它定位数据。

与普通缓冲区的区别

行为普通线性缓冲区KFIFO 环形缓冲区
写入当前实现会覆盖旧内容追加到队尾
读取依赖 *ppos 和有效长度从队头取出并消费
位置管理驱动手工维护KFIFO 内部维护
已读空间需要自行回收或重置环形复用
空/满策略驱动决定仍由驱动决定

KFIFO 不等于阻塞 I/O

当 FIFO 为空时,kfifo_to_user() 可以成功但复制 0 字节;简单 read() 随后返回 0,用户空间会把它视作 EOF。当 FIFO 已满时,写入也可能复制 0 字节。

KFIFO 只负责保存和取出数据。下面这些策略需要驱动在其外层实现:

  • O_NONBLOCK 下返回 -EAGAIN。
  • 阻塞模式下使用等待队列睡眠。
  • 数据到来或空间释放后唤醒等待者。
  • 多生产者或多消费者场景下增加锁。

并发边界

KFIFO 在“单一生产者 + 单一消费者”场景可以不额外加锁。若存在多个写者或多个读者,仍需要由驱动使用自旋锁、互斥锁等方式保护相应操作。

意义

这个实验的重点不是少写几行 copy_to_user(),而是把设备数据从“一个可覆盖的内存块”升级成“有顺序、有容量、有消费语义的数据通道”。这正是串口接收、日志、传感器数据流和许多生产者—消费者设备常见的基础模型。

证据与来源

  • Running Linux Kernel 5.0:include/linux/kfifo.h。
  • 实验手册 v2.0.7,第 6.4 节“使用 KFIFO 改进设备驱动”。
  • 实验源码:/home/martin-zou/workspace/gcc-lab/char_mod/kfifo_mailbox/kfifo_mailbox.c。
  • Linux 5.0 ARM64 Guest 实际结果:写入 test3 后,从 /dev/kfifo_mailbox 读回 test3。

关联

待验证

  • 实验 6-5 验证 FIFO 为空或已满时的非阻塞 -EAGAIN 语义。
  • 后续实验用等待队列实现真正的阻塞读写。
  • 第 6 章结束后阅读 include/linux/kfifo.h 的索引和掩码实现。