/dev/xxx 打不开、mknod 后读写进不了驱动:问题常在 cdev 与 VFS 的衔接 ,而不是 read 实现本身。本文沿 cdev_init/cdev_add → register_chrdev_region → chrdev_open → file_operations 走通一次打开路径,便于对照主次设备号与 fops 排障。
源码锚点
| 路径 | 作用 |
|---|---|
include/linux/cdev.h |
struct cdev、cdev_init/cdev_add/cdev_del |
fs/char_dev.c |
字符设备号区域、chrdev_open、cdev_add |
include/linux/fs.h |
struct file_operations、struct inode/file |
fs/devices.c / fs/inode.c(随版本) |
设备 inode 与打开路径协作 |
Documentation/admin-guide/devices.txt |
设备号约定(参考) |
核心结构:
c
/* include/linux/cdev.h */
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
dev_t dev;
unsigned int count;
};
/* include/linux/fs.h --- 用户可见操作集 */
struct file_operations {
struct module *owner;
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
int (*open)(struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
/* ... */
};
注册骨架:
c
/* 典型驱动写法(概念) */
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
alloc_chrdev_region(&dev, 0, count, "mydev"); /* 或 register_chrdev_region */
cdev_add(&my_cdev, dev, count);
调用链
text
驱动初始化
→ alloc_chrdev_region / register_chrdev_region /* 占用主/次设备号 */
→ cdev_init(cdev, fops)
→ cdev_add(cdev, dev, count) /* 挂到内核 cdev 映射 */
用户态 open("/dev/mydev")
→ do_sys_open / vfs_open
→ chrdev_open(inode, file) /* fs/char_dev.c */
→ 按 inode->i_rdev 找到 cdev
→ file->f_op = cdev->ops
→ 若存在 fops->open → 调用驱动 open
→ 之后 read/write/ioctl 走 file->f_op
设备节点创建(二选一):
text
userspace: mknod /dev/mydev c <major> <minor>
或 udev/devtmpfs:驱动侧 device_create(class, ...) 自动生成节点
重点知识
1. 设备号是查找键,cdev 是操作载体
VFS 打开字符设备时用 i_rdev(主+次)找到 cdev,再切换 file->f_op。只 mknod 而不 cdev_add,或 major/minor 与 cdev_add 不一致,会得到 ENXIO 或进错驱动。
2. register_chrdev 与 cdev_* 别混用错生命周期
老接口 register_chrdev(major, name, fops) 内部仍落到字符设备层;新代码推荐 alloc_chrdev_region + cdev_init + cdev_add,卸载时对称 cdev_del + unregister_chrdev_region。模块 owner 必须设对,否则 fops 还在用时模块可被卸掉。
3. open 里挂私有数据,勿依赖全局单例
常见写法:file->private_data = my_struct。多开、多 minor 时用 iminor(inode) 区分实例。read/write 必须正确处理 __user 指针(copy_to_user/copy_from_user)与返回值(ssize_t,部分成功要返回已传字节数)。
4. 配置与观测
bash
cat /proc/devices | grep -i mydev # 主设备号是否注册
ls -l /dev/mydev # 主次是否与驱动一致
# 动态分配时看 dmesg / 驱动打印的 major
# 类设备
ls /sys/class/<your_class>/
# 打开失败
strace -e openat,open cat /dev/mydev
dmesg | tail
权限:节点默认模式与 udev 规则决定是否需 root;EACCES 先查权限再查驱动。
5. 设计意图与踩坑
- 意图 :字符设备 = 字节流式访问抽象,用
fops把系统调用落到驱动。 - 坑 :
cdev_add成功但未device_create,节点不存在;手动mknod又写错次设备号。 - 坑 :
read返回-EFAULT却不区分「无数据可读」与「用户缓冲非法」。 - 坑 :阻塞
read未实现poll/fasync,用户态无法用select/epoll等待。 - 坑 :
ioctl命令码与用户头文件不同步,导致参数解释错乱。
Checklist
- 能指出
cdev_add与chrdev_open所在文件,并画出 open 到fops->open的链 -
/proc/devices中的 major 与/dev节点、cdev_add三者一致 - 模块
owner、加载/卸载与cdev_del/unregister_chrdev_region成对 -
open设置private_data;多 minor 用次设备号索引 - 用户缓冲区一律
copy_*_user;strace能看到 open/read 进入驱动路径 - 需要事件通知时实现
poll,避免用户态空转