中断和异常的区别:

  • 同步中断(称为异常),由CPU控制单元产生,只有在执行某一条指令后才会触发异常。
  • 异步中断,由外部硬件产生,如硬盘、网络等。

产生异常的原因以下两种:

  • 程序错误,这种情况下内核通常发送信号进程。
  • 需要内核处理的异常条件,如缺页、系统调用(intsysenter指令)。

中断信号的作用

中断和异常

中断(interrupt):

  • 可屏蔽中断(maskable interrupt),控制单元可以忽略中断,I/O设备产生的中断请求都可屏蔽。
  • 不可屏蔽中断(nonmaskable interrupt, NMI),危急事件(例如硬件故障)引起的中断。

异常(expception):

  • 处理器探测异常(processor-detected exception),CPU执行指令时检测到的异常。
    • 故障(fault),可以纠正,纠正后重新执行指令。例如page fault。
    • 陷阱(trap),主要为了调试程序,当内核把控制权返回给应用程序后,程序继续执行下一条指令。
    • 终止(abort),严重错误,例如控制单元故障,强制终止程序。
  • 编程异常(programmed exception),在应用程序发出请求时发生,由intint3指令触发。或者由into指令检查溢出或bound指令检查边界触发。控制单元把programmed exception作为trap处理,也叫做软件中断(software interrupt)。

IRQ和中断

每个能够发出的中断请求的硬件设备控制器都有一条名为IRQ(interrupt request)的输出线,与可编程中断控制器(programmable interrupt controller,PIC,现在已经被淘汰)的输入引脚相连。

IRQ线从0开始编号,例如第一条线表示为IRQ0,默认关联到中断向量的第n+32项。

可以有选择地禁止或激活每条IRQ线,禁止的中断不会丢失

有选择的激活、禁止IRQ线不同于全局屏蔽、非屏蔽中断,当eflags寄存器的IF标志清0时,所有的中断都会被屏蔽,当eflags寄存器的IF标志清1时,所有的中断都会被激活。用cli清除标志,sti设置标志。

高级可编程中断控制器 APIC

为了支持SMP体系结构,每个CPU有一个本地APIC,所有本地APIC都连接到一个外部的I/O APIC,形成一个多APIC系统。支持中断分发处理器间中断(interprocessor interrupt)

详细介绍可以看CSDN这篇文章知乎这篇文章

异常

中断向量的0~31项预留给了处理器硬件异常,已经定义的有大约20项。

IVT Offset INT # Description 信号
0x0000 0x00 (0) Divide by 0 SIGFPE
0x0004 0x01 (1) Reserved SIGTRAP
0x0008 0x02 (2) NMI Interrupt
0x000C 0x03 (3) Breakpoint (INT3) SIGTRAP
0x0010 0x04 (4) Overflow (INTO) SIGSEGV
0x0014 0x05 (5) Bounds range exceeded (BOUND) SIGSEGV
0x0018 0x06 (6) Invalid opcode (UD2) SIGILL
0x001C 0x07 (7) Device not available (WAIT/FWAIT)
0x0020 0x08 (8) Double fault
0x0024 0x09 (9) Coprocessor segment overrun SIGFPE
0x0028 0x0A (10) Invalid TSS SIGSEGV
0x002C 0x0B (11) Segment not present SIGBUS
0x0030 0x0C (12) Stack-segment fault SIGBUS
0x0034 0x0D (13) General protection fault SIGSEGV
0x0038 0x0E (14) Page fault SIGSEGV
0x003C 0x0F (15) Reserved
0x0040 0x10 (16) x87 FPU error SIGFPE
0x0044 0x11 (17) Alignment check SIGSEGV
0x0048 0x12 (18) Machine check
0x004C 0x13 (19) SIMD Floating-Point Exception SIGFPE
0x00xx 0x14-0x1F (20-31) Reserved
0x0xxx 0x20-0xFF (32-255) User definable
0x0080 0x80 System call

完整的定义可以看下面的trap_init函数。

中断描述符表 Interrupt descriptor table IDT

  • IDT由256项组成,其中每项都是一个中断描述符,8字节,总共 8*256=2048 字节。
  • IDT可以位于内存中的任何地方。
  • IDT的基址和最大长度存在idtr寄存器中。
  • 在允许中断前,必须用lidt指令初始化idtr

idt_table定义如下:

// include/asm/processor.h
struct desc_struct {
	unsigned long a,b;
};

// arch/i386/kernel/traps.c
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };  // 长度为256的IDT表

中断描述符定义如下图所示:

  • 每个描述符占8字节,64位。
  • 40~43位表示描述符gate的类型。
  • 2位DPL表示特权级,通常都为0,表示内核态。

interrupt descriptor

