Linux内核-中断-中断向量表和中断请求队列的初始化

来源:互联网 发布:十月妈咪淘宝旗舰店 编辑:程序博客网 时间:2024/05/17 03:59

一、中断

中断通常被定义为一个事件,该事件改变处理器执行的指令顺序。中断有两种,一种是由CPU外部产生的,对于执行中的软件来说,这种中断的发生完全是“异步”的,根本无法预料此类中断会在什么时候发生,一般由其他硬件设备产生(例如键盘中断);另一种是由CPU本身在执行程序的过程中产生的,例如X86中的“INT n”。Intel手册中分别将这两种中断称为中断和异常。Intel文档中,中断又可分为可屏蔽中断和非屏蔽中断,异常分为:故障、陷阱、异常中止和编程异常。不管是哪种中断,CPU的相应过程基本上一致,即:在执行完当前指令以后,或者在执行当前指令中途,根据中断源提供的“中断向量”,在内存中找到相应的服务程序入口并调用该服务程序。外部中断的向量是由软件或硬件设置好了的,陷阱的向量是在“自陷”指令中发出的(INT n中的n),而各种异常的向量则是CPU的硬件结构中预先规定好的。系统调用一般是通过INT指令实现的,所以也与中断密切相关。

Intel X86 CPU支持256个不同的中断向量,早期X86 CPU的中断响应机制是非常简单的,内存中从0开始的1K字节作为一个中断向量表,表中每个表项占四个字节,由两字节的段地址和两字节的位移组成,构成的地址便是相应中断服务程序的入口地址。由于中断发生时,有可能涉及用户态和内核态的切换,那种简单的表项无法实现这种运行模式的切换,所以在Intel实现保护模式时,中断向量表中的表项从单纯的入口地址改成了更为复杂的描述项,称为“门”,当中断发生时必须通过这些门,才能进入相应的服务程序。按不同的用途和目的,CPU中一共有四种门:任务门、中断门、陷阱门和系统门(除了系统门外,其它三种都是用户态进程无法访问的中断门)。其中除任务门外其它三种门的结构基本相同,先看任务门,其大小为64位,结构如下图:

这里写图片描述

当中断发生时,CPU在中断向量表中找到相应表项,如果是一个任务门,CPU就会将当前任务的运行现场保存在相应的TSS中,并将任务门所指向的TSS作为当前任务,将其内存装入CPU中的各个寄存器,从而完成一次任务切换;

其它三种门大小也是64位,如下图:

这里写图片描述

三种门之间的区别为3位的类型码。与任务门相比,不同之处主要在于:任务门中不需要段内位移,因为任务门指向一个段;而其它门则指向一个子程序,所以必须结合使用段选择码和段内位移。

前面讲过,内核通过门描述符来实现运行模式的切换,这是怎么实现的呢?答案就在描述符的DPL字段,CPU先根据中断向量找到一扇门描述项,然后,将这个门的DPL与CPU的CPL相比,CPL必须小于或等于DPL,也就是优先级别不低于DPL,才能穿过这扇门。不过,如果中断是由外部产生或是因CPU异常而产生的话,就免去这一层检验。所以通过将系统门的DPL设为3,而其它的门的DPL设为0,就可以使用户态进程只能访问系统门,从而防止用户通过int指令模拟非法的中断和异常。

进入中断服务程序时,如果中断服务程序的运行级别,也就是目标代码段的DPL,与中断发生时的CPL不同,那就要引起堆栈的更换。Linux中是这样更换堆栈的:如果进程thread_union结构大小为8KB,则当前进程的内核栈被用于所有类型的内核控制路径;相反,如果thread_union结构大小为4KB,内核使用三种类型的内核栈:

  • 异常栈:用于处理异常(包括系统调用)。这个栈包括在每个进程的thread_union数据结构中。
  • 硬中断请求栈:用于处理中断。系统中每个CPU都有一个硬中断请求栈,而且每个栈占用一个单独的页框。
  • 软中断请求栈:用于处理可延迟函数(软中断或tasklet)。系统中每个CPU都有一个软中断请求栈,而且每个栈占用一个单独的页框。

二、中断向量表的初始化

