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 框架的驱动侧实现。