一、IO模型
- I/O 模型在操作系统中用于处理应用程序与设备驱动之间的数据传输。
- I/O 通信模型的核心是解决程序与设备之间如何高效、合理地进行数据通信。不同的模型通过阻塞、非阻塞、同步、异步的方式来控制数据流和处理 I/O 请求。
注:在驱动开发中可以定义一个全局结构体
- 用于全局管理驱动程序的状态和资源。
- 在驱动开发中,有时需要一个全局的数据结构来保存设备的状态、驱动配置、缓存、锁等信息,这样不同的驱动程序函数可以通过该结构体访问和修改共享的资源。
- 作用
- 统一管理驱动状态: 全局结构体通常保存驱动程序的各种状态信息。例如,设备的注册信息、分配的内存、硬件寄存器映射等都可以保存在这个结构体中。
- 共享资源: 驱动程序通常会有多个函数被内核调用,比如初始化函数、读写函数、中断处理函数等。使用全局结构体,可以让这些函数方便地共享和访问同样的数据或资源。
- 简化代码结构: 将驱动程序涉及的所有全局变量封装在一个结构体中,使代码更加清晰、结构化,同时也减少了全局变量命名冲突的可能性。
一、阻塞 I/O 模型
- 阻塞 I/O 是最常见的 I/O 模型。当应用程序发起 I/O 请求时,如果数据没有准备好,应用程序将进入阻塞状态,等待数据准备完毕后,驱动程序会唤醒阻塞的应用程序。此时,应用程序才能继续执行。
- 工作流程
cpp
应用程序向驱动发送读取数据的请求。
如果驱动程序中数据尚未准备好,应用程序进入 睡眠状态,等待数据到来。
当数据准备好后,驱动程序会唤醒应用程序,程序从睡眠状态恢复,继续读取数据。
完成 I/O 操作后,应用程序继续执行其他任务。
- 具体步骤
a. 定义并初始化等待队列
c
wait_queue_head_t wqhead;//在全局结构体中
在结构体 global_struct 中定义了 wqhead,表示等待队列头,用于管理睡眠进程的队列。
在mod_init() 初始化函数中,通过 init_waitqueue_head() 初始化等待队列头。
init_waitqueue_head(&gstruct.wqhead);
b. 进程睡眠机制
c
在 chdev_read 函数中,当应用程序尝试从驱动读取数据时,程序会检查数据是否可用。如果数据不可用,进程就会进入睡眠状态,挂到等待队列上。
wait_event_interruptible(pt_gstruct->wqhead, pt_gstruct->data_flag);
c. 唤醒机制
cpp
当数据到达时,会通过 chdev_write 函数进行处理
pt_gstruct->data_flag = 1;//数据写入后,data_flag 被设置为 1,表示数据已准备好。
wake_up_interruptible(&pt_gstruct->wqhead);
wake_up_interruptible() 函数用于唤醒等待队列 wqhead 上的所有进程,唤醒后,之前处于睡眠状态的进程会继续执行 chdev_read 函数中的代码,读取数据。
二、非阻塞I/O实现
- 非阻塞 I/O 的实现逻辑与阻塞 I/O 类似,但不进入睡眠状态。如果数据尚未准备好,非阻塞 I/O 立即返回错误 -EAGAIN,告诉应用程序稍后重试。
cpp
if (file->f_flags & O_NONBLOCK) { // 检查文件标志是否为非阻塞模式
if (pt_gstruct->data_flag == 0) // 没有数据可用
return -EAGAIN; // 返回 EAGAIN 错误,表示无数据,稍后重试
}
三、异步通知
- 异步通知(Asynchronous Notification)是 Linux 内核提供的一种机制,它允许设备驱动在数据准备好或状态发生变化时,主动向应用程序发送信号(通常是 SIGIO 信号),通知应用程序及时处理。
- 这种机制在应用程序不需要不断轮询设备状态的情况下非常有用。
- 设备驱动中的结构定义
cpp
struct global_struct {
struct class *cls;
int major, minor;
struct cdev cdev_obj;
wait_queue_head_t wqhead; // 等待队列头
char sharebuf[128]; // 共享缓冲区
int data_flag; // 标记数据是否可用,0 表示无数据,1 表示有数据
struct fasync_struct *fapp; // 异步通知结构,用于管理异步通知
};
- 驱动程序的 fasync 实现
cpp
int chdev_fasync(int fd, struct file *file, int on)
{
struct global_struct *pt_gstruct = file->private_data;
// fasync_helper 负责将文件描述符和进程与异步通知关联或解除关联
return fasync_helper(fd, file, on, &pt_gstruct->fapp);
}
- 实现数据写入时的异步通知
cpp
//当驱动程序中有新数据时,通过 kill_fasync 向注册了异步通知的进程发送信号。
ssize_t chdev_write(struct file *file, const char __user *usr, size_t sz, loff_t *loff)
{
struct global_struct *pt_gstruct = file->private_data;
long ret;
// 将用户数据复制到共享缓冲区
ret = copy_from_user(pt_gstruct->sharebuf, usr, sz);
if (ret > 0) {
printk("%s-%d copy_from_user err\n", __func__, __LINE__);
return -3;
}
// 数据写入完成后,标记数据已经准备好
pt_gstruct->data_flag = 1;
// 唤醒所有等待在等待队列上的进程
wake_up_interruptible(&pt_gstruct->wqhead);
// 发送异步通知信号,通知应用程序有数据可读
kill_fasync(&pt_gstruct->fapp, SIGIO, POLL_IN);
return sz;
}
二、内核中断驱动
- Linux 内核中的中断处理系统使得硬件能够通知内核发生了特定事件(如数据就绪、设备完成某项任务等),并让内核采取相应的处理措施。
1. 内核中获取中断号
- 在 ARM 等嵌入式平台上,设备通常使用设备树 (Device Tree)来描述硬件信息。驱动程序可以从设备树节点中获取与设备相关的中断号。
- 直接写入
cpp
device_node {
compatible = "mydevice";
reg = <0x12340000 0x1000>;
interrupts = <30 2>; // 中断号 30,触发类型 2(下降沿触发)
};
-
- 设备树描述了硬件信息并提供给内核使用。在编译设备树时,我们使用交叉编译工具和特定的架构。
cpp
make dtbs ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
- 拷贝到共享目录
编译完成的 .dtb 文件需要通过某种方式传输到开发板。通常,我们会将 .dtb 文件拷贝到一个共享目录(如 NFS、TFTP 或本地磁盘),以便通过 U-Boot 加载和烧录。
cpp
cp arch/arm/boot/dts/your_device.dtb /path/to/shared/directory/
- 通过 U-Boot 烧录设备树文件
cpp
loady 41000000
- movi write dtb 命令将 .dtb 文件写入设备存储
- 验证设备树文件是否生效
- 重启linux,在 /proc/device-tree/ 目录会有你的 节点文件目录
实现代码
- of_find_node_by_path("/key2"); // 从设备树中查找路径为 /key2 的节点
- irq_of_parse_and_map(of_node, 0); // 从设备节点获取中断号
- 具体代码
cpp
// Linux 内核中断驱动程序
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/irqreturn.h>
#include <linux/of.h>
#include <linux/interrupt.h>
#include <linux/of_irq.h>
/* 定义全局结构体 */
struct global_struct {
int irqno; // 保存从设备树中解析出的中断号
int xxx; // 其他私有数据,用户可以根据需要扩展
};
/* 定义全局变量 */
struct global_struct gstruct;
/* 中断处理函数 */
irqreturn_t irq_key_handle(int irqno, void *args)
{
struct global_struct *pt_gstruct = args; // 从传入的参数中获取私有数据
printk(KERN_INFO "%s - 中断发生,irqno = %d, 设备中断号 = %d\n",
__func__, irqno, pt_gstruct->irqno);
// 执行中断处理逻辑,例如:读取硬件状态、清除中断标志等
return IRQ_HANDLED; // 返回 IRQ_HANDLED 表示中断已处理完成
}
/* 模块初始化函数 */
int mod_init(void)
{
int ret;
int irqno;
struct device_node *of_node;
printk(KERN_INFO "中断驱动模块初始化\n");
/* 1. 通过设备树路径查找设备节点 */
of_node = of_find_node_by_path("/key2"); // 从设备树中查找路径为 /key2 的节点
if (!of_node) {
printk(KERN_ERR "%s - 无法找到设备树节点 /key2\n", __func__);
return -EINVAL; // 返回错误码
}
/* 2. 从设备树节点解析中断号 */
irqno = irq_of_parse_and_map(of_node, 0); // 从设备节点获取中断号
if (irqno < 0) {
printk(KERN_ERR "%s - 无法从设备树节点中解析中断号\n", __func__);
return -EINVAL; // 返回错误码
}
printk(KERN_INFO "%s - 获取的中断号为 %d\n", __func__, irqno);
/* 3. 保存中断号到全局结构体中 */
gstruct.irqno = irqno;
gstruct.xxx = 10086; // 示例私有数据
/* 4. 注册中断处理程序 */
ret = request_irq(irqno, irq_key_handle, IRQF_TRIGGER_FALLING, "key2_intr", &gstruct);
if (ret < 0) {
printk(KERN_ERR "%s - 注册中断处理程序失败,错误码:%d\n", __func__, ret);
return ret; // 返回错误码
}
printk(KERN_INFO "中断处理程序注册成功,中断号为 %d\n", irqno);
return 0; // 模块初始化成功
}
/* 模块退出函数 */
void mod_exit(void)
{
/* 释放中断 */
free_irq(gstruct.irqno, &gstruct); // 释放之前注册的中断
printk(KERN_INFO "中断驱动模块卸载\n");
}
/* 声明模块的初始化和退出函数 */
module_init(mod_init);
module_exit(mod_exit);
/* 模块的许可信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple IRQ Device Driver using Device Tree");
MODULE_VERSION("1.0");
简明步骤
- 编写设备树节点:在设备树(.dts 文件)中定义设备的中断号和触发类型。
- 在设备树(.dts 文件)中定义设备的中断号和触发类型。
cpp
key2: gpio-keys {
compatible = "gpio-keys";
interrupts = <30 IRQ_TYPE_EDGE_FALLING>; // 中断号 30,下降沿触发
};
- 编写中断驱动程序:
- 定义驱动的全局结构体,保存中断号和其他私有数据。
- 实现中断处理函数。
cpp
irqreturn_t irq_key_handle(int irqno, void *args)
{
printk(KERN_INFO "中断发生,irqno = %d\n", irqno);
return IRQ_HANDLED;
}
- 从设备树获取中断号
- 使用 of_find_node_by_path("/key2") 查找设备树节点。
- 使用 irq_of_parse_and_map() 解析设备树中的中断号。
cpp
struct device_node *of_node = of_find_node_by_path("/key2");
int irqno = irq_of_parse_and_map(of_node, 0);
- 注册中断处理程序:使用 request_irq() 函数注册中断处理函数。
cpp
request_irq(irqno, irq_key_handle, IRQF_TRIGGER_FALLING, "key2_intr", &gstruct);
- 释放中断资源(在模块退出时):在 mod_exit 函数中,使用 free_irq() 释放中断资源。
cpp
free_irq(gstruct.irqno, &gstruct);
- 编译并加载内核模块:编译驱动模块并加载到内核中,验证中断是否正常工作。
中断下半部分
- 在 Linux 内核中,中断处理分为上半部分和下半部分。上半部分(Top Half)是中断处理函数(ISR,Interrupt Service Routine),它在中断发生时立即执行,尽量简短以提高系统响应速度。为了避免在中断上下文中执行复杂或耗时的操作,Linux 提供了下半部分(Bottom Half)机制,用于延迟处理不需要立即执行的任务。
- LINUX系统中执行单元具体分别如下:
执行单元 | 优先级 | 是否允许睡眠 | 适合任务类型 |
---|---|---|---|
进程 | 低 | 可以睡眠 | 耗时或不耗时任务,不紧急任务 |
中断下半部分 | 中 | 否 | 耗时任务,紧急任务 |
中断上半部分 | 高 | 否(禁止睡眠) | 短时间任务,不耗时,紧急任务 |
中断下半部分机制
中断下半部分机制 | 优先级 | 是否允许睡眠 | 适用场景 | 使用方式 |
---|---|---|---|---|
软中断 | 很高 | 否 | 内核专用,处理大量事件 | 内核开发者使用,驱动开发者不使用 |
Tasklet | 中等 | 否 | 驱动开发中常用,处理短任务 | 1. 定义并初始化 Tasklet 对象 |
2. 在中断上半部分中调度 Tasklet | ||||
工作队列 | 低 | 是 | 处理复杂、长时间或阻塞任务 | 1. 定义并初始化 Workqueue 对象 |
2. 在中断上半部分中调度 Workqueue |
Tasklet的使用方法
- 定义tasklet_struct用于管理和定义Tasklet
cpp
struct tasklet_struct {
void (*func)(unsigned long data); // Tasklet 的处理函数
unsigned long data; // 传递给处理函数的私有数据
};
- 初始化Tasklet_struct
- tasklet_init() 函数用于初始化 tasklet_struct 结构体,指定 Tasklet 的处理函数和私有数据。
cpp
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);
- 调度Tasklet
- tasklet_schedule() 用于调度 Tasklet,告诉内核这个 Tasklet 需要执行。
- tasklet_schedule() 实际上不会立即执行 Tasklet,而是将 Tasklet 标记为可执行的。
- 当中断处理完成后,内核会检测到 Tasklet 已经被调度,会在稍后的软中断上下文中执行 Tasklet。
cpp
void tasklet_schedule(struct tasklet_struct *t);
- 注意事项
- tasklet_kill():在模块卸载时,需要调用 tasklet_kill(),它会确保 Tasklet 完成后才退出,以免在卸载模块时 Tasklet 仍在执行。
- 软中断上下文:Tasklet 是在软中断上下文中执行的,因此不能进行睡眠操作,也不能执行阻塞的 I/O 操作。
- 具体代码
cpp
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
// Tasklet 处理函数
void my_tasklet_func(unsigned long data)
{
printk(KERN_INFO "Tasklet 执行,数据: %lu\n", data);
}
// 定义 Tasklet 对象
struct tasklet_struct my_tasklet;
irqreturn_t irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "中断发生,调度 Tasklet\n");
// 调度 Tasklet 执行
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
static int __init my_init(void)
{
int irq = 19; // 假设使用中断号 19
// 初始化 Tasklet,传递处理函数和私有数据
tasklet_init(&my_tasklet, my_tasklet_func, 100);
// 注册中断处理程序
if (request_irq(irq, irq_handler, IRQF_SHARED, "my_tasklet_device", NULL)) {
printk(KERN_ERR "无法注册中断处理程序\n");
return -1;
}
printk(KERN_INFO "模块加载成功,Tasklet 初始化完成\n");
return 0;
}
static void __exit my_exit(void)
{
// 杀掉 Tasklet 确保它已经完成
tasklet_kill(&my_tasklet);
// 释放中断
free_irq(19, NULL);
printk(KERN_INFO "模块卸载,Tasklet 资源已释放\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Tasklet 示例");
工作队列的使用方法
- 工作队列(Workqueue)是 Linux 内核中提供的一种机制,用于将任务推迟到进程上下文中执行。
- 与 Tasklet 和软中断不同,工作队列允许进行阻塞操作和睡眠,因此适用于处理更为复杂、耗时的任务,如文件 I/O 或长时间的计算任务。
- 工作队列是在内核线程中执行的,运行环境与普通内核线程一致,能够进行各种内核操作。
具体代码
- 定义工作队列
- 工作队列通过 struct work_struct 定义
cpp
struct work_struct workqueue_obj;
- 初始化工作队列
- 在模块初始化时,使用 INIT_WORK() 初始化工作队列对象,并关联任务函数 workqueue_handle
cpp
INIT_WORK(&gstruct.workqueue_obj, workqueue_handle);
- 调度工作队列
- 在中断处理函数的下半部分 tasklet_fun 中,通过 schedule_work() 来调度工作队列
c
schedule_work(&gstruct.workqueue_obj);
- 工作队列处理函数
- 工作队列函数 workqueue_handle 是实际执行任务的地方,它运行在内核线程上下文中,因此可以执行阻塞操作
cpp
void workqueue_handle(struct work_struct *work)
{
struct global_struct *pt_gstruct = container_of(work, struct global_struct, workqueue_obj);
/* 中断产生数据 */
pt_gstruct->keycnt++;
sprintf(pt_gstruct->sharebuf, "hello usrread, keycnt=%d", pt_gstruct->keycnt);
/* 唤醒等待队列,通知有数据可读 */
pt_gstruct->data_flag = 1;
wake_up_interruptible(&pt_gstruct->wqhead);
/* 发送信号给用户进程,通知数据到达 */
kill_fasync(&pt_gstruct->fapp, SIGIO, POLL_IN);
printk("%s-%d irqno=%d\n", __func__, __LINE__, pt_gstruct->irqno);
}
- 模块退出时的清理
- 在模块卸载时,需要确保工作队列中的任务已完成或取消,避免在模块卸载后还有未完成的任务
cpp
cancel_work_sync(&gstruct.workqueue_obj);
- 代码示例
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
/* 假设 GPIO 中断号为 17 */
#define GPIO_IRQ_NUM 17
/* 全局工作队列对象 */
static struct work_struct my_workqueue;
/* 工作队列处理函数 */
void workqueue_func(struct work_struct *work)
{
printk(KERN_INFO "工作队列任务执行:处理复杂的任务\n");
/* 模拟耗时操作 */
msleep(2000); // 模拟阻塞操作
printk(KERN_INFO "工作队列任务完成\n");
}
/* 中断处理程序(上半部分) */
irqreturn_t irq_handler(int irq, void *dev_id)
{
printk(KERN_INFO "中断触发:调度工作队列\n");
/* 调度工作队列 */
schedule_work(&my_workqueue);
return IRQ_HANDLED;
}
/* 模块初始化 */
static int __init my_module_init(void)
{
int ret;
/* 初始化工作队列 */
INIT_WORK(&my_workqueue, workqueue_func);
/* 注册中断处理程序 */
ret = request_irq(GPIO_IRQ_NUM, irq_handler, IRQF_SHARED, "my_workqueue_device", &my_workqueue);
if (ret) {
printk(KERN_ERR "无法注册中断处理程序\n");
return ret;
}
printk(KERN_INFO "工作队列模块已加载\n");
return 0;
}
/* 模块卸载 */
static void __exit my_module_exit(void)
{
/* 确保工作队列任务已完成或被取消 */
cancel_work_sync(&my_workqueue);
/* 释放中断 */
free_irq(GPIO_IRQ_NUM, &my_workqueue);
printk(KERN_INFO "工作队列模块已卸载\n");
}
/* 模块入口和出口 */
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("工作队列使用示例");