Linux内核分析之设备驱动-01
This language version is unavailable; showing the other language.
40.1 字符设备注册 —— register_chrdev
字符设备注册 = 拿一块设备号区间 + 挂一个 cdev(携带 file_operations)。老式一步接口与新式两级接口并存,cdev 本身是个 kobject——39 章的模型在此落地。
40.1.1 设备号与两级注册
// include/linux/kdev_t.h:7-12
#define MINORBITS 20 /* 次设备号 20 位 */
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
// fs/char_dev.c:197/233/265(三种申请方式)
int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
三种注册的选型:
register_chrdev_region: 静态指定 major (知名驱动,
/proc/devices 有常量约定, 如 mem=1)
alloc_chrdev_region: 动态分配 (现代推荐, 避免冲突)
__register_chrdev: 老式一步到位 (major+minor 区间
直接绑 fops, 无需自己建 cdev — 小驱动偷懒用)
设备号 = major(12位) + minor(20位) → dev_t
次设备号的含义由驱动自定 (通常 = 每板卡/每队列实例号)
40.1.2 cdev:行为本体
// include/linux/cdev.h:14-22
struct cdev {
struct kobject kobj; /* 39.1 节基类: sysfs/引用 */
struct module *owner; /* 防模块卸载时仍有 fd */
const struct file_operations *ops; /* 行为表 */
struct list_head list; /* 本 cdev 的全部 inode */
dev_t dev;
unsigned int count; /* 覆盖的次设备号数 */
} __randomize_layout;
// fs/char_dev.c:476
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
owner 字段的生命周期是字符设备的经典死锁防线:chrdev_open 成功后 try_module_get(owner)——fd 存在期间模块不可卸载,release 时归还。cdev_add 之后设备即"活的":即使用户态还没有设备节点,持设备号的程序已经能通过 mknod 访问——注册顺序的约定是"cdev_add 放最后"(所有状态就绪再开门)。cdev_device_add()(cdev.h:30-31)是现代驱动的联挂接口:一次调用同时挂 cdev 与对应 struct device(39 章模型设备),设备节点/uevent 由 core 统一处理。
40.1.3 open 握手:f_op 替换
// fs/char_dev.c:370-430(要点)
static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *cdev;
...
cdev = cdev_get(inode->i_cdev); /* :345 从 inode 取 cdev */
...
filp->f_op = cdev->ops; /* 操作表替换! */
...
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp); /* 驱动 open */
...
}
}
"操作表替换"是 VFS 与驱动世界的边界开关:inode 的 i_fop 原是 def_chr_fops(只有一个 chrdev_open),open 的第一击触发了它——替换后该 file 的后续 read/write/ioctl 全部直达驱动。驱动 open 的典型职责:container_of(inode->i_cdev) 取回私有结构、按 iminor(inode) 区分实例、初始化会话状态挂 filp->private_data。
设备节点的建立两条路:udev 按 uevent 动态建(39.1.3 节),或驱动在 /dev 用 device_create 请求 core 建——现代驱动不自己 mknod。
小结
字符设备注册 = alloc_chrdev_region 拿号 + cdev_init/add 挂行为表,cdev 借 kobject 获得模型身份、借 owner 字段锁住模块生命周期;open 的 f_op 替换(chrdev_open:407 区)让 VFS 的通用读写直达驱动,inode->i_cdev 与 private_data 是驱动的两个私有通道。下一节逐成员解剖 file_operations 的设备语义。
40.2 file_operations 与设备文件操作
file_operations(33.1.4 节 fs.h:1926-1960)的每个成员对应一种用户态系统调用语义。驱动不必全实现——未实现的成员使对应调用返回 -EINVAL/ENOSYS。本节按"数据、控制、异步、映射"四组解剖设备驱动的常用成员。
40.2.1 数据通路:read/write 与迭代器
// include/linux/fs.h:1929-1933
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
驱动的读实现的三个纪律:
[1] 用户指针必须 copy_to_user, 不得解引用
(用户/内核地址空间隔离, SMAP 1.6 节)
copy 失败返回未拷贝字节数 (EFAULT)
[2] 部分读写合法且必须如实返回 (33.4.3 节)
[3] 阻塞语义: 无数据 → wait_event(wq, 有数据)
(42.2 节等待队列; 非阻塞 fd → EAGAIN)
read_iter 的新世界:
iov_iter 抽象缓冲 (用户/内核/管道/bvec, 33.4.1)
→ 驱动可被 splice/io_uring 消费
copy_page_to_iter 家族代替 copy_to_user
O_NONBLOCK/位置 来自 kiocb 而非参数
llseek 对流设备返回 -ESPIPE(管道/套接字的标准回应);可寻址设备实现它维护 *ppos。
40.2.2 控制通路:unlocked_ioctl 与兼容
// include/linux/fs.h:1938 区 (设备驱动最常用的成员)
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
#ifdef CONFIG_COMPAT
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
#endif
ioctl 是设备的"私有 RPC":命令号以 _IO/_IOR/_IOW/_IOWR(dir, nr, size) 宏编码(方向+序号+载荷大小),32/64 位指针大小差异经 compat_ioctl 或 compat_ptr 处理——gpu/媒体驱动的主要控制面。权限检查责任在驱动:capable(CAP_SYS_ADMIN) 或文件级检查,open 之后 sysfs 权限不再保护 ioctl。
40.2.3 异步与映射:poll/mmap/fasync
poll (32 章三件套、epoll 45.3 的设备侧实现):
__poll_t my_poll(struct file *f, poll_table *wait)
{
poll_wait(f, &dev->wq, wait); /* 注册等待队列 */
__poll_t mask = 0;
if (dev->have_data) mask |= EPOLLIN | EPOLLRDNORM;
if (dev->tx_free) mask |= EPOLLOUT;
return mask;
}
协议: poll_wait 只注册不睡; 返回值告知就绪;
事件到达 → wake_up_interruptible(&wq) → epoll 重查
(26.2.3 节 pipe_poll 同款模板)
mmap: 设备寄存器/DMA 缓冲的用户映射
remap_pfn_range(vma, addr, phys>>PAGE_SHIFT, size, prot)
(20 章 vmap 家族的设备面; fb/声卡的零拷贝通道)
fasync: O_ASYNC 信号驱动 IO (SIGIO)
fasync_helper 注册队列, kill_fasync 发信号
(32.3 节 signalfd 之前时代的事件通知)
release: filp 关闭 → 释放 private_data 会话态;
cdev->owner 引用在此归还 (40.1.2 节)
40.2.4 操作表实现清单
| 成员 | 用户态对应 | 不实现的后果 |
|---|---|---|
| read/write | read(2)/write(2) | -EINVAL |
| read_iter/write_iter | splice/io_uring 直通 | 高级 I/O 不可用 |
| unlocked_ioctl | ioctl(2) | -ENOTTY |
| poll | select/epoll | 永远就绪(可写不可读的假象) |
| mmap | mmap(2) | -ENODEV |
| llseek | lseek(2) | no_seek_end_llseek 类默认或 -ESPIPE |
小结
file_operations 的设备语义分四组:数据(read/write + 迭代器变体,三个纪律是 copy_to_user/如实短读写/阻塞语义)、控制(ioctl 命令编码 + 自查权限)、异步(poll 三段模板 = 注册+掩码+唤醒;mmap 暴露寄存器/DMA 缓冲;fasync 信号通知)、生命周期(open 挂 private_data / release 收会话)。驱动按需实现、未实现者被 VFS 以标准错误回应。下一节看 miscdevice 如何把这套注册压缩成十行。
40.3 miscdevice 与简化字符设备
miscdevice 是"最小驱动"框架:主设备号固定 10,次设备号动态分配,一次 misc_register 完成设备号申请、cdev 挂接、设备模型注册、uevent 全流程。内核自带的大量简单驱动(/dev/random、/dev/hwrng、kvm(49 章)、BPF fs 前身的设备面)都是 misc。
40.3.1 miscdevice:一次注册全包
// include/linux/miscdevice.h:84(节选)
struct miscdevice {
int minor; /* MISC_DYNAMIC_MINOR = 动态 */
const char *name; /* 节点名 /dev/<name> */
const struct file_operations *fops; /* 行为表 (40.2 节) */
struct list_head list;
struct device *this_device;
const struct attribute_group **groups;
...
};
// drivers/char/misc.c:209
int misc_register(struct miscdevice *misc)
最小驱动模板 (内核 misc 样例的骨架):
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.unlocked_ioctl = my_ioctl,
};
static struct miscdevice my_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mydev", /* → /dev/mydev */
.fops = &my_fops,
};
module_misc_device(my_misc); /* 一宏完成注册/注销 */
misc_register 内部: 分配 minor → cdev → device 注册
→ uevent → /dev/mydev 由 udev 落地
misc_deregister 对称拆除
为什么只占一个 major 还够用:misc 用链表管理次设备号(misc_list),所有 misc 设备共享主 10 号的 misc_fops 前置层——open 时按 minor 二次分发到各驱动的 fops(与 40.1.3 节 chrdev_open 同构的第二次替换)。this_device 使 misc 设备自动获得 39.3 节的 sysfs 属性挂点(groups 字段直接带属性组)。
40.3.2 何时用 misc 何时自建
| 维度 | miscdevice | 完整 cdev |
|---|---|---|
| 注册成本 | 一个结构体 + 一宏 | region+cdev+device 三步 |
| 设备号 | 固定 major 10,minor 动态 | 独立 major(需 API 空间) |
| 多实例 | 链表可挂多个 minor | 区间自定(成百上千实例友好) |
| sysfs 位置 | /sys/class/misc/ |
自定 class 下 |
| 适用 | 单实例控制设备/演示驱动 | 块队列对、GPU 节点族 |
内核内的 misc 用户群证明了它不是玩具:/dev/random(混沌池)、/dev/kvm(49.1 节 KVM 入口)、/dev/mem 变体、/dev/watchdog、/dev/binderfs 家族——"设备是控制面而非数据面"的场景全部 misc 起步,长大后才考虑独立 major。
40.3.3 字符驱动的完整闭环
40 章的闭环串联 (一个 misc 驱动的一生):
module_misc_device → misc_register
→ misc 设备加入链表 + device 注册 (39 章)
→ uevent (39.1.3) → udev 建 /dev/mydev
用户 open → chrdev_open (40.1.3) → f_op 替换
→ my_open: private_data = 会话态
read/poll/ioctl → 40.2 节语义
(内部: 等待队列 42.2 / DMA 43 章 / 中断 42 章)
close → release → owner 引用归还
rmmod → misc_deregister → device 拆除
小结
miscdevice 以"固定主 10 号 + 链表分发 + 一宏注册"把字符驱动压缩到最小形态:misc_register 一次完成设备号、cdev、设备模型、uevent 四件事;二次 f_op 分发让所有 misc 共享一个 major 而互不干扰。单实例控制设备(含 /dev/kvm 这类关键入口)以 misc 起步,多实例与独立命名空间才升级完整 cdev。第七部分前三章完成设备与字符面;下一章进入块设备驱动——35 章 blk-mq 框架的驱动侧实现。