内核代码阅读(14) - 中断

今天周日天朗气清惠风和畅。开始进入内核中断代码的学习。

中断,异常和系统调用

3种机制:

1) 中断(interrupt)

中断向量: 外部硬件或者软件设置

2) 陷阱 (trap)

中断向量: 由汇编 int n, n是中断向量

3) 异常 (exception)

中断向量: 是在CPU内部硬件

X86 CPU中断的硬件支持

x86保护模式的中断机制

1) 中断服务程序的入口变成一个描述项 - gate。
2) 发生CPU状态的切换都要经过一道门。
3) 4种门
   task gate
   interrupt gate
   trap gate
   call gates
4) 允许在中断发生后进行一次进程切换,这样提供了一种机制:所有的中断都可以交给一个进程来处理。

中断堆栈切换

运行级别不一致时,会发生堆栈的切换:
1) 当CPU在用户态时,发生中断,则处理中断的代码会在一个新的栈上。
2) 当CPU在内核态时,发生中断,则处理中断的代码会在原来的栈上。

IDTR 中断向量寄存器

X86 CPU中断的硬件支持

x86保护模式的中断机制

1) 中断服务程序的入口变成一个描述项 - gate。
2) 发生CPU状态的切换都要经过一道门。
3) 4种门
   task gate
   interrupt gate
   trap gate
   call gates
4) 允许在中断发生后进行一次进程切换,这样提供了一种机制:所有的中断都可以交给一个进程来处理。

中断堆栈切换

运行级别不一致时,会发生堆栈的切换:
1) 当CPU在用户态时,发生中断,则处理中断的代码会在一个新的栈上。
2) 当CPU在内核态时,发生中断,则处理中断的代码会在原来的栈上。

IDTR 中断向量寄存器

IDTR + v(中断向量号) -> 中断门
GDTR + 中断门(段选择码和偏移) -> 中断服务程序的入口

中断向量表IDT的初始化

内核在初始化阶段,完成对页式内存管理的初始化后,开始初始化中断trap_init和init_IRQ。

trap_init - CPU专用和系统调用的陷阱门设置

trap_init - 初始化系统保留的中断

void __init trap_init(void)
     {
         set_trap_gate(0,&divide_error);
         set_trap_gate(1,&debug);
         set_intr_gate(2,&nmi);
         set_system_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_trap_gate(8,&double_fault);
         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_trap_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);
         set_call_gate(&default_ldt[0],lcall7);
         set_call_gate(&default_ldt[4],lcall27);
         /*
          * Should be a barrier for any external CPU state.
          */
         cpu_init();
     }
1) 0号中断就是除以0。
 2) 14号中断就是 page_fault。
           每当系统在处理页式映射进行不下去了就会产生 0xe 中断, 系统必须在0xe中断上进行缺页服务.
 3) set_system_gate(SYSCALL_VECTOR,&system_call);
           #define SYSCALL_VECTOR                0x80
           0x80 系统调用中断.
 4) set_trap_gate(unsigned int n, void *addr) { set_gate(idt_table+n,15,0,addr); }
trap_init 之 中断类型和权限DPL
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };
     
     void set_intr_gate(unsigned int n, void *addr)
     {
         _set_gate(idt_table+n,14,0,addr);
     }
     static void __init set_trap_gate(unsigned int n, void *addr)
     {
         _set_gate(idt_table+n,15,0,addr);
     }    
     static void __init set_system_gate(unsigned int n, void *addr)
     {
         _set_gate(idt_table+n,15,3,addr);
     }
1) idt_table 是一个全局的数组.
 2) 可以看到3种中断的初始化都是调用 _set_gate函数.
 3) _set_gate(idt_table+n,15,0,addr);
           第一个参数是中断类型, 15是trap, 14是中断.
           第二个参数是权限, 0是最高的内核态, 3是用户态.
 4) system_gate 的类型是trap, 权限是3, 允许用户态调用INT n进入.
 5) 陷阱门和中断门除了在DPL上不同外,还有就是trap是可中断的, 中断是不能中断的(进入中断服务子程序后会屏蔽中断)。
trap_init 之 _set_gate
#define _set_gate(gate_addr,type,dpl,addr) \
     do { \
       int __d0, __d1; \
       __asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
         "movw %4,%%dx\n\t" \
         "movl %%eax,%0\n\t" \
         "movl %%edx,%1" \
         :"=m" (*((long *) (gate_addr))), \
          "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
         :"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
          "3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
     } while (0)
1) 对 idt_table 的设置采用汇编完成.
 2) 输出部:
           %0 -> gate_addr,
           %1 -> gate_addr+1,
           %2 -> __d0 (%eax)
           %3 -> __dl (%edx)
 3) 输入部:
           %4 -> ((short) (0x8000+(dpl<<13)+(type<<8)))
           "3" ((char *) (addr))  参数addr和%3(edx)绑定
           "2" (__KERNEL_CS << 16)) 参数_KERNEL_CS放入 %2(%eax)的高16位.
 4) 其中 _KERNEL_CS
           #define __KERNEL_CS        0x10