Linux内核在初始化阶段完成了页式虚存管理的初始化以后,便调用trap_init()和init_IRQ两个函数进行中断机制的初始化。其中trap_init()中主要是对一些系统保留的中断向量的初始化,而init_IRQ()则主要是用于外设的中断,这两个函数源码如下:

  1. trap_init()是在arch/i386/kelnel/traps.c中定义的:

        void __init trap_init(void)    {    #ifdef CONFIG_EISA        void __iomem *p = ioremap(0x0FFFD9, 4);        if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {            EISA_bus = 1;        }        iounmap(p);    #endif    /*     * APIC为高级可编程中断控制器     */    #ifdef CONFIG_X86_LOCAL_APIC        init_apic_mappings();    #endif        /*         * 程序中先设置中断向量表开头的19个陷阱门,这些中断向量都是CPU保留用于         * 异常处理的,例如中断向量14就是为页面异常保留的         */        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);    #ifdef CONFIG_X86_MCE        set_trap_gate(18,&machine_check);    #endif        set_trap_gate(19,&simd_coprocessor_error);        /*         * 然后是对系统调用的初始化,常数SYSCALL_VECTOR定义为0x80         */        set_system_gate(SYSCALL_VECTOR,&system_call);        /*         * Should be a barrier for any external CPU state.         */        cpu_init();        trap_init_hook();    }

    从程序中可以看到,这里用了三个函数来进行这些表项的初始化,分别为set_trap_gate()、set_system_gate()和set_intr_gate(),在同文件中定义如下:

     /*     * 陷阱门     */    static void __init set_trap_gate(unsigned int n, void *addr)    {        _set_gate(idt_table+n,15,0,addr,__KERNEL_CS);    }    /*     * 系统门     */    static void __init set_system_gate(unsigned int n, void *addr)    {        _set_gate(idt_table+n,15,3,addr,__KERNEL_CS);    }    /*     * 中断门     */    void set_intr_gate(unsigned int n, void *addr)    {        _set_gate(idt_table+n,14,0,addr,__KERNEL_CS);    }    /*      * _set_gate函数如下,代码的理解需要结合上面门描述符的格式     * %i为输出输入的变量,依次编号,%0与参数gate_addr结合,%1与(gate_addr+1)结合二者为内存单元     * %2与局部变量__d0结合,存放在寄存器%%eax中,%3与局部变量__d1结合,存放在寄存器%%edx中     * 输入部中第一个变量为%4,后面的两个变量等价于输出部的%3和%2,即存放在edx和eax中    #define _set_gate(gate_addr,type,dpl,addr,seg) \    do { \      int __d0, __d1; \      __asm__ __volatile__ (        /*          * 将edx设为addr,eax设为(__KERNEL_CS << 16),edx的低16位移入eax的低16位,这样,在         * %%eax中就形成了所需要的中断门的第一个长整数,其高16位为__KERNEL_CS,低16位为addr的低16位         */        "movw %%dx,%%ax\n\t" \        /*         * 将(0x8000+(dpl<<13)+(type<<8)))装入%%edx的低16位,这样,%%edx中高16位为addr高16位,         * 而低16位的P位为1,DPL位段为dpl(因为dpl<<13),D位加上类型位段为type,其余各位都为0,这就是中断门的第二个长整数         */        "movw %4,%%dx\n\t" \        /*          * 将%%eax写入*gate_addr         */        "movl %%eax,%0\n\t" \        /*         * 将%%edx写入*(gate_addr+1)         */        "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" ((seg) << 16)); \    } while (0)
  2. 系统初始化时,在trap_init()中设置了一些为CPU保留的专用的IDT表项以及系统调用所用的陷阱门以后,就要进入init_IRQ()设置大量用于外设的通用中断门了,函数init_IRQ()源码在arch/i386/kernel/i8259.c中:

    void __init init_IRQ(void)    {        int i;        pre_intr_init_hook();        for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {            int vector = FIRST_EXTERNAL_VECTOR + i;            if (i >= NR_IRQS)                break;            if (vector != SYSCALL_VECTOR)                 set_intr_gate(vector, interrupt[i]);        }        /* setup after call gates are initialised (usually add in         * the architecture specific gates)         */        intr_init_hook();        /*         * Set the clock to HZ Hz, we already have a valid         * vector now:         */        setup_pit_timer();        /*         * External FPU? Set up irq13 if so, for         * original braindamaged IBM FERR coupling.         */        if (boot_cpu_data.hard_math && !cpu_has_fpu)            setup_irq(FPU_IRQ, &fpu_irq);        irq_ctx_init(smp_processor_id());    }

    pre_intr_init_hook()函数相当于一下代码:

    #ifndef CONFIG_X86_VISWS_APIC        init_ISA_irqs();    #else        init_VISWS_APIC_irqs();    #endif    void __init init_ISA_irqs (void)    {        int i;    #ifdef CONFIG_X86_LOCAL_APIC        init_bsp_APIC();    #endif        init_8259A(0);        for (i = 0; i < NR_IRQS; i++) {            irq_desc[i].status = IRQ_DISABLED;            irq_desc[i].action = NULL;            irq_desc[i].depth = 1;            if (i < 16) {                /*                 * 16 old-style INTA-cycle interrupts:                 */                irq_desc[i].handler = &i8259A_irq_type;            } else {                /*                 * 'high' PCI IRQs filled in on demand                 */                irq_desc[i].handler = &no_irq_type;            }        }    }

    init_ISA_irqs()和init_VISWS_APIC_irqs()分别是对8259A中断控制器和高级可编程中断控制器的初始化,并且初始化一个结构数组irq_desc[]。为什么要有这么一个结构数组呢?i386系统结构支持256个中断向量,除去CPU本身保留的,很难说剩下的这些向量是否够用。而且,很多外部设备由于各种原因本来就不得不共用中断向量,所以,Linux通过IRQ共享和IRQ动态分配。因此,系统中为每个中断向量设置一个队列,根据每个中断源所使用的中断向量,将其中断服务程序挂到相应的队列中去,而irq_desc[]中的每个元素则是这样一个队列头部及控制结构。当中断发生时,首先执行与中断向量相对应的一段总服务程序,根据具体的中断源的设备号在其所属队列中找到特定的服务程序加以执行,该结构在下文再详细分析。
    回到init_IRQ()函数,接下来的循环从FIRST_EXTERNAL_VECTOR开始,设立NR_IRQS个中断向量的IDT表项常数FIRST_EXTERNAL_VECTOR为0x20,这里设置的服务程序入口地址都来自一个函数指针interrupt[],该函数指针数组定义如下(arch/i386/kernel/entry.S):

     .data    ENTRY(interrupt)    .text    vector=0    ENTRY(irq_entries_start)    .rept NR_IRQS        ALIGN    1:  pushl $vector-256        jmp common_interrupt    .data        .long 1b    .text    vector=vector+1    .endr

    数组包括NR_IRQS个元素,这个宏产生的数为224或16,当内核支持I/O APIC芯片时,为224,当内核支持8259A时,为16。可以看出,数组中索引为n元素把中断号减256的结果保存在栈中(原因:内核用负数表示所有中断,正数用来表示系统调用,用汇编指令可以很容易判断正负)。由此可以看出,实际上由外设产生的中断处理全部进入一段公共的程序common_interrupt中,在同一文件中定义如下:

    common_interrupt:        SAVE_ALL        movl %esp,%eax        call do_IRQ        jmp ret_from_intr    /* SAVE_ALL如下 */    #define SAVE_ALL \    cld; \    pushl %es; \    pushl %ds; \    pushl %eax; \    pushl %ebp; \    pushl %edi; \    pushl %esi; \    pushl %edx; \    pushl %ecx; \    pushl %ebx; \    movl $(__USER_DS), %edx; \    movl %edx, %ds; \    movl %edx, %es;

    SAVE_ALL在栈中保存中断处理程序可能会使用的所有CPU寄存器,但eflags、cs、eip、ss及esp除外,因为这几个寄存器已经由控制单元自动保存了,然后这个宏把用户数据段选择符装到ds和es寄存器中。保存寄存器值以后,栈顶地址被存放到eax寄存器中,然后中断处理程序调用do_IRQ()函数(这个函数在稍后的博客中将独立分析),函数结束时,控制转到ret_from_intr()(见后面博客)。


