2. Linux 字符设备驱动基础回顾:file_operations 结构体、cdev 注册、设备号分配、简单的 open/read/write 实现
在深入阻塞与非阻塞 I/O 及 poll 机制之前,必须牢固掌握字符设备驱动的核心骨架。本章将系统回顾 file_operations 结构体、cdev 注册流程、设备号分配以及最基本的 open/read/write 实现。这些基础组件是后续所有高级 I/O 模型的基石。
2.1 字符设备驱动核心数据结构
字符设备驱动的灵魂是 struct file_operations,它定义了用户空间系统调用与内核驱动函数之间的映射关系。每个打开的设备文件在内核中都对应一个 struct file 实例,而 struct inode 则代表底层的设备节点。
关键概念: 用户态调用
read() → VFS 层 → 根据 file->f_op 找到驱动注册的 xxx_read() 函数 → 执行硬件或缓冲区操作 → 返回数据到用户空间。
2.1.1 file_operations 结构体成员速查
| 成员函数 | 系统调用对应 | 典型用途 |
|---|---|---|
open |
open() |
初始化设备、递增使用计数、分配私有数据 |
release |
close() |
清理资源、递减使用计数 |
read |
read() |
从设备或内核缓冲区拷贝数据到用户空间 |
write |
write() |
从用户空间拷贝数据到内核缓冲区或设备 |
poll |
poll() / select() / epoll() |
查询设备是否可读/可写,支持非阻塞等待 |
unlocked_ioctl |
ioctl() |
设备特定的控制命令(无大内核锁) |
mmap |
mmap() |
将设备内存映射到用户空间 |
驱动编写提示: 并非所有成员都必须实现。对于简单的字符设备,通常只需实现
open、release、read、write 四个函数。未实现的成员应设置为 NULL,VFS 会返回 -ENOTTY 或 -EINVAL。
2.2 设备号分配:静态 vs 动态
每个字符设备在内核中由 主设备号(Major Number)和 次设备号(Minor Number)唯一标识。主设备号对应驱动程序,次设备号对应具体的设备实例。
2.2.1 设备号分配方式对比
| 分配方式 | 函数/宏 | 优点 | 缺点 |
|---|---|---|---|
| 静态分配 | register_chrdev_region() |
设备号固定,适合广泛使用的标准设备 | 需要提前申请,可能与其他驱动冲突 |
| 动态分配 | alloc_chrdev_region() |
自动分配未使用的主设备号,避免冲突 | 设备号不固定,需要手动 mknod 或依赖 udev |
2.2.2 设备号操作宏
#include <linux/fs.h>
#include <linux/kdev_t.h>
// 从主/次设备号合成 dev_t
dev_t dev = MKDEV(major, minor);
// 从 dev_t 提取主设备号
int major = MAJOR(dev);
// 从 dev_t 提取次设备号
int minor = MINOR(dev);
2.3 cdev 结构体与注册流程
struct cdev 是内核中表示字符设备的对象。驱动必须初始化一个 cdev 实例并将其添加到内核的设备表中。
2.3.1 cdev 注册标准步骤
- 分配设备号:使用
alloc_chrdev_region()或register_chrdev_region()。 - 初始化 cdev:调用
cdev_init(),绑定file_operations结构体。 - 添加 cdev 到内核:调用
cdev_add(),将设备与设备号关联。 - 创建设备节点:手动
mknod或使用device_create()自动创建(需配合类)。
常见错误:
cdev_add() 必须在设备准备好处理 I/O 之后调用。如果在 cdev_add() 之后才初始化硬件或缓冲区,用户空间可能在设备未就绪时发起操作,导致内核崩溃。
2.3.2 典型注册代码模板
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
static int __init my_init(void)
{
// 1. 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到内核
cdev_add(&my_cdev, dev_num, 1);
// 4. 创建 device class 和 device(自动生成 /dev/my_device)
my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
return 0;
}
static void __exit my_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(my_init);
module_exit(my_exit);
2.4 简单的 open/read/write 实现
以下是一个基于内核缓冲区的虚拟字符设备实现,展示了最基础的读写操作。该实现使用一个简单的环形缓冲区或固定数组来模拟设备数据。
2.4.1 驱动内部数据结构
#define BUF_SIZE 1024
struct my_device_data {
char buffer[BUF_SIZE];
int head;
int tail;
struct mutex lock;
};
static struct my_device_data *dev_data;
2.4.2 open 与 release 实现
static int my_open(struct inode *inode, struct file *filp)
{
// 将私有数据指针存入 filp->private_data
filp->private_data = dev_data;
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
// 清理工作(本例中无特殊操作)
return 0;
}
2.4.3 read 实现(阻塞版本)
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_device_data *data = filp->private_data;
ssize_t ret;
mutex_lock(&data->lock);
// 计算可读字节数
size_t available = data->head - data->tail;
if (available == 0) {
mutex_unlock(&data->lock);
return 0; // 无数据可读
}
size_t to_read = min(count, available);
// 从内核缓冲区拷贝到用户空间
if (copy_to_user(buf, data->buffer + data->tail, to_read)) {
mutex_unlock(&data->lock);
return -EFAULT;
}
data->tail += to_read;
*f_pos += to_read;
mutex_unlock(&data->lock);
return to_read;
}
2.4.4 write 实现(阻塞版本)
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_device_data *data = filp->private_data;
ssize_t ret;
mutex_lock(&data->lock);
// 计算剩余空间
size_t space = BUF_SIZE - (data->head - data->tail);
if (space == 0) {
mutex_unlock(&data->lock);
return -ENOSPC; // 缓冲区满
}
size_t to_write = min(count, space);
// 从用户空间拷贝到内核缓冲区
if (copy_from_user(data->buffer + data->head, buf, to_write)) {
mutex_unlock(&data->lock);
return -EFAULT;
}
data->head += to_write;
*f_pos += to_write;
mutex_unlock(&data->lock);
return to_write;
}
2.5 字符设备驱动整体架构图
下图展示了用户空间、VFS 层、字符设备驱动核心以及硬件之间的完整调用链路与数据流关系。
2.6 本章核心要点总结
| 组件 | 关键函数/宏 | 注意事项 |
|---|---|---|
| 设备号分配 | alloc_chrdev_region() |
动态分配优先,避免主设备号冲突 |
| cdev 初始化 | cdev_init() |
必须在 cdev_add() 之前完成 |
| cdev 注册 | cdev_add() |
确保设备已就绪后再注册 |
| 用户数据拷贝 | copy_to_user() / copy_from_user() |
必须检查返回值,失败返回 -EFAULT |
| 并发保护 | mutex_lock() / mutex_unlock() |
多进程同时访问时必须加锁 |
| 设备节点创建 | device_create() |
配合 class_create() 自动生成 /dev 节点 |