3. 字符设备驱动核心概念:主设备号与次设备号、设备文件、file_operations 结构体

字符设备驱动是 Linux 驱动开发中最基础、最典型的驱动类型。理解其核心概念——设备号、设备文件与 file_operations 结构体——是编写任何字符设备驱动的第一步。这三者共同构成了用户空间与内核空间交互的桥梁。

核心认知: 在 Linux 中,“一切皆文件”。字符设备通过设备文件暴露给用户程序,而设备号则唯一标识了内核中的驱动程序。用户对设备文件的读写操作,最终通过 file_operations 结构体映射到驱动内核函数。

3.1 主设备号与次设备号

设备号是内核用来标识和管理设备驱动的一种数字标识,由两部分组成:主设备号 (Major Number) 和 次设备号 (Minor Number)。在 Linux 内核中,设备号使用 dev_t 类型(32 位无符号整数)表示,其中高 12 位为主设备号,低 20 位为次设备号。

概念 位数 作用 分配方式
主设备号 12 位 (0-4095) 标识设备对应的驱动程序。内核通过主设备号找到正确的驱动。 静态分配(cat /proc/devices 查看已占用)或动态分配(推荐)
次设备号 20 位 (0-1048575) 由驱动内部使用,用于区分同一驱动管理的多个设备实例。 驱动开发者自行定义,通常用于索引设备或区分不同硬件通道
关键操作: 使用 MAJOR(dev) 和 MINOR(dev) 宏从 dev_t 中提取主、次设备号;使用 MKDEV(major, minor) 宏将主、次设备号组合成 dev_t。

设备号的分配有两种方式:

  • 静态分配: 调用 register_chrdev_region(dev_t first, unsigned int count, char *name)。需要开发者提前知道可用的主设备号,容易造成冲突,不推荐用于通用驱动。
  • 动态分配: 调用 alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)。内核自动分配一个未使用的主设备号,是推荐的做法。
设备号结构:dev_t 类型分解 设备号 (dev_t) 结构分解 主设备号 (Major) 12 位 (0-4095) 次设备号 (Minor) 20 位 (0-1048575) MAJOR(dev) / MINOR(dev) 提取 | MKDEV(major, minor) 组合

3.2 设备文件

设备文件(又称设备节点)是用户空间访问设备驱动的入口。它位于文件系统中(通常为 /dev 目录),通过 mknod 命令或 device_create 函数创建。设备文件本身不包含数据,它只是一个指向内核驱动程序的接口。

设备文件通过 ls -l /dev 查看时,文件类型列会显示为 c(字符设备)或 b(块设备)。例如:

crw-rw-rw- 1 root root 1, 3  Apr 10 10:00 /dev/null
crw------- 1 root root 10, 60 Apr 10 10:00 /dev/random

在输出中,1, 3 表示主设备号为 1,次设备号为 3;10, 60 表示主设备号为 10,次设备号为 60。

创建方式 命令/函数 说明
手动创建 mknod /dev/mydev c 240 0 手动指定主设备号 240,次设备号 0,类型为字符设备 (c)。
自动创建 (推荐) device_create(class, parent, devt, drvdata, fmt) 配合 class_create 使用,驱动加载时自动在 /dev 下创建设备文件。
注意事项: 手动使用 mknod 需要 root 权限,且主设备号必须与驱动注册的一致。现代驱动开发强烈推荐使用 device_create 自动创建设备文件,避免手动管理带来的错误。
用户空间通过设备文件访问内核驱动 用户空间与内核空间交互:设备文件作为桥梁 用户空间 (User Space) 应用程序 (App) — open / read / write / close 系统调用层 (VFS) 内核空间 (Kernel Space) 字符设备驱动 — file_operations 回调函数 /dev/mydev (c, 240, 0)

3.3 file_operations 结构体

file_operations 结构体是字符设备驱动的核心,它定义了用户空间程序能够对设备文件执行的所有操作。该结构体包含一系列函数指针,驱动开发者通过填充这些指针,将用户空间的系统调用(如 open、read、write)与内核中的驱动函数关联起来。

以下是 file_operations 结构体中最重要的成员:

成员 系统调用 驱动函数原型 说明
owner — struct module *owner 通常设置为 THIS_MODULE,防止模块正在使用时被卸载。
open open() int (*open)(struct inode *, struct file *) 打开设备时调用,用于初始化设备或递增使用计数。
release close() int (*release)(struct inode *, struct file *) 关闭设备时调用,用于释放资源或递减使用计数。
read read() ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据。数据通过 copy_to_user 传递给用户空间。
write write() ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备写入数据。数据通过 copy_from_user 从用户空间获取。
unlocked_ioctl ioctl() long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令接口,用于实现非标准操作(如设置波特率、启动传输等)。

一个典型的 file_operations 结构体定义示例如下:

// 驱动函数声明
static int mydev_open(struct inode *inode, struct file *filp);
static int mydev_release(struct inode *inode, struct file *filp);
static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
static ssize_t mydev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

// file_operations 结构体定义
static struct file_operations mydev_fops = {
    .owner          = THIS_MODULE,
    .open           = mydev_open,
    .release        = mydev_release,
    .read           = mydev_read,
    .write          = mydev_write,
    .unlocked_ioctl = mydev_ioctl,
};
重要提示: 在 read 和 write 函数中,用户空间缓冲区指针(__user *)不能在内核中直接解引用。必须使用 copy_to_user() 和 copy_from_user() 函数进行安全的数据拷贝,以避免内核崩溃或安全漏洞。
file_operations 结构体映射关系 file_operations 结构体:系统调用到驱动函数的映射 用户系统调用 open() read() write() ioctl() close() file_operations .open .read .write .unlocked_ioctl .release .owner = THIS_MODULE 驱动回调函数 mydev_open() mydev_read() mydev_write() mydev_ioctl() mydev_release()

3.4 三者的协作关系

主设备号、设备文件和 file_operations 结构体三者协同工作,构成了字符设备驱动的完整访问链路:

  1. 注册阶段: 驱动模块加载时,通过 alloc_chrdev_region 分配设备号,通过 cdev_init 和 cdev_add 将 file_operations 结构体与设备号绑定到内核。
  2. 设备文件创建: 使用 device_create 在 /dev 下创建设备文件,该文件记录了主设备号和次设备号。
  3. 用户访问: 用户程序打开设备文件时,VFS 根据文件中的主设备号找到对应的驱动,并调用驱动注册的 file_operations 中的 open 函数。
  4. 数据操作: 用户程序调用 read/write 时,VFS 通过 file_operations 中的函数指针,直接调用驱动实现的 read/write 函数。
学习建议: 在编写第一个字符设备驱动时,建议先实现 open 和 release 两个函数,验证驱动加载和卸载流程。然后逐步添加 read 和 write 功能,最后再考虑 ioctl 等复杂操作。循序渐进是掌握驱动开发的最佳路径。