嵌入式 Linux 驱动开发学习笔记:中断、等待队列与工作队列

一、前言

在嵌入式 Linux 系统中,外部设备可能会在任意时刻产生事件,例如按键按下、数据接收完成或传感器状态发生变化。如果 CPU 不断查询设备状态,不仅会浪费处理器资源,也不利于系统响应其他任务。

中断机制能够让设备在事件发生时通知 CPU。驱动程序接收到事件后,可以进行必要的处理,并在适当的时候唤醒等待该事件的进程。

本文围绕 Linux 驱动中的中断处理、阻塞 I/O、等待队列以及工作队列展开介绍。

二、Linux 中断处理

2.1 什么是中断

中断是指 CPU 暂停当前执行流程,转而响应某个需要及时处理的事件。

以按键驱动为例:用户按下按键后,GPIO 状态发生变化,硬件产生中断,内核调用对应的中断处理函数,驱动再根据事件更新状态或通知用户空间。

与轮询相比,中断方式通常能够减少无效的状态检查。

2.2 获取 GPIO 对应的中断号

在较早的 Linux 内核中,常见的做法是通过 GPIO 编号获取中断号:

int irq;

irq = gpio_to_irq(gpio);

if (irq < 0) {

pr_err("gpio_to_irq failed\n");

return irq;

}

需要注意,GPIO 编号和中断号并不一定相同。gpio_to_irq() 的返回值应当进行检查,不能直接假设转换成功。

2.3 申请中断

在支持相应接口的内核中,可以使用 request_irq() 申请中断:

ret = request_irq(irq,

key_irq_handler,

IRQF_TRIGGER_FALLING,

"key_irq",

dev);

if (ret) {

pr_err("request_irq failed\n");

return ret;

}

主要参数包括:

irq:中断号。

key_irq_handler:中断处理函数。

IRQF_TRIGGER_FALLING:示例中指定下降沿触发。

"key_irq":中断名称。

dev:传递给中断处理函数的设备参数。

下降沿只是示例,实际触发方式必须根据硬件电路和设备树配置确定。

2.4 中断处理函数

典型的中断处理函数形式如下:

static irqreturn_t key_irq_handler(

int irq, void *dev_id)

{

/* 执行必要的快速处理 */

return IRQ_HANDLED;

}

中断上下文具有一定限制。处理函数应尽可能短小,不应执行可能睡眠的操作,例如直接获取可能睡眠的互斥锁,或调用需要等待资源的函数。

对于耗时操作,可以考虑将工作交给合适的下半部机制。

2.5 释放中断

在驱动退出或设备移除时,应释放已经申请的中断:

free_irq(irq, dev);

其中传入的设备参数应与申请时使用的参数保持一致。对于共享中断,还需要遵循共享中断的申请和处理规则。

三、阻塞 I/O 与等待队列

3.1 为什么需要阻塞 I/O

假设用户程序读取按键状态,但此时没有任何按键事件。如果驱动不断检查状态,就会形成忙等待。

阻塞 I/O 提供了另一种处理方式:没有数据或事件时,进程进入睡眠状态;事件发生后,再被唤醒继续执行。

这样可以减少无意义的 CPU 占用。

3.2 等待队列的基本用法

较早的 Linux 内核中,可以使用等待队列实现进程等待。

static DECLARE_WAIT_QUEUE_HEAD(wait_que);

static int wait_que_cond;

当条件满足时,驱动可以调用:

wait_que_cond = 1;

wake_up(&wait_que);

这里有两个关键点:

wait_que 用于管理等待该事件的进程。

wait_que_cond 用于表示事件是否已经发生。

不过,上述代码只是概念示例。实际驱动中,如果中断处理函数与进程上下文会同时访问条件变量,就需要考虑并发访问与内存同步问题。对于跨上下文共享的状态,应根据实际场景选择合适的原子变量、锁或其他同步方法。

3.3 让进程等待条件

可以使用 wait_event_interruptible() 等接口:

ret = wait_event_interruptible(wait_que, wait_que_cond != 0);

if (ret)

return ret;

/* 条件满足后继续执行 */

该接口会在条件不满足时让进程等待;当条件满足或等待被信号中断时,调用会返回。

需要注意:条件变量应在唤醒之前正确更新,且在实际驱动中还应处理事件重复、条件清零和多个进程同时等待等情况。

四、中断与等待队列如何配合

一个常见的驱动流程如下:

1.用户程序调用 read()。

2.驱动检查设备是否有事件。

3.如果没有事件,进程进入等待队列睡眠。

4.硬件产生中断。

5.中断处理函数更新事件状态。

6.驱动调用 wake_up() 唤醒等待进程。

7.用户进程被调度运行,重新检查条件并读取事件。

可以看出,中断负责通知内核发生了硬件事件,而等待队列负责管理等待该事件的进程。二者解决的是不同层面的问题。

