技术知识库
字符设备如何把设备节点连接到 file_operations
字符设备如何把设备节点连接到 file_operations
摘要
字符设备的数据入口是 /dev 下的设备节点,但真正处理 open()、read() 和 write() 的是驱动注册的 struct file_operations。设备节点携带主、次设备号,内核用设备号找到 cdev,再由 cdev->ops 把系统调用分发到驱动函数。class_create() 和 device_create() 负责把设备呈现给设备模型并触发节点创建,不是存放邮箱数据的地方。
问题/背景
在 martin_mailbox 实验中,用户空间执行:
printf 'test\n' > /dev/martin_mailbox
cat /dev/martin_mailbox
需要解释 /dev 节点、设备号、cdev、file_operations 和内核缓冲区分别承担什么职责。
核心机制
用户进程
│ open/read/write("/dev/martin_mailbox")
▼
/dev/martin_mailbox ── 主设备号 247、次设备号 0
│
▼
内核字符设备映射 ── dev_t → struct cdev
│ │
│ └─ cdev->ops
▼
struct file_operations
├─ .open → martin_mailbox_open()
├─ .read → martin_mailbox_read()
└─ .write → martin_mailbox_write()
│
▼
martin_mailbox_buf[]
设备号与 cdev
alloc_chrdev_region() 分配一段没有冲突的主、次设备号;cdev_add() 把这段设备号与一个 struct cdev 建立映射。动态方式使用 cdev_alloc() 得到 cdev 指针,并设置 ops 和 owner。
file_operations 是唯一的一组约定原型
内核中当然存在许多名为 read、open 的函数,但字符设备驱动必须遵守 struct file_operations 对应成员的函数签名。驱动可以只实现当前需要的成员;未实现的操作保持为空。
class 和 device 的职责
class_create() 创建设备类别,device_create() 在设备模型中注册一个设备并携带 dev_t。在当前 Debian systemd 环境中,设备事件配合 devtmpfs/udev 生成 /dev/martin_mailbox。
因此,写操作仍然写向 /dev/martin_mailbox。/sys/class/martin_mailbox/ 是设备模型的属性视图,不是字符数据通道。
write 的数据与返回值
用户指针不能被内核直接当作普通指针使用,因此用 copy_from_user() 把数据复制到内核缓冲区。若缓冲区为 256 字节并需要以 \0 结尾,单次最多保存 255 字节有效数据。
驱动应返回实际接收的字节数 write_len,而不是缓冲区容量,也不应把额外补入的 \0 算入有效数据长度。
read 的偏移与 EOF
*ppos 是当前文件位置。一次 read() 只复制调用者请求长度和剩余数据长度中的较小值,并把偏移向后推进。后续调用发现 *ppos >= martin_mailbox_len 时返回 0,含义是 EOF。
返回 0 不代表“本次复制成功”,而是告诉用户程序“没有更多数据”。因此即使 copy_to_user() 已复制内容,若处理函数最后仍返回 0,cat 也会把这次调用当成 EOF。
证据与来源
- 实验内核:Running Linux Kernel 5.0,ARM64,
5.0.0+。 - 实验模块:
/home/martin-zou/workspace/gcc-lab/char_mod/martin_mailbox/martin_mailbox.c。 - 相关头文件:
include/linux/cdev.h、include/linux/fs.h、include/linux/device.h、include/linux/uaccess.h。 - 实际验证:向
/dev/martin_mailbox写入test\n后,cat输出test。
关联
- 字符设备驱动 MOC
- 实现可读写的 martin_mailbox 字符设备
- misc 设备为什么称为杂项设备
- copy_to_user 成功但 cat 没有输出
- 交叉编译并加载 Linux 5.0 ARM64 Hello World 模块
待验证
- 第 6 章完成后阅读 VFS 到
chrdev_open()、cdev和具体file_operations的调用路径。 - 第 6 章完成后补充
device_create()到 devtmpfs/udev 节点创建的源码与事件路径。 - 后续实验补充并发访问、互斥和阻塞读写语义。