字符设备注册:从cdev_add 到chrdev_open 的 VFS路径

/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,避免用户态空转
相关推荐
代码山河10 小时前
Python就业方向全解析:后端、爬虫、数据分析、AI你选哪个?
python·学习·架构·教程·面向对象·项目
在世修行11 小时前
智学文枢:题目流水线
python·多线程·状态机·pyside
梯度下降者11 小时前
零基础入门Python 爬虫
爬虫·python·数据分析·入门指南·webcrawler
“AI国潮设计-小江”13 小时前
【AIGC实战】Python+Stable Diffusion打造“英歌舞财神”IP,解锁“马到成功”等爆款情绪营销文案设计
开发语言·人工智能·python·prompt·aigc
benchmark_cc14 小时前
用 Python 拉 A 股全市场实时快照:Notebook 请求、结果校验与易错点
python·数据分析·pandas·量化交易·股票数据·quantdash
伞伞悦读14 小时前
【第70期】Adam二阶矩不是方差:偏差校正、动量与AdamW完整实验
python
外收内放14 小时前
Python基础语法练习题(73)
java·前端·python
锋行天下15 小时前
Langgraph HITL 中断运行,用户输入后输出打字效果
人工智能·python
时早玖沐15 小时前
给 AstrBot 写一个天气推送插件:12 次提交、6 个坑
python·开源·ai编程
触底反弹15 小时前
🚀 FastAPI 五大重难点深度剖析:从 500 报错到彻底搞懂 Depends、Pydantic 与 SQLAlchemy
python·fastapi