实际实现时,不应仅仅依赖一次唤醒就认定事件一定可用。进程恢复执行后仍应重新检查等待条件。

五、上半部与下半部

5.1 为什么要区分上下半部

中断发生时,内核需要尽快完成必要处理。如果在中断处理函数中执行复杂计算、耗时操作或可能睡眠的任务,就可能影响系统响应。

因此,驱动通常把工作分为两部分:

上半部:快速响应中断,完成必要的状态确认。

下半部:延后执行相对复杂的后续工作。

不同的下半部机制具有不同的执行上下文和限制。

5.2 常见下半部机制

|-----------------|-------------------|--------------|
| 机制 | 主要特点 | 能否睡眠 |
| 软中断(softirq) | 主要用于内核中的高频、延后处理任务 | 不能 |
| tasklet | 基于软中断机制的较早期延后处理方式 | 不能 |
| 工作队列(workqueue) | 在内核工作线程上下文中执行 | 通常可以 |

工作队列适用于需要调用可能睡眠的接口或执行较复杂处理的任务。软中断和 tasklet 则不能执行会导致睡眠的操作。

5.3 工作队列示例

static void work_handler(struct work_struct *work)

{

/* 在工作线程上下文中处理后续任务 */

wait_que_cond = 1;

wake_up(&wait_que);

}

该函数将条件设为满足,并唤醒等待队列中的进程。

如果要从中断处理函数中安排工作队列,可以使用相应的工作项和调度接口。实际项目中,还要保证工作项初始化正确,并在驱动退出时妥善取消或等待未完成的工作。

此外,示例中的共享变量如果同时被中断、工作线程和进程访问,需要根据并发关系设计同步方案。

六、驱动中的同步与互斥

当多个执行上下文访问同一份数据时,可能出现竞争问题。例如,中断处理函数修改事件状态,而用户进程同时读取或清零该状态。

常见同步机制包括:

原子操作:适合简单的原子计数或状态更新。

自旋锁:适合保护短小的共享数据访问,不能在持锁期间睡眠。

互斥锁(mutex):适合进程上下文中可能睡眠的临界区。

信号量:可用于资源计数或特定的同步场景。

完成量(completion):适合等待某个任务或事件完成。

选择同步机制时,应重点考虑访问发生在哪种上下文中、临界区是否可能睡眠,以及是否需要保护多个变量之间的一致性。

七、常见问题排查

7.1 中断没有触发

建议检查:

1.GPIO 与中断号转换是否成功。

2.设备树中断配置是否正确。

3.触发方式是否与硬件信号变化一致。

4.中断是否成功申请。

5.硬件是否确实产生了相应电平变化。

7.2 进程一直处于阻塞状态

应检查等待条件是否能够被正确修改、唤醒函数是否被执行,以及等待条件是否在进入睡眠前后存在竞争问题。

7.3 使用工作队列后仍然出现问题

应检查工作项是否正确初始化、是否成功调度、共享数据是否受到保护,以及模块卸载时是否存在尚未结束的工作。

7.4 为什么不能在中断处理中随意睡眠

因为硬中断处理函数运行在中断上下文中,不能像普通进程那样主动睡眠等待资源。如果任务需要睡眠,应考虑工作队列等合适机制。

八、总结

中断、等待队列和工作队列是 Linux 驱动开发中的重要组成部分。

中断机制负责响应硬件事件,等待队列负责让进程在条件不满足时等待,工作队列则能够将后续任务交给内核工作线程执行。

学习时应重点理解三者之间的配合关系,并注意中断上下文、进程上下文和并发访问的区别。只有明确执行上下文的限制,才能正确选择同步机制和延后处理方式。

相关推荐
张文君34 分钟前
Ubuntu iSCSI CHAP认证与Win11挂载
linux·运维·ubuntu
程序员-Benothing44 分钟前
Linux防火墙配置教程:iptables、firewalld、nftables、ufw
linux·运维·服务器
我命由我123451 小时前
5-2.Python 编码进阶 - 字符串进阶(拼接、普通格式化、普通格式化数字精度控制、快速格式化、遍历、常用方法)
java·开发语言·后端·python·学习·java-ee·学习方法
齐鲁大虾1 小时前
如何通过 CUPS Web 界面(http://localhost:631)安装打印机驱动
linux
yunwei371 小时前
eBPF 实例教程:使用内核跟踪点监控 GPU 驱动活动
linux·后端·性能优化
HPT_Lt1 小时前
ZCC70X带看门狗功能的电压复位芯片,替代TPS3832
嵌入式硬件
AlfredZhao2 小时前
用脚本按顺序执行初始化 DB 的 SQL 文件
linux
瑞克-桑切斯3 小时前
企业生产环境:https部署+nginx高可用(业务不中断)
linux·服务器·数据库
LCG米9 小时前
STM32串口接收卡死根因排查:ORE溢出标志陷阱、错误标志清除顺序与中断恢复实测
stm32·单片机·嵌入式硬件·串口·嵌入式