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)。内核自动分配一个未使用的主设备号,是推荐的做法。
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 自动创建设备文件,避免手动管理带来的错误。
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() 函数进行安全的数据拷贝,以避免内核崩溃或安全漏洞。
3.4 三者的协作关系
主设备号、设备文件和 file_operations 结构体三者协同工作,构成了字符设备驱动的完整访问链路:
- 注册阶段: 驱动模块加载时,通过
alloc_chrdev_region分配设备号,通过cdev_init和cdev_add将file_operations结构体与设备号绑定到内核。 - 设备文件创建: 使用
device_create在/dev下创建设备文件,该文件记录了主设备号和次设备号。 - 用户访问: 用户程序打开设备文件时,VFS 根据文件中的主设备号找到对应的驱动,并调用驱动注册的
file_operations中的open函数。 - 数据操作: 用户程序调用
read/write时,VFS 通过file_operations中的函数指针,直接调用驱动实现的read/write函数。
学习建议: 在编写第一个字符设备驱动时,建议先实现
open 和 release 两个函数,验证驱动加载和卸载流程。然后逐步添加 read 和 write 功能,最后再考虑 ioctl 等复杂操作。循序渐进是掌握驱动开发的最佳路径。