第五讲 中断、异常和信号

本文详细介绍了Linux内核对中断、异常和信号的处理机制。内容涵盖x86架构下的中断硬件支持,中断向量、异常处理程序的进入和返回,中断和异常的嵌套处理,IDT的初始化,I/O中断处理,中断向量、IRQ数据结构,以及中断在多处理器系统上的分发。此外,还讨论了软中断、tasklet、工作队列的概念和实现,以及信号的产生、传递和处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在第一讲中提到过异常的分类,根据同步或异步产生、无意或故意产生以及最终的的返回行为可以分为四类。但不管是哪种,CPU的响应过程基本一致。即CPU根据中断向量,在内存中找到相应的服务程序入口并调用该服务程序。中断向量和中断处理函数内内核定义。处理方法也不尽相同。

对于异常,一般是抛出一个信号给当前进程,因此在本讲的最后将涉及信号相关的内容。

x86对中断的硬件支持

中断和异常向量

x86中,每个中断和异常由0~255之间的一个数来标识,称为中断向量。非屏蔽中断和异常的向量是固定的,范围在0~31之间,而可屏蔽中断的向量可以通过对中断控制器的编程来改变,范围在32~255间。

在单处理器中,可编程中断控制器PIC来负责监视所有硬件并向CPU产生中断。在多处理器上,引入I/O高级可编程控制器(I/O APIC)的组件,形成一个多APIC的系统来将中断分发给所有CPU。这个分发过程既可以是静态的,即传递给指定的某个、某些或所有CPU;也可以是动态的,即分发给一个当前负荷最低的CPU上或通过仲裁确定分发到哪个CPU上。另外多APIC系统还可以在处理器间产生中断。
CPU中断

x86处理器有约20种不同的异常。内核必须为每种异常提供一个专门的异常处理程序。下面是一些重要的异常及向量、名称、类型和描述。
0 - “Divide error”故障:整数除0操作时产生。
1 - “Debug”陷阱或故障:调试程序时使用。
2 - 未用。
3 - “Breakpoint”陷阱:由int 3指令引起,通常由debugger程序插入。
4 - “Overflow”陷阱:计算溢出。
5 - “Bounds check”故障:地址边界有误。
6 - “Invalid opcode”:无效操作码。
7 - “Device not available”故障:
8 - “Double Fault”异常中止:少数情况下,异常处理过程中又出现异常。
10 - “Invalid TSS”故障:CPU试图让一个CPU切换到有无效TSS的进程。
13 - “General Protection”故障:违反了x86保护模式下的保护规则。
14 - “Page Fault”故障:寻址的页不在内存,或违反了一种页保护机制。
15 - 由Intel保留
16 - “Floating Point Error”故障:浮点单元出错。
17 - “Alignment Check”故障:操作数的地址没有正确对齐。
18 - “Machine Check”异常中止:机器检测到一个CPU错误或总线错误。
19 - “SIMD Floating Point Exception”故障:SSE或SSE2单元浮点操作异常。
20~31由Intel留作将来使用。

进入中断服务程序

我们接下来着重讨论CPU在得到中断向量后如何进入相应中断服务程序。
最早在实模式中,CPU把内存从0开始的1KB字节作为一个中断向量表,每个表项由两字节的段地址和两字节的位移组成一个四字节地址,即中断服务程序的入口地址。这与ARM等CPU处理方式一致。但是这个机制中并没有提供运行模式的转换,即ARM中PSW寄存器的功能。

后来Intel在实现保护模式时,对中断响应机制作了大幅修改。中断向量表变为中断描述符表IDT,其中的表项也从单纯的入口地址改成了一个描述符表项,称为“门”。门中附带一个段选择符,再按照段式寻址的方法找到中断服务程序的入口。
中断门
门的引入在增加了一个寻址步骤的同时实现了CPU运行优先级的切换。只要想切换CPU的运行状态,就必须通过一道门。按不同用途和目的,有任务门、中断门、陷阱门和调用门四种。
任务门描述符
其他门

除任务门外,其余三种门的结构基本相同。区别是类型码不同。中断门和陷阱门在使用上的区别只在于进入中断服务程序时CPU是否自动关中断。

门中的段选择码字段就是段选择符,与位移字段一起决定了一个地址。可以参考第一讲中段式寻址的内容。
在Linux内核中,实际上只使用GDT。对于中断门、陷阱门和调用门来说,段描述表中的相应表项显然应该是一个代码段描述项,而任务门指向的描述项则是专门为TSS而设置的TSS描述项。

每个段描述符中都有一个两位的DPL位段,当CPU通过中断门找到一个代码段描述符,从而转入相应的服务程序时,就把这个代码段描述符装入CPU中,而描述项的DPL就变成CPU的当前运行级别,称为CPL。中断门中还有额外一个DPL字段,这种重复是由于x86出奇复杂的优先级别检验机制造成的。这里不详述,最终导致通过中断门时只允许保持或提升CPU的运行级别,而不允许降低其级别。如果出现错误,会产生一次全面保护异常。

