中断详解(四) ——异常与异常处理

异常

8Ox86微处理器发布了大约20种不同的异常,内核必须为每一种异常提供一个异常处理程序。对于某些异常,CPU控制单元在开始执行异常处理程序前会产生一个硬件出错码(hardwar eerror code) , 并且压入内核态堆钱。(见本文最后面的几张大图)

异常处理

CPU的大部分异常都被Linux解释为出错条件。

当其中一个异常发生肘,内核就向引起异常的进程发送一个信号向色通知一个反常条件.例如,如果进程执行了一个被0除的操作,CPU就产生一个"Devde error" 异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程将采取若干必要的步骤来(从出错中)恢复或者中
止运行(如果没有为这个信号设置处理程序的话)。

但是,在两种情况下,Linux利用CPU异常更有效地管理硬件资源。一种是,"Device not availeble" 异常与crO寄存器的TS标志一起用来把新值装入浮点寄存器。第二种情况是"page Fault"异常,该异常推迟给进程分配新的页框,直到不能再推迟为止.
异常处理程序有一个标准的结构,以下三部分组成:
1.  在内核堆校中保存大多数寄存器的内容(这部分用汇编语言实现)。
2.  用高级的C函数处理异常.
3.  通过ret_
from_exception()函数从异常处理程序退出.
为了利用异常,必须对IDT进行适当的初始化,使得每个被确认的异常都有一个异常处理程序. trap_init ()函数的工作是将一些最终值(即处理异常的函数)插入到IDT的非屏蔽中断及异常表项中.这是由函数set_trap_gate() 、 set_intr…gate() 、set_system_gate()、 set_system_intr_gate()和set_task_gate( )来完成的.

void __init trap_init(void)
{
......
    set_intr_gate(0, &devide_error);
    set_intr_gate_ist(1, &debug, DEBUG_STACK);
    set_intr_gate_ist(2, &nmi, NMI_STACK);
    /* int3 can be called from all */
    set_system_intr_gate_ist(3, &int3, DEBUG_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(14, &page_fault);
    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);
......
}

为异常处理程序保存寄存器的值

让我们用handler_name来表示一个通用的异常处理程序的各字.(所有异常处理程序的实际名字都出现在前一部分的宏列表中。)每一个异常处理程序都以下列的汇编指令开始:
ENTRY(handler_name)
    RING0_INT_FRAME
    pushl $0            # no error code
    CFI_ADJUST_CFA_OFFSET 4  //可能不一定是4这里
    pushl $do_handler_name
    CFI_ADJUST_CFA_OFFSET 4
    jmp error_code
    CFI_ENDPROC
END(handler_name)
如divide_error异常:
ENTRY(divide_error)
    RING0_INT_FRAME
    pushl $0            # no error code
    CFI_ADJUST_CFA_OFFSET 4
    pushl $do_divide_error
    CFI_ADJUST_CFA_OFFSET 4
    jmp error_code
    CFI_ENDPROC
END(divide_error)
当异常发生时,如果控制单元没有自动地把一个硬件出错代码插入到战中,相应的汇编语言片段会包含一条pushl $0指令,在战中垫上一个空值.然后,把高级C函数的地址压进枝中,它的名字由异常处理程序名与do一前强组成. 标号为error_code的汇编语言片段对所有的异常处理程序都是相同的,除了"Oevicenot avaiJabJe" 这一个异常。这段代码执行以下步骤:
1) 把高级C函数可能用到的寄存器保存在战中.
2
产生一条cld指令来清eflags的方向标志DF, 以确保调用字符串指令时会自动增加edi 和esi寄存器的值。
3
把校中位于espi36处的硬件出错码拷贝到edx中,给战中这一位置存上值-1 ,正如我们将在第十一章的"系统 调用的重新执行"一节中所看到的那样,这个值用来把Ox80异常与其他异常隔离开.
4
把保存在战中esp+32位置的do_handler_name() 高级C函数的地址装入edi寄存器中.然后,在栓的这个位置与入es的值。
5
把内核战的当前锚顶拷贝到eax寄存器.这个地址表示内存单元的地址,在这个单元中存放的是第1 步所保存的最后一个客存器的值。
6
把用户数据段的选择符拷贝至IJ ds和es寄存器中。
7
调用地址在edi中的高级C函数.
被调用的函数从eax和edx寄存器而不是从钱中接收参数。我们已经遇见过一个从CPU
寄存器获取参数的函数__switch_to 仆,在第三章"执行进程切换"一节我们讨论过
这个函数.

进入和离开异常处理程序

如前所述,执行异常处理程序的C函数名总是由do_前缀和处理程序名组成.其中的大部分函数把硬件出错码和异常向量保存在当前进程的描述符中,然后向当前进程发送一个适当的信号.用代码描述如下

    current->thread.trap_no = 19; 
    current->thread.error_code = error_code;
    die_if_kernel("cache flush denied", regs, error_code);
异常处理程序刚一终止,当前进程就关注这个信号.i主信号要么在用户态由进程自己的信号处理程序(如果存在的话)来处理.要么由 内核来处理。
如果是由内核处理的话,内核一般的做法是杀掉进程。



中断详解(四) ——异常与异常处理_第1张图片

中断详解(四) ——异常与异常处理_第2张图片


中断详解(四) ——异常与异常处理_第3张图片




你可能感兴趣的:(Linux网络内核协议栈)