三、中断请求队列的初始化

从上面的分析可以看出,trap_init()是为特定的中断源初始化中断向量表项的,而init_IRQ()是为外设的通用中断门设置表项的,不过还没完成,也就是说,通用中断的服务程序为空,所以需要进一步的初始化,即本节分析的中断请求队列的初始化。由于通用中断是让多个中断源共用的,而且允许这种共用的结构在系统运行过程中动态地变化,所以在IDT的初始化阶段只是为每个中断向量(表项)准备下一个“中断请求队列”,从而形成一个中断请求队列数组,这就是irq_desc[],定义如下:

    /*     * 主要是一些函数指针,用于该队列或者说该共用“中断通道”的控制(而并不是对具体中断源的服务)     * 具体函数取决于所用的中断控制器。例如,enable和disable用来开启和关闭其所属通道,ack用于对中断控制器的响应     * 而end则用于每次中断服务返回前夕,这些函数都是在init_IRQ()中调用pre_intr_init_hook()设置好的     */    struct hw_interrupt_type {        const char * typename;        unsigned int (*startup)(unsigned int irq);        void (*shutdown)(unsigned int irq);        void (*enable)(unsigned int irq);        void (*disable)(unsigned int irq);        void (*ack)(unsigned int irq);        void (*end)(unsigned int irq);        void (*set_affinity)(unsigned int irq, cpumask_t dest);    };    typedef struct hw_interrupt_type  hw_irq_controller;    typedef struct irq_desc {        /*         * 指向PIC对象,服务于IRQ线         */        hw_irq_controller *handler;        /*         * PIC方法所使用的数据         */        void *handler_data;        /*         * 用来维持一个由中断服务程序描述项构成的单链队列         */        struct irqaction *action;   /* IRQ action list */        unsigned int status;        /* IRQ status */        unsigned int depth;     /* nested irq disables */        /* 下面两项在诊断时使用 */        unsigned int irq_count;     /* For detecting broken interrupts */        unsigned int irqs_unhandled;        spinlock_t lock;    } ____cacheline_aligned irq_desc_t;    extern irq_desc_t irq_desc [NR_IRQS];    struct irqaction {        /*         * 最主要的就是函数指针handle,指向具体的中断服务程序         */        irqreturn_t (*handler)(int, void *, struct pt_regs *);        unsigned long flags;        cpumask_t mask;        /* I/O设备名和私有字段 */        const char *name;        void *dev_id;        struct irqaction *next;        int irq;        struct proc_dir_entry *dir;    };

