2. Linux字符设备驱动回顾:file_operations结构体、cdev注册、设备号分配、设备节点创建
在深入UART中断与工作队列优化之前,必须牢固掌握Linux字符设备驱动的基础骨架。UART驱动本质上是字符设备驱动的一种特殊实现,其注册、文件操作接口、设备号管理以及节点创建机制与标准字符设备完全一致。本节将系统回顾这四个核心要素,为后续的中断处理与延迟工作奠定基础。
2.1 file_operations结构体:驱动与用户空间的契约
file_operations 是字符设备驱动的核心数据结构,它定义了用户空间程序通过系统调用(open、read、write、ioctl等)与内核驱动交互的接口函数指针表。每个字符设备驱动都必须实例化并填充此结构体。
关键理解: file_operations 是驱动开发者向内核注册的“回调函数表”,当用户程序对设备文件执行系统调用时,VFS(虚拟文件系统)通过该表找到对应的驱动函数执行。
以下是一个典型的UART驱动file_operations定义示例:
// uart_drv_fops.c — 典型UART字符设备文件操作接口
static int uart_open(struct inode *inode, struct file *filp) {
// 获取私有数据、初始化硬件、申请中断等
return 0;
}
static ssize_t uart_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
// 从环形缓冲区拷贝数据到用户空间
return copied;
}
static ssize_t uart_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
// 将用户数据写入发送FIFO或DMA缓冲区
return written;
}
static long uart_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg) {
// 处理波特率设置、流控、中断使能等控制命令
return 0;
}
static int uart_release(struct inode *inode, struct file *filp) {
// 释放资源、关闭硬件
return 0;
}
static struct file_operations uart_fops = {
.owner = THIS_MODULE,
.open = uart_open,
.read = uart_read,
.write = uart_write,
.unlocked_ioctl = uart_ioctl,
.release = uart_release,
};
| 成员 | 系统调用 | UART驱动典型实现 |
|---|---|---|
.open |
open() | 初始化UART硬件、申请中断号、分配环形缓冲区 |
.read |
read() | 从接收FIFO或环形缓冲区拷贝数据到用户空间 |
.write |
write() | 将用户数据写入发送FIFO或启动DMA传输 |
.unlocked_ioctl |
ioctl() | 设置波特率、数据位、校验位、流控模式 |
.release |
close() | 关闭中断、释放DMA通道、释放缓冲区 |
避坑提示: 在UART驱动中,
.read 和 .write 通常需要支持非阻塞操作(O_NONBLOCK),否则用户态程序在无数据时会阻塞在系统调用中。务必检查 filp->f_flags 并返回 -EAGAIN。
2.2 cdev注册:将驱动与内核设备模型绑定
cdev 是内核用于管理字符设备的核心对象。驱动必须初始化一个 cdev 结构体,将其与 file_operations 关联,然后通过 cdev_add() 将其注册到内核的字符设备驱动模型中。只有完成这一步,内核才能根据设备号找到对应的驱动函数。
标准注册流程包含三个步骤:
- 分配并初始化cdev: 使用
cdev_alloc()或静态定义后调用cdev_init()。 - 关联file_operations: 将
cdev->ops指向已填充的file_operations结构体。 - 添加到内核: 调用
cdev_add()传入设备号范围和数量。
// cdev_registration.c — 字符设备注册核心代码片段
#include <linux/cdev.h>
#include <linux/fs.h>
static dev_t uart_dev_num; // 设备号(主+次)
static struct cdev uart_cdev; // cdev对象
static int __init uart_init(void) {
int ret;
// 步骤1:分配设备号(假设已通过 alloc_chrdev_region 获取)
// uart_dev_num = MKDEV(major, minor);
// 步骤2:初始化cdev并关联fops
cdev_init(&uart_cdev, &uart_fops);
uart_cdev.owner = THIS_MODULE;
// 步骤3:注册到内核(1个次设备号)
ret = cdev_add(&uart_cdev, uart_dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
return ret;
}
pr_info("UART driver registered with major=%d minor=%d\n",
MAJOR(uart_dev_num), MINOR(uart_dev_num));
return 0;
}
static void __exit uart_exit(void) {
cdev_del(&uart_cdev); // 从内核移除cdev
unregister_chrdev_region(uart_dev_num, 1); // 释放设备号
}
关键时序:
cdev_add() 一旦成功,内核立即可以访问该设备。因此,必须在cdev_add()之前完成所有硬件初始化,否则在设备打开瞬间可能因硬件未就绪而导致系统崩溃。
2.3 设备号分配:主设备号与次设备号的策略
设备号是内核识别字符设备的唯一标识,由主设备号(major)和次设备号(minor)组成。主设备号对应驱动,次设备号对应同一驱动管理的不同设备实例(如多个UART端口)。
内核提供两种分配策略:
| 策略 | 函数 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(如调试、固定平台) | 设备号固定,易于预测 | 可能与其他驱动冲突 |
| 动态分配 | alloc_chrdev_region() |
通用驱动、模块化设计 | 避免冲突,自动分配 | 设备号不固定,需查看/proc/devices |
// device_number_allocation.c — 两种分配方式示例
#include <linux/fs.h>
// 方式一:静态分配(已知主设备号 240)
#define UART_MAJOR 240
#define UART_MINOR 0
#define UART_COUNT 4 // 4个UART端口
static dev_t uart_dev_static = MKDEV(UART_MAJOR, UART_MINOR);
ret = register_chrdev_region(uart_dev_static, UART_COUNT, "uart_drv");
if (ret < 0) {
pr_err("Static region registration failed\n");
return ret;
}
// 方式二:动态分配(推荐)
static dev_t uart_dev_dynamic;
ret = alloc_chrdev_region(&uart_dev_dynamic, 0, UART_COUNT, "uart_drv");
if (ret < 0) {
pr_err("Dynamic region allocation failed\n");
return ret;
}
pr_info("Allocated major=%d minor=%d\n",
MAJOR(uart_dev_dynamic), MINOR(uart_dev_dynamic));
最佳实践: 在UART驱动中,通常使用动态分配
alloc_chrdev_region(),因为嵌入式平台的主设备号分配可能因内核版本或板级配置而异。动态分配后,通过 cat /proc/devices 查看分配结果,并使用 mknod 或 udev 创建设备节点。
2.4 设备节点创建:从内核到用户空间的桥梁
设备节点是用户空间访问驱动的入口,位于 /dev 目录下。节点创建有两种方式:手动 mknod 和自动 udev/mdev。在嵌入式系统中,mdev(BusyBox)或 udev(桌面系统)通过内核发送的 uevent 自动创建设备节点。
现代内核驱动推荐使用 device_create() 配合类(class)来自动管理节点:
// device_node_creation.c — 自动创建设备节点
#include <linux/device.h>
static struct class *uart_class;
static struct device *uart_device;
static int __init uart_init(void) {
// ... 之前的设备号分配和cdev_add ...
// 创建设备类
uart_class = class_create(THIS_MODULE, "uart_class");
if (IS_ERR(uart_class)) {
pr_err("Failed to create class\n");
return PTR_ERR(uart_class);
}
// 创建设备节点 /dev/uart0
uart_device = device_create(uart_class, NULL, uart_dev_num,
NULL, "uart%d", 0);
if (IS_ERR(uart_device)) {
pr_err("Failed to create device\n");
class_destroy(uart_class);
return PTR_ERR(uart_device);
}
pr_info("Device node /dev/uart0 created automatically\n");
return 0;
}
static void __exit uart_exit(void) {
device_destroy(uart_class, uart_dev_num);
class_destroy(uart_class);
cdev_del(&uart_cdev);
unregister_chrdev_region(uart_dev_num, 1);
}
完整注册流程总结:
①
②
③
④ 用户程序通过
①
alloc_chrdev_region() → 获取动态设备号②
cdev_init() + cdev_add() → 注册字符设备③
class_create() + device_create() → 自动生成 /dev/uartX 节点④ 用户程序通过
open("/dev/uart0") 触发 uart_open() → 驱动开始工作