中断
中断
是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。中断包括硬中断
和软中断
。
整个操作系统可理解为一个由中断驱动的死循环,操作系统原理可用下面的伪代码解释:
while(true) {
doNothing();
}
其他所有事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成,点击鼠标和键盘,执行一个程序,都是用中断的方式来通知操作系统来处理这些事件,当没有任何需要操作系统处理的事件时,它就停在死循环里。
1 中断的分类
中断分为硬中断和软中断,其分类依据是实现机制
,而不是触发机制,比如CPU硬中断,它是由CPU这个硬件实现的中断机制,但它的触发可以通过外部硬件,也可以通过软件的 INT 指令。
类似地,软中断是由软件实现的中断,是纯粹由软件实现的一种类似中断的机制,实际上是模仿硬件,在内存中存储着一组软中断的标志位,然后由内核的一个守护线程不断轮询这些标志位,如果有哪个标志位有效,则再去执行这个软中断对应的中断处理程序。
2 硬中断工作流程
本文以Intel CPU为例介绍硬中断,基本处理流程如下图:
收到中断号有三种方式,CPU 收到中断号n
后,会去中断描述符表
中寻找第n个中断描述符
,从中断描述符中找到中断处理程序的地址
,然后跳过去执行。下面将分别介绍。
2.1 收到中断号
CPU提供两种机制来中断程序的执行:中断(interrupt)
和异常(exception)
。
中断是一个异步事件,通常由 IO 设备触发。比如点击鼠标、敲击键盘等。
异常是一个同步事件,是CPU在执行指令时检测到一些预先定义的条件时发生的。比如除法异常、错误指令异常和缺页异常等。
这两种机制的目的都是让CPU收到一个中断号
。
如何让CPU收到一个中断号呢?有三种方式:
1)通过中断控制器给 CPU 的 INTR 引脚发送信号。
可编程中断控制器有很多 IRQ 引脚线,接入了一堆能发出中断请求的硬件设备(鼠标、键盘、等),且可编程中断控制器提前被设置好了 IRQ 与中断号的对应关系,当某个硬件设备给 IRQ 引脚线发一个信号时,就转化成了对应的中断号,可编程中断控制器把这个中断号存储在自己的一个端口上,然后给 CPU 的 INTR 引脚发送一个信号,CPU 收到 INTR 引脚信号后,去可编程中断控制器上的对应端口读取这个中断号的值。
最终目标是让 CPU 知道有中断了,并且知道中断号是多少。
2)CPU 执行某条指令发现了异常。
CPU执行指令时检测到的一些异常情况,会给自己一个中断号,无需外界设备给出。
3)执行INT n
指令。
INT
指令后面跟一个数字,就相当于直接用指令的形式,告诉CPU一个中断号。比如INT 0x80
,就是告诉 CPU 中断号是 0x80,Linux 内核提供的系统调用,就是用了 INT 0x80 这种指令。
一些资料会把以上三种方式做区分,把INT n
这种方式叫做软件中断,因为是由软件程序主动触发的,把中断和异常叫做硬件中断,因为他们都是硬件自动触发的。
2.2 中断描述符表
简单来说,中断描述符表是一个在内存中的数组,以linux-2.6.0 源码为例,
struct desc_struct idt_table[256] = { {0, 0}, };
是一个大小为 256 的数组。idt_table
即 Interrupt Descriptor Table,中断描述符表。
中断描述符表是由操作系统写进内存的,
void __init trap_init(void) {
set_trap_gate(0, ÷_error);
...
set_trap_gate(6, &invalid_op);
...
set_intr_gate(14, &page_fault);
...
set_system_gate(0x80, &system_call);
}
上面提到的除法异常、非法指令异常、缺页异常,以及之后可能通过INT 0x80触发的系统调用的中断处理函数 system_call,就这样被写到了中断描述符表里。
2.3 中断描述符
中断描述符表这个数组里的存储的每个元素都是中断描述符
,是一个叫 desc_struct
的结构。
struct desc_struct {
unsigned long a,b;
};
每个中断描述符的大小为 64 位,中断描述符里面有段选择子(Segment Selector)
和段内偏移地址(Offset)
。此处的段
指的是内存管理中的段页式管理,相当于给出逻辑地址,转换成线性地址,再转换成真正的物理地址,在此地址中存放着中断处理程序。
由上图可知,每个中断号都对应一个中断描述符,那CPU收到一个中断号后,如何找到对应的中断描述符呢?
CPU中预留了一个 IDTR寄存器
,操作系统通过LIDT指令
,将中断描述符表的地址放在这个寄存器里,此寄存器的结构如下:
IDTR寄存器里的值一共有48位,前16位是中断描述符表的大小(字节数),后32位是中断描述符表的起始内存地址,即idt_table 的位置。
小结:CPU收到一个中断号后,从IDTR 寄存器中可以知道中断描述符表的起始位置,而且里面的每个中断描述符都是 64 位大小,也就是 8 个字节,自然就可以找到这个中断号对应的中断描述符。
2.4 执行中断处理程序
找到中断处理程序后,就可以执行了,具体实现是做一些压栈操作
:
1)如果发生了特权级转移,压入之前的堆栈段寄存器 SS 及栈顶指针 ESP 保存到栈中,并将堆栈切换为 TSS 中的堆栈。
2)压入标志寄存器 EFLAGS。
3)压入之前的代码段寄存器 CS 和指令寄存器 EIP,相当于压入返回地址。
4)如果此中断有错误码的,压入错误码 ERROR_CODE
5)结束(跳转回中断前的程序)
简单来说,就是压栈,并跳转到入口地址处执行代码;而压栈的目的,就是保护现场(原来的程序地址、原来的程序堆栈、原来的标志位)和传递信息(错误码),执行结束后再跳转回原程序继续执行。
3 软中断工作流程
关于软中断的具体实现,简单说就是有一个单独的守护进程,不断轮询一组标志位,如果哪个标志位有值了,那去这个标志位对应的软中断向量表数组的相应位置,找到软中断处理函数,然后跳过去执行。
关于硬中断的具体实现,就是 CPU 在每一条指令周期的最后,都会留一个CPU时钟周期去查看是否有中断,如果有,就把中断号取出,去中断向量表中寻找中断处理程序,然后跳过去执行。
根据以上流程,分别介绍。
3.1 开启内核软中断处理的守护进程
下图是开机启动流程,可以看出有开启中断机制的一个步骤:
跟踪入口方法:
asmlinkage void __init start_kernel(void) {
...
trap_init();
sched_init();
time_init();
...
rest_init(); // 进入此方法
}
方法里面是各种初始化。
static void rest_init(void) {
kernel_thread(init, NULL, CLONE_KERNEL);
}
static int init(void * unused) {
do_pre_smp_initcalls();
}
static void do_pre_smp_initcalls(void) {
spawn_ksoftirqd(); // spawn kernel soft irt daemon,开启内核软中断守护进程
}
spawn_ksoftirqd();
即开启内核软中断守护进程,进入此方法,主要内容如下:
// 这就是软中断处理函数表(软中断向量表)
// 和硬中断的中断描述符表类似
static struct softirq_action softirq_vec[32];
asmlinkage void do_softirq(void) {
// h = 软中断向量表起始地址指针
h = softirq_vec;
// pending是一个int值,32位,代表一组软中断标志位,一次性拿到所有的软中断标志位
pending = local_softirq_pending();
do {
// 此时的软中断标志位有值(说明有软中断)
if (pending & 1) {
// 去对应的软中断向量表执行对应的处理函数
h->action(h);
// 软中断向量表指针向后移动,步长为1
h++;
// 同时软中断处理标志位也向后移动,步长为1
pending >>= 1;
} while (pending);
}
可以看出软中断标志位的一位对应着软中断向量表中的一个元素,所以中断向量表这个数组大小是32,而中断标志位也有32个。
所以,内核软中断处理守护进程的工作过程如下:
不断遍历pending这个软中断标志组的每一位,如果是 0 就忽略,如果是 1,则从h软中断向量表中找到对应的元素,然后执行其中的action方法,action对应着不同的软中断处理函数。
开机启动后,就会开启内核软中断处理守护进程,软中断机制开始生效。
3.2 注册软中断向量表
注册软中断向量表
即给softirq_vec这个软中断向量表里面的每一个元素的action赋值,赋的就是软中断处理函数的函数地址。
3.3 触发一次软中断
表示软中断标志位组的pending通过如下方式取值:
pending = local_softirq_pending();
取出来的是一个32位的int值,触发一次软中断只需将对应的标志位置为1即可,比如要触发一个2号软中断:
可以采用如下代码方式:
local_softirq_pending() |= 1UL << 2;
3.4 小结
- 软中断的实现方式:一组软中断标志位,对应着软中断向量表中每个中断处理函数,有一个内核守护进程不断循环判断中断标志位,如果为1就调用对应的中断处理函数。
- 由各个子系统调用
open_softirq
,负责把软中断向量表赋上值。 - 由各个需要触发软中断的地方调用
raise_softirq_irqoff
,修改中断标志位的值。 - 软中断守护线程循环判断中断标志位并调用对应的处理函数。
- Linux一般会把中断分成上下两半部分执行,上半部分处理最简单的逻辑,下半部分直接交给一个软中断异步处理。
本文整理自公众号“低并发编程”:
- https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247498208&idx=1&sn=b784f8b4e627ebd1bfb9810d194fdb80&chksm=c2c5834df5b20a5bdee331002bfc61c90eb468da325bf67abeef780c303a9f51c8543e1a5981&scene=21#wechat_redirect
- https://mp.weixin.qq.com/s/g9rGKRQofAlWjdq8lDTTkQ