x86中断机制
打印一个字符,中断似乎和函数类似?
中断的概念和意义
- 概念:正在执行任务时,出现某个请求暂停当前任务,转而处理这个请求(类似函数调用)处理结束后继续任务的执行
- 意义:中断是提高系统整体性能的必要方式
中断与外设
- 中断是一种处理器与外设进行通信的机制
- 用于“通知”处理器外部有“重要事件”发生
- 一般情况下,中断需要被处理器响应
操作系统的本质是中断驱动的死循环!
从摁下开机键一直到任务完成,一直死循环等待中断,OS kernel就用于处理这些中断。
比如摁下‘F’键,操作系统有关于键盘按键的处理方式,操作系统会用相应处理方式处理键盘中断
中断的分类:
- 外部中断:包括不可屏蔽中断(内存读写错误,总线校验错误,电源掉电),可屏蔽中断(外部设备键盘网卡硬盘等等来数据了)。
- 内部中断:包括软中断(程序自身执行的时候发出的中断请求,比如汇编int 10h,看起来像是函数调用)和异常(程序执行的时候有指令错误,比如除法操作中除数是0)
中断处理
中断服务程序(Interrupt Service Routine):对于处理器而言, 处理中断的方式就是执行一段事先写好的代码, 这段代码叫做中断服务程序(由操作系统内核提供)
Linux处理中断的方式
为了缩短处理器对中断的响应时间, 可以把中断分为:
- 中断上半部 (ISR)
中断应答或硬件复位等重要紧迫的工作
实时性要求高,不可被打断 - 中断下半部
相对耗时的数据处理工作, 后续调度执行 (通过软中断来执行中断服务程序或者通过触发新任务的方式)
中断与对应的服务程序间如何建立关联?
在代码层面如何进行转移?
实模式下的中断处理
- 使用中断向量表映射不同中断与中断服务程序
- 中断向量表 (Interrupt Vector Table)(指针数组)
起始于物理地址0, 长度为 1 KB
每个单元4 字节, 连续 256 个单元
每个单元存放一个中断服务程序的入口地址
处理器接收到中断信号时, 能够询问到中断向量(中断类型号,其实就是一个整数下标去访问一维数组);进而,通过中断向量查找 IVT(一维指针数组), 获取 ISR 入口地址(中断服务程序就可以开始执行了); 之后跳转执行(类似于函数调用)
实模式下的中断向量表 (IVT) 和中断服务程序 (ISR) 需要操作系统内核来建立吗?
计算机上电,处理器直接在实模式执行,这个时候硬件需要做一些特殊工作:
将主板ROM中的code(BIOS)拷贝到内存当中,将CS:IP寄存器的值设置为BIOS入口地址
BIOS扫描各个存储介质,会建立中断向量表,将主引导区中的主引导程序载入内存0x7c00并且会交出控制权(jmp 0x7c00)
保护模式下的中断处理
使用中断描述符表(IDT)映射不同中断与中断服务程序
- 中断描述符表(Interrupt Descriptor Table)
- 中断描述符表可以包含中断门, 陷阱门, 任务门(之前说的调用门和这几个没太大关系,只是结构比较相似)
门描述符
包含中断服务程序的入口(选择子 : 偏移)
包含各种用于合法性检查的属性(如: 特权级)
中断描述符
Note:调用门,中断门,陷阱门的字段布局完全相同;当 Type 表示中断门 (1110) 和陷阱门 (1111) 时, Param 字段未使用。
中断描述符表 (IDT)
- 中断描述表是中断描述符的线性集合 (类似 GDT)
- 每个元素的大小为 8 字节64bits(即: 中断描述符)
- 使用前将起始地址及界限载入 IDTR 中 (专用指定: lidt)
1 | [section .idt] |
保护模式下中断处理寻址
- 每个外中断产生一个中断向量 (由硬件发送的中断类型号)
- 通过中断向量在 IDT 中查找对应的中断描述符
- 通过中断描述符中的选择子和偏移可找到ISR的入口地址
注意:
- IDT 除了提供ISR入口地址(偏移地址),还提供了特权级等属性
- 中断发生后,转移执行ISR 代码前需要进行特权级检查
- 实模式与保护模式的中断向量完全一致 (硬件不变)
- IDT 中必须提供每种中断所对应的 ISR 入口地址
中断代理-8259A
不同外设如何向处理器发送中断信号?
当多个外设同时产生中断时,如何进行处理?
想象中的连接方式
处理器有多少INTR引脚?能接入多少外设? 处理器有必要与外设直接相连吗?
超高速的处理器和超低速的外设相连合适吗?肯定是不合适的,因此可以引入一个中断代理
8259A构造
8259A 是处理器的中断功能模块, 用于管理和裁决外部设备的中断请求。
8259A 是专为处理器设计的中断管理芯片
- 可通过编程对 8259A 进行功能配置
- 屏蔽外设中断, 对中断进行优先级判决
- 向处理器提供中断向量
对 8259A 的编程控制是操作系统内核的重要工作
- INT:选出优先级最高的中断请求后, 发信号通知 CPU
- INTA:中断响应信号, 接收来自 CPU 的 INTA 接口的中断响应信号
- PR:优先级仲裁器, 当多个中断同时发生时, 找出优先级最高的中断
- IMR: 中断屏蔽寄存器, 用来屏蔽某个外设的中断。IMR的位与引脚一一对应,被设置为1的位对应的引脚被屏蔽了,被设置为0的位,其对应引脚的中断被放行。
- ISR:中断服务寄存器, 当某个中断正在被处理时, 保存庄此寄存器中。ISR中的位与引脚一一对应,被设置为1的位对应的引脚表示中断正在被处理
- IRR:中断请求寄存器, 用来接受经过 IMR 寄存器过滤后的中断信号并锁存,此寄存器中全是等待处理的中断。IRR中的位与引脚一一对应,被设置为1的位对应的引脚有中断请求,类似于一个请求队列
中断触发方式
边沿触发(推荐):中断引脚电平变化的一瞬间认为中断申请到来(上升沿触发)
电平触发:中断引脚上的信号保持稳定电平一定时间后认为中断申请到来
8259A工作方式
数据连接方式
- 非缓冲方式:将8259A 直接与数据总线相连
- 缓冲方式:将8259A 通过总线驱动器和数据总线相连
中断优先的方式
- 固定优先级方式:优先级由高到低的顺序是: IRO,IR1,IR2, …, IR7(存在饥饿现象)
- 自动循环方式:某一中断请求被响应后, 该中断原优先级自动成为最低
- 特殊循环方式:
通过编程指定某中断源优先级成为最低
其它中断源优先级自动改变
中断嵌套方式
- 完全嵌套方式(默认方式):执行中断服务程序期间, 不响应本级中断和较低级中断
- 特殊完全嵌套方式:执行中断服务程序期间, 可响应本级中断, 不响应较低级中断(在多片级联的情况下,当某从片的中断得到响应、进入中断服务期间,来自该从片的更高级的中断请求仍能为主8259A所识别(对主8259A来说,同一从8259A的8个中断都是一个级别),并向CPU提出请求。)
中断屏蔽方式
- 普通屏蔽方式: IMR中的某一位或几位置为 1, 屏蔽掉相应级别的中断请求
- 特殊屏蔽方式:未被屏蔽的中断源均可在某个中断服务程序中被响应 即低优先级中断可以打断正在服务的高优先级中断
中断结束方式
- 自动结束方式(只适用节非多重中断情况,任意中断之间都能相互打断):8259A自动清除 ISR中已置位的优先级最高的位
- 手动结束方式(在中断服务程序里面像8259A发EOI命令,这样不存在低优先级中断打断高优先级中断):在中断服务程序的最后,向8259A 发中断结束命令,将 ISR中相应的位清除,表明中断服务程序已完成
8259A控制编程
一般而言,x86系统中使用2个8259A级联作为中断代理
初始化命令字 (Initialization Command Word)
用于确定是否需要级联, 设置起始中断向量, 等
- ICW1: 初始化 8259A连接方式和中断触发方式 (如:设置主从级联)
- ICW2: 设置起始中断向量(给外部设备编号,IRQ0对应的中断向量)
- ICW3: 指定主从 8259A 的级联引脚(如:从片连接到主片 IRQ2)
- ICW4: 设置 8259A 的工作模式 (中断嵌套方式)
ICW1: 初始化 8259A 连接方式和中断触发方式
第2,5,6,7位固定为0,第4位标记为1这是ICW1的固定标记
ICW1需要写入主片的0x20端口和从片的0xA0端口
ICW2: 设置起始中断向量(IR0 对应的中断向量)
0-19(0x0-0x13)非屏蔽中断和异常,计:20个,Intel保留,不分配IRQ
必须设置高5位的值为中断向量,低三位固定为0
ICW2需要写入主片的0x21端口和从片的0xA1端口
ICW3: 指定主从 8259A 的级联引脚
ICW3需要写入主片的0x21端口和从片的0xA1端口
ICW4: 初始化 8259A 数据连接方式和中断触发方式
ICW4需要写入主片的0x21端口和从片的0xA1端口
实战初始化代码
1 | Init8259A: |
操作命令字
用于设置中断优先级方式,中断结束模式,等
- OCW1: 屏蔽连接在 8259A 上的中断源
- OCW2: 设置中断结束方式和优先级模式
- OCW3: 设置特殊屏蔽方式
OCW1 命令字最终写入 IMR 寄存器
- IMR 寄存器为初级中断屏蔽寄存器 (分开关)
- 如果标志寄存器中的 IF 位为 0, 则屏麵有外部中断(总开关)
注: OCW1 需要写入主片的 0x21 端口和从片的 0xA1 端口
OCW2: 设置中断结束方式和优先级模式
OCW3: 设置特殊屏蔽方式及查询方式
注: OCW3 需要写入主片的 0x20 端口和从片的 OxA0 端口
实战代码
1 | ;-------------------------- |
中断编程实践
- 预备工作:8259A 初始化, 读写 IMR 寄存器, 发送 EOI 控制字, 等
- 实践一:自定义软中断的实现(内部中断处理)
- 实践二:时钟中断的响应及处理 (外部中断处理)
8359A初始化
将控制字发送到主从片的那些端口提前预定义好
读写中断屏蔽寄存器IMR的值
需要借助OCW1操作命令字(设置IMR的值对应的位设置为1即可),写入对应端口0x21(主片端口)或者0xA1(从片端口)
1 | ; al --> IMR register value |
主函数数中设置全1没有任何屏蔽的中断
1 | mov ax, 0xFF |
汇编指令rep
- 汇编语言中支持预处理语句 (如: %include)
- 与 C 语言中的情况类似, 汇编预处理语句常用于文本替换
- 示例: 语句重复 (%rep)
1 | Delay: |
x86 处理器一共支持 256 个中断类型, 因此中断描述符表中需要有 256 个描述符与之对应。
1 | [section .idt] |
Init8259A
1 | Init8259A: |
自定义保护模式软中断
实现还是很简单的,只需要调用已有函数PrintString,注意返回使用iret
1 | DefaultHandleFunc: |
主函数中
1 | mov ebp, INT_80H_OFFSET ; 目标字符串 |
处理外部时钟中断
外部时钟中断无需过于频繁,一般取20mS(50Hz)即可。
由于 8259A 初始化为手动结束中断的方式,因此,外部中断服务程序中需要手动发送结束控制字。
这里借助OCW2操作命令,OCW2: 手动清除 ISR中优先级最高的位, 各引脚优先级固定。
先前设置了IR0中断向量为0x20,而时钟中断会向IRQ0设置发送信号,因此这里向IRQ0发送信号,需要在IDT中注册20号中断描述符
1 | ; dx --> 8259A port |
打开时钟中断(IRQ0),外部可屏蔽中断的发生受到两个因素的影响,只有当IF位为1,并且IMR相应位为0时才会发生。那么,如果我们想打开时钟中断的话,一方面不仅要设计一个中断处理程序,另一方面还要设置IMR,并且设置IF位。
主函数中
1 | sti ; 打开外部中断总开关 |
中断处理函数:
1 | TimerHandlerFunc: |
0~9一直变化闪烁,时钟中断还是成功了。
注意ICW4设置mov al, 00010001B
特殊完全中断,意味着会去响应同级中断请求。
假设: 时钟中断请求周期为 5ms , 对应的中断服务程序执行时间为 10ms; 那么, 中断服务程序是否会被新的时钟中断请求打断?
- 中断优先级由 8259A 管理 (高优先级中断请求优先送往处理器),新的时钟中断请求当然也会送给处理器,但处理器不一定处理
- 处理器决定是否响应中断请求,依据于总开关是否打开 (处理器没有中断优先级的概念)
- 在默认情况下,中断服务程序执行时,即便来了新的外部中断,处理器会屏蔽外部中断请求 (IF == 0),直到中断服务程序返回结束后,重新响应外部中断 (总开关打开即IF == 1)
因此,如果希望高优先级中断请求打断当前中断服务程序, 可以在中断服务程序中打开 IF , 即: 将 IF 设置为 1 ( sti )。
中断处理与特权级处理
中断特权级转移过程
- 处理器通过中断向量找到对应的中断描述符
- 特权级检查:
软中断:(目标代码段 DPL <= CPL)&&(CPL <= 中断描述符 DPL 即当前特权级>=中断描述符特权级),中断门类似于“蹦床”
外部中断: CPL >=目标代码段 DPL - 加载目标代码段选择子到 cs,加载偏移地址到ip
中断中栈变化
压栈
- 首先用户态执行操作,影响的仅仅是用户态下相应的栈Stack_DPL3
- 然后
int 0x80
会调用中断,先去查找是否注册了TSS全局段描述符,如果注册了从TSS中取出esp和ss0的值(内核栈信息),sp会转移到高特权级的栈Stack_DPL0,同时Stack_DPL0(注意是内核栈)压入ss,esp,eflag,cs,ip等寄存器用于执行完中断服务程序后iret返回 - 然后cs和ip寄存器修改为中断服务程序对应的段和段内偏移地址,就可以开始执行中断服务程序直到遇到iret返回
中断服务程序返回iret
- iret 使得处理器从内核态返回用户态
- 返回时进行特权级检查
• CPL<=
目标代码段 DPL (高特权级–>低特权级)
• 对相关段寄存器强制清零 (指向高特权级数据的段寄存器)
栈恢复
恢复之后,ss和esp指向原先的栈,eflags标志寄存器恢复,eflags寄存器是个标志寄存器,标志寄存器每一位都是一个状态标志位。
所谓的中断的上下文保存下来
中的上下文
就是将这些寄存器ss,esp,eflags,cs,ip
压入栈中保存下来
eflags标志寄存器
- IF: 系统标志位, 决定是否响应外部中断
IF==
1,响应外部中断
IF==
0,屏蔽外部中断 - IOPL: 系统标志位, 决定是否允许进行I/O操作
CPL<=
IOPL 才能允许访问I/O 端口
当且仅当 CPL==
0 时才能改变 IOPL的值
x86汇编语句并没有提供直接修改eflags寄存器的值的直接语句。只能借助pushf,popf指令间接改变。
1 | pushf ;eflags值压入栈中 |
使用软中断实现系统调用
- 定义 32 位核心代码段(包括一些初始化操作中断函数, 系统函数)
- 定义 32 位用户代码段和数据段(用户程序)
- 通过软中断 (int 0x80) 转移到内核态调用系统函数(低—>高)
- 在任务代码段使用软中断 (int 0x80) 实现功能函数
注意事项:
- 将 IOPL 设置为 3 使得用户态和内核态均可访问10 端口
- 特权级转移时会发生栈的变换(定义 TSS 结构, 定义不同栈段)
- 在用户态通过 sti 指令打开总开关使得处理器响应外部中断 (必须用户态)
0x80中断自行设计(系统调用设计)一个中断门调用多个系统函数
- ax == 0: 外部设备中断初始化 (InitDevInt)
- ax == 1: 字符串打印 (Printf)
- ax == 2: 启动时钟中断 (EnableTimer)
1 | %include "inc.asm" |