技术知识库

使用 KFIFO 实现可读写邮箱设备

使用 KFIFO 实现可读写邮箱设备

摘要

本实验保留 misc 设备注册框架,把普通字符数组、有效长度和 *ppos 管理替换为 Linux KFIFO。驱动通过 kfifo_from_user() 追加数据,通过 kfifo_to_user() 读取并消费数据。模块加载后生成 /dev/kfifo_mailbox,实际写入 test3 后成功读回,实验 6-4 的核心目标已完成。

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

环境与路径

宿主机:Ubuntu 26.04,x86-64
目标机:QEMU ARM64,Linux 5.0.0+,Debian 12
源码目录:/home/martin-zou/workspace/gcc-lab/char_mod/kfifo_mailbox
内核构建目录:/home/martin-zou/workspace/runninglinuxkernel_5.0/build/arm64
交叉编译器前缀:aarch64-linux-gnu-
SSH 转发:127.0.0.1:2222 → ARM64 Guest:22

前置实验:使用 misc 机制实现可读写邮箱设备。

1. 创建 KFIFO 邮箱驱动

mkdir -p /home/martin-zou/workspace/gcc-lab/char_mod/kfifo_mailbox
cd /home/martin-zou/workspace/gcc-lab/char_mod/kfifo_mailbox
vim kfifo_mailbox.c

写入经过本轮代码检查整理的版本:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/kfifo.h>

#define KFIFO_MAILBOX_NAME "kfifo_mailbox"
#define KFIFO_MAILBOX_SIZE 256

DEFINE_KFIFO(kfifo_mailbox_fifo, char, KFIFO_MAILBOX_SIZE);

static int kfifo_mailbox_open(struct inode *inode, struct file *file)
{
	pr_info("%s: major=%d, minor=%d\n",
		__func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
	return 0;
}

static ssize_t kfifo_mailbox_read(struct file *file,
				  char __user *buf,
				  size_t count,
				  loff_t *ppos)
{
	unsigned int copied;
	int ret;

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

	pr_info("kfifo_mailbox: read %u bytes\n", copied);
	return copied;
}

static ssize_t kfifo_mailbox_write(struct file *file,
				   const char __user *buf,
				   size_t count,
				   loff_t *ppos)
{
	unsigned int copied;
	int ret;

	ret = kfifo_from_user(&kfifo_mailbox_fifo, buf, count, &copied);
	if (ret)
		return ret;
	if (count != 0 && copied == 0)
		return -ENOSPC;

	pr_info("kfifo_mailbox: wrote %u bytes\n", copied);
	return copied;
}

static const struct file_operations kfifo_mailbox_ops = {
	.owner = THIS_MODULE,
	.open = kfifo_mailbox_open,
	.read = kfifo_mailbox_read,
	.write = kfifo_mailbox_write,
};

static struct miscdevice kfifo_mailbox_device = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = KFIFO_MAILBOX_NAME,
	.fops = &kfifo_mailbox_ops,
};

static int __init kfifo_mailbox_init(void)
{
	int ret;

	ret = misc_register(&kfifo_mailbox_device);
	if (ret) {
		pr_err("kfifo_mailbox: misc_register failed: %d\n", ret);
		return ret;
	}

	pr_info("kfifo_mailbox: registered, major=%d, minor=%d\n",
		MISC_MAJOR, kfifo_mailbox_device.minor);
	return 0;
}

static void __exit kfifo_mailbox_exit(void)
{
	misc_deregister(&kfifo_mailbox_device);
	pr_info("kfifo_mailbox: unregistered\n");
}

module_init(kfifo_mailbox_init);
module_exit(kfifo_mailbox_exit);

MODULE_AUTHOR("martin.zou");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("KFIFO misc mailbox");

与实际验证版本相比,整理版删除了不再使用的普通缓冲区长度和 cdev 头文件,统一了 KFIFO 命名及日志,并让用户复制失败时返回原始错误码。它还临时用 -ENOSPC 阻止非零写请求在 FIFO 满时返回 0;正常 test3 读写路径没有改变。

2. 创建 Makefile

vim Makefile

写入:

KERNEL_DIR ?= /home/martin-zou/workspace/runninglinuxkernel_5.0/build/arm64
ARCH ?= arm64
CROSS_COMPILE ?= aarch64-linux-gnu-

obj-m += kfifo_mailbox.o

.PHONY: all clean

all:
	$(MAKE) -C $(KERNEL_DIR) \
		M=$(CURDIR) \
		ARCH=$(ARCH) \
		CROSS_COMPILE=$(CROSS_COMPILE) \
		modules

