中断下半部 tasklet
当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。也就是说这段时间内,系统是关中断的。
如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?
中断下半部的实现有很多种方法,主要有两种
- tasklet(小任务)
- work queue(工作队列)
当下半部比较耗时是能忍受的,并且它的处理比较简单时,可以用 tasklet 来处理下半部。tasklet 是使用软件中断来实现。

假设硬件中断A 的上半部函数为 irq_top_half_A ,下半部为 irq_bottom_half_A 。
使用情景分析,才能理解上述代码:
-
硬件中断A 处理过程中,没有其他中断发生:
一开始,preempt_count = 0;
上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次 -
硬件中断 A 处理过程中,又再次发生了中断 A:
一开始, preempt_count = 0;
执行到第⑥时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。
注意:
这时 preempt_count 等于1,并且中断下半部的代码并未执行。
CPU 又从①开始再次执行中断 A 的上半部代码;
在第①步 preempt_count 等于 2;
在第③步 preempt_count 等于 1;
在第④步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理;
注意:重点来了
第2次中断发生后,打断了第一次中断的第⑦步处理。当第2次中断处理完毕, CPU 会继续去执行第⑦步。可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下半部代码只执行了一次。
所以,同一个中断的上半部、下半部,在执行时是多对一的关系。
- 硬件中断 A 处理过程中,又再次发生了中断 B:
一开始, preempt_count = 0;
执行到第⑥时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表。
注意: 这时 preempt_count 等于 1,并且中断 A 下半部的代码并未执行。
CPU 又从①开始再次执行中断 B 的上半部代码:
在第①步 preempt_count 等于 2;
在第③步 preempt_count 等于 1;
在第④步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理;
注意:重点来了, 第2次中断发生后,打断了第一次中断A的第⑦步处理。当第2次中断B处理完毕, CPU 会继续去执行第⑦步。在第⑦步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。
所以,多个中断的下半部,是汇集在一起处理的。
总结:
- 中断的处理可以分为上半部,下半部
- 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
- 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
- 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
- 中断上半部执行完后,触发中断下半部的处理
- 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?

当发生某一个硬件中断时,内核会调用对应的硬件中断处理函数,处理完之后,会去处理软件中断;从类似soft数组中取出action函数,对于lastlet这种软件中断,对应的函数是tasklet_action,这个函数会从某个队列/链表里取出每一个tasklet结构体,执行里面的函数。
处理硬件中断是上半部(中断禁止);处理软件中断的是下半部(中断打开,有可能被打断)。
上半部可以使能/调用下半部:把一个tasklet结构体放进队列/链表。

软件中断不会叠加
下半部就是构造tasklet结构体,提供函数。
内核函数
- 定义 tasklet,中断下半部使用结构体tasklet_struct来表示,它在内核源码
include\linux\interrupt.h中定义:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
-
其中的 state 有 2 位:
- bit0 表示 TASKLET_STATE_SCHED等于 1 时表示已经执行了 tasklet_schedule 把该 tasklet 放入队列了;tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把tasklet 放入队列。
- bit1 表示 TASKLET_STATE_RUN等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0。
-
其中的 count 表示该 tasklet 是否使能:等于 0 表示使能了,非 0 表示被禁止了。对于 count 非 0 的 tasklet,里面的 func 函数不会被执行。
使用下半部执勤,要实现一个 tasklet_struct 结构体,这可以用2个宏来定义结构体:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = {
NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = {
NULL, 0, ATOMIC_INIT(1), func, data }
- 使用
DECLARE_TASKLET定义的tasklet结构体,他是使能的; - 使用
DECLARE_TASKLET_DISABLED定义tasklet结构体,它是禁止的;使用之前要先调用tasklet_enable使能它。
也可以使用函数来初始化 tasklet 结构体
extern void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);
- 使能/禁止 tasklet
static inline void tasklet_enable(struct tasklet_struct *t);
static inline void tasklet_disable(struct tasklet_struct *t);
tasklet_enable把 count 减1;tasklet_disable把 count 增1。
- 调度
tasklet
static inline void tasklet_schedule(struct tasklet_struct *t);
把 tasklet 放入链表,并且设置它的 TASKLET_STATE_SCHED 状态为 1
- kill tasklet
extern void tasklet_kill(struct tasklet_struct *t);
- 如果一个 tasklet 未被调度, tasklet_kill 会把它的TASKLET_STATE_SCHED 状态清 0;
- 如果一个 tasklet 已被调度, tasklet_kill 会等待它执行完华,再把它的TASKLET_STATE_SCHED 状态清 0。
通常在卸载驱动程序时调用 tasklet_kill。
tasklet 使用方法
先定义 tasklet,需要使用时调用 tasklet_schedule,驱动卸载前调用tasklet_kill。tasklet_schedule 只是把 tasklet 放入内核队列,它的 func 函数会在软件中断的执行过程中被调用。
驱动程序
驱动程序在定时器的基础上修改。
- 在原来的gpio_key结构体中添加一个tasklet属性,每个按键都有一个tasklet;在probe函数中初始化tasklet;在remove函数中删除tasklet;
- 在tasklet处理函数中打印数据;
- 在中断服务程序,启动一个下半部。
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
中断下半部任务:处理耗时操作

最低0.47元/天 解锁文章
2327

被折叠的 条评论
为什么被折叠?



