3. Linux内核中断分层¶
中断是Linux内核与硬件交互的核心机制,是实现硬件异步响应、提升系统资源利用率的关键技术。 在嵌入式与服务器领域,硬件设备(如按键、串口、网卡等)的中断请求具有随机性、突发性, 且不同中断的处理需求存在显著差异——部分中断要求极速响应、执行逻辑简单, 部分中断则需要执行复杂耗时操作。若将所有中断处理逻辑集中在一个函数中,必然会导致中断响应延迟、系统吞吐量下降, 甚至引发硬件异常,因此,Linux内核引入了中断分层设计,构建了“顶半部+底半部”的分层处理架构,从根本上解决了中断处理“快响应”与“强处理”的矛盾。
3.1. 中断分层逻辑¶
Linux内核中断分层的本质是“拆分中断处理流程,实现上下文分离”,核心分为两大模块:
顶半部(Top Half):也称中断处理程序(Interrupt Handler),运行在硬中断上下文,由硬件触发后立即执行,核心职责是“快速响应、简单处理”,具体包括:屏蔽同类中断(避免嵌套干扰)、保存中断上下文(寄存器、标志位等)、清除硬件中断标志、触发底半部执行,执行时间严格控制在微秒级,不允许调用阻塞函数,不允许主动让出CPU。
底半部(Bottom Half):运行在软中断上下文或进程上下文,由顶半部触发,负责处理耗时、非紧急的后续操作,具体包括:数据解析、设备控制、日志打印、唤醒线程等,执行时间可稍长,可响应其他中断(软中断上下文)或调用阻塞函数(进程上下文)。
底半部是中断分层的核心实现载体,Linux内核提供了四种常用的底半部机制:软中断、tasklet、工作队列、线程irq,它们的上下文、执行时机、适用场景各有差异,开发者需根据实际需求选择合适的机制。
3.2. 中断分层执行流程¶
典型的中断分层执行流程如下:
硬件触发中断(如按键按下、数据接收),CPU暂停当前任务,切换到硬中断上下文,执行顶半部中断处理程序。
顶半部快速完成必要操作(屏蔽中断、保存上下文、清除中断标志),触发对应的底半部机制(如tasklet、工作队列)。
顶半部执行完毕,CPU恢复被中断的任务,底半部由内核调度器统筹安排,在合适的时机执行。
底半部执行完毕,释放相关资源,完成整个中断处理流程。
3.3. 底半部机制详解¶
3.3.1. 软中断¶
3.3.1.1. 软中断概念¶
软中断是Linux内核中最底层的底半部机制,运行在 软中断 上下文(介于硬中断和进程之间),由内核统一管理,优先级高于进程,低于硬中断。 软中断是一种“延迟执行”的机制,允许在硬中断处理完毕后,延迟处理一些非紧急的中断后续操作,且可以被更高优先级的硬中断打断。
软中断的核心特点是“可并发执行”——多个不同类型的软中断可以在多个CPU上同时执行,同一类型的软中断在同一CPU上串行执行,避免竞争。
3.3.1.2. 软中断常用函数¶
Linux内核中软中断的相关函数均定义在<linux/interrupt.h>中,核心函数如下:
3.3.1.2.1. 软中断类型定义¶
内核预定义了部分软中断类型,类型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | enum
{
HI_SOFTIRQ=0, // 高优先级软中断
TIMER_SOFTIRQ, // 定时器软中断
NET_TX_SOFTIRQ, // 网络发送软中断
NET_RX_SOFTIRQ, // 网络接收软中断
BLOCK_SOFTIRQ, // 块设备软中断
IRQ_POLL_SOFTIRQ, // 中断轮询软中断
TASKLET_SOFTIRQ, // tasklet依赖的软中断
SCHED_SOFTIRQ, // 调度软中断
HRTIMER_SOFTIRQ, // 高精度定时器软中断
RCU_SOFTIRQ, // RCU软中断
NR_SOFTIRQS // 软中断类型总数
};
|
3.3.1.2.2. open_softirq函数¶
open_softirq函数用于注册软中断,绑定软中断处理函数,内核启动时调用,是软中断使用的前置步骤。
函数原型:
1 | void open_softirq(int nr, void (*action)(struct softirq_action *));
|
参数说明:
nr:软中断类型(如TASKLET_SOFTIRQ),取值范围0~NR_SOFTIRQS-1。
action:软中断的处理函数,参数为struct softirq_action结构体,包含软中断的相关信息。
3.3.1.2.3. raise_softirq函数¶
raise_softirq函数用于触发软中断执行,将指定编号的软中断加入调度队列,等待内核调度其处理函数。
函数原型:
1 | void raise_softirq(int nr);
|
参数说明:
nr:需要触发的软中断类型,必须是已注册的软中断类型。
3.3.1.3. 软中断应用场景¶
软中断适用于“高频、低延迟、可并发”的底半部处理场景,尤其是内核核心模块,例如:
网络协议栈:网络接收(NET_RX_SOFTIRQ)、网络发送(NET_TX_SOFTIRQ),需要高频处理数据,且支持多CPU并发执行。
定时器处理:TIMER_SOFTIRQ,用于处理定时器到期后的延迟操作,优先级较高。
RCU同步:RCU_SOFTIRQ,用于RCU机制的延迟回收操作,确保系统并发性能。
注意:软中断的处理函数不能调用阻塞函数(如msleep、wait_event),因为软中断上下文不支持调度,调用阻塞函数会导致内核死锁。
3.3.2. tasklet¶
3.3.2.1. tasklet概念¶
tasklet是基于软中断实现的一种简化版底半部机制,运行在 软中断 上下文,依赖于TASKLET_SOFTIRQ和HI_SOFTIRQ两种软中断类型。
tasklet的核心特点是“简单易用、串行执行”——同一时刻,同一CPU上的所有tasklet串行执行,不同CPU上的tasklet可并行执行; 同一tasklet不会在多个CPU上同时执行,无需额外加锁(除非访问全局资源),大大降低了开发者的编程难度。
tasklet是Linux驱动开发中最常用的底半部机制,适用于大多数简单的延迟中断处理场景。
3.3.2.2. tasklet常用函数¶
3.3.2.2.1. tasklet结构体定义¶
开发者需定义struct tasklet_struct结构体变量,用于描述tasklet的属性和处理函数,原型如下:
1 2 3 4 5 6 7 | struct tasklet_struct {
struct tasklet_struct *next; // 链表指针,用于链接多个tasklet
unsigned long state; // tasklet状态,0:未触发;TASKLET_STATE_SCHED:已触发待执行
atomic_t count; // 引用计数,0:可执行;非0:不可执行
void (*func)(unsigned long); // tasklet处理函数
unsigned long data; // 传递给处理函数的参数
};
|
3.3.2.2.2. tasklet_init函数¶
tasklet_init函数用于初始化tasklet结构体,绑定回调函数,传递私有数据。
函数原型:
1 | void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
|
参数说明:
t:待初始化的tasklet结构体指针;
func:tasklet回调函数指针;
data:传递给回调函数的私有数据。
3.3.2.2.3. tasklet_schedule函数¶
tasklet_schedule函数用于调度tasklet执行,将tasklet加入软中断队列,触发软中断,等待内核调度回调函数。
函数原型:
1 | void tasklet_schedule(struct tasklet_struct *t);
|
参数说明:
t:已初始化的tasklet结构体指针。
3.3.2.2.4. tasklet_kill函数¶
tasklet_kill函数用于终止tasklet执行,等待回调函数执行完毕后再释放资源,防止并发问题,驱动退出时必须调用。
函数原型:
1 | void tasklet_kill(struct tasklet_struct *t);
|
参数说明:
t:已初始化的tasklet结构体指针。
3.3.2.3. tasklet应用场景¶
Tasklet适用于“简单、低延迟、无需阻塞”的底半部处理场景,是驱动开发中最常用的底半部机制,例如:
按键中断消抖后的后续处理(如翻转LED、上报按键事件)。
串口数据接收后的简单解析(如提取关键数据、打印日志)。
GPIO中断的延迟处理(如控制设备状态切换)。
注意:Tasklet运行在软中断上下文,不能调用阻塞函数,不能主动让出CPU;若处理逻辑需要阻塞,需使用工作队列或线程irq。
注解
tasklet 本身不支持主动阻塞/主动延迟(如msleep),它的“延迟”是被动调度延迟——调用tasklet_schedule()后不会立即执行,而是等待内核在软中断上下文的安全时机执行,属于“调度延迟”而非“主动等待延迟”。
3.3.3. 工作队列¶
3.3.3.1. 工作队列概念¶
工作队列是运行在 进程 上下文的底半部机制,核心是将延迟处理的任务交给内核线程执行,因此支持调用阻塞函数(如msleep、copy_from_user、wait_event),也支持主动让出CPU,是唯一支持阻塞操作的底半部机制。
工作队列的核心特点是“进程上下文、可阻塞、可调度”——工作队列的处理函数运行在用户态进程(kworker线程)中,受内核调度器管理,可被其他进程或中断打断,执行时间可较长,适合处理耗时且需要阻塞的操作。
Linux内核提供了两种工作队列:默认工作队列(共享队列)和自定义工作队列(私有队列),默认工作队列适用于简单场景,自定义工作队列适用于对并发性能要求较高的场景。
3.3.3.2. 工作队列常用函数¶
工作队列的相关函数均定义在<linux/workqueue.h>中,核心函数如下:
3.3.3.2.1. 工作结构体定义¶
开发者需定义struct work_struct结构体变量,用于描述工作任务,原型如下:
1 2 3 4 5 6 7 8 | struct work_struct {
atomic_long_t data; // 传递给处理函数的参数
struct list_head entry; // 链表指针,用于链接到工作队列
work_func_t func; // 工作处理函数
struct workqueue_struct *wq; // 所属的工作队列
struct list_head pending; // 挂起链表
...
};
|
其中,work_func_t是处理函数的类型定义:typedef void (*work_func_t)(struct work_struct *work);
3.3.3.2.2. INIT_WORK宏¶
INIT_WORK用于动态初始化工作任务,宏定义如下:
1 | INIT_WORK(struct work_struct *work, work_func_t func);
|
参数说明:
work:指向工作结构体的指针;
func:工作处理函数。
3.3.3.2.3. schedule_work函数¶
schedule_work函数用于将工作任务提交到默认工作队列(kworker线程)。
函数原型:
1 | bool schedule_work(struct work_struct *work);
|
参数说明:
work:已初始化的工作队列结构体指针;
返回值:true:提交成功;false:工作已在队列中。
3.3.3.2.4. cancel_work_sync函数¶
cancel_work_sync函数用于取消工作任务,若工作已提交但未执行,会等待其执行完毕后取消;若未提交,则直接取消。
函数原型:
1 | bool cancel_work_sync(struct work_struct *work);
|
参数说明:
work:已初始化的工作队列结构体指针;
3.3.3.2.5. INIT_DELAYED_WORK宏¶
INIT_DELAYED_WORK用于初始化延迟执行的工作(延迟指定时间后执行),宏定义如下:
1 | INIT_DELAYED_WORK(struct delayed_work *work, work_func_t func);
|
参数说明:
work:指向延时工作队列结构体指针;
func:工作处理函数。
3.3.3.2.6. schedule_delayed_work函数¶
schedule_delayed_work函数用于调度延时工作队列执行,延迟指定时间后,触发工作队列回调函数。
函数原型:
1 | bool schedule_delayed_work(struct delayed_work *work, unsigned long delay);
|
参数说明:
dwork:已初始化的延时工作队列结构体指针;
delay:延迟时间,单位为jiffiess。
返回值:true:调度成功;false:调度失败。
3.3.3.2.7. cancel_delayed_work_sync函数¶
cancel_delayed_work_sync函数用于取消延时工作队列调度,等待工作队列回调函数执行完毕后再取消,防止并发问题。
函数原型:
1 | bool cancel_delayed_work_sync(struct delayed_work *dwork);
|
参数说明:
dwork:已初始化的延时工作队列结构体指针;
返回值:true:成功取消、false:工作队列已执行完毕。
3.3.3.3. 工作队列应用场景¶
工作队列适用于“耗时、需要阻塞”的底半部处理场景,例如:
数据量大的解析操作(如网卡接收大数据包后的解析、存储)。
需要调用阻塞函数的场景(如从用户空间拷贝数据、等待某个设备就绪)。
执行时间较长的操作(如日志写入、设备固件升级)。
注意:工作队列运行在进程上下文,可调用阻塞函数,但受内核调度影响,执行延迟相对较高,不适合对延迟要求极高的场景。
3.3.4. 线程irq¶
3.3.4.1. 线程irq概念¶
线程irq(线程化中断)是将中断处理程序直接运行在内核线程中的一种机制,本质是“顶半部+底半部合并为线程执行”——中断触发后,内核会唤醒对应的内核线程, 中断处理逻辑全部在 进程 上下文(内核线程)中执行,支持阻塞、调度,且可设置线程优先级。
线程irq的核心特点是“进程上下文、可阻塞、可设置优先级”,与工作队列类似,但比工作队列更简洁(无需手动创建工作任务), 且中断触发与线程唤醒的关联更紧密,适用于中断处理逻辑复杂、需要阻塞且对优先级有要求的场景。
线程irq分为两种模式:
纯线程irq:顶半部为空,所有中断处理逻辑都在线程中执行,适用于无紧急处理需求、需要阻塞的场景。
混合线程irq:顶半部执行简单紧急操作(如清除中断标志),底半部(线程)执行耗时阻塞操作,适用于既有紧急处理需求、又有阻塞操作的场景。
3.3.4.2. 线程irq常用函数¶
3.3.4.2.1. devm_request_threaded_irq函数¶
devm_request_threaded_irq函数用于申请线程irq,绑定顶半部中断服务函数(handler)和底半部线程函数(thread_fn), 带设备资源托管,无需手动释放;内核自动创建并管理线程,无需调用kthread_run。
函数原型:
1 2 3 4 5 6 | int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags,
const char *name,
void *dev_id);
|
参数说明:
dev:设备指针;
irq:中断号;
handler:顶半部中断服务函数;
thread_fn:底半部线程irq函数;
flags:中断标志;
name:中断名称;
dev_id:传递给中断函数的私有数据。
返回值:0:申请成功;负值:申请失败,返回错误码。
3.3.4.3. irq_wake_thread函数¶
irq_wake_thread函数用于主动唤醒线程irq的底半部线程函数,触发线程执行;
若顶半部返回 IRQ_WAKE_THREAD ,内核会自动唤醒底半部线程函数,无需手动调用。
若顶半部返回 IRQ_HANDLED ,则需手动调用irq_wake_thread函数唤醒底半部线程函数。
函数原型:
1 | irqreturn_t irq_wake_thread(int irq, void *dev_id);
|
参数说明:
irq:中断号;
dev_id:传递给线程函数的私有数据。
返回值:IRQ_WAKE_THREAD:唤醒成功,通知内核调度线程。
3.3.4.4. 线程irq应用场景¶
线程irq适用于“中断处理逻辑复杂、需要阻塞、对优先级有要求”的场景,例如:
需要与用户空间交互的中断处理(如通过字符设备接口上报中断事件)。
需要等待资源就绪的中断处理(如等待I2C、SPI设备响应)。
对实时性要求较高的中断处理(可设置线程为实时优先级)。
注意:线程irq运行在进程上下文,可调用阻塞函数,但线程的创建、调度会带来一定的系统开销,不适合高频中断场景。
3.3.5. 选择原则¶
高频、低延迟、无阻塞需求:优先选择软中断或tasklet。
有阻塞需求、处理逻辑耗时:优先选择工作队列或线程irq。
驱动开发、简单延迟处理:优先选择tasklet。
需要与用户空间交互、等待资源:优先选择工作队列或线程irq。
高频中断、无阻塞、多CPU并发:优先选择软中断。
3.4. 中断分层实验¶
本实验在Linux中断子系统实验基础上进行修改,设备树插件完全一致,驱动仅说明修改部分,重复部分不作说明。
中断分层设计如下:
顶半部:按键中断触发后,启动消抖定时器。
消抖定时器回调函数翻转led电平,调度tasklet,唤醒线程irq底半部
tasklet:调度200ms延时的工作队列。
工作队列:调用msleep()执行耗时打印逻辑。
线程irq底半部:统计按键按下次数并打印。
注意
本实验仅演示中断底半部各机制使用方法,实际开发编写驱动时并不需要全部都用上,根据实际需求选择即可。
本章的示例代码目录为: linux_driver/19_irq_layering
3.4.1. 驱动代码详解¶
核心数据结构定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 自旋锁 */
spinlock_t spinlock;
/* LED 状态 */
int led_state;
/* LED 的 GPIO 描述符 */
struct gpio_desc *led_gpio;
/* 按钮的 GPIO 描述符 */
struct gpio_desc *button_gpio;
/* 消抖定时器 */
struct timer_list debounce_timer;
/* 中断号 */
int irq;
/* 统计有效按键按下次数 */
unsigned int btn_press_count;
/* tasklet */
struct tasklet_struct btn_tasklet;
/* 工作队列 */
struct delayed_work btn_delayed_work;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
设备私有结构体整合tasklet、延时工作队列资源,线程irq由内核管理,无需在结构体中定义task_struct指针; 新增btn_press_count用于线程irq的按键计数。
驱动初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配(省略重复代码)
/* 第一步:提取平台设备提供的资源 */
if (pdev->dev.of_node) {
/* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
if (IS_ERR(led_cdev->led_gpio)) {
ret = PTR_ERR(led_cdev->led_gpio);
printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
return ret;
}
/* 获取按钮的 GPIO 描述符,GPIOD_IN 表示设置为输入模式 */
led_cdev->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(led_cdev->button_gpio)) {
ret = PTR_ERR(led_cdev->button_gpio);
printk(KERN_ERR "Failed to get button GPIO: %d\n", ret);
return ret;
}
} else {
printk("Platform device matching is not supported in this driver\n");
return -ENOMEM;
}
//字符设备注册(省略重复代码)
/* 初始化自旋锁 */
spin_lock_init(&led_cdev->spinlock);
/* 初始化tasklet */
tasklet_init(&led_cdev->btn_tasklet, btn_tasklet_callback, (unsigned long)led_cdev);
/* 初始化延时工作队列 */
INIT_DELAYED_WORK(&led_cdev->btn_delayed_work, btn_delayed_callback);
/* 初始化消抖定时器 */
timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);
/* 初始化按键按下次数 */
led_cdev->btn_press_count = 0;
/* 默认关闭 LED */
led_cdev->led_state = 0;
/* 获取按键GPIO对应的中断号 */
led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
if (led_cdev->irq < 0) {
ret = led_cdev->irq;
/* 打印获取中断号失败 */
printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
/* 申请中断,下降沿触发 */
ret = devm_request_threaded_irq(&pdev->dev, led_cdev->irq,
button_irq_handler, // 顶半部(硬中断上下文,无睡眠)
button_irq_thread_fn, // 底半部(内核线程,进程上下文,支持睡眠)
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"button_irq", led_cdev);
if (ret < 0) {
/* 打印申请中断失败 */
printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
return 0;
// 错误处理(省略重复代码)
}
|
第34行:初始化tasklet;
第37行:初始化延时工作队列;
第43行:初始化按下次数为0;
第59-63行:申请线程irq,通过devm_request_threaded_irq绑定顶半部和线程底半部,内核自动创建并管理线程,无需手动调用kthread_run。
中断顶半部:按键中断服务函数
1 2 3 4 5 6 7 8 9 10 11 12 13 | static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
struct led_chrdev *led_cdev = (struct led_chrdev *)dev_id;
/* 重启消抖定时器,20ms后执行回调 */
mod_timer(&led_cdev->debounce_timer, jiffies + msecs_to_jiffies(DEBOUNCE_TIME));
/* 如果返回 IRQ_HANDLED 会认为顶半部已经完成了所有中断处理,不会再调度底半部线程*/
return IRQ_HANDLED;
/* 如果返回 IRQ_WAKE_THREAD 会主动唤醒底半部线程执行 */
// return IRQ_WAKE_THREAD;
}
|
顶半部仅启动消抖定时器,无任何耗时操作,快速退出中断,保证中断实时响应; 返回IRQ_HANDLED,不自动唤醒线程irq,线程唤醒由定时器回调中的irq_wake_thread触发。
中断底半部:消抖定时器回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | static void button_debounce_callback(struct timer_list *t)
{
/* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);
/* 用于保存中断状态信息 */
unsigned long flags;
/* 存储按键状态 */
int btn_val;
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 读取稳定后的按键电平 */
btn_val = gpiod_get_value(led_cdev->button_gpio);
/* 低电平表示按键稳定按下 */
if(btn_val == 0){
/* 翻转LED电平状态 */
gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
/* 调度 tasklet */
tasklet_schedule(&led_cdev->btn_tasklet);
/* 唤醒线程irq底半部 */
irq_wake_thread(led_cdev->irq, led_cdev);
}
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
/* 按键按下打印日志 */
if(btn_val == 0){
printk(KERN_INFO "按键已按下,LED状态翻转\n");
}
}
|
定时器消抖后,确认按键稳定按下后翻转电平,同时触发两个底半部逻辑:调度tasklet、唤醒线程irq; 遵循“快进快出”原则,不执行耗时操作。
中断底半部:tasklet回调函数
1 2 3 4 5 6 7 8 9 10 11 | static void btn_tasklet_callback(unsigned long data)
{
/* 转换为自定义的设备结构体指针 */
struct led_chrdev *led_cdev = (struct led_chrdev *)data;
/* 打印回调函数执行信息 */
printk(KERN_INFO "tasklet回调函数执行\n");
/* 调度延时工作队列,200ms后执行延时工作队列回调函数 */
schedule_delayed_work(&led_cdev->btn_delayed_work, msecs_to_jiffies(200));
}
|
tasklet运行于中断上下文,不可睡眠、无耗时操作,此处仅调度延时工作队列,200ms后执行延时工作队列回调函数。
中断底半部:工作队列回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | static void btn_delayed_callback(struct work_struct *work)
{
/* 计数器变量 */
int counter = 1;
/* 打印回调函数执行信息 */
printk(KERN_INFO "延时工作队列回调函数执行,开始耗时打印\n");
/* 5次延时打印 */
msleep(200);
printk(KERN_INFO "irq_thread counter = %d \n", counter++);
msleep(200);
printk(KERN_INFO "irq_thread counter = %d \n", counter++);
msleep(200);
printk(KERN_INFO "irq_thread counter = %d \n", counter++);
msleep(200);
printk(KERN_INFO "irq_thread counter = %d \n", counter++);
msleep(200);
printk(KERN_INFO "irq_thread counter = %d \n", counter++);
}
|
工作队列运行于进程上下文,支持msleep睡眠,此处执行极耗时打印逻辑,无需唤醒其他线程;延时200ms后触发,承接tasklet的调度,属于底半部第二层级。
中断底半部:线程irq底半部函数
1 2 3 4 5 6 7 8 9 10 11 | static irqreturn_t button_irq_thread_fn(int irq, void *dev_id)
{
struct led_chrdev *led_cdev = (struct led_chrdev *)dev_id;
/* 有效按键触发一次,计数 +1 */
led_cdev->btn_press_count++;
printk(KERN_INFO "线程化中断底半部执行,按键有效按下总次数:%u\n", led_cdev->btn_press_count);
return IRQ_HANDLED;
}
|
线程irq由内核通过devm_request_threaded_irq自动管理,无需手动创建,运行于进程上下文,支持睡眠,此处是记录按键有效按下次数并打印。
remove函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | static int pdrv_led_remove(struct platform_device *pdev)
{
struct led_chrdev *led_cdev = platform_get_drvdata(pdev);
/* 打印平台驱动移除信息 */
printk("pdrv_led remove\n");
/* 取消延时工作,等待其执行完成 */
cancel_delayed_work_sync(&led_cdev->btn_delayed_work);
/* 终止tasklet,等待其执行完成 */
tasklet_kill(&led_cdev->btn_tasklet);
/* 删除消抖定时器 */
del_timer_sync(&led_cdev->debounce_timer);
/* 销毁设备节点 */
device_destroy(class, devno);
/* 删除字符设备 */
cdev_del(&led_cdev->dev);
/* 释放设备号 */
unregister_chrdev_region(devno, DEV_CNT);
/* 销毁设备类 */
class_destroy(class);
return 0;
}
|
驱动卸载时,按“工作队列->tasklet->定时器”的顺序释放分层组件资源; 线程irq由devm_request_threaded_irq托管,无需手动停止,内核会自动释放线程资源。
3.4.3. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
3.4.3.1. 实验操作¶
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #加载驱动
insmod irq_layering.ko
#信息输出如下
[19727.967875] led platform driver init
[19727.968512] led platform driver probe
[19727.968788] major=236, minor=0
# 查看中断号
cat /proc/interrupts | grep button_irq
#信息输出如下
105: 1611 0 0 0 gpio1 10 Edge button_irq
|
使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | [19731.978896] 按键已按下,LED状态翻转
[19731.979072] 线程化中断底半部执行,按键有效按下总次数:1
[19731.979127] tasklet回调函数执行
[19732.182438] 延时工作队列回调函数执行,开始耗时打印
[19732.388898] irq_thread counter = 1
[19732.598878] irq_thread counter = 2
[19732.808953] irq_thread counter = 3
[19733.018919] irq_thread counter = 4
[19733.225615] irq_thread counter = 5
[19740.385983] 按键已按下,LED状态翻转
[19740.386035] 线程化中断底半部执行,按键有效按下总次数:2
[19740.386070] tasklet回调函数执行
[19740.589465] 延时工作队列回调函数执行,开始耗时打印
[19740.796062] irq_thread counter = 1
[19741.002777] irq_thread counter = 2
[19741.209358] irq_thread counter = 3
[19741.416058] irq_thread counter = 4
[19741.622879] irq_thread counter = 5
[19743.469522] 按键已按下,LED状态翻转
[19743.469648] 线程化中断底半部执行,按键有效按下总次数:3
[19743.469693] tasklet回调函数执行
[19743.672986] 延时工作队列回调函数执行,开始耗时打印
[19743.879505] irq_thread counter = 1
[19744.086271] irq_thread counter = 2
[19744.292867] irq_thread counter = 3
[19744.499581] irq_thread counter = 4
[19744.706197] irq_thread counter = 5
|
从内核打印信息可以看到线程底半部的按键按下计数功能打印正常,tasklet回调函数执行正常,调度延时工作队列执行耗时打印正常。
3.4.4. 实验注意事项¶
线程irq由irq_wake_thread手动唤醒,若需改为“顶半部自动唤醒”,可将顶半部返回值改为IRQ_WAKE_THREAD,需注意避免重复唤醒导致计数异常。
schedule_delayed_work的延时基于内核jiffies,若系统负载过高,可能出现延时偏差。
禁止在中断上下文调用阻塞函数。