中断概念
中断是一种通知机制,中断使得硬件得以发出通知给处理器。中断的本质是一种特殊的电信号,由硬件设备发向处理器,处理器接收到中断后,会马上向操作系统反映此信号的到来,然后由操作系统负责处理这些新来的数据。所以中断的生成并不考虑与处理器同步,即中断是可以随时产生的,所以内核随时都可能会被中断打断。
中断通过中断请求(IRQ)线来识别是哪种中断。
说到中断就需要提一下异常了:
异常和中断不同,异常在产生的时候需要考虑预处理器时钟同步,所以异常常常也被称为同步中断,异常的发生一般都是内部一些编程失误(如除0)或者一些特殊情况下(如缺页)产生的,因为这些操作需要内核处理,而进入内核的方式就是通过异常。
中断处理程序
有了中断信号之后,那么就需要处理这个中断相关的事务,则完成这些操作是由中断处理程序去完成的。
中断处理程序会在特定中断时,被内核执行,中断处理程序也叫中断服务例程。
每个中断的触发情况都不同,所以每个中断对应的处理程序也不同,这在注册中断的时候需要关联对应的中断处理程序。
中断处理程序其实就是个普通的C函数,但是需要按照特定的类型声明,方便内核以标准的形式传递处理程序中的信息,最本质的区别在于:中断处理程序是被内核调用来响应终端的,其是运行在中断上下文(原子上下文)的特殊上下文中。
中断处理程序发生时会打断内核的执行,且中断处理程序时会让系统停滞,所以中断处理程序需要快速的执行,一般都是负责通知硬件设备中断已被接收,不会有太多的其他操作。
但是当需要在中断处理程序中完成一些比较复杂费时的操作呢?如网络数据包的处理。内核提供了上下部分机制来解决这类问题。
中断上半部分和下半部分
为了让中断处理程序运行得快且完成的工作量多,内核把中断处理分为两个部分:上半部和下半部。
接收到中断时,立即执行上半部分,处理一些有严格时限的工作,如复位硬件或应答等。然后在合适的时机处理下半部分,处理一些复杂耗时的工作。
下半部的任务就是执行与中断处理密切相关但中断处理程序本省不执行的工作。
下半部什么时候执行呢?
通常下半部在中断处理程序已返回就会马上执行。且下半部执行过程中运行响应所有中断。
下半部实现的机制:任务队列,软中断和tasklet。
软中断
一个软中断不会抢占另外一个软中断,软中断只会被中断处理程序抢占。
软中断实在编译期间静态分配的。
① 注册软中断的函数 open_softirq参见 kernel/softirq.c文件)
/*
* 将软中断类型和软中断处理函数加入到软中断序列中
* @nr - 软中断类型
* @(*action)(struct softirq_action *) - 软中断处理的函数指针
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
/* softirq_vec是个struct softirq_action类型的数组 */
softirq_vec[nr].action = action;
}
软中断类型目前有10个,其定义在 include/linux/interrupt.h 文件中:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
struct softirq_action 的定义也在 include/linux/interrupt.h 文件中
/*
* 这个结构体的字段是个函数指针,字段名称是action
* 函数指针的返回指是void型
* 函数指针的参数是 struct softirq_action 的地址,其实就是指向 softirq_vec 中的某一项
* 如果 open_softirq 是这样调用的: open_softirq(NET_TX_SOFTIRQ, my_tx_action);
* 那么 my_tx_action 的参数就是 softirq_vec[NET_TX_SOFTIRQ]的地址
*/
struct softirq_action
{
void (*action)(struct softirq_action *);
};
② 触发软中断的函数 raise_softirq 参见 kernel/softirq.c文件
/*
* 触发某个中断类型的软中断
* @nr - 被触发的中断类型
* 从函数中可以看出,在处理软中断前后有保存和恢复寄存器的操作
*/
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
③ 执行软中断 do_softirq 参见 kernel/softirq.c文件
一个注册的软中断必须在被标记后才会执行,这杯称作出发软中断。
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
/* 判断是否在中断处理中,如果正在中断处理,就直接返回 */
if (in_interrupt())
return;
/* 保存当前寄存器的值 */
local_irq_save(flags);
/* 取得当前已注册软中断的位图 */
pending = local_softirq_pending();
/* 循环处理所有已注册的软中断 */
if (pending)
__do_softirq();
/* 恢复寄存器的值到中断处理前 */
local_irq_restore(flags);
}
④ 执行相应的软中断 - 执行自己写的中断处理
linux中,执行软中断有专门的内核线程,每个处理器对应一个线程,名称ksoftirqd/n (n对应处理器号)
通过top命令查看我的单核虚拟机,linux系统中的ksoftirqd线程如下:
tasklet
tasklet也是利用软中断实现的,但是它的接口更简单,锁保护也要求更低,一般建议使用tasklet方式来实现自己的软中断。
tasklet对应的结构体在 <linux/interrupt.h> 中
struct tasklet_struct
{
struct tasklet_struct *next; /* 链表中的下一个tasklet */
unsigned long state; /* tasklet状态 */
atomic_t count; /* 引用计数器 */
void (*func)(unsigned long); /* tasklet处理函数 */
unsigned long data; /* tasklet处理函数的参数 */
};
工作队列
工作队列子系统是一个用于创建内核线程的接口,通过它可以创建一个工作者线程来专门处理中断的下半部工作。
工作队列和tasklet不一样,不是基于软中断来实现的。
总结中断下半部实现机制
下半部机制 | 上下文 | 复杂度 | 执行性能 | 顺序执行保障 |
---|---|---|---|---|
软中断 | 中断 | 高(需要自己确保软中断的执行顺序及锁机制) | 好 (全部自己实现,便于调优) | 没有 |
tasklet | 中断 | 中(提供了简单的接口来使用软中断) | 中 | 同类型不能同时执行 |
工作队列 | 进程 | 低(在进程上下文中运行,与写用户程序差不多) | 差 | 没有 |
注册一个中断处理程序
驱动程序可以通过request_irq()
来注册一个中断处理程序,并且激活给定的中断线,以处理中断:
注册中断:
/*
* irg - 表示要分配的中断号
* handler - 实际的中断处理程序
* flags - 标志位,表示此中断的具有特性
* name - 中断设备名称的ASCII 表示,这些会被/proc/irq和/proc/interrupts文件使用
* dev - 用于共享中断线,多个中断程序共享一个中断线时(共用一个中断号),依靠dev来区别各个中断程序
* 返回值:
* 执行成功:0
* 执行失败:非0
*/
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char* name,
void *dev)
释放中断:
/*
如果不是共享中断线,则直接删除irq对应的中断线。
如果是共享中断线,则判断此中断处理程序是否中断线上的最后一个中断处理程序,
是最后一个中断处理程序 -> 删除中断线和中断处理程序
不是最后一个中断处理程序 -> 删除中断处理程序
*/
void free_irq(unsigned int irq, void *dev);
重入和中断处理程序
Linux中的中断处理程序必须是无需重入的,因为当执行中断处理程序时,相应的中断线会在所有的处理器被屏蔽掉,防止重复接收同一个中断,所以不会出现处理程序的重入现象,即中断处理程序不会被同时调用以处理嵌套的中断。
中断上下文
当执行一个中断处理程序时,内核处于中断上下文中。
由于中断上下文需要快速、简洁,所以中断上下文不可以睡眠,而是把耗时操作放到下半部分执行。
中断处理程序拥有自己的栈,大小为一页,成为中断栈。
进程上下文:调用系统调用函数时,内核处理系统调用事件,代替进程执行时或运行内核线程时。
进程上下文可以睡眠,也可以调度程序,通过current宏关联当前进程。
中断处理机制的实现
中断处理过程:
- 硬件设备产生中断,然后通过总线把电信号发送给中断控制器。
- 中断控制器会把中断发往处理器。
- 处理器会停止正在做的事,关闭中断系统,然后调到内存中预定的位置开始执行那里的代码(中断处理程序入口)。
流程如下:
中断处理的过程主要涉及3函数:
do_IRQ
与体系结构有关,对所接收的中断进行应答
handle_IRQ_event
调用中断线上所有中断处理
ret_from_intr
恢复寄存器,将内核恢复到中断前的状态
在处理器处理中断时会先根据每个中断线跳到对应的一个唯一的位置(这样就获得了中断的IRQ号了),然后调用do_IRQ()
函数,do_IRQ()
会做一下工作:
do_IRQ()
函数运行时,会把中断号提取出来(读取寄存器);- 对接收的中断进行应答,禁止这条线上的中断传递;
- 调用
handle_IRQ_event()
来运行中断线所安装的中断处理程序。
处理完之后就会返回内核运行中断的代码。
常用的中断控制方法见下:
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
我们在Linux系统运行时可以通过procfs
虚拟文件系统下interrupts
文件来查看系统中与中断相关的统计信息:
cat /proc/interrupts
按列顺序分别是:中断线、CPU对应的计数器、中断控制器、设备名字。
小结
中断,其实就是一种由设备使用的硬件资源异步向处理器发信号,可以打断操作系统。
因为中断可以打断操作系统,所以中断必须迅速执行,只能干一些有严格时限的工作,大量的工作需要推迟到下半部执行,通过上半部和下半部的机制,使得中断可以快速执行且能够处理大量工作。