中断体系
目的:
- 硬件的中断相应—>内核驱动中的中断
- 系统调用的函数相应(sys_call)—>系统调用
- 自定义中断–>软件的软中断模式
- 信号中断(kill -signanum) —>对了解信号的使用、创建等
- 系统的异常和错误—>系统的异常获取 了解系统异常的作用
Linux的中断机制
分类:硬中断、软中断
硬中断 | 由电脑主句的8259A类似的硬件中断控制芯片发出 的中断, ARM中断控制器发出额中断 |
---|---|
软中断 | 异常 第一类:CPU自行保留的中断系统的调用异常 |
代码结构:
asm.s | trap.c |
---|---|
system_call.s | fork.c signal.c sys.c exit.c |
中断的工作流程:
2.1 做CPU工作模式的转化
进行寄存器的拷贝与压栈
设置中断异常向量表
保证正常运行的函数返回值
跳转到对应的中断服务函数上运行
进行模式的复原以及寄存器的复原
跳转回正常工作的函数地址继续运行
2.2 Linux中中断的工作流程:
- 将所有的寄存器值入栈:8086)SS EFLAGS ESP CS EIP (错误码) ARM中的(r0 ~ r15)
- 将异常码入栈(中断号)
- 将当前的函数返回值进行入栈(为了在中断执行后能够找到在哪里中断,能够复原)
- 调用对应的中断服务函数
- 出栈函数返回值
- 返回所有入栈的寄存器值
如图所示:
中断前的处理过程,中断的回复过程 | 中断的执行过程 | |
---|---|---|
硬件中断的处理过程 | asm.s | trap.c |
软件异常及系统调用的处理过程 | system_call.s | fork.c signal.c sys.c |
中断的代码实现过程
中断前的处理过程,中断的回复过程 | 中断的执行过程 | |
---|---|---|
硬件中断的处理过程 | asm.s | trap.c |
中断下半部
把中断处理程序分为两部分:上半部(top half, th)和下半部(bottom half, bh)
上半部:在关闭中断的情况下执行,只做对时间非常敏感、与硬件相关或者不能被其他中断打断的工作
下半部:在开启中断的情况下执行,可以被其他中断打断
上半部称为硬中断(hardirq),下半部有3种:软中断(softirq)、小任务(tasklet)和工作队列(workqueue)。
3种下半部的区别如下:
- 软中断和小任务不允许睡眠;工作队列是使用内核线程实现的,处理函数可以睡眠
- 软中断的种类是编译时静态定义的,在运行时不能添加或删除;小任务可以在运行时添加或删除
- 同一种软中断的处理函数可以在多个处理器上同时执行,处理函数必须是可以重入的,需要使用锁保护临界区;一个小任务同一时刻只能在处理器上执行,不要求处理函数是可以重入的
软中断
软中断作为下半部机制的代表,是随着 SMP 的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。特性是:
-> 产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
-> 可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
小任务 tasklet
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
-> 一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
-> 多个不同类型的tasklet可以并行在多个CPU上。
-> 软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择.
一般而言,在可延迟函数上可以执行四种操作:初始化/激活/执行/屏蔽。屏蔽我们这里不再叙述,前三个则比较重要。下面将软中断和tasklet的三个步骤分别进行对比介绍:
初始化
初始化是指在可延迟函数准备就绪之前所做的所有工作。一般包括两个大步骤:首先是向内核声明这个可延迟函数,以备内核在需要的时候调用;然后就是调用相应的初始化函数,用函数指针等初始化相应的描述符。
如果是软中断则在内核初始化时进行,其描述符定义如下:
struct softirq_action {
void (*action)(struct softirq_action *);
void *data;
};
kernel/softirq.c 中的软中断描述符数组:static struct softirq_action softirq_vec[32]
前 6 个已经被内核注册使用:
- tasklet 使用的 HI_SOFTIRQ
- tasklet 使用的 TASKLET_SOFTIRQ
- 网络协议栈使用的 NET_TX_SOFTIRQ
- 网络协议栈使用的 NET_RX_SOFTIRQ
- SCSI 存储
- 系统计时器
其余的软中断描述符可以由内核开发者使用。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
例如网络子系统通过以下两个函数初始化软中断(net_tx_action/net_rx_action是两个函数):
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
当内核中产生 NET_TX_SOFTIRQ 软中断之后,就会调用 net_tx_action 这个函数。
tasklet 则可以在运行时定义,例如加载模块时。定义方式有两种:
- 静态声明
DECLARE_TASKET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
- 动态声明
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
其参数分别为描述符,需要调用的函数和此函数的参数。初始化生成的就是一个实际的描述符,假设为 my_tasklet。
激活
激活:标记一个可延迟函数为挂起(pending)状态,表示内核可以调用这个可延迟函数。类似处于 TASK_RUNNING 状态的进程,处在这个状态的进程只是准备好了被 CPU 调度,但并不一定马上就会被调度。
软中断使用 raise_softirq() 函数激活,接收的参数就是上面初始化时用到的数组索引 nr。
tasklet 使用 tasklet_schedule() 激活,该函数接受 tasklet 的描述符作为参数,例如上面生成的 my_tasklet:
tasklet_schedule(&my_tasklet)
执行
执行就是内核运行可延迟函数的过程,但是执行只发生在某些特定的时刻。
每个CPU上都有一个32位的掩码__softirq_pending,表明此CPU上有哪些挂起(已被激活)的软中断。此掩码可以用local_softirq_pending()宏获得。所有的挂起的软中断需要用do_softirq()函数的一个循环来处理。
对于 tasklet,软中断初始化时设置了发生 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 软中断时所执行的函数:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
tasklet_action 和 tasklet_hi_action 内部实现不同,软中断和 tasklet 因此具有了不同的特性
工作队列
上面的可延迟函数运行在中断上下文中(如上章所述,软中断的一个检查点就是 do_IRQ 退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。