Intel提供3种类型的中断描述符,分为3种gate:

  • task gate:当中断发生时,必须取代当前进程的那个进程的TSS segment selector放在task gate中。这个gate用来处理Double Fault异常。
  • interrupt gate:包含16位segment selector和32位offset,当控制权转移到一个合适的段时,处理器清IF标志关中断。这个gate用来处理中断。
  • trap gate:和interrupt gate类似,区别是不修改IF标志。这个gate用来处理异常。

中断和异常处理程序的嵌套执行

2种处理程序:

  • 异常处理程序
  • 中断处理程序

nested interrupt

  • 每个中断或异常都会引起一个内核控制路径(kernel control flow),内核控制路径代表当前进程在内核态执行单独的指令序列。至于为什么中断一定要在内核态处理(interrupt gate 中断门),可能是出于安全的考量,可以看知乎上的这篇回答,理论上可以将中断描述符的DPL设置成3用户态,但是没有操作系统这么做。
  • 内核控制路径可以任意嵌套,一个中断处理程序可以被另一个中断处理程序打断。所以当嵌套层数>1时,中断处理程序的最后一部分指令不能使当前进程返回用户态。
  • 允许内核控制路径嵌套的代价,中断处理程序必须永不阻塞(不能发生进程切换)。
  • 大多数异常只在用户态发生,异常要么由编程错误引起,要么由调试程序触发。
  • 内核态能触发的唯一异常是缺页异常page fault。缺页异常处理程序挂起当前进程,从不进一步引发异常。,所以和异常相关的内核处理路径最多会有两个叠在一起(系统调用和缺页异常)。
  • 中断处理程序和当前运行的进程无关。
  • 中断处理程序可以抢占其他中断处理程序,也可以抢占异常处理程序。异常处理程序不能抢占中断处理程序。
  • 中断处理程序不能执行导致缺页的操作。
  • 多处理器系统上,多个内核控制路径可以并发执行。

初始化IDT

Linux将异常和中断分为5种gate,不同于Intel的3种:

  • interrupt gate,中断门,Linux系统所有中断都通过中断门来处理,限制在内核态,DPL字段为0。
void set_intr_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,14,0,addr,__KERNEL_CS);  // 0表示内核态
}
// 例子
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
  • system interrupt gate,系统中断门,用户态进程可以访问的Intel中断门,,DPL字段为3。用于注册中断向量3的处理函数int3,由汇编指令int3触发。
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n, 14, 3, addr, __KERNEL_CS);  // 3表示用户态
}
// 例子
set_system_intr_gate(3, &int3);
  • system gate,系统门,用户态可以访问的陷阱门,DPL字段为3。用来注册中断向量4(into指令)、5(bound指令)、128(int $0x80指令,系统调用)的处理函数。
static void __init set_system_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,15,3,addr,__KERNEL_CS);
}
// 例子
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_system_gate(SYSCALL_VECTOR,&system_call);
  • trap gate,陷阱门,只有内核态可以访问,DPL字段为0。大部分的Linux异常都注册位陷阱门。
static void __init set_trap_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,15,0,addr,__KERNEL_CS);
}
// 例子
set_trap_gate(0,&divide_error);
  • task gate,任务门,只有内核态可以访问,,DPL字段为0。用来注册Double fault异常的处理函数。
static void __init set_task_gate(unsigned int n, unsigned int gdt_entry)
{
	_set_gate(idt_table+n,5,0,0,(gdt_entry<<3));
}
// 例子
set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
gate DPL 例子
interrupt gate 0 所有中断处理程序,set_intr_gate(2,&nmi);
system interrupt gate 3 set_system_intr_gate(3, &int3);
system gate 3 set_system_gate(SYSCALL_VECTOR,&system_call);
trap gate 0 set_trap_gate(0,&divide_error);
task gate 0 set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);

异常处理

中断处理

中断向量

中断和异常由8 bit整数表示,这个整数称为中断向量,范围0~255。

trap_init函数:

// arch/i386/kernel/traps.c
void __init trap_init(void)
{
	set_trap_gate(0,&divide_error);
	set_intr_gate(1,&debug);
	set_intr_gate(2,&nmi);
	set_system_intr_gate(3, &int3); /* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
	set_trap_gate(9,&coprocessor_segment_overrun);
	set_trap_gate(10,&invalid_TSS);
	set_trap_gate(11,&segment_not_present);
	set_trap_gate(12,&stack_segment);
	set_trap_gate(13,&general_protection);
	set_intr_gate(14,&page_fault);
	set_trap_gate(15,&spurious_interrupt_bug);
	set_trap_gate(16,&coprocessor_error);
	set_trap_gate(17,&alignment_check);
	set_trap_gate(18,&machine_check);
	set_trap_gate(19,&simd_coprocessor_error);
	set_system_gate(SYSCALL_VECTOR,&system_call);

	cpu_init();
	trap_init_hook();
}

参考: