中断的硬件背景
-
IOAPIC:所有外设都会连接到IOAPIC引脚上面
- 基本的配置方法
- IOREGSEL: 用来选择配置的功能
- IOWIN: 用来读取更改具体的配置项
- 配置功能选项
- IOAPICID: 唯一标识
- IOAPICVER: 版本识别
- IOAPICARB: 用于发送信息的仲裁,按优先级轮转自动转换
- IOREDTBL: 0~23条中断线的配置
- 中断向量号 bit 0~7
- 传送中断的模式
- 指定目标CPU
- 禁用启用中断线
- 几个状态位
- 中断流程
- 连接IOAPIC引脚的设备触发中断
- 检测Delivery Status是否为0,不是0则等待,否则设其为1 (sent pending)
- 将中断发送至对应的CPU
- CPU响应中断
- 对于level触发的中断,设置IRR为1,并等待CPU触发EOI,再将IRR复位0
- Delivery Status置位0。到此时,方可继续响应此条线上的中断
- 基本的配置方法
-
每个CPU都有Local APIC,接收IOAPIC的数据
- 用来响应本地中断和发送至本CPU的中断,或者用来相应其它CPU发送的中断(IPI)
- CPU里面有Performance Counter,专门用来提供CPU数据的
- 几个状态标识
- IRR: interrupt request register 中断请求
- ISR: in-service register 正在服务的中断
- TMR: trigger mode register 记录中断模式
- 相应中断的流程
- 判断接收到的中断目的是否为本CPU,如果不是则丢弃
- 如果非fixed类型的中断则直接传给CPU
- IRR->ISR->CPU
- CPU触发EOI,Local APIC处理此EOI并将之传给IOAPIC,完成此次中断的处理
-
CPU对中断和异常的处理
-
CPU LINTIN0, LINTIN1:用于CPU本地设备中断,其设备直接连在CPU上,不经过IOAPIC,通常用于单CPU的8259A兼容模式(经常用于watch dog等检测CPU数据的软件)
-
SMI:system management interrupt,用于进入SMI模式
-
MSI:message signalled interrupts,往特定地址写特定格式的消息来产生中断
-
中断/异常分类
- 异常:在指令执行过程中发生的错误,包括单步调试,为同步事件
- 软件中断:由INT指令产生的事件,为同步事件
- 外部中断:由外部设备产生的事件,为异步事件
- 同步事件在可预测的指令流中发生,异步事件不可预测
-
异常的分类
- Fault:可以重试触发异常的指令, 例如 page fault
- Trap: 处理完trap后,直接执行下一条指令, 例如 INT
- Abort: 程序不能可靠地执行下去,例如#MC
-
屏蔽中断/异常
- 处理NMI外的外部中断都是可屏蔽的
- RFLAGS.IF = 0
- 可在特权层有条件地屏蔽掉某些异常
- #AC alignment check
- #NM device-not-available
- #MC machine check
- 处理NMI外的外部中断都是可屏蔽的
-
中断处理流程(以x86-64为例)
- 找到中断处理程序
- 中断描述符表中有中断号和中断处理程序的位置
- 通过中断门会关闭中断,而陷阱门不会
- 切换堆栈
- 如果有特权级转变,新的SS被强制设为NULL
- 如果 IDT-entry.IST!=0, 则以此为序号,到IST表中找到对应的堆栈(无条件切换堆栈)
- 如果 IDT-entry.IST==0,且特权级发生改变,则从当前TSS表中找到新的堆栈(无条件切换堆栈)。只要在特权级发生改变时才会切换堆栈
- 保存环境
- 无条件推入SS和ESP
- $iret
-
Linux Kernel 中断子系统
-
中断/异常初始化
-
trap_init(void)
-
几点需要注意的地方
-
x86_64
全部使用的是中断门(interrupt gate)- 在进入到处理函数后需要快速地打开中断
-
set_intr_gate()
VSset_intr_gate_ist()
- 前者没有使用IST, 后者可以自行指定IST
-
set_intr_gate()
VSset_system_intr_gate()
-
前者将门的权限设为0, 即用户空间不能访问。后者将门的权限设为3,即所有特权层上都可以访问
-
只有在INT指令等触发的软件中断才需要检查,由程序指令执行产生的异常则不需要做此检查,例如除0操作
-
触发INT异常
#include <stdio.h> #include <stdlib.h> #define INTNUM "0x0" static void intfun(void) { asm volatile(".byte 0xcd, "INTNUM); } int main(int argc, char* argv[]) { int i, x; x = 0; i = 3 / x; intfun(); return i; }
运行后,从
dmesg
中能看到general protection
表示权限检查没有通过
-
-
-
-
异常处理
-
所有的异常处理最终都会转到
do_xxx()
中,例如divide_error=>do_divide_error()
zeroentry divide_error do_divide_error /* * Exception entry points. */ .macro zeroentry sym do_sym ENTRY(\sym) INTR_FRAME PARAVIRT_ADJUST_EXCEPTION_FRAME pushq_cfi $-1 /* ORIG_RAX: no syscall to restart */ subq $15*8,%rsp CFI_ADJUST_CFA_OFFSET 15*8 call error_entry DEFAULT_FRAME 0 movq %rsp,%rdi /* pt_regs pointer */ xorl %esi,%esi /* no error code */ call \do_sym jmp error_exit /* %ebx: no swapgs flag */ CFI_ENDPROC END(\sym) .endm
-
do_xxx()
的实现#define DO_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) \ dotraplinkage void do_##name(struct pt_regs *regs, long error_code) \ { \ siginfo_t info; \ info.si_signo = signr; \ info.si_errno = 0; \ info.si_code = sicode; \ info.si_addr = (void __user *)siaddr; \ if (notify_die(DIE_TRAP, str, regs, error_code, trapnr, signr) \ == NOTIFY_STOP) \ return; \ conditional_sti(regs); \ do_trap(trapnr, signr, str, regs, error_code, &info); \ }
-
-
中断初始化
- 在
trap_init()
中可以看到,处理中断使用的向量是从0x20
开始的 - 再将其它的向量号注册为外部中断
- 所以,对于所有外部中断而言,首先会经过
interrupt[]
中对应的函数 interrupt[]
的初始化 (arch/x86/kernel/entry_64.S
)
- 在
-
中断处理
-
向量(vector)与中断号(IRQ)的对应关系:对中断号取反再加
0x80
unsigned vector = ~regs->orig_ax; unsigned irq; irq_enter(); exit_idle(); irq = __this_cpu_read(vector_irq[vector]);
-
最终的处理
do_irq()
vector_irq
,由ioapic
来配置,用来控制中断分发到不同的CPU。
-
设备中断的处理
- 中段描述符表的每一项都指向一个
struct irqaction
链表 - 通常多个设备共享一个中断号
- 发生中断后kernel会检查每个设备的状态寄存器是否产生了中断
- 中段描述符表的每一项都指向一个
-