进入中断服务程序时,CPU要将当前EFLAGS寄存器的内容以及返回地址压入堆栈,返回地址是由段寄存器CS的内容和取指令指针EIP的内容共同组成的。如果中断是由异常引起的,则还要将一个表示异常原因的出错代码也压入堆栈。进一步,如果中断前后运行级别不同,就要更换堆栈。
具体到Linux内核。当中断发生在用户态,中断后进入内核态,因此要引起堆栈的更换,即从用户栈更换到系统堆栈。而当中断发生在系统状态时,则不需要更换堆栈。

最后,在保护模式中,中断向量表在内存中的位置不再限于从地址0开始,而是像GDT和LDT一样可以放在内存的任意地方,由新增的寄存器IDTR指示。x86的256个中断向量一共需要占用2KB空间来存放IDT。Linux利用中断门处理中断,用陷阱门处理异常,因此IDT内存放的主要是中断门或陷阱门,还有特殊异常是任务门。

当中断发生后,根据上图中的方式,最终寻址到中断服务程序开始执行。当执行完毕退出时,需要产生一条IRET指令,使CPU进行一些硬件检查,具体的返回策略在后面从中断和异常返回中描述。

中断和异常的嵌套

每个中断或异常都会引起一个内核控制路径,即代表当前进程在内核态执行。
内核控制路径可以任意嵌套,即一个中断处理程序可以被另一个中断处理程序“中断”,如下图所示。
内核控制路径的嵌套
中断处理程序必须永不阻塞,即中断处理程序运行期间不能发生进程切换。整个过程中所需要的数据都存放在进程的内核态堆栈中。

中断处理程序可以抢占其他中断处理程序或异常处理程序,但异常处理程序从不抢占中断处理程序。在内核态唯一会触发的异常是缺页异常,其他所有异常都只发生在用户态。中断处理程序从不执行可以导致缺页的操作,因为这样的操作有可能引起进程切换。

初始化IDT

从上面可见,在允许中断前,内核必须适当初始化IDT。

当计算机还在实模式时,IDT被初始化并由BIOS例程使用。然而当Linux接管,IDT就被移到RAM的另一个区域,并进行第二次初始化。
IDT存放在idt_table表中,共有256个表项。6字节的idt_descr变量指定了IDT的大小和它的地址,内核用lidt汇编指令使用idt_descr变量初始化IDTR寄存器。
在内核初始化过程中,setup_idt()汇编语言函数用同一个中断门来填充所有表项,该中断门指向ignore_int()处理程序。这个程序什么都不做,也不应该被执行,否则就意味着出现了硬件或内核的问题。
然后IDT被第二次初始化,用有意义的陷阱和中断处理程序替换这个空处理程序。这个过程在start_kernel()中调用trap_init()init_IRQ()完成。

==================== arch/x86/kernel/traps.c 827 884 ====================
void __init trap_init(void)
{
    int i;

#ifdef CONFIG_EISA
    void __iomem *p = early_ioremap(0x0FFFD9, 4);

    if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
        EISA_bus = 1;
    early_iounmap(p, 4);
#endif

    set_intr_gate(0, &divide_error);
    set_intr_gate_ist(2, &nmi, NMI_STACK);
    /* int4 can be called from all */
    set_system_intr_gate(4, &overflow);
    set_intr_gate(5, &bounds);
    set_intr_gate(6, &invalid_op);
    set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32
    set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
    set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
    set_intr_gate(9, &coprocessor_segment_overrun);
    set_intr_gate(10, &invalid_TSS);
    set_intr_gate(11, &segment_not_present);
    set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);
    set_intr_gate(13, &general_protection);
    set_intr_gate(15, &spurious_interrupt_bug);
    set_intr_gate(16, &coprocessor_error);
    set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCE
    set_intr_gate_ist(18, &machine_check, MCE_STACK);
#endif
    set_intr_gate(19, &simd_coprocessor_error);

    /* Reserve all the builtin and the syscall vector: */
    for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
        set_bit(i, used_vectors);

#ifdef CONFIG_IA32_EMULATION
    set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
    set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif

#ifdef CONFIG_X86_32
    set_system_trap_gate(SYSCALL_VECTOR, &system_call);
    set_bit(SYSCALL_VECTOR, used_vectors);
#endif

    /*
     * Should be a barrier for any external CPU state:
     */
    cpu_init();

    x86_init.irqs.trap_init();
}

忽略一开始的EISA设置,程序先设置中断向量表开头的19个陷阱门,我们注意到其中漏掉几个,这在之前其他函数如early_trap_init()中已经设置完毕。然后是对系统调用向量的初始化,常数SYSCALL_VECTOR定义为0x80。
这里一共使用了五个函数。

=========== arch/x86/include/asm/desc.h 330 334 =============
static inline void set_intr_gate(unsigned int n, void *addr)
{
    BUG_ON((unsigned)n > 0xFF);
    _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
}

它插入一个中断门,段选择符指向内核代码段,DPL为0。
set_intr_gate_ist()在32位系统上与set_intr_gate()相同。

=============== arch/x86/include/asm/desc.h 359 363 ===============
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
    BUG_ON((unsigned)n > 0xFF);
    _set_gate(n, GATE_INTERRUPT, addr, 0x3, 0, __KERNEL_CS);
}

它插入一个中断门,段选择符指向内核代码段,DPL为3。

================= arch/x86/include/asm/desc.h 377 381 =================
static inline void set_task_gate(unsigned int n, unsigned 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值