Interrupts and Interrupt Handlers - Jokacer/Learn GitHub Wiki
硬件需要发送通知给处理器时会发送一个特殊的电信号--中断,处理器接收到中断信号后会通知操作系统反映该信号,然后操作系统负责处理该中断。对于来自不同设备的不同中断采用唯一的数字标志,通常称其为中断请求线(IRQ),在响应一个中断时,内核会执行响应的中断处理程序,为了使得中断响应迅速同时又保证其的工作量,采用将中断分为上下部分的方式进行处理,上部分在收到中断后立即完成响应的数据拷贝等操作并答复硬件,而下半部分则会在合适的时机进行该中断的其他工作,比如数据的处理。
注册一个中断处理程序:
int request_irq(unsigned int irq,//分配的中断号
irq_handler_t handler,//指向中断处理程序的指针
unsigned long flags,//对该中断的标志
const char *name,//与中断相关设备的ASCII文本
void *dev)//共享中断线中的中断处理程序的唯一标志request_irq()函数有可能会睡眠。
对于参数flags
| 标志 | 作用 |
|---|---|
| IRQF_DISABLED | 在内核处理该中断处理程序期间禁止其他所有中断 |
| IRQF_SAMPLE_RANDOM | 该标志对设备产生的中断对内核熵池有贡献,可用作产生随机数 |
| IRQF_TIMER | 为系统定时器的中断处理而准备 |
| IRQF_SHARED | 可共享中断线 |
卸载中断处理程序
void free_irq(unsigned int irq,void *dev)若是共享中断线则删除dev对应的中断处理程序,否则在删除中断处理程序的同时禁用该中断线。
共享中断和非共享中断在注册和运行方式上比较相似,差异如下:
- request_irq()的flag参数为IRQF_SHARED
- 对每个注册的中断处理程序来说dev参数必须唯一,用以判断在共享中断线上指定的中断处理程序
- 中断处理程序需要得到它的设备的反馈,区分是否真的发生了中断
内核接收到一个中断过后会依次调用在该中断线上注册的每一个处理程序,因此处理程序需要知道它是否应该对此负责。
当内核在执行中断处理程序的时候处于中断上下文,由于没有后备进程,所以中断上下文不可睡眠。由于中断处理程序可能正在打断正在执行的程序,所以所有的中断处理程序必须尽可能的迅速、简洁,尽量将工作放在下半部分进行处理。
中断处理机制示意图:
以下是中断处理函数中中断控制方法的列表
| 函数 | 说明 |
|---|---|
| local_irq_disable() | 禁止本地中断传递 |
| local_irq_enable() | 激活本地中断传递 |
| local_irq_save() | 保存本地中断传递的当前状态,然后禁止本地中断传递 |
| local_irq_restore() | 恢复本地中断传递到给定的状态 |
| disable_irq() | 机制给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行 |
| disable_irq_nosync() | 禁止给定中断线 |
| enable_irq() | 激活给定中断线 |
| irqs_disabled() | 如果本地中断传递被禁止返回非0否则返回0 |
| in_interrupt() | 如果在中断上下文中返回非0,在进程上下文中返回0 |
| in_irq() | 当前正在执行中断处理程序则返回非0,否则返回0 |
中断处理程序以异步的方式执行,有可能会打断其他重要的代码,因此为了避免被打断的代码停止时间过长,需要中断处理程序执行的越快越好,在中断的上半部分完成对中断的快速响应,在下半部分完成其他对于时间要求相对宽松的任务。
对于上下部分工作的划分可以借鉴:
- 对时间非常敏感的任务放在上半部分
- 与硬件相关的任务放在上半部分
- 保证不被其他中断打断的任务放在上半部分
- 其他所有任务可以考虑放在下半部分
下半部分的实现主要有三种,软中断(softirq)、tasklet和工作队列(work queues),其中tasklet是通过软中断实现的,是下半部分常用的一种形式。
软中断的数据结构表示为:
struct softirq_action {
void (*action)(struct softirq_action *);
}软中断在编译期间静态分配,在kernel/softirq.c中定义了一个包含32个软中断结构体的数组,因此最多可能由32个软中断,在2.6版本只用了9个。
软中断程序原型为:
void softirq_handler(struct softirq_action *)一个软中断不会抢占另一个软中断,只有中断处理程序可以抢占软中断,但是其他软中断可以在其他处理器上同时执行。
中断处理程序会在返回前标记触发它的软中断,在合适的时刻该软中断就会运行,无论用什么方式唤起,软中断都会在do_softirq()中执行,该函数会调用每一个待处理的软中断的处理程序。
由于软中断可以同时被触发并在不同的处理器上执行,因此共享数据需要严格的锁保护,因此tasklet更受青睐。
tasklet通过软中断实现,接口简单,锁保护要求较低。软中断只在那些执行频率很高且连续性要求很高的情况下才需要使用,而tasklet有更广泛的用途,大多数情况下使用效果都不错。
tasklet由两种软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,HI_SOFTIRQ优先于TASKLET_SOFTIRQ软中断执行。
tasklet结构体为:
struct tasklet_struct {
struct tasklet_struct *next;//链表的下一个tasklet
unsigned long state;//tasklet状态
atomic_t count;//引用计数器
void (*func)(unsigned long);//处理函数
unsigned long data;//tasklet处理函数的参数
};其中state参数只能是0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值,TASKLET_STATE_SCHED表明tasklet已被调度,准备投入运行,TASKLET_STATE_RUN表明tasklet正在运行。
tasklet调度执行步骤:
- 检查tasklet状态是否为TASKLET_STATE_SCHED,若是则表明tasklet正在被调度,函数立即返回。
- 调用_tasklet_schedule();
- 保存中断状态,禁止本地中断,保证处理器上的数据不乱
- 把需要调度的tasklet添加到每个处理器的tasklet_vec链表或者tasklet_hi_vec链表表头
- 唤起HI_SOFTIRQ或TASKLET_SOFTIRQ软中断
- 恢复中断到原状态,并返回
同一时刻只有一个给定类别的tasklet会被执行,但其他类别的tasklet可以同时执行。
工作队列可以将工作推后交由一个内核线程去执行,使得下半部分在进程上下文中执行,这样工作队列运行重新调度甚至睡眠。
工作队列和软中断/tasklet的实现机制不一样,如果推后执行的任务需要睡眠则选择工作队列,否则选择软中断/tasklet
下半部分机制比较
| 下半部 | 上下文 | 顺序执行保障 |
|---|---|---|
| 软中断 | 中断 | 没有 |
| tasklet | 中断 | 同类型不能同时执行 |
| 工作队列 | 进程 | 与进程调度一样 |
工作队列创建的内核线程被称为工作者线程,数据结构表示为:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int rt;
};工作队列的实现机制如图:
举一个简单的例子就是,除了系统默认的events工作者类型外,如果再添加一个falcon工作者类型,使用拥有4个处理器的计算机,那么系统由4个events类型的线程(拥有4个cpu_workqueue_struct结构体)和4个falcon类型的线程,一个对应与events类型的workqueue_struct和一个对应falcon类型的workqueue_struct.