init_IRQ 外设的中断设置

void __init init_IRQ(void)
    {
        int i;
        for (i = 0; i < NR_IRQS; i++) {
                int vector = FIRST_EXTERNAL_VECTOR + i;
                if (vector != SYSCALL_VECTOR) 
                        set_intr_gate(vector, interrupt[i]);
        }
        set_intr_gate(FIRST_DEVICE_VECTOR, interrupt[0]);
        set_intr_gate(RESCHEDULE_VECTOR, reschedule_interrupt);
        set_intr_gate(INVALIDATE_TLB_VECTOR, invalidate_interrupt);
        set_intr_gate(CALL_FUNCTION_VECTOR, call_function_interrupt);
        
        outb_p(0x34,0x43);                /* binary, mode 2, LSB/MSB, ch 0 */
        outb_p(LATCH & 0xff , 0x40);        /* LSB */
        outb(LATCH >> 8 , 0x40);        /* MSB */
    }
1) Linux中提供不同的中断源共用中断向量的机制。
2) 每个共同的中断向量都对应一个数组 irq_desc。
3) 中断产生时,使用具体设备号到中断向量对应的irq_desc数组中遍历查找。
4) #define FIRST_EXTERNAL_VECTOR        0x20
   #define SYSCALL_VECTOR                0x80
5) for (i = 0; i < NR_IRQS; i++)
   遍历所有的外部中断向量NR_IRQS,依次设置外部中断的服务程序。
6) int vector = FIRST_EXTERNAL_VECTOR + i;
   第一个外部设备的中断号 FIRST_EXTERNAL_VECTOR = 0x20,所以要
7) if (vector != SYSCALL_VECTOR)
   跳过 SYSCALL_VECTOR。
8) interrupt[i]
   中断服务程序的函数数组。

interrupt 数组 - c语言宏编程的艺术

#define IRQ(x,y) \
        IRQ##x##y##_interrupt
    #define IRQLIST_16(x) \
        IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
        IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
        IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
        IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
    void (*interrupt[NR_IRQS])(void) = {
        IRQLIST_16(0x0),
1) void (*interrupt[NR_IRQS])(void)
   函数指针数组,数组大小是NR_IRQS
2) IRQLIST_16(0x0)
   数组的初始化使用了GNU c的风格。
3) #define IRQLIST_16(x) IRQ(x,2)
   IRQLIST_16 被定义为了 IRQ(x, 2)。
4) #define IRQ(x,y) IRQ##x##y##_interrupt
   IRQ(x, y) 就是字符串拼接。比如 IRQ(0x0, 4) = IRQ0x04_interrupt
   也就是,最终interrupt数组的内容是 {IRQ0x00_interrupt ~ IRQ0x0f_interrupt}
   而函数 IRQ0x00_interrupt在哪里定义的呢?
IRQ0x04_interrupt 的定义
#define BI(x,y) \
        BUILD_IRQ(x##y)
      #define BUILD_16_IRQS(x) \
        BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
        BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
        BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
        BI(x,c) BI(x,d) BI(x,e) BI(x,f)
        
      BUILD_16_IRQS(0x0)
1) BUILD_16_IRQS(0x0)
     最终会被宏展开为 BUILD_IRQ(0x00), BUILD_IRQ(0x00) ~ BUILD_IRQ(0x0f)
  而 BUILD_IRQ 是和硬件相关的,在 include/asm-i386/hw_irq.h中定义
#define IRQ_NAME2(nr) nr##_interrupt(void)
      #define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)
      #define BUILD_IRQ(nr) \
         asmlinkage void IRQ_NAME(nr); \
             __asm__( \
            "\n"__ALIGN_STR"\n" \
            SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
            "pushl $"#nr"-256\n\t" \
            "jmp common_interrupt");
展开后:
asmlinkage void IRQ0x04_interrupt();
      __asm__( \
          "\n" \
          ""IRQ0x04_interrupt: \n\t" \
            "pushl $"#nr"-256\n\t" \
            "jmp common_interrupt");
1) 可以看出 IRQ0x04_interrupt 不是真正的函数,只是一个symbol。
2) 做两件事情:
   a) 把中断向量pushl到栈上。
   b) 调用 common_interrupt。

第二次总结一下c语言的宏编程

1) c语言宏编程可以实现循环的逻辑。通过枚举循环的两个维度。
    IRQ(0x0), IRQ(0x1), 而每个IRQ也是枚举。
2) 宏编程的思想是:尽量避免枯燥的类似的字符串。消除冗余。
上一篇:《ranch 源代码分析 (Erlang实现的网络库)》


下一篇:《gen_server.erl源码》