字符设备注册:从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,避免用户态空转
相关推荐
weixin199701080167 小时前
《1688图片空间API踩坑:img.upload 与 album.* 的防盗链与CDN缓存问题》(附Python源码)
开发语言·python·缓存
布吉岛的石头7 小时前
Java 程序员第 49 阶段11:Java 接 BERT:用 ONNX Runtime 做句向量推理
java·人工智能·python·深度学习·bert·transformer
第25小时记录7 小时前
数据结构与算法 -第 3 章 常用算法-动态规划
数据结构·python
Dmz丶8 小时前
前端转 AI 100 天 Day 18:pytest 入门——为什么你必须要写测试
python
用户019027581618 小时前
如何用 Python 计算 DMA 平行线差指标?
python
奈葵8 小时前
Python
开发语言·python
happylifetree8 小时前
Python54-55:核心语法-数据容器-字典dict-案例
python
小小张说故事8 小时前
Python 迭代器和生成器到底啥区别?3 个例子讲透,别再背概念了
python
外收内放9 小时前
Python基础语法练习题(56优化版补充)
开发语言·python
专注API从业者9 小时前
告别复杂页面解析,OpenClaw 快速搭建电商商品监控与数据分析脚本
大数据·数据库·python·数据挖掘·数据分析