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

/dev/xxx 打不开、mknod 后读写进不了驱动:问题常在 cdev 与 VFS 的衔接 ,而不是 read 实现本身。本文沿 cdev_init/cdev_addregister_chrdev_regionchrdev_openfile_operations 走通一次打开路径,便于对照主次设备号与 fops 排障。

源码锚点

路径 作用
include/linux/cdev.h struct cdevcdev_init/cdev_add/cdev_del
fs/char_dev.c 字符设备号区域、chrdev_opencdev_add
include/linux/fs.h struct file_operationsstruct 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_chrdevcdev_* 别混用错生命周期

老接口 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_addchrdev_open 所在文件,并画出 open 到 fops->open 的链
  • /proc/devices 中的 major 与 /dev 节点、cdev_add 三者一致
  • 模块 owner、加载/卸载与 cdev_del/unregister_chrdev_region 成对
  • open 设置 private_data;多 minor 用次设备号索引
  • 用户缓冲区一律 copy_*_userstrace 能看到 open/read 进入驱动路径
  • 需要事件通知时实现 poll,避免用户态空转
相关推荐
baopixiaoz8 分钟前
BeeQuant × BeeAgent:用AI加速策略验证
大数据·人工智能·python·区块链
浩瀚地学26 分钟前
deepagents学习打卡day04
python·agent
安易算力1 小时前
PUE优化工程实践:从1.5到1.2的制冷架构与气流组织改造路径
网络·python·容器·架构·kubernetes
troy1281 小时前
Codex 安全盲区:代码漏洞生成实测
windows·python·ci/cd·pycharm·django·github·fastapi
嵌入式学习菌2 小时前
Workbuddy自动写一个RS485 / LoRa 参数调试工具
python
钱栈up2 小时前
番茄小说榜单爬虫失效排查:从页面路由变更到API参数映射的全流程复盘
python
唐璜Taro3 小时前
Agent Harness 系列 · 第 1 篇|Agent 不只是换一个更强的模型
人工智能·python
泡茶喝茶写代码3 小时前
量化数据进阶:多维实战篇(第 11 篇):历史涨跌停价:涨跌停序列与止损线测算
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据api
ikun_文4 小时前
Django开启跨域请求
python·pycharm·django
外收内放4 小时前
Python与AI应用(项目开发实战:AI智能伴侣第三版)
python·学习·ai编程