clean:
	$(MAKE) -C $(KERNEL_DIR) \
		M=$(CURDIR) \
		ARCH=$(ARCH) \
		CROSS_COMPILE=$(CROSS_COMPILE) \
		clean

3. 交叉编译并检查模块

make clean
make -j"$(nproc)"

file kfifo_mailbox.ko
modinfo kfifo_mailbox.ko |
  grep -E '^(license|description|author|vermagic):'

预期模块架构为 ARM AArch64,vermagic 与 Guest 的 5.0.0+ 一致。

4. 复制并加载模块

宿主机执行:

scp \
  -P 2222 \
  kfifo_mailbox.ko \
  martin@127.0.0.1:~/

登录 ARM64 Guest:

ssh -p 2222 martin@127.0.0.1

加载并检查:

sudo insmod /home/martin/kfifo_mailbox.ko

lsmod |
  grep '^kfifo_mailbox'

grep kfifo_mailbox /proc/misc
ls -l /dev/kfifo_mailbox

sudo dmesg |
  tail -n 20

5. 验证 KFIFO 读写

普通用户可使用:

printf 'test3\n' |
  sudo tee /dev/kfifo_mailbox >/dev/null

sudo cat /dev/kfifo_mailbox

本轮在 root shell 中的实际结果:

root@rlk-arm64:/home/martin# echo "test3" > /dev/kfifo_mailbox
root@rlk-arm64:/home/martin# cat /dev/kfifo_mailbox
test3

echo 写入的换行也进入 FIFO,所以 cat 输出后自然换行。读取完成后,该段数据已经从 FIFO 中消费。

6. 验证 FIFO 追加与消费语义

以下验证比单次 echo/cat 更能区分 KFIFO 与覆盖式普通缓冲区:

printf 'first-' |
  sudo tee /dev/kfifo_mailbox >/dev/null

printf 'second' |
  sudo tee /dev/kfifo_mailbox >/dev/null

sudo cat /dev/kfifo_mailbox
sudo cat /dev/kfifo_mailbox

预期第一次得到 first-second;第二次不再读到这批数据,因为第一次读取已经消费它们。

7. 已知边界:FIFO 写满

连续写入且不读取会填满 FIFO。早期版本在满时返回 0,导致 echo 持续重试并洪泛日志;修复后会返回 -ENOSPC,详见 KFIFO 写满后 echo 持续重试。

当前阶段至少保证非零写请求不返回 0:

if (count != 0 && copied == 0)
	return -ENOSPC;

下一实验将把它扩展为正式的非阻塞 -EAGAIN 语义;真正的阻塞等待留给等待队列实验。

8. 卸载并验证清理

sudo rmmod kfifo_mailbox

lsmod |
  grep kfifo_mailbox ||
  echo 'module unloaded'

test ! -e /dev/kfifo_mailbox &&
  echo 'device node removed'

完成标准

  • kfifo_mailbox.ko 成功交叉编译并加载到 Linux 5.0 ARM64。
  • /dev/kfifo_mailbox 自动生成。
  • kfifo_from_user() 能接收用户空间数据。
  • kfifo_to_user() 能读出并消费数据。
  • 写入 test3 后能够完整读回。
  • 驱动不再用 *ppos 和有效长度手工维护普通数组。
  • 能说明 KFIFO 只管理数据,不自动提供阻塞或非阻塞策略。
  • 已复现 FIFO 写满后零长度写导致用户程序持续重试的问题。
  • 已重新构建并验证临时 -ENOSPC 修复;部分写后能明确报错并结束。

代码检查结论

本轮实际代码的核心 KFIFO 路径正确,实验完成。仍有以下非阻塞整理项:

  • copy_len 和 write_len 已正确使用 unsigned int;日志格式建议对应使用 %u。
  • kfifo_to_user()、kfifo_from_user() 的错误码可直接 return ret,不必统一改成 -EIO。
  • martin_mailbox_len 已无用途,可以删除。
  • mymailbox_device 当前没有被继续使用,可以删除。
  • misc + KFIFO 版本不再需要 #include <linux/cdev.h>。
  • 注册失败应返回 ret,日志名称可以统一为 kfifo_mailbox。

这些整理项不影响本轮 test3 的成功验证。

关联

待验证

  • 实验 6-5:为空或已满的 KFIFO 加入 O_NONBLOCK 和 -EAGAIN 语义。
  • 后续实验:等待队列、阻塞 I/O、poll 和并发保护。