2. 字符设备驱动基础:file_operations结构体、主次设备号、设备节点创建
字符设备驱动是Linux驱动开发中最基础、最常用的驱动类型。本章将系统讲解字符设备驱动的核心概念:file_operations结构体、主次设备号以及设备节点的创建方法,并通过一个完整的“Hello驱动”示例串联所有知识点。
学习目标:掌握字符设备驱动的注册流程,理解用户空间与内核空间的交互桥梁,能够独立编写并加载一个简单的字符设备驱动。
2.1 字符设备驱动架构概览
字符设备(Character Device)以字节流的形式进行数据传输,典型的字符设备包括串口、键盘、GPIO等。用户空间程序通过文件操作接口(open、read、write、ioctl等)与字符设备交互,内核中的字符设备驱动则负责实现这些操作的具体逻辑。
2.2 file_operations 结构体
file_operations 是字符设备驱动的核心数据结构,定义在 <linux/fs.h> 中。它包含一系列函数指针,每个指针对应一个系统调用在内核中的实现。驱动开发者通过填充该结构体,将用户空间的文件操作与内核中的具体实现绑定。
2.2.1 关键成员
| 成员名称 | 函数原型 | 说明 |
|---|---|---|
| owner | struct module *owner |
通常设置为 THIS_MODULE,防止模块被卸载时仍有打开的文件 |
| open | int (*open)(struct inode *, struct file *) |
设备打开时调用,用于初始化硬件或分配资源 |
| release | int (*release)(struct inode *, struct file *) |
设备关闭时调用,用于释放资源 |
| read | ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) |
从设备读取数据,数据通过 copy_to_user 传递给用户空间 |
| write | ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
向设备写入数据,数据通过 copy_from_user 从用户空间获取 |
| unlocked_ioctl | long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
设备控制命令处理,用于非阻塞I/O控制(现代内核推荐) |
| llseek | loff_t (*llseek)(struct file *, loff_t, int) |
修改文件读写位置 |
关键提示:在Linux 2.6.35之后的内核中,
ioctl 已被 unlocked_ioctl 取代。驱动中应使用 unlocked_ioctl,并注意加锁保护共享数据。
2.2.2 file_operations 结构体定义示例
// hello_driver.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "hello_device"
#define BUF_SIZE 256
static char device_buffer[BUF_SIZE] = {0};
static int major_num;
// open 回调
static int hello_open(struct inode *inodep, struct file *filep) {
pr_info("Hello device opened\n");
return 0;
}
// release 回调
static int hello_release(struct inode *inodep, struct file *filep) {
pr_info("Hello device closed\n");
return 0;
}
// read 回调
static ssize_t hello_read(struct file *filep, char __user *buf,
size_t len, loff_t *offset) {
size_t available = strlen(device_buffer);
size_t to_copy = min(len, available);
if (*offset >= available)
return 0;
if (copy_to_user(buf, device_buffer + *offset, to_copy))
return -EFAULT;
*offset += to_copy;
return to_copy;
}
// write 回调
static ssize_t hello_write(struct file *filep, const char __user *buf,
size_t len, loff_t *offset) {
size_t to_copy = min(len, (size_t)(BUF_SIZE - 1));
if (copy_from_user(device_buffer, buf, to_copy))
return -EFAULT;
device_buffer[to_copy] = '\0';
pr_info("Received %zu bytes from user: %s\n", to_copy, device_buffer);
return to_copy;
}
// 填充 file_operations 结构体
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
2.3 主次设备号
设备号是内核标识设备的唯一编号,由两部分组成:主设备号(Major Number)和次设备号(Minor Number)。主设备号标识设备对应的驱动程序,次设备号标识由同一驱动管理的具体设备实例。
2.3.1 设备号分配方式
| 方式 | 函数 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(如实验、固定设备) | 设备号固定,易于管理 | 可能与其他驱动冲突 |
| 动态分配 | alloc_chrdev_region() |
大多数实际项目 | 自动分配未使用的主设备号,避免冲突 | 设备号不固定,需通过 /proc/devices 查看 |
避坑指南:不要使用
register_chrdev() 旧接口(无参数版本),该接口已过时且会占用整个主设备号下的所有次设备号范围(0~255),导致资源浪费。推荐使用 alloc_chrdev_region() + cdev_init() + cdev_add() 组合。
2.4 设备节点创建
设备节点是用户空间访问设备的入口,位于 /dev 目录下。设备节点的创建有两种方式:手动创建和自动创建。
2.4.1 手动创建设备节点
使用 mknod 命令手动创建设备节点:
# 查看已分配的设备号
cat /proc/devices | grep hello
# 手动创建设备节点(需要root权限)
sudo mknod /dev/hello_device c 240 0
# 参数说明:c 表示字符设备,240 是主设备号,0 是次设备号
# 设置权限
sudo chmod 666 /dev/hello_device
2.4.2 自动创建设备节点
现代Linux系统使用 udev 或 devtmpfs 自动管理设备节点。驱动中通过 class_create 和 device_create 接口,在加载时自动创建节点,卸载时自动删除。
// 在模块初始化函数中添加自动创建设备节点代码
static struct class *hello_class;
static struct device *hello_device;
static int __init hello_init(void) {
// 1. 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major_num = MAJOR(dev_num);
pr_info("Allocated major number: %d\n", major_num);
// 2. 初始化并添加cdev
cdev_init(&hello_cdev, &hello_fops);
cdev_add(&hello_cdev, dev_num, 1);
// 3. 创建类
hello_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(hello_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(hello_class);
}
// 4. 创建设备节点(自动出现在 /dev/hello_device)
hello_device = device_create(hello_class, NULL, dev_num,
NULL, DEVICE_NAME);
if (IS_ERR(hello_device)) {
class_destroy(hello_class);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(hello_device);
}
pr_info("Hello driver loaded successfully\n");
return 0;
}
static void __exit hello_exit(void) {
// 反向销毁:设备 -> 类 -> cdev -> 设备号
device_destroy(hello_class, dev_num);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Hello driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
2.5 完整的 Hello 驱动示例
下面是一个完整的字符设备驱动示例,整合了上述所有知识点:动态分配设备号、填充 file_operations、自动创建设备节点。
// hello_driver_complete.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "hello_device"
#define CLASS_NAME "hello_class"
#define BUF_SIZE 256
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver example");
static int major_num;
static struct cdev hello_cdev;
static struct class *hello_class;
static struct device *hello_device;
static dev_t dev_num;
static char device_buffer[BUF_SIZE] = "Hello from kernel!\n";
static int hello_open(struct inode *inodep, struct file *filep) {
pr_info("Hello device opened\n");
return 0;
}
static int hello_release(struct inode *inodep, struct file *filep) {
pr_info("Hello device closed\n");
return 0;
}
static ssize_t hello_read(struct file *filep, char __user *buf,
size_t len, loff_t *offset) {
size_t available = strlen(device_buffer);
size_t to_copy = min(len, available);
if (*offset >= available)
return 0;
if (copy_to_user(buf, device_buffer + *offset, to_copy))
return -EFAULT;
*offset += to_copy;
return to_copy;
}
static ssize_t hello_write(struct file *filep, const char __user *buf,
size_t len, loff_t *offset) {
size_t to_copy = min(len, (size_t)(BUF_SIZE - 1));
if (copy_from_user(device_buffer, buf, to_copy))
return -EFAULT;
device_buffer[to_copy] = '\0';
pr_info("Received %zu bytes: %s\n", to_copy, device_buffer);
return to_copy;
}
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
static int __init hello_init(void) {
// 1. 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major_num = MAJOR(dev_num);
pr_info("Allocated major number: %d\n", major_num);
// 2. 初始化并添加cdev
cdev_init(&hello_cdev, &hello_fops);
hello_cdev.owner = THIS_MODULE;
if (cdev_add(&hello_cdev, dev_num, 1)) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
// 3. 创建类
hello_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(hello_class)) {
pr_err("Failed to create class\n");
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(hello_class);
}
// 4. 创建设备节点
hello_device = device_create(hello_class, NULL, dev_num,
NULL, DEVICE_NAME);
if (IS_ERR(hello_device)) {
pr_err("Failed to create device\n");
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(hello_device);
}
pr_info("Hello driver loaded. Device /dev/%s created.\n", DEVICE_NAME);
return 0;
}
static void __exit hello_exit(void) {
device_destroy(hello_class, dev_num);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Hello driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
2.5.1 编译与测试
使用 Makefile 编译驱动,然后加载并测试:
# Makefile
obj-m += hello_driver_complete.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
# 编译
make
# 加载驱动
sudo insmod hello_driver_complete.ko
# 查看设备号
cat /proc/devices | grep hello
# 查看设备节点
ls -l /dev/hello_device
# 测试读操作
cat /dev/hello_device
# 测试写操作
echo "Hello from user!" > /dev/hello_device
# 查看内核日志
dmesg | tail -10
# 卸载驱动
sudo rmmod hello_driver_complete
测试结果预期:执行
cat /dev/hello_device 应输出 "Hello from kernel!";执行 echo "Hello from user!" > /dev/hello_device 后,通过 dmesg 可看到内核打印的接收信息。
2.6 本章小结
- file_operations 是字符设备驱动的核心,定义了用户空间文件操作与内核回调函数的映射关系。
- 主设备号标识驱动,次设备号标识设备实例,推荐使用动态分配方式避免冲突。
- 设备节点是用户空间访问设备的入口,手动创建使用
mknod,自动创建依赖class_create+device_create。 - 完整的字符设备驱动流程:分配设备号 → 初始化cdev → 添加cdev → 创建类 → 创建设备节点。
- 卸载时需按反向顺序销毁资源,避免内存泄漏或设备节点残留。
课后练习:修改上述Hello驱动,增加
unlocked_ioctl 回调,实现一个简单的命令:IOCTL_CLEAR_BUFFER 清空设备缓冲区。提示:使用 _IO 宏定义命令号。