技术知识库

实现可读写的 martin_mailbox 字符设备

实现可读写的 martin_mailbox 字符设备

摘要

本实验在 Linux 5.0 ARM64 QEMU 平台上实现了一个 256 字节内存邮箱字符设备。模块动态申请设备号和 cdev,注册 open/read/write,并通过 class_create()、device_create() 自动暴露 /dev/martin_mailbox。最终写入 test 后可由 cat 读回,字符设备的最小数据通路已经闭环。

原理见 字符设备如何把设备节点连接到 file_operations。

环境与路径

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

前置实验:

1. 创建实验目录和驱动源码

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

写入以下教学版源码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define MARTIN_MAILBOX_NAME "martin_mailbox"
#define MARTIN_MAILBOX_SIZE 256

static dev_t mailbox_dev;
static unsigned int device_count = 1;
static struct cdev *martin_mailbox_cdev;
static struct class *martin_mailbox_class;
static struct device *martin_mailbox_device;
static char martin_mailbox_buf[MARTIN_MAILBOX_SIZE];
static size_t martin_mailbox_len;

static int martin_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 martin_mailbox_read(struct file *file,
				   char __user *buf,
				   size_t count,
				   loff_t *ppos)
{
	size_t remaining;
	size_t read_len;

	if (*ppos >= martin_mailbox_len)
		return 0;

	remaining = martin_mailbox_len - *ppos;
	read_len = count;
	if (read_len > remaining)
		read_len = remaining;

	if (copy_to_user(buf, martin_mailbox_buf + *ppos, read_len))
		return -EFAULT;

	*ppos += read_len;
	pr_info("martin_mailbox: read %zu bytes\n", read_len);
	return read_len;
}

static ssize_t martin_mailbox_write(struct file *file,
				    const char __user *buf,
				    size_t count,
				    loff_t *ppos)
{
	size_t write_len = count;

	if (write_len > sizeof(martin_mailbox_buf) - 1)
		write_len = sizeof(martin_mailbox_buf) - 1;

	if (copy_from_user(martin_mailbox_buf, buf, write_len))
		return -EFAULT;

	martin_mailbox_buf[write_len] = '\0';
	martin_mailbox_len = write_len;
	*ppos += write_len;

	pr_info("martin_mailbox: received %zu bytes: %s\n",
		write_len, martin_mailbox_buf);
	return write_len;
}

static const struct file_operations martin_mailbox_ops = {
	.owner = THIS_MODULE,
	.open = martin_mailbox_open,
	.read = martin_mailbox_read,
	.write = martin_mailbox_write,
};

static int __init martin_mailbox_init(void)
{
	int ret;

	ret = alloc_chrdev_region(&mailbox_dev, 0, device_count,
				  MARTIN_MAILBOX_NAME);
	if (ret)
		return ret;

	martin_mailbox_cdev = cdev_alloc();
	if (!martin_mailbox_cdev) {
		ret = -ENOMEM;
		goto unregister_chrdev;
	}

	martin_mailbox_cdev->ops = &martin_mailbox_ops;
	martin_mailbox_cdev->owner = THIS_MODULE;

	ret = cdev_add(martin_mailbox_cdev, mailbox_dev, device_count);
	if (ret)
		goto delete_cdev;

	martin_mailbox_class = class_create(THIS_MODULE,
					    MARTIN_MAILBOX_NAME);
	martin_mailbox_device = device_create(martin_mailbox_class,
					      NULL,
					      mailbox_dev,
					      NULL,
					      MARTIN_MAILBOX_NAME);

	pr_info("succeeded register char device: %s\n",
		MARTIN_MAILBOX_NAME);
	pr_info("Major number = %d, minor number = %d\n",
		MAJOR(mailbox_dev), MINOR(mailbox_dev));
	return 0;

delete_cdev:
	cdev_del(martin_mailbox_cdev);
unregister_chrdev:
	unregister_chrdev_region(mailbox_dev, device_count);
	return ret;
}

static void __exit martin_mailbox_exit(void)
{
	device_destroy(martin_mailbox_class, mailbox_dev);
	class_destroy(martin_mailbox_class);
	cdev_del(martin_mailbox_cdev);
	unregister_chrdev_region(mailbox_dev, device_count);
	pr_info("goodbye, martin_mailbox_exit\n");
}

module_init(martin_mailbox_init);
module_exit(martin_mailbox_exit);

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

这里使用动态 cdev 方式,所以 cdev_alloc() 之后直接设置 ops 和 owner,不再调用 cdev_init()。本轮实际写入很短;整理版让 write() 返回实际接收的 write_len,从而在输入超过 255 字节时也符合短写语义。

2. 创建 Makefile

vim Makefile

写入:

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

obj-m += martin_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 martin_mailbox.ko
modinfo martin_mailbox.ko |
  grep -E '^(license|description|author|vermagic):'

预期:file 显示 ARM AArch64 可重定位 ELF,vermagic 与目标机 uname -r 的 5.0.0+ 一致。

4. 复制到 ARM64 Guest

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

登录目标机:

ssh -p 2222 martin@127.0.0.1

5. 加载并检查设备注册

在 ARM64 Guest 中执行:

sudo insmod /home/martin/martin_mailbox.ko

lsmod |
  grep '^martin_mailbox'

grep martin_mailbox /proc/devices
ls -l /dev/martin_mailbox
ls -l /sys/class/martin_mailbox/

sudo dmesg |
  tail -n 20

本轮实际分配结果为:

247 martin_mailbox

主设备号是动态分配的,重启或重新构建后不保证始终为 247,因此验证时应以 /proc/devices 为准。

6. 验证读写闭环

普通用户若没有写权限,用 sudo tee 让真正打开设备节点的进程拥有 root 权限:

printf 'test\n' |
  sudo tee /dev/martin_mailbox >/dev/null

sudo cat /dev/martin_mailbox

本轮以 root shell 执行的实际过程:

root@rlk-arm64:/home/martin# cat /dev/martin_mailbox
root@rlk-arm64:/home/martin# echo "test" > /dev/martin_mailbox
root@rlk-arm64:/home/martin# cat /dev/martin_mailbox
test

第一次为空是因为尚未写入数据,martin_mailbox_len 为 0;写入后,read() 复制 5 字节(包含 echo 产生的换行),随后返回 EOF。

7. 卸载并验证清理

sudo rmmod martin_mailbox

lsmod |
  grep martin_mailbox ||
  echo 'module unloaded'

grep martin_mailbox /proc/devices ||
  echo 'device number released'

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

sudo dmesg |
  tail -n 20

完成标准

  • martin_mailbox.ko 能为 Linux 5.0 ARM64 目标内核交叉编译。
  • insmod 成功,lsmod 能看到模块。
  • /proc/devices 能看到动态设备号。
  • /dev/martin_mailbox 自动出现,不再手工 mknod。
  • 写入 test 后,cat 能读回 test。
  • 能解释 /dev 数据节点与 /sys/class 设备模型视图的区别。
  • 补充 class/device 的完整错误回滚和并发保护。
  • 第 6 章完成后补充框架与源码调用链。

关联

待验证

  • 多进程同时读写时,当前全局缓冲区缺少互斥保护。
  • class_create() 和 device_create() 失败时的完整逆序释放尚未加入教学代码。
  • 第 6 章完成后阅读 fs/char_dev.c、VFS 分发和设备模型源码。