真正的中断服务要到具体设备的初始化程序将其中断服务程序通过request_irq()向系统“登记”,挂入某个中断请求队列以后才会发生,该函数如下:

    /*     * 参数irq为中断请求队列的序号,也就是人们通常说的“中断请求号”,对应于中断控制器中的一个通道,     * 有时候要在接口卡上通过微型开关或跳线来设置,注意与中断向量的区别     * 在内核中,设备驱动程序一般都要通过request_irq()向系统登记其中断服务程序     */    int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *),         unsigned long irq_flags, const char * devname, void *dev_id)    {        unsigned long retval;        struct irqaction *action;        if (irq >= NR_IRQS || !irq_desc[irq].valid || !handler ||            (irq_flags & SA_SHIRQ && !dev_id))            return -EINVAL;        action = (struct irqaction *)kmalloc(sizeof(struct irqaction), GFP_KERNEL);        if (!action)            return -ENOMEM;        action->handler = handler;        action->flags = irq_flags;        cpus_clear(action->mask);        action->name = devname;        action->next = NULL;        action->dev_id = dev_id;        retval = setup_irq(irq, action);        if (retval)            kfree(action);        return retval;    }

在分配并设置了一个irqaction数据结构action后,便调用setup_irq(),将其链入相应的中断请求队列中,函数如下:

    int setup_irq(unsigned int irq, struct irqaction *new)    {        int shared = 0;        struct irqaction *old, **p;        unsigned long flags;        struct irqdesc *desc;        if (new->flags & SA_SAMPLE_RANDOM) {            /*             * 为中断请求队列初始化一个数据结构,用来记录该中断的时序             */            rand_initialize_irq(irq);        }        /*         * The following block of code has to be executed atomically         */        desc = irq_desc + irq;        spin_lock_irqsave(&irq_controller_lock, flags);        p = &desc->action;        if ((old = *p) != NULL) {            /*              * 检查是否允许共用一个中断通道,只有在新加入的结构以及队列中的第一个结构都允许共用时才将其链入队列的尾部             */            if (!(old->flags & new->flags & SA_SHIRQ)) {                spin_unlock_irqrestore(&irq_controller_lock, flags);                return -EBUSY;            }            /* add new interrupt at end of irq queue */            do {                p = &old->next;                old = *p;            } while (old);            shared = 1;        }        *p = new;        if (!shared) {            desc->probing = 0;            desc->running = 0;            desc->pending = 0;            desc->disable_depth = 1;            if (!desc->noautoenable) {                desc->disable_depth = 0;                desc->chip->unmask(irq);            }        }        spin_unlock_irqrestore(&irq_controller_lock, flags);        return 0;    }
0 0