4. 字符设备驱动核心概念:主设备号与次设备号、设备文件、file_operations 结构体
字符设备驱动是 Linux 驱动开发中最基础、最典型的驱动类型。理解其核心概念——设备号、设备文件与 file_operations 结构体——是编写任何字符设备驱动的第一步。这三者共同构成了用户空间与内核空间交互的桥梁。
4.1 主设备号与次设备号:设备的“身份证”
Linux 内核使用设备号来唯一标识一个设备。设备号是一个 dev_t 类型的 32 位无符号整数,其中高 12 位表示主设备号,低 20 位表示次设备号。
- 主设备号 (Major Number):标识设备对应的驱动程序。内核通过主设备号找到正确的驱动。
- 次设备号 (Minor Number):由驱动程序内部使用,用于区分同一种驱动控制下的不同物理设备或逻辑单元。
类比理解: 主设备号好比“公司总机号码”,次设备号好比“分机号码”。拨打总机(主设备号)找到公司,再拨分机(次设备号)找到具体的人(设备实例)。
4.1.1 设备号分配方式
| 分配方式 | 函数/宏 | 适用场景 | 特点 |
|---|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(如实验、特定硬件) | 需手动指定,可能冲突 |
| 动态分配 | alloc_chrdev_region() |
通用驱动、开源项目 | 内核自动分配,推荐使用 |
动态分配是现代驱动开发的首选方式,它避免了主设备号冲突的问题。以下是动态分配设备号的典型代码片段:
#include <linux/fs.h>
#include <linux/kdev_t.h>
dev_t dev_num;
int major, minor;
int ret;
/* 动态分配一个设备号,名称用于 /proc/devices 显示 */
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_device");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev_num); /* 提取主设备号 */
minor = MINOR(dev_num); /* 提取次设备号 */
printk(KERN_INFO "Allocated major=%d, minor=%d\n", major, minor);
4.1.2 设备号管理工具
用户空间可以通过以下命令查看系统中已分配的设备号:
cat /proc/devices:显示已注册的主设备号与驱动名称。ls -l /dev/:查看设备文件的设备号(第5、6列)。
关键点: 设备号是内核资源,驱动卸载时必须使用
unregister_chrdev_region(dev_num, count) 释放,否则会导致资源泄漏。
4.2 设备文件:用户空间的“操作手柄”
在 Linux 中,一切皆文件。设备文件(又称设备节点)是用户空间程序访问内核驱动的入口。它位于 /dev 目录下,通过 mknod 命令或 udev 设备管理器自动创建。
4.2.1 设备文件类型
| 类型 | 文件类型标识 | 说明 |
|---|---|---|
| 字符设备文件 | c |
以字节流方式访问,无缓冲区,如串口、键盘 |
| 块设备文件 | b |
以块为单位访问,有缓冲区,如硬盘、U盘 |
使用 ls -l /dev/ 查看设备文件时,输出格式如下:
crw-rw---- 1 root root 10, 235 Mar 15 10:30 /dev/my_device
^ ^ ^ ^ ^ ^
| | | | | +-- 文件名
| | | | +-- 次设备号 (235)
| | | +-- 主设备号 (10)
| | +-- 所属组
| +-- 所有者
+-- 文件类型 'c' 表示字符设备
4.2.2 创建设备文件的方式
- 手动创建:
mknod /dev/my_device c 240 0(主设备号240,次设备号0)。 - 自动创建: 使用
udev或devtmpfs,驱动注册后自动在/dev下生成节点。
注意: 手动创建时,主设备号必须与驱动注册的设备号一致,否则操作设备文件时会返回“No such device”错误。
4.3 file_operations 结构体:驱动的“功能菜单”
file_operations 是字符设备驱动的核心数据结构。它定义了一个函数指针集合,将用户空间的系统调用(如 open、read、write)映射到驱动中具体的实现函数。
4.3.1 核心成员函数
| 函数指针 | 对应系统调用 | 功能描述 |
|---|---|---|
open |
open() |
打开设备,进行初始化 |
release |
close() |
关闭设备,释放资源 |
read |
read() |
从设备读取数据 |
write |
write() |
向设备写入数据 |
llseek |
lseek() |
修改文件读写位置 |
unlocked_ioctl |
ioctl() |
设备控制命令(无大内核锁) |
4.3.2 典型定义与初始化
以下是一个典型的 file_operations 结构体定义:
#include <linux/fs.h>
#include <linux/uaccess.h>
/* 驱动函数声明 */
static int my_open(struct inode *inode, struct file *filp);
static int my_release(struct inode *inode, struct file *filp);
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off);
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off);
static loff_t my_llseek(struct file *filp, loff_t offset, int whence);
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
/* file_operations 结构体实例化 */
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
.unlocked_ioctl = my_ioctl,
};
关键点:
.owner = THIS_MODULE 是必需的,它防止模块在使用中被卸载。如果某个函数指针不需要实现,可以设置为 NULL,内核将使用默认行为。
4.3.3 函数实现示例:open 与 read
static int my_open(struct inode *inode, struct file *filp)
{
/* 从 inode 中获取次设备号,用于区分不同设备实例 */
unsigned int minor = iminor(inode);
printk(KERN_INFO "Device opened, minor=%d\n", minor);
/* 将私有数据关联到 file 结构体 */
filp->private_data = &my_device_data[minor];
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
struct my_device *dev = filp->private_data;
size_t available = dev->data_len - *off;
size_t to_read = min(len, available);
if (to_read == 0)
return 0; /* EOF */
/* 将内核数据拷贝到用户空间 */
if (copy_to_user(buf, dev->buffer + *off, to_read))
return -EFAULT;
*off += to_read;
return to_read;
}
安全提示: 在
read 和 write 中,必须使用 copy_to_user() 和 copy_from_user() 进行数据拷贝,不能直接使用 memcpy。直接访问用户空间指针会导致内核崩溃或安全漏洞。
4.4 三者的协作流程
当用户空间程序操作一个字符设备时,内核按照以下流程完成调用:
- 打开设备: 用户调用
open("/dev/my_device", O_RDWR)。 - VFS 查找: 内核虚拟文件系统 (VFS) 根据设备文件中的主设备号,找到对应的驱动。
- 调用 open: 内核调用驱动中
file_operations的open函数。 - 数据操作: 用户调用
read()/write(),VFS 再次通过file_operations映射到驱动函数。 - 关闭设备: 用户调用
close(),内核调用release函数释放资源。
总结: 主设备号定位驱动,次设备号定位设备实例,设备文件提供用户空间入口,file_operations 提供操作函数映射。掌握这四者的关系,就掌握了字符设备驱动的核心骨架。