操作系统之中断
中断概念
整个操作系统是中断驱动的死循环,其他事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成的。
说人话就是当我们点一下鼠标、敲一下键盘、执行一个程序,都是通过中断的方式来通知操作系统帮我们处理这些问题,当没有任何需要操作系统处理的事件时,它就老老实实的呆在死循环里。
中断分类
中断可以分为中断和异常,异常又可以分为故障、陷阱和中止。
中断:异步事件,通常由 IO 设备触发。
异常:同步事件,是 CPU 在执行指令时检测到的反常条件。
本质:都是让 CPU 收到一个中断号。
具体实现
- 先来讲讲中断具体是怎么实现的:
是由一个叫可编程中断控制器的设备,它有很多 IRQ 引脚线,并接入了一些能发出中断请求的硬件设备,可编程中断控制器设置了 IRQ 和中断号的对应关系。当硬件设备发出中断请求,可编程中断控制器就会找到对应的中断号,并存在自己的一个端口上,然后给 CPU 的 INTR 引脚发送信号。
- 再讲讲异常是怎么实现的:
异常顾名思义,就是 CPU 自己执行指令的时候检测到一些反常情况,然后自己给自己一个中断号。
还有一种方式可以给 CPU 一个中断号,那就是 INT 指令(注:它不是中断!而是越过中断这个方式直接给系统中断号):
就比如最常见的 INT 0x80,就是告诉 CPU 中断号是 0x80,CPU 将其翻译成系统调用。
所以总结一下 CPU 获取中断号的三种方式就是:
- 通过可编程中断控制器给 CPU 的 INTR 引脚发送信号
- CPU 执行过程中自己发现了异常
- 执行 INT 指令
收到中断号后 CPU 的处理
CPU 收到一个中断号n,会去中断描述符表中寻找第 n 个中断描述符,从中断描述符中找到中断处理程序的地址,再跳过去。
补充一下中断描述符中找到中断处理程序的地址的过程,从中断描述符里找到的不直接是程序的地址,而是段选择子和段内偏移地址。段选择子又会去全局描述符表中寻找段描述符,从中取出基地址。最后再利用段基址+段内偏移地址的方式找到真正的地址。
中断描述符表:内存中的数组,一般表都是数组
例如:
1 | struct desc_struct idt_table[256] = { {0, 0}, }; |
中断描述符:中断描述符表这个数组里的存储的数据结构,分为任意门描述符、中断门描述符、陷阱门描述符
例如:
1 | struct desc_struct { |
CPU 怎么找到中断描述符表
中断描述符表想放哪放哪,只需要告诉CPU位置即可。
怎么告诉呢?操作系统的代码可以通过 LIDT 指令,将中断描述符表的起始地址,和中断描述符表的大小存放在 IDTR 寄存器 中。IDTR 寄存器前 16 位存放了表的大小,后 32 位存放的就是 表的起始地址。
是谁把中断描述符表写在内存的?
答:操作系统
例如 Linux-2.6.0 的 traps.c
1 | void __init trap_init(void) { |
CPU 对中断处理程序地址的处理
之前有说,CPU 在接收到中断号的地址后会去找中断处理程序地址,然后跳过去。那么找到地址后跳过去的这个过程具体是怎么实现的呢?
并不是像想象中的,直接把地址取出来然后放入 CS:IP 寄存器中。而是额外做了很多压栈操作:
- 如果发生了特权级转移,压入中断前的 SS 和 ESP,将堆栈切换为 TSS(不懂这个切换)
- 压入EFLAGS(标志寄存器)
- 压入中断前的 CS 和 EIP,
- 如果中断有错误码,压入错误码 ERROR_CODE
- 结束(跳转到中断程序)
压栈的目的就是为了保护现场(原来的程序地址、原来的程序堆栈、原来的标志位)和传递信息(错误码)
整个操作结束后就会变成这样:
压栈之后配合程序中写入的 IRET 或 IRETD 指令返回
以 Linux-0.11 版源码中的 除法异常的中断处理函数 asm.s 为例
1 | _divide_error: |
可以看到最后一行确实用了 iretd 指令
这个指令会依次弹出栈顶的三个元素,把它们分别赋值给 EIP,CS 和 EFLAGS,也恰好符合了之前讲到的栈顶的前三个元素。
补充
中断大体可以分为硬中断和软中断。之前讲的这些其实都是硬中断。
硬中断:并不是指硬件中断,而是指 CPU 这个硬件实现的中断机制(注意不是触发机制)。
软中断:纯粹由软件实现的一种类似中断的机制,实际上就是模仿硬件,在内存中存储着软中断的标志位,然后由内核的一个线程不断轮询这些标志位,哪个有效,就再去别的地方找这个软中断对应的中断处理程序。
软中断要和 INT n 这种软件中断区分开来,软件中断最好称其为,由软件触发的中断,而软中断称其为软件实现的中断。