技术知识库
实现可读写的 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 章完成后补充框架与源码调用链。
关联
- 字符设备驱动 MOC
- 字符设备如何把设备节点连接到 file_operations
- 模块已加载但字符设备没有保留
- copy_to_user 成功但 cat 没有输出
- 使用 misc 机制实现可读写邮箱设备
待验证
- 多进程同时读写时,当前全局缓冲区缺少互斥保护。
class_create()和device_create()失败时的完整逆序释放尚未加入教学代码。- 第 6 章完成后阅读
fs/char_dev.c、VFS 分发和设备模型源码。