Intel 80x86 Linux Kernel Interrupt(中断)、Interrupt Priority、Interrupt nesting、Prohibit Things Whthin CPU In The Interrupt Off State

目录

0. 引言
1. Linux 中断的概念
2. 中断处理流程
3. Linux 中断相关的源代码分析
4. Linux 硬件中断
5. Linux 软中断
6. 中断优先级
7. CPU在关中断状态下编程要注意的事项

 

0. 引言

中断是现代计算机体系结构的重要组成部分,我们回顾历史,现代体系结构的基本输入输出方式有三种

1. 程序查询:
CPU周期性询问外部设备是否准备就绪。该方式的明显的缺点就是浪费CPU资源,效率低下。但是在特定的场景下这种"程序查询"的方式还有有它的用武之地的
例如,在网络驱动中,通常接口(Interface)每接收一个报文,就发出一个中断。而对于高速网络,每秒就能接收几千个报文,在这样的负载下,系统性能会受到极大的损害。为了提高系统性能,内核开发者已经为网络子系统开发了一种可选的基于查询的接口NAPI(代表new API)。当系统拥有一个高流量的高速接口时,系统通常会收集足够多的报文,而不是马上中断CPU 

2. 中断方式
这是现代CPU最常用的与外围设备通信方式。相对于轮询,该方式不用浪费稀缺的CPU资源,所以高效而灵活。中断处理方式的缺点是每传送一个字符都要进行中断,启动中断控制器,还要保留和恢复现场以便能继续原程序的执行,花费的工作量很大,这样如果需要大量数据交换,系统的性能会很低 

3. DMA方式
通常用于高速设备,设备请求直接访问内存,不用CPU干涉。但是这种方式需要DMA控制器,增加了硬件成本。在进行DMA数据传送之前,DMA控制器会向CPU申请总线控制 权,CPU如果允许,则将控制权交出,因此,在数据交换时,总线控制权由DMA控制器掌握,在传输结束后,DMA控制器将总线控制权交还给CPU 

今天我们要重点学习的就是"中断"这种输入输出机制

Relevant Link:

http://blog.sina.com.cn/s/blog_5d0e8d0d01019cds.html

 

1. Linux 中断的概念

今天我们谈论"中断",会发现这个词在很多场景下都会以不同的身份出现,我们今天对这些概念进行一个梳理


对于这张图,有一点需要说明的是,在linux 2.6内核之后,系统调用已经不使用int 0x80的方式进行了,关于这部分的知识,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/4111692.html

0x1: 中断的分类

翻阅不同的书籍和内核相关资料,我们会发现关于中断的分类有很多种标准,事实情况也就是这样的,根据不同的观察角度,可以有如下的分类

1. 异步(外部中断) OR 同步(内部中断):根据发生中断的时机来分类

1. 异步中断(外部中断)
外部中断是由外部设备引发的中断,这类中断的特点是中断的发生完全是"瞬发"的,例如
    1) 鼠标
    2) 键盘

2. 同步中断(内部中断)
从CPU的角度看,内部中断是一个同步事件,它是执行某条指令时产生的,例如
    1) 缺页异常
    2) 除零异常
    3) 系统调用systemcall trap

2. 可屏蔽中断 OR 不可屏蔽中断:根据处于中断状态中是否可以被其他中断递归打断来分类

中断的屏蔽基于CPU中的"中断屏蔽寄存器"进行实现

1. 可被屏蔽中断:允许延迟响应
2. 不可被屏蔽中断:高实时场景需求,必须立即被响应

3. 硬件中断 OR 软件中断:根据中断源来分类

1. 软件中断
    1) Ring3代码中触发的指令中断
    2) 操作系统自身触发的缺页中断,这是操作系统提供的对CPU中断的一个接口,以中断号的形式进行编号
2. 硬件中断
    1) 外设处理过程中产生的,通过硬件控制器通知cpu自己的状态变化
    2) 外部硬件设备触发的硬件中断(电平信号)
    3) 硬中断应该很快完成,才能有快的响应,所以将一部分可以延迟的处理从硬中断里独立出来,当硬中断处理完之后再处理这部分,就是软中断

可以看到,中断归根结底是CPU提供的一种"硬件机制",是CPU的引脚配置总线和中断控制器在最底层的硬件实现,我们以Linux为例,来深入学习一下中断的概念

0x2: 中断控制器

需要明白的是,中断最本质的概念是一种硬件机制,从单片机到intel CPU都具备中断机制,CPU(微控制器)提供了专门的引脚用于实现中断功能,我们以51单片机进行概念解释,在其他更高级的CPU上本质道理都是相同的

1. 51单片机中有5个中断源 
    1) 中断号: 0、中断源: 1(最高)、中断入口地址:外部中断0 0003H
    2) 中断号: 1、中断源: 2、中断入口地址: 定时器0 000BH
    3) 中断号: 2、中断源: 3、中断入口地址: 外部中断1 0013H
    4) 中断号: 3、中断源: 4、中断入口地址: 定时器1 0018H
    5) 中断号: 4、中断源: 5、中断入口地址: 串口总段 0023H
2. Linux/Windows在内核中提供的"软中断"是一个封装后的概念,实际上是CPU给内核单独提供了一个中断引脚,用于实现软中断,由程序员控制/或者内核进行中断响应处理
3. 所有的中断,包括软中断和外设的中断响应都最终都需要通过CPU上的中断引脚由硬件方式实现
4. 中断源当发生中断事件时,向对应的中断引脚发送下降沿电平信号,这个动作将将对应的中断寄存器置位
5. 51单片机有6个中断寄存器,分别是
    1) 总选通中断寄存器EA,类似于Linux中的"关CPU"中断,当这个寄存器未置位时,所有中断将无法响应,即所有中断引脚将不响应
    2) 每个中断源都和一个中断引脚相连,对应一个中断寄存器,只有EA置位的前提下,中断源对应的中断寄存器才有效
6. 中断优先级是在所有中断寄存器"更后面"的一个选通逻辑,通过与非门的bit选通逻辑,来控制优先响应哪些中断
7. 每个中断源都对应一个中断源,这是一个内置的硬件例程(也可以通过软件方式写入),我们说的软中断也有一个硬件例程,而软中断号可以理解为传入的参数,进入软中断例程后,程序会根据中断号进行一次再分发,分发到相应的软中断子程序中
8. CPU在每个时钟周期的stage5 s2下降沿会检查所有的中断寄存器,以此判断当前是否有中断事件发生

中断控制器是连接设备和 CPU 的桥梁,一个设备产生中断后,需要经过中断控制器的转发,才能最终到达 CPU。时代发展至今,中断控制器经历了几个阶段

1. PIC(Programmable Interrupt Controller 可编程中断控制器): 在 UP(Uni-processor 单处理器) 上应用较广
2. APIC (Advanced Programmable Interrupt Controller 高级可编程中断控制器): 随着 SMP (Symmetric Multiple Processor 对称多处理器) 的流行,APIC 已广为流行并将最终取代 PIC 

8259A (PIC) 管脚图

1. IR0~IR7 (Interrupt Request0~7): 用于连接设备
2. INT: 连接CPU,当有中断请求时,拉高该管脚(提高电平)以通知 CPU 中断的到来
3. INTA: 连接CPU,CPU通过该管脚应答中断请求,并通知PIC提交中断的vector(中断向量)到数据线
4. CS: 片选,用于将两个8259A串联成可连接15个设备的PIC

8259A中的寄存器

1. ICW(Initialization Command Word): 初始化命令寄存器,用于初始化8259A
2. OCW(Operation Command Word 操作命令字): 用于控制8259A
3. IRR(Interrupt Request Register 中断请求寄存器): 共 8bit,对应 IR0~IR7 八个中断管脚。当某个管脚的中断请求到来后,若该管脚没有被屏蔽,IRR 中对应的 bit 被置1。表示 PIC 已经收到设备的中断请求,但还未提交给 CPU。
4. ISR(In Service Register 服务中寄存器): 共 8bit,每 bit 对应 IR0~IR7 八个中断管脚。当 IRR 中的某个中断请求被发送给 CPU 后,ISR 中对应的 bit 被置1。表示中断已发送给 CPU,但 CPU 还未处理完
5. IMR(Interrupt Mask Register 中断屏蔽寄存器): 共 8bit,每 bit 对应IR0~IR7 八个中断管脚。用于屏蔽中断。当某 bit 置1时,对应的中断管脚被屏蔽(通过电路上的与非门实现)

arch/x86/kernel/i8259.c 中通过位运算来开启和关闭中断

void disable_8259A_irq(unsigned int irq)
{
    unsigned int mask = 1 << irq;
    unsigned long flags;

    // 用 spinlock 锁住
    spin_lock_irqsave(&i8259A_lock, flags);
    // 将 IRQ 的相应位置1,屏蔽中断
    cached_irq_mask |= mask;
    if (irq & 8)
        outb(cached_slave_mask, PIC_SLAVE_IMR);
    else
        outb(cached_master_mask, PIC_MASTER_IMR);
    // 解开自旋锁
    spin_unlock_irqrestore(&i8259A_lock, flags);
}

void enable_8259A_irq(unsigned int irq)
{
    unsigned int mask = ~(1 << irq);
    unsigned long flags;

    // 用 spinlock 锁住
    spin_lock_irqsave(&i8259A_lock, flags);
    // 将 IRQ 的相应位置0,开启中断
    cached_irq_mask &= mask;
    if (irq & 8)
        // IR2 管脚负责 8259A 的级联,为0时使用主片,为1时使用从片
        outb(cached_slave_mask, PIC_SLAVE_IMR);
    else
        outb(cached_master_mask, PIC_MASTER_IMR);
    // 解开自旋锁
    spin_unlock_irqrestore(&i8259A_lock, flags);
}

PIC 的每个管脚具有优先级,连接号码较小的设备具有较高的中断优先级,在 PIC 默认的 Full Nested 模式下,通过 PIC 发起中断的流程如下

1. 一个或多个 IR 管脚上产生电平信号,若对应的中断没有被屏蔽,IRR 中相应的 bit 被置1,表示已接收中断请求
2. PIC 拉高 INT 管脚通知 CPU 中断发生
3. CPU 通过 INTA 管脚应答 PIC,表示中断请求收到
4. PIC 收到 INTA 应答后,将 IRR 中具有最高优先级的 bit 清零,并设置 ISR 中对应的 bit
5. CPU 通过 INTA 管脚第二次发出脉冲,PIC 收到后计算最高优先级中断的 vector,并将它提交到数据线上,vector用于之外的中断例程的寻址
6. 等待 CPU 写 EOI (End of Interrupt)。收到 EOI 后(表示中断处理结束),ISR 中最高优先级的 bit 被清零(中断例程处理结束)。如果 PIC 处于 AEOI 模式,当第二个 INTA 脉冲收到后,ISR 中最高优先级的 bit 自动清零

PIC 还有优先级轮转模式,即 PIC 在服务完一个管脚之后将其优先级临时降低,并升高未服务管脚的优先级,以实现类似轮询的模式,避免一个管脚持续发出中断导致其他设备"饿死"
下图是一个典型的 PIC 中断分配,管脚基本上都被古董级设备占据了

arch/x86/kernel/i8259.c 中 8259A 引脚的分配

void init_8259A(int auto_eoi)
{
    unsigned long flags;

    i8259A_auto_eoi = auto_eoi;

    spin_lock_irqsave(&i8259A_lock, flags);

    outb(0xff, PIC_MASTER_IMR);    /* mask all of 8259A-1 */
    outb(0xff, PIC_SLAVE_IMR);    /* mask all of 8259A-2 */

    /*
     * outb_pic - this has to work on a wide range of PC hardware.
     */
    outb_pic(0x11, PIC_MASTER_CMD);    /* ICW1: select 8259A-1 init */

    /* ICW2: 8259A-1 IR0-7 mapped to 0x30-0x37 on x86-64,
       to 0x20-0x27 on i386 */
    outb_pic(IRQ0_VECTOR, PIC_MASTER_IMR);

    /* 8259A-1 (the master) has a slave on IR2 */
    outb_pic(1U << PIC_CASCADE_IR, PIC_MASTER_IMR);

    if (auto_eoi)    /* master does Auto EOI */
        outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR);
    else        /* master expects normal EOI */
        outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR);

    outb_pic(0x11, PIC_SLAVE_CMD);    /* ICW1: select 8259A-2 init */

    /* ICW2: 8259A-2 IR0-7 mapped to IRQ8_VECTOR */
    outb_pic(IRQ8_VECTOR, PIC_SLAVE_IMR);
    /* 8259A-2 is a slave on master's IR2 */
    outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR);
    /* (slave's support for AEOI in flat mode is to be investigated) */
    outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR);

    if (auto_eoi)
        /*
         * In AEOI mode we just have to mask the interrupt
         * when acking.
         */
        i8259A_chip.mask_ack = disable_8259A_irq;
    else
        i8259A_chip.mask_ack = mask_and_ack_8259A;

    udelay(100);        /* wait for 8259A to initialize */

    outb(cached_master_mask, PIC_MASTER_IMR); /* restore master IRQ mask */
    outb(cached_slave_mask, PIC_SLAVE_IMR);      /* restore slave IRQ mask */

    spin_unlock_irqrestore(&i8259A_lock, flags);
}

从代码中可以看出,PIC 能接的设备数量实在太少了,而且不支持多处理器,为了使用 8259A 级联连接较多的设备,可以采用两种方式

1. IRQ 共享:中断处理程序执行多个中断服务程序(ISR),每个 ISR 是一个与共享 IRQ 线相关的函数 
2. IRQ 共享需要满足两个条件
    1) 每个 ISR 都愿意共享 IRQ,即 request_irq() 时指定了 IRQF_SHARED
    2) 所有 ISR 具有相同的触发条件(电平触发或边沿触发、高低电平或上下边沿)
3. IRQ 动态分配:在可能的最后时刻,才把 IRQ 线分配给一个设备 

当然,APIC 是现代的解决方案。即使是 APIC,也需要使用 IRQ 共享

这张图的核心: I/O APIC 的组成为

1. 一组 24 条 IRQ 线
2. 一张 24 项的中断重定向表
3. 可编程寄存器,通过 APIC 总线发送和接收 APIC 信息的一个信息单元 

与 8259A 不同,中断优先级不与引脚号相关联,中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器和选择处理器的方式
来自外部硬件设备的中断以两种方式在可用 CPU 之间分发

1. 静态分发
2. 动态分发

0x3: 中断描述符

Intel 提供了三种类型的中断描述符

1. 任务门
2. 中断门
3. 陷阱门描述符

1. 系统将所有的中断信号统一进行了编号(一共256个: 0 ~ 255),这个号称为中断向量,它们统一组织成IDT,具体哪个中断向量表示哪种中断有的是规定好的,也有的是在给定范围内自行设定的  
2. 中断向量和中断服务程序的对应关系主要是由IDT(中断向量表)负责。操作系统在IDT中设置好各种中断向量对应的中断描述符,从概念类型上来说,Linux对这些中断号进行了一次分类,Linux 使用与 Intel 稍有不同的分类,把中断描述符分为五类    
    1) 中断门(interrupt gate)
    用户态的进程不能访问Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态
    set_intr_gate(n,addr)
    上述系统调用在 IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addr,DPL 字段设置为0  
    
    2) 系统门(system gate)
    用户态的进程可以访问Intel陷阱门(trap)(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量分别对应用户态下对应的汇编指令
        2.1) 4: into
        2.2) 5: bound
        2.3) 128: int $0x80(系统调用)
    set_system_gate(n,addr)
    
    3) 系统中断门(system interrupt gate)
    能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3
    set_system_intr_gate(n,addr)

    4) 陷阱门(trap gate)
    用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序(例如除零、缺页异常)都通过陷阱门来激活
    set_trap_gate(n,addr)
    
    5) 任务门(task gate)
    不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对"Double fault"异常的处理程序是由任务门激活的
    set_task_gate(n,gdt)
//IDT本身的位置是由idtr寄存器保存的,当然这个地址也是由OS填充的 

门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数
在 IDT 中插入门的函数定义在 \linux-2.6.32.63\arch\x86\include\asm\desc.h 中,这些函数以不同的参数调用内部函数 _set_gate()。_set_gate 调用两个内部函数

1. pack_gate: 设置门的数据结构
    1) 中断号
    2) 门类型
    3) 处理函数地址
    4) DPL
    5) ist
    6) 目录段寄存器
2. write_idt_entry: 宏定义为 native_write_idt_entry,用 memcpy 将设置好的门写入 IDT

\linux-2.6.32.63\arch\x86\include\asm\desc.h

static inline void _set_gate(int gate, unsigned type, void *addr, unsigned dpl, unsigned ist, unsigned seg)
{
    gate_desc s;
    pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
    /*
     * does not need to be atomic because it is only done once at
     * setup time
     */
    write_idt_entry(idt_table, gate, &s);
}

static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func, unsigned dpl, unsigned ist, unsigned seg)
{
    gate->offset_low = PTR_LOW(func);
    gate->segment = __KERNEL_CS;
    gate->ist = ist;
    gate->p = 1;
    gate->dpl = dpl;
    gate->zero0 = 0;
    gate->zero1 = 0;
    gate->type = type;
    gate->offset_middle = PTR_MIDDLE(func);
    gate->offset_high = PTR_HIGH(func);
}

#define write_idt_entry(dt, entry, g)        \
    native_write_idt_entry(dt, entry, g)

static inline void native_write_idt_entry(gate_desc *idt, int entry, const gate_desc *gate)
{
    memcpy(&idt[entry], gate, sizeof(*gate));
}

0x4: 中断数据结构

在 Linux 中,中断描述符的核心数据结构是 include/linux/irq.h 中的 irq_desc 结构体。每个 irq_desc 实例描述一条中断线

struct irq_desc 
{
    //1. interrupt number for this descriptor
    unsigned int        irq;

    //2. irq stats per cpu
    unsigned int            *kstat_irqs;
#ifdef CONFIG_INTR_REMAP
    //3. iommu with this irq
    struct irq_2_iommu      *irq_2_iommu;
#endif
    /*
    4. highlevel irq-events handler [if NULL, __do_IRQ()],中断事件处理函数
    handle_irq 是函数指针,指向 kernel/irq/chip.c 中的中断事件处理函数
        1) handle_simple_irq
        2) handle_level_irq
        3) handle_fasteoi_irq
        4) handle_edge_irq
        5) handle_percpu_irq
    这个函数指针是由 kernel/irq/chip.c 中的 __set_irq_handler() 设置
    */
    irq_flow_handler_t    handle_irq;

    /*
    5. low level interrupt hardware access,irq_chip 指针,描述了一些硬件信息
    chip 是 irq_chip 结构体指针,include/linux/irq.h 中的 irq_chip 结构体定义了对每根中断线的底层硬件操作
    struct irq_chip 
    {
        /*
        1. name for /proc/interrupts
        包含一个短的字符串,用于标识硬件控制器
            1) IA-32: XTPIC
            2) AMD64: IO-APIC
        */
        const char    *name;

        //2. start up the interrupt (defaults to ->enable if NULL)
        unsigned int    (*startup)(unsigned int irq);

        //3. shut down the interrupt (defaults to ->disable if NULL)
        void        (*shutdown)(unsigned int irq);

        //4. enable the interrupt (defaults to chip->unmask if NULL)
        void        (*enable)(unsigned int irq);

        //5. disable the interrupt (defaults to chip->mask if NULL)
        void        (*disable)(unsigned int irq);

        //6. start of a new interrupt
        void        (*ack)(unsigned int irq);

        //7. mask an interrupt source
        void        (*mask)(unsigned int irq);

        //8. ack and mask an interrupt source
        void        (*mask_ack)(unsigned int irq);

        //9. unmask an interrupt source
        void        (*unmask)(unsigned int irq);

        //10. end of interrupt - chip level
        void        (*eoi)(unsigned int irq);

        //11. end of interrupt - flow level
        void        (*end)(unsigned int irq);

        //12. set the CPU affinity on SMP machines
        int        (*set_affinity)(unsigned int irq, const struct cpumask *dest);

        //13. resend an IRQ to the CPU
        int        (*retrigger)(unsigned int irq);

        //14. set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
        int        (*set_type)(unsigned int irq, unsigned int flow_type);

        //15. enable/disable power-management wake-on of an IRQ
        int        (*set_wake)(unsigned int irq, unsigned int on);

        //16. function to lock access to slow bus (i2c) chips
        void        (*bus_lock)(unsigned int irq);

        //17. function to sync and unlock slow bus (i2c) chips
        void        (*bus_sync_unlock)(unsigned int irq);

        /* Currently used only by UML, might disappear one day.*/
    #ifdef CONFIG_IRQ_RELEASE_METHOD
        //18. release function solely used by UML
        void        (*release)(unsigned int irq, void *dev_id);
    #endif
        /*
         * For compatibility, ->typename is copied into ->name.
         * Will disappear.
         */
        //19. obsoleted by name, kept as migration helper
        const char    *typename;
    };
    */
    struct irq_chip        *chip;

    //6. MSI descriptor
    struct msi_desc        *msi_desc;

    //7. per-IRQ data for the irq_chip methods
    void            *handler_data;

    //8. platform-specific per-chip private data for the chip methods, to allow shared chip implementations
    void            *chip_data;

    /* IRQ action list */
    /*
    9. the irq action chain,irqaction 指针链
    action 是 irqaction 结构体指针,指向一个 irqaction 链表。irqaction 在 include/linux/interrupt.h 中定义,每个结构体描述一个中断处理程序
    struct irqaction
    {
        // 中断处理程序的函数指针
        irq_handler_t handler;
        unsigned long flags;

        // 中断处理程序名称,显示在 /proc/interrupts 中
        const char *name;

        // 设备 ID
        void *dev_id;

        // 指向链表中的下一个 irqaction 结构体
        struct irqaction *next;

        // 中断通道号
        int irq;

        // 在 /proc 文件系统中的目录
        struct proc_dir_entry *dir;
        irq_handler_t thread_fn;
        struct task_struct *thread;
        unsigned long thread_flags;
    };
    */
    struct irqaction    *action;    

    /* IRQ status */
    /*
    10. status information,IRQ 线状态标志
    status 是描述 IRQ 线状态的一组标志。在同一文件中宏定义
    #define IRQ_TYPE_NONE        0x00000000    /* Default, unspecified type */
    #define IRQ_TYPE_EDGE_RISING    0x00000001    /* Edge rising type */
    #define IRQ_TYPE_EDGE_FALLING    0x00000002    /* Edge falling type */
    #define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
    #define IRQ_TYPE_LEVEL_HIGH    0x00000004    /* Level high type */
    #define IRQ_TYPE_LEVEL_LOW    0x00000008    /* Level low type */
    #define IRQ_TYPE_SENSE_MASK    0x0000000f    /* Mask of the above */
    #define IRQ_TYPE_PROBE        0x00000010    /* Probing in progress */

    /* Internal flags */
    #define IRQ_INPROGRESS        0x00000100    /* IRQ handler active - do not enter! */
    #define IRQ_DISABLED        0x00000200    /* IRQ disabled - do not enter! */
    #define IRQ_PENDING        0x00000400    /* IRQ pending - replay on enable */
    #define IRQ_REPLAY        0x00000800    /* IRQ has been replayed but not acked yet */
    #define IRQ_AUTODETECT        0x00001000    /* IRQ is being autodetected */
    #define IRQ_WAITING        0x00002000    /* IRQ not yet seen - for autodetection */
    #define IRQ_LEVEL        0x00004000    /* IRQ level triggered */
    #define IRQ_MASKED        0x00008000    /* IRQ masked - shouldn't be seen again */
    #define IRQ_PER_CPU        0x00010000    /* IRQ is per CPU */
    #define IRQ_NOPROBE        0x00020000    /* IRQ is not valid for probing */
    #define IRQ_NOREQUEST        0x00040000    /* IRQ cannot be requested */
    #define IRQ_NOAUTOEN        0x00080000    /* IRQ will not be enabled on request irq */
    #define IRQ_WAKEUP        0x00100000    /* IRQ triggers system wakeup */
    #define IRQ_MOVE_PENDING    0x00200000    /* need to re-target IRQ destination */
    #define IRQ_NO_BALANCING    0x00400000    /* IRQ is excluded from balancing */
    #define IRQ_SPURIOUS_DISABLED    0x00800000    /* IRQ was disabled by the spurious trap */
    #define IRQ_MOVE_PCNTXT        0x01000000    /* IRQ migration from process context */
    #define IRQ_AFFINITY_SET    0x02000000    /* IRQ affinity was set from userspace*/
    #define IRQ_SUSPENDED        0x04000000    /* IRQ has gone through suspend sequence */
    #define IRQ_ONESHOT        0x08000000    /* IRQ is not unmasked after hardirq */
    #define IRQ_NESTED_THREAD    0x10000000    /* IRQ is nested into another, no own handler thread */

    #ifdef CONFIG_IRQ_PER_CPU
    # define CHECK_IRQ_PER_CPU(var) ((var) & IRQ_PER_CPU)
    # define IRQ_NO_BALANCING_MASK    (IRQ_PER_CPU | IRQ_NO_BALANCING)
    #else
    # define CHECK_IRQ_PER_CPU(var) 0
    # define IRQ_NO_BALANCING_MASK    IRQ_NO_BALANCING
    #endif
    */
    unsigned int        status;        

    /* nested irq disables */
    //11. disable-depth, for nested irq_disable() calls
    unsigned int        depth;        

    /* nested wake enables */
    //12. enable depth, for multiple set_irq_wake() callers
    unsigned int        wake_depth;    

    /* For detecting broken IRQs */
    //13. stats field to detect stalled irqs,中断计数
    unsigned int        irq_count;    

    /* Aging timer for unhandled count */
    //14. aging timer for unhandled count,无法处理的中断计数
    unsigned long        last_unhandled;    

    //15. stats field for spurious unhandled interrupts
    unsigned int        irqs_unhandled;

    //16. locking for SMP
    spinlock_t        lock;
#ifdef CONFIG_SMP
    //17. IRQ affinity on SMP,多处理器中的处理器亲和性
    cpumask_var_t        affinity;

    //18. node index useful for balancing
    unsigned int        node;
#ifdef CONFIG_GENERIC_PENDING_IRQ
    //19. pending rebalanced interrupts
    cpumask_var_t        pending_mask;
#endif
#endif
    //20. number of irqaction threads currently running
    atomic_t        threads_active;

    //21. wait queue for sync_irq to wait for threaded handlers
    wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FS
    //22. /proc/irq/ procfs entry,在 /proc 文件系统中的目录
    struct proc_dir_entry    *dir;
#endif
    //23. flow handler name for /proc/interrupts output,中断名称
    const char        *name;
} ____cacheline_internodealigned_in_smp;

irq_desc 在 kernel/irq/handle.c 中被使用,此文件是 IRQ 机制的核心入口,描述了各中断线
sourcecode\kernel\irq\handle.c

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = 
{
    /*
    IRQ的最大数量主要取决于辅助CPU管理IRQ的辅助芯片
    1. alpha: 32个中断
    2. wildfire: 2048个中断
    3. IA-64: 256个中断
    4. IA-32、8256A控制器: 16个中断
    */
    [0 ... NR_IRQS-1] = 
    {
        .status = IRQ_DISABLED,                            // 默认屏蔽中断
        .chip = &no_irq_chip,                            // 没有与 chip 相关联
        .handle_irq = handle_bad_irq,                    // 未知(坏的)IRQ 处理程序,输出 IRQ 信息供调试,更新 CPU IRQ 次数计数器,回应 IRQ
        .depth = 1,                                        // 默认是第一层(没有嵌套中断)
        .lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock),    // 还没有自旋锁
    }
};

综上所述,内核中的中断描述符表是一个 irq_desc 数组,数组的每一项描述一根中断线的信息,包括芯片中断处理程序、底层硬件操作函数、注册的中断处理程序链表等
中断向量表可以通过 /proc/interrupts 查看

          CPU0       CPU1       
  0:  659558570          0    IO-APIC-edge  timer
  1:          8          0    IO-APIC-edge  i8042
  6:          2          0    IO-APIC-edge  floppy
  8:          0          0    IO-APIC-edge  rtc
  9:          0          0   IO-APIC-level  acpi
 12:         63          0    IO-APIC-edge  i8042
169:   41216157          0   IO-APIC-level  xen-platform-pci
177:         23          0   IO-APIC-level  uhci_hcd:usb1
NMI:          0          0 
LOC:  659562619  659562537 
ERR:          0
MIS:          0

在Linux 2.6的内核开发期间,重构出了一个新的通用的IRQ子系统。它能够以统一的方式处理不同的中断控制器和不同类型的中断

1. 高层ISR(high-level interrupt service routines 高层中断服务例程): irq_flow_handler_t    handle_irq
针对设备驱动程序端(或其他内核组件)的中断,执行由此引起的所有必要的工作。例如,如果设备使用中断通知一些数据已经到达,那么高层ISR的工作应该是将数据复制到适当的位置

2. 中断电流处理(interrup flow handling): struct irq_chip *chip
处理不同的中断电流类型之间的各种差别,如
    1) 边沿触发(edge-triggering): 硬件通过感知线路上的点位差来检测中断
    2) 电平触发(level-triggering): 根据特定的电势值检测中断,与电势是否改变无关
http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:struct irq_chip
//搜索:struct irqaction
在2.6之后的内核代码中,电流处理的实现已经被极大地简化了,内核代码封装了大量的体系结构相关的代码,并在高层提供了一个几乎可以使用于所有硬件的通用框架 
    2.1. 设置控制器硬件
    内核提供了一系列的标准函数,用于注册irq_chip和设置电流程序
        1) extern int set_irq_chip(unsigned int irq, struct irq_chip *chip);
        将一个IRQ芯片以irq_chip实例的形式关联到某个特定的中断

        2) static inline void set_irq_handler(unsigned int irq, irq_flow_handler_t handle);
        3) static inline void set_irq_chained_handler(unsigned int irq, irq_flow_handler_t handle);
        为某个给定的IRQ编号设置电流处理程序

        4) extern void set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle);
        5) extern void set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip, irq_flow_handler_t handle, const char *name);
        这是一个快捷方式,它相当于连续调用上述的各函数

    2.2. 电流处理
        1) 边沿触发中断
        2) 电平触发中断
        3) 其他中断类型

3. 芯片级硬件封装(chip-level hardware encapsultion)
需要与在电子学层次上产生中断的底层硬件直接通信,该抽象层可以视为中断控制器的某种"设备驱动程序"

我们继续回到irq_desc[NR_IRQS]这个保存中断处理程序的全局数组上来,用于表示IRQ描述符的结构定义请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html

从内核中高层代码的角度来看,每个IRQ都可以由该结构完全描述,事实上,操作系统的每一种机制在背后都一定有一个完善的数据结构提供支持,回到上面的那张IRQ子系统的架构图
其中的3个抽象层在该结构(struct irq_desc)中表示如下

1. 电流层ISR: 由handle_irq提供
handler_data可以指向任意数据,该数据可以是特定于IRQ或处理程序的。每当发生中断时,特定于体系结构的代码都会调用handle_irq。该函数负责使用chip中提供的特定于控制器的方法,进行处理中断所必需的一些底层操作 

2. action提供了一个操作链: 需要在中断发生时执行
由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处

3. 电流处理和芯片相关操作被封装在chip中
chip_data指向可能与chip相关的任意数据

4. name指定了电流层处理程序的名称,将显示在/proc/interrupts中。对边沿触发中断,通常是"edge",对电平触发中断,通常是"level"

5. delth: 用于确定IRQ电路是启用的还是禁用的
    1) 0: 表示启用
    2) 正值: 表示禁用

6. irq_count、irq_unhandled字段提供了一些统计量,可用于检测停顿和未处理,但持续发生的中断。即假中断(spurious interrupt)

7. status: 描述了IRQ的当前状态
IRQ不仅可以在处理程序安装期间改变其状态,而且可以在运行时改变,根据status当前的值,内核很容易获知某个IRQ的状态,而无需了解底层实现的硬件相关特性
irq.h中定义了各种表示当前状态的常数,可用于描述IRQ电路当前的状态。每个常数表示位串中的一个置为的标志位(可以同时设置)

0x5: 中断的初始化

中断机制的初始化分为三步

1. arch/x86/kernel/head_32.S 中 setup_IDT 
2. init/main.c 的 start_kernel() 中的 trap_init()
3. init/main.c 的 start_kernel() 中的 init_IRQ()

1. setup_IDT

\linux-2.6.32.63\arch\x86\kernel\head_32.S

setup_idt:
    lea ignore_int,%edx
    movl $(__KERNEL_CS << 16),%eax
    movw %dx,%ax        /* selector = 0x0010 = cs */
    movw $0x8E00,%dx    /* interrupt gate - dpl=0, present */

    lea idt_table,%edi
    mov $256,%ecx
rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne rp_sidt

.macro    set_early_handler handler,trapno
    lea \handler,%edx
    movl $(__KERNEL_CS << 16),%eax
    movw %dx,%ax
    movw $0x8E00,%dx    /* interrupt gate - dpl=0, present */
    lea idt_table,%edi
    movl %eax,8*\trapno(%edi)
    movl %edx,8*\trapno+4(%edi)
.endm

    set_early_handler handler=early_divide_err,trapno=0
    set_early_handler handler=early_illegal_opcode,trapno=6
    set_early_handler handler=early_protection_fault,trapno=13
    set_early_handler handler=early_page_fault,trapno=14

    ret

2. trap_init()

trap_init() 定义于 arch/x86/kernel/traps.c,作用是设置中断向量

1. 初始化 APIC 映射表
2. 调用 set_trap_gate、set_intr_gate、set_task_gate、set_system_gate 等,初始化中断描述符表 
3. 调用 set_system_gate,初始化系统调用
4. 将已设置的中断向量置保留位
5. 将已设置的系统调用置保留位
6. 初始化 CPU 作为屏障
7. 执行 trap_init 的钩子函数

3. init_IRQ

init_IRQ() 定义于 arch/x86/kernel/paravirt.c,由以下两者组成

1. paravirt_ops.init_IRQ()
2. native_init_IRQ(): 定义于 arch/x86/kernel/i8259.c。该函数主要将 IDT 未初始化的各项初始化为中断门

0x6: 内核接口

中断处理程序不是编译内核时就完全确定的,因此要为开发者留下编程接口
2.6.17 内核引入了 generic IRQ 机制,支持 i386、x86-64 和 ARM 三个体系结构。generic IRQ 层的引入,是为了剥离 IRQ flow 和 IRQ chip 过于紧密的耦合。为驱动开发者提供通用的 API 来 request/enable/disable/free 中断,而不需要知道任何底层的中断控制器细节,这些中断 API 是在内核中用 EXPORT_SYMBOL 导出的

1. 请求中断

/source/include/linux/interrupt.h

/*
1. irq: 中断通道号,无符号整数
2. handler: 中断处理程序的函数指针 (irq_return_t isr_func(int irq, void *dev_id))
3. irqflags: 标志位
    1) IRQF_SHARED: 共享中断通道
    2) IRQF_DISABLED: 中断处理程序执行时关中断
    3) IRQF_SAMPLE_RANDOM: 随机发生中断,可用于产生随机数
    4) IRQF_TRIGGER_LOW:2.6.26 中没有,低电平有效
    5) IRQF_TRIGGER_HIGH: 2.6.26 中没有,高电平有效
    6) IRQF_TRIGGER_RISING: 2.6.26 中没有,上升沿有效
    7) IRQF_TRIGGER_FALLING: 2.6.26 中没有,下降沿有效
4. name: 名称,显示在 /proc/interrupts 中
5. dev_id:设备 ID,区分不同的设备
*/
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

2. 清除中断

\linux-2.6.32.63\kernel\irq\manage.c

/**
 *    free_irq - free an interrupt allocated with request_irq
 *    @irq: Interrupt line to free
 *    @dev_id: Device identity to free
 *
 *    Remove an interrupt handler. The handler is removed and if the
 *    interrupt line is no longer in use by any driver it is disabled.
 *    On a shared IRQ the caller must ensure the interrupt is disabled
 *    on the card it drives before calling this function. The function
 *    does not return until any executing interrupts for this IRQ
 *    have completed.
 *
 *    This function must not be called from interrupt context.
 1. irq: 中断通道号,无符号整数
 2. dev_id: 请求中断时指定的设备 ID
 */
void free_irq(unsigned int irq, void *dev_id)
{
    struct irq_desc *desc = irq_to_desc(irq);

    if (!desc)
        return;

    chip_bus_lock(irq, desc);
    kfree(__free_irq(irq, dev_id));
    chip_bus_sync_unlock(irq, desc);
}
EXPORT_SYMBOL(free_irq);

3. 启用中断

void enable_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);
    unsigned long flags;

    if (!desc)
        return;

    chip_bus_lock(irq, desc);
    spin_lock_irqsave(&desc->lock, flags);
    __enable_irq(desc, irq, false);
    spin_unlock_irqrestore(&desc->lock, flags);
    chip_bus_sync_unlock(irq, desc);
}
EXPORT_SYMBOL(enable_irq);

4. 关闭中断

void disable_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);

    if (!desc)
        return;

    disable_irq_nosync(irq);
    if (desc->action)
        synchronize_irq(irq);
}
EXPORT_SYMBOL(disable_irq);

5. 关闭中断 (无等待)

disable_irq 会保证存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即关中断并返回。事实上,disable_irq 首先调用 disable_irq_nosync,然后调用 synchronize_irq 同步

void disable_irq_nosync(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);
    unsigned long flags;

    if (!desc)
        return;

    chip_bus_lock(irq, desc);
    spin_lock_irqsave(&desc->lock, flags);
    __disable_irq(desc, irq, false);
    spin_unlock_irqrestore(&desc->lock, flags);
    chip_bus_sync_unlock(irq, desc);
}
EXPORT_SYMBOL(disable_irq_nosync);

6. 同步中断 (多处理器)

void synchronize_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);
    unsigned int status;

    if (!desc)
        return;

    do {
        unsigned long flags;

        /*
         * Wait until we're out of the critical section.  This might
         * give the wrong answer due to the lack of memory barriers.
         */
        while (desc->status & IRQ_INPROGRESS)
            cpu_relax();

        /* Ok, that indicated we're done: double-check carefully. */
        spin_lock_irqsave(&desc->lock, flags);
        status = desc->status;
        spin_unlock_irqrestore(&desc->lock, flags);

        /* Oops, that failed? */
    } while (status & IRQ_INPROGRESS);

    /*
     * We made sure that no hardirq handler is running. Now verify
     * that no threaded handlers are active.
     */
    wait_event(desc->wait_for_threads, !atomic_read(&desc->threads_active));
}
EXPORT_SYMBOL(synchronize_irq);

7. 设置 IRQ 芯片

kernel/irq/chip.c: set_irq_chip()

8. 设置 IRQ 类型

kernel/irq/chip.c: set_irq_type()

9. 设置 IRQ 数据

kernel/irq/chip.c: set_irq_data()

10. 设置 IRQ 芯片数据

kernel/irq/chip.c: set_irq_chip_data()

Relevant Link:

http://home.ustc.edu.cn/~boj/courses/linux_kernel/2_int.html

 

2. 中断处理流程

0x1: CPU的中断处理流程

每个能够发出中断请求的硬件设备控制器都有一条名为 IRQ 的输出线。所有现有的 IRQ 线都与一个名为可编程中断控制器(PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作

1. 监视 IRQ 线,检查产生的信号。如果有两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线(优先级较高)
2. 如果一个引发信号出现在 IRQ 线上 
    1) 把接收到的引发信号转换成对应的向量号
    2) 把这个向量存放在中断控制器的一个 I/O 端口(0x200x21),从而允许 CPU 通过数据总线读此向量 
    3) 把引发信号发送到处理器的 INTR 引脚,即产生一个中断
    4) 等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它;当这种情况发生时,清 INTR 线 
3. 返回第1步,继续循环

当执行了一条指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,CPU就会在相应的时钟脉冲到来时从总线上读取中断请求对应的中断向量(对于"异常、系统调用"那样的"软中断",因为中断向量是直接给出的,所以和通过IRQ(中断请求)线发送的硬件中断请求不同,不会再专门去取其对应的中断向量),控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作

1. 确定与中断或异常关联的向量i(0 ≤ i ≤ 255)
CPU根据得到的中断向量到IDT表里找到该向量对应的"中断描述符"(中断门),中断描述符里保存着中断服务程序的"段选择符" 

2. 根据取得的段选择符到GDT中找相应的段描述符
读由idtr寄存器指向的 IDT表中的第i项(IDT表项中包含的是一个中断门或一个陷阱门),从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址

3. 确信中断是由授权的(中断)发生源发出的
首先将"当前特权级CPL"(存放在cs寄存器的低两位)与"段描述符"(存放在GDT中)的描述符"特权级DPL"比较
    1) 如果CPL小于DPL,就产生一个"General protection"异常,因为中断处理程序的特权不能低于引起中断的程序的特权(即可中断)
    2) 对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个"General protection"异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门 

4. 检查是否发生了特权级的变化
也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点 
    1) 读tr寄存器,以访问运行进程的TSS段 
    2) 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到 
    3) 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址 

5. 如果故障已发生,用引起异常的指令地址装载CS和eip寄存器,从而使得这条指令能再次被执行
7. 在栈中保存eflags、CS及eip的内容
8. 如果异常产生了一个硬件出错码,则将它保存在栈中
9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址
10. 建立一个适当的环境,即保存现场(entry path)
CPU开始利用栈保护被暂停执行的程序的现场,进入路径的关键任务有:
    1) 从用户态栈切换到和心态栈(内存栈的切换)
    2) 保存用户应用程序当前的寄存器状态,以便在中断活动结束后恢复
11. 调用处理程序自身: 跳转到中断服务程序的第一条指令开始执行
CPU利用中断服务程序的段描述符将其第一条指令的地址加载到cs和eip寄存器中,开始执行中断服务程序。这意味着先前的程序被暂停执行,中断服务程序正式开始工作
12. 将系统还原,即还原现场(exit path): 中断服务程序处理完毕,恢复执行先前中断的程序
退出路径的关键任务有:
    1) 调度器是否应该选择一个新进程代替旧的进程
    2) 是否有信号必须投递到原进程
    3) 从中断返回后,只有确认了以上1.)、2.)问题后,内核才能完成其常规任务,即还原寄存器集合、切换到用户态栈、切换到适用于用户应用程序的适当的处理器状态

下图显示了整个流程

中断例程寻址

中断处理流程中的栈变化情况

0x2: 保存中断信息

Linux 内核的中断处理机制自始至终贯穿着 "重要的事马上做,不重要的事推后做" 的思想
中断处理程序首先要做:

1. 将中断号压入栈中,以便找到对应的中断服务程序
2. 将当前寄存器信息压入栈中,以便中断退出时恢复上下文
//这两步都是不可重入的。因此在进入中断服务程序时,CPU 已经自动禁止了本 CPU 上的中断响应,即中断响应的时候是关中断的

0x3: 处理中断

以中断发生时寄存器的信息为参数,调用 arch/x86/kernel/irq32.c 中的 do_IRQ 函数

1. 保存寄存器上下文
2. 调用 irq_enter:
\linux-2.6.32.63\kernel\softirq.c
/*
void irq_enter(void)
{
    int cpu = smp_processor_id();

    rcu_irq_enter();
    if (idle_cpu(cpu) && !in_interrupt()) {
        /*
         * Prevent raise_softirq from needlessly waking up ksoftirqd
         * here, as softirq will be serviced on return from interrupt.
         */
        local_bh_disable();
        tick_check_idle(cpu);
        _local_bh_enable();
    }

    __irq_enter();
}
*/
3. 如果可用空间不足 1KB,可能会引发栈溢出,输出内核错误信息
4. 如果 thread_union 是 4KB 的,进行一些特殊处理
5. 调用 desc->handle_irq(irq, desc),调用 __do_IRQ() (kernel/irq/handle.c)
6. 执行 irq_exit(),在 kernel/softirq.c 中:
    1) 递减中断计数器
    2) 检查是否有软中断在等待执行,若有则执行软中断
    3) 如果使用了无滴答内核看是不是该休息了
7. 恢复寄存器上下文,跳转到 ret_from_intr (跳转点早在 common_interrupt 中就被指定了)

在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock 是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU 处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个 CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有 spinlock 保护

0x4: 从中断返回

整个处理过程是持续占有CPU的(除开中断情况下可能被新的中断打断外),这样导致

1. 连续的低优先的中断可能持续占有 CPU, 而高优先的某些进程则无法获得 CPU
对于这个问题,较新的 linux 内核增加了 ksoftirqd 内核线程,如果持续处理的软中断超过一定数量,则结束中断处理过程,唤醒 ksoftirqd,由它来继续处理

2. 中断处理的这几个阶段中不能调用可能导致睡眠的函数
需要注意的是,关中断的时候不能调用可能导致睡眠的函数,原因如下
    1) 睡眠是通过系统调用实现的
    2) 系统调用是通过中断陷门实现的用户态/内核态切换
    3) 在关中断情况下,CPU无法响应这个陷门中断请求
    4) 睡眠系统调用的发起者一直处于等待睡眠唤醒,导致haung住
对于这个问题,linux 内核提供了 workqueue(工作队列)机制,定义一个 work 结构(包含了处理函数),然后在上述的中断处理的几个阶段的某一步中调用 schedule_work 函数,work 便被添加到 workqueue 中,等待处理。工作队列有着自己的处理线程, 这些 work 被推迟到这些线程中去处理。处理过程只可能发生在这些工作线程中,不会发生在内核中断处理路径中,所以可以睡眠

0x5: 编写中断处理程序

编写一个简单的中断处理程序 (catchirq) 作为内核模块,演示捕获网卡中断
catchirq.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>  
#include <linux/interrupt.h> 
#include <linux/timer.h>

#define DEBUG  

#ifdef DEBUG  
#define MSG(message, args...) printk(KERN_DEBUG "catchirq: " message, ##args)  
#else  
#define MSG(message, args...)  
#endif  
  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("boj");  

int irq;
char *interface;

// module_param(name, type, perm)
module_param(irq, int, 0644);
module_param(interface, charp, 0644);

int irq_handle_function(int irq, void *device_id)
{
    static int count = 1;
    MSG("[%d] Receive IRQ at %ld\n", count, jiffies);
    count++;
    return IRQ_NONE;
}

int init_module()
{
    if (request_irq(irq, irq_handle_function, IRQF_SHARED, interface, (void *)&irq))
    {
        MSG("[FAILED] IRQ register failure.\n");
        return -EIO;
    }
    MSG("[OK] Interface=%s IRQ=%d\n", interface, irq);
    return 0;
}

void cleanup_module()
{
    free_irq(irq, &irq);
    MSG("IRQ is freed.\n");
}

Makefile

obj-m := catchirq.o
KERNELDIR := /lib/modules/$(shell uname -r)/build

default:
    make -C $(KERNELDIR) M=$(shell pwd)

clean:
    make -C $(KERNELDIR) M=$(shell pwd) clean

/*
make
sudo insmod catchirq.ko interface=eth1 irq=21
*/

Relevant Link:

http://www.cnblogs.com/LittleHann/p/3850655.html
http://www.cnblogs.com/LittleHann/p/3850653.html
http://www.cnblogs.com/LittleHann/p/3871630.html
http://blog.csdn.net/droidphone/article/details/7445825
http://blog.sina.com.cn/s/blog_5ffeae360100ftwt.html
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CB8QFjAB&url=%68%74%74%70%3a%2f%2f%64%6f%77%6e%6c%6f%61%64%2e%69%6e%74%65%6c%2e%63%6f%6d%2f%64%65%73%69%67%6e%2f%70%72%6f%63%65%73%73%6f%72%2f%6d%61%6e%75%61%6c%73%2f%32%35%33%36%36%35%2e%70%64%66&ei=mtoPVaCvN42F8gWaxoC4Bw&usg=AFQjCNHw995h0UJxe5XBo2nbeA1xam7QTg&bvm=bv.88528373,d.dGc&cad=rjt
http://home.ustc.edu.cn/~boj/courses/linux_kernel/2_int.html

 

3. Linux 中断相关的源代码分析

中断是一个CPU硬件级的机制,是CPU为上层提供的一种响应机制,就像一棵树的根,因为有了中断,才有了上层的一些很有用的功能,例如

1. 系统调用(在2.6以后,系统调用不采用中断而是采用sysenter进入内核了)
2. 键盘鼠标外设
3. 硬盘的数据传输
4. 网卡的数据通信
...

我们从源代码角度逐步分析一下CPU中断的整个逻辑流程

0x1: 切换到和内核态

到内核态的切换,是基于每个中断之后由处理器自动执行的汇编代码的(暂不考虑Linux sysenter)。该代码是C语言和汇编语言的混合体,位于:sourcecode\arch\x86\kernel\entry_32.S中,其中定义了各个入口点,在中断发生时处理器可以将控制流转到这些入口点。通常,只有那些最为必要的操作直接在汇编语言代码中执行,内核试图尽快的返回到常规的C代码,因为C代码更容易处理。为此,必须创建一个环境。在C语言中调用函数时,需要将所需的数据(返回地址和参数)按一定的顺序放到栈上,在用户态和内核态之间切换时,还需要将最重要的寄存器保存到栈上,以便以后恢复。这2个操作由平台相关的汇编代码执行。

sourcecode\arch\x86\kernel\entry_32.S

..
common_interrupt:
    addl $-0x80,(%esp)    /* Adjust vector into the [-256,-1] range */
    SAVE_ALL
    TRACE_IRQS_OFF
    movl %esp,%eax
    call do_IRQ
    jmp ret_from_intr
ENDPROC(common_interrupt)
    CFI_ENDPROC
..

在大多数平台上,控制流接下来传递到C函数do_IRQ,其实现也是平台相关的,但在Linux 2.6内核之后,情况仍然得到了很大的简化,根据平台不同,该函数的参数或者是处理器寄存器的集合

1. x86
\linux-3.15.5\arch\x86\kernel\irq.c
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)

2. arm
\linux-3.15.5\arch\arm\kernel\irq.c
asmlinkage void __exception_irq_entry asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

pt_regs用于保存内核使用的寄存器集合,各个寄存器的值被依次压栈(通过汇编代码),在C函数调用之前,一直保存在栈上(为了遵循C函数的调用约定)。pt_regs的定义可以确保栈上的各个寄存器项与该结构的各个成员相对应,这些值并不是仅仅保存用于后续的使用,C代码也可以读取这些值

struct pt_regs的定义是平台相关的,因为不同的处理器提供了不同的寄存器集合,pt_regs中包含了内核使用的寄存器,其中不包含的寄存器,可能只能由用户态应用程序使用

1. IA-32 CPU体系结构下,pt_regs通常定义如下
\linux-3.15.5\arch\x86\include\asm\ptrace.h 
struct pt_regs {
    unsigned long bx;
    unsigned long cx;
    unsigned long dx;
    unsigned long si;
    unsigned long di;
    unsigned long bp;
    unsigned long ax;
    unsigned long ds;
    unsigned long es;
    unsigned long fs;
    unsigned long gs;
    unsigned long orig_ax;
    unsigned long ip;
    unsigned long cs;
    unsigned long flags;
    unsigned long sp;
    unsigned long ss;
};
#else /* __i386__ */
struct pt_regs {
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long bp;
    unsigned long bx;
/* arguments: non interrupts/non tracing syscalls only save up to here*/
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long ax;
    unsigned long cx;
    unsigned long dx;
    unsigned long si;
    unsigned long di;
    unsigned long orig_ax;
/* end of arguments */
/* cpu exception frame or undefined */
    unsigned long ip;
    unsigned long cs;
    unsigned long flags;
    unsigned long sp;
    unsigned long ss;
/* top of stack page */
};


2. PA-Risc CPU体系结构下
\linux-3.15.5\arch\parisc\include\uapi\asm\ptrace.h 
struct pt_regs {
    unsigned long gr[32];    /* PSW is in gr[0] */
    __u64 fr[32];
    unsigned long sr[ 8];
    unsigned long iasq[2];
    unsigned long iaoq[2];
    unsigned long cr27;
    unsigned long pad0;     /* available for other uses */
    unsigned long orig_r28;
    unsigned long ksp;
    unsigned long kpc;
    unsigned long sar;    /* CR11 */
    unsigned long iir;    /* CR19 */
    unsigned long isr;    /* CR20 */
    unsigned long ior;    /* CR21 */
    unsigned long ipsw;    /* CR22 */
};

我们知道,在IA-32体系结构的系统上,被引发中断的编号(中断号)保存在orig_eax的高8位中,这是个很重要的概念,即传统的"基于80中断的系统调用"、以及"外设引发的中断",都必须将中断号置入eax寄存器中,然后再触发trap陷门

0x2: 执行中断服务例程(ISR interrupt service routines)

在触发了CPU中断,ENTRY_32.S将寄存器集合的当前状态传递到do_IRQ

__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
    //将一个指向寄存器集合的指针保存在一个全局的CPU变量中(中断发生之前,变量中保存的旧指针会保留下来,供后续使用)。需要访问寄存器集合的中断处理程序,可以从该变量中访问
    struct pt_regs *old_regs = set_irq_regs(regs);

    /* high bit used in ret_from_ code  */
    unsigned vector = ~regs->orig_ax;
    unsigned irq;

    /*
    irq_enter负责更新一些统计量,对于具备动态时钟周期特性的系统,如果系统已经有很长一段时间没有发生时钟中断,则更新全局计时变量jiffiles
    接下来,调用所述IRQ注册的ISR的任务委托给体系结构无关的函数generic_handle_irq,它调用irq_desc[irq]->handle_irq来激活电流控制处理程序
    */
    irq_enter();
    exit_idle();

    irq = __this_cpu_read(vector_irq[vector]);

    if (!handle_irq(irq, regs)) 
    {
        ack_APIC_irq(); 
        if (irq != VECTOR_RETRIGGERED) 
        {
            pr_emerg_ratelimited("%s: %d.%d No irq handler for vector (irq %d)\n", __func__, smp_processor_id(), vector, irq);
        } 
        else 
        {
            __this_cpu_write(vector_irq[vector], VECTOR_UNDEFINED);
        }
    }

    /*
    irq_exit负责记录一些统计量,另外还要调用do_softirq来处理任何待决的软件IRQ
    */
    irq_exit(); 
    set_irq_regs(old_regs);
    return 1;
}

在实现处理程序例程时,必须要注意一些要点,这些会极大地影响系统的性能和稳定性

1. 限制
需要注意的是,内核代码的运行会处于两种上下文状态
    1) 中断上下文(interrupt context)
    2) 常规上下文
在实现ISR(interrupt service routines)时,ISR一定是在"中断上下文(interrupt context)"中执行的,为了区分当前内核的这两种情况,内核提供了in_interrupt函数,用于指明当前是否在处理中断
/*
中断上下文和普通上下文的区别之处在于
0x1: 中断是异步执行的,它可以在任何时间发生。因而从用户空间来看,处理程序例程不是在一个明确定义的环境中执行。这种情况下,禁止访问用户空间,特别是与用户空间地址之间来回复制内存数据的行为。
例如:网络驱动程序不能将接收到的数据直接转发到等待的应用程序,因为内核无法确定等待数据的应用程序此时是否正在运行(是否获得CPU调度资源)

0x2: 中断上下文中不能调用调度器,因而不能自愿地放弃控制权
换句话说,这个时候不能使用自旋锁等操作,因为在关中断的情况下,CPU已经不响应外部的中断请求了,这个时候进行CPU调度会直接导致panic

0x3: 处理程序例程不能进入睡眠状态。只有在外部事件导致状态改变并唤醒进程时,才能解除睡眠状态。但中断上下文不允许中断,进程睡眠后,内核只能永远等待下去,因为也不能调用调度器,不能选择进程来执行。
要特别注意的是:只确保处理程序例程的直接代码不进入睡眠状态这是不够的,其中调用的所有过程和函数(以及这些函数的子函数/子过程,以此类推)都不能进入睡眠状态
*/

2. 实现处理程序
中断处理程序只能使用两种返回值:
    1) 正确地处理了IRQ: 返回IRQ_HANDLED
    2) ISR不负责该ISR: 返回IRQ_NONE

0x3: 退出路径

当ISR执行完毕后,代码逻辑继续回到ENTRY_32.S中,代码对当前内核栈和寄存器进行现场恢复,准备回到用户态继续运行

ENTRY(resume_userspace)
    LOCKDEP_SYS_EXIT
     DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    andl $_TIF_WORK_MASK, %ecx    # is there any work to be done on
                    # int/exception return?
    jne work_pending
    jmp restore_all
END(ret_from_exception)

Relevant Link:

深入linux内核架构(中文版).pdf 14章

 

4. Linux 硬件中断

0x1: 硬中断的嵌套

linux下的硬中断处理是可以嵌套的,并且没有优先级。也就是说,一个中断可以打断正在执行的中断(同种中断除外)。无优先级地支持硬中断嵌套有两个主要原因

1. 短时间内接受更多的中断,可以有大的设备控制吞吐量
2. 无优先级可以简化内核,提高移植性 

0x2: 硬中断Linux实现流程

硬中断的具体linux实现流程是

1. 硬中断的汇编处理->
2. do_IRQ->
3. handle_irq->
4. handle_edge_irq(handle_level_irq)->
5. handle_irq_event->
6. 具体设备的硬中断处理

同种中断不嵌套是通过设置该种中断的数据结构的IRQD_IRQ_INPROGRESS标志位来屏蔽(本cpu和其它cpu的同种中断)的,置位表示已经在处理该种中断了,同种中断会判断此标志而退出,同时置上IRQS_PENDING标志位表示此种中断还需继续处理。本质上类似于软中断的防止嵌套的套路

硬中断的提速很重要,不管是拆分出软中断还是中断线程化,都是希望硬中断不要过多影响用户进程(特别是实时进程),毕竟有些硬中断和某些高优先级用户进程相比,其实并不重要

Relevant Link:

http://blog.csdn.net/droidphone/article/details/7445825
http://mp.weixin.qq.com/s?__biz=MzAxNjM3MDkyOQ==&mid=205285282&idx=1&sn=c2ac2b0c70b75089984d8d38e423cd4a&wm=3333_2001#rd

 

5. Linux 软中断

回到之前的那张CPU中断示意图,CPU的中断机制是CPU提供的一种硬件机制,从中断源来说,有两种中断

1. 来自外设硬件的硬件中断,电气级别的中断触发
2. 由CPU自身执行中触发的软中断

软中断(software interrupt)是完全用软件实现的,内核借助软中断来获知异常情况的发生,而该情况将在稍后由专门的处理程序例程解决。内核在do_IRQ末尾处理所有待决软中断,因而可以确保软中断能够定期得到处理。从一个更抽象的角度来看,可以将软中断描述为一种延迟到稍后时刻执行的内核活动。

例如,网卡接收数据包,从网卡产生中断信号,CPU将网络数据包拷贝到内核,然后进行协议栈的处理,最后将数据部分传递给用户空间,这个过程都可以说是中断处理函数需要做的部分,但硬件中断处理仅仅做从网卡拷贝数据的工作,而协议栈的处理的工作就交给"可延迟执行"部分处理
而软中断,正是"可延迟处理"部分的一种机制,之所以叫软中断,是因为类似于硬件中断的过程

1. 产生中断信号
2. 维护软中断向量
3. 进行中断处理 

0x1: 软中断的执行流程

1. 初始化: 定义一个新的可延迟函数,这个操作通常在内核自身初始化或加载过模块时进行 
2. 激活: 标记一个可延迟函数为"挂起"状态(可延迟函数的下一轮调度中可执行),激活可以在任何时后进行(即是正在处理中断)
3. 屏蔽: 有选择的屏蔽一个可延迟函数,这样,即使它被激活,内核也不执行它 
4. 执行: 执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数,执行是在特定的时间内进行的 

0x2: 软中断的类型

enum
{
        HI_SOFTIRQ=0,
        TIMER_SOFTIRQ,
        NET_TX_SOFTIRQ,
        NET_RX_SOFTIRQ,
        BLOCK_SOFTIRQ,
        BLOCK_IOPOLL_SOFTIRQ,
        TASKLET_SOFTIRQ,
        SCHED_SOFTIRQ,
        HRTIMER_SOFTIRQ,
        RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

        NR_SOFTIRQS
};

Linux内核需要将枚举值转化为字符串数组,\linux-3.15.5\kernel\softirq.c

const char * const softirq_to_name[NR_SOFTIRQS] = 
{
    "HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "BLOCK_IOPOLL",
    "TASKLET", "SCHED", "HRTIMER", "RCU"
};

0x3: 软中断涉及的数据结构

软中断机制的核心部分是一个表(softirq_vec),它包含32个softirq_action类型的的数据项

\linux-3.15.5\include\linux\interrupt.h

struct softirq_action
{
    //指向处理程序例程的指针,在软中断发生时由内核执行该处理程序例程
    void    (*action)(struct softirq_action *);
};

该数据结构的定义是和体系结构无关的,即它和下层的CPU中断已经没有直接耦合关系了,而软中断机制的整个实现也是如此

0x4: 软中断的初始化

我们知道,内部中断(软中断)和外部中断(硬件中断)的区别,对于内部中断的初始化,主要是设置中断向量表(IDT)。而对于外部中断,除了中断向量表之外,还要初始化中断控制器(8259A),以及中断控制器相关的管理结构

软中断必须先注册,然后内核才能执行软中断

\linux-3.15.5\kernel\softirq.c

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

open_softirq在softirq_vec表中指定的位置写入新的软中断,在softirq_init()函数中循环调用,对全局变量softirq_vec(软中断向量表)进行赋值设置

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) 
    {
        per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

0x5: 软中断的激活

\linux-3.15.5\kernel\softirq.c

asmlinkage __visible void __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;
    int softirq_bit;
    int cpu;

    /*
     * Mask out PF_MEMALLOC s current task context is borrowed for the
     * softirq. A softirq handled such as network RX might set PF_MEMALLOC
     * again if the socket is related to swap
     */
    current->flags &= ~PF_MEMALLOC;

    /*
    首先要确认当前不处于中断上下文中,如果处于中断上下文,则立即结束,因为软中断用于执行ISR中非时间关键部分,所以其代码本身一定不能在中断处理程序内调用
    通过local_softirq_pending,确定当前CPU软中断位图中所置位的比特位
    */
    pending = local_softirq_pending();
    account_irq_enter_time(current);

    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    in_hardirq = lockdep_softirq_start();

    cpu = smp_processor_id();
restart:
    /* Reset the pending bitmask before enabling irqs */
    set_softirq_pending(0);

    local_irq_enable();

    h = softirq_vec;

    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;

        h += softirq_bit - 1;

        vec_nr = h - softirq_vec;
        prev_count = preempt_count();

        kstat_incr_softirqs_this_cpu(vec_nr);

        trace_softirq_entry(vec_nr);
        h->action(h);
        trace_softirq_exit(vec_nr);
        if (unlikely(prev_count != preempt_count())) {
            pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
                   vec_nr, softirq_to_name[vec_nr], h->action,
                   prev_count, preempt_count());
            preempt_count_set(prev_count);
        }
        rcu_bh_qs(cpu);
        h++;
        pending >>= softirq_bit;
    }

    local_irq_disable();

    pending = local_softirq_pending();
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;

        wakeup_softirqd();
    }

    lockdep_softirq_end(in_hardirq);
    account_irq_exit_time(current);
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

softirq_vec中的action函数在一个while循环中针对各个待决的软中断被调用。在处理了所有标记出的软中断之后,内核检查在此期间是否有新的软中断标记到位图中,这种操作会一直持续下去,直至在执行所有处理程序之后没有新的未处理软中断未知
0x6: 软中断的屏蔽
0x7: 软中断的执行

0x8: 软中断的嵌套

同种软中断不可以嵌套,但可以并行在不同CPU上,即使是同种类型

Relevant Link:

http://rock3.info/blog/2013/11/20/%E8%BD%AF%E4%B8%AD%E6%96%AD%E5%8E%9F%E7%90%86/
http://blog.csdn.net/droidphone/article/details/7518428http://www.crashcourse.ca/wiki/index.php/Softirqs

 

6. 中断优先级

在CPU硬件这个概念级别上,CPU对中断的处理顺序如下

1. 对内核来说,假设当前有两个设备同时发出中断请求 
2. CPU首先处理优先级最高的中断
3. 中断控制器向CPU报告优先级高的中断(中断A),低优先级的中断(中断B)被延迟
4. 内核把当前运行级别提升到高优先级的IRQL
5. 在高优先级的中断处理例程中,如果出现了一个比当前优先级更高优先级的IRQL请求(中断C),则将当前中断处理例程(中断A)放入就绪队列挂起
6. 当最高优先级的中断处理例程(中断C)处理完毕之后,然后处理延迟的调度请求(中断A、中断B)
/*
中断A->中断C->中断A->中断B
*/
7. 当全部中断都处理完毕后,就会处理延迟的软件中断(即在do_IRQ的末尾检测是否有软中断需要处理)
8. 最后再返回用户态

在CPU的中断处理过程中,有一个隐含的优先级,对于Linux内核我们同样可以模仿windows内核使用IRQL优先级的概念,把统一的优先级称为IRQL,每个级别有一个对应的IRQL,优先级别越高,对应的IRQL越高

0x1: 高级可编程中断控制器(Advanced Programmable Interrupt Controller APIC)

8259A只适用于单处理器,为了满足多处理器的需要,出现了高级可编程中断控制器

大多数x86平台都包含了APIC,每一个CPU内部都有一个本地APIC,本地APIC可以产生时钟中断,可以向其他的处理发送处理器间中断等。系统可以有一个或多个I/O APIC,每个I/O APIC支持24个中断出入信号,I/O APIC和Local APIC之间通过总线连接

APIC中的每个LINTn和IRQn分别有一个64位配置寄存器,被称为Interrupt Redirection Table,这些寄存器被映射到内存地址空间

详细的信息请参阅《Intel® 64 and IA-32 Architectures Developer's Manual: Vol. 3A》
http://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html

0x2: APIC初始化

系统启动阶段需要根据配置,重新设置CPU的中断向量等相关信息,这些配置是硬件相关的,为此Intel指定了MultiProcessor Specification。它规定了主板设计者必须把硬件相关的信息按照统一的数据格式集成到BIOS中,这样操作系统就可以通过BIOS获取到相关信息

\linux-3.15.5\arch\x86\kernel\mpparse.c

void __init default_find_smp_config(void)
{
    unsigned int address;

    /*
     * FIXME: Linux assumes you have 640K of base ram..
     * this continues the error...
     *
     * 1) Scan the bottom 1K for a signature
     * 2) Scan the top 1K of base RAM
     * 3) Scan the 64K of bios
     */
    if (smp_scan_config(0x0, 0x400) ||
        smp_scan_config(639 * 0x400, 0x400) ||
        smp_scan_config(0xF0000, 0x10000))
        return;
    /*
     * If it is an SMP machine we should know now, unless the
     * configuration is in an EISA bus machine with an
     * extended bios data area.
     *
     * there is a real-mode segmented pointer pointing to the
     * 4K EBDA area at 0x40E, calculate and scan it here.
     *
     * NOTE! There are Linux loaders that will corrupt the EBDA
     * area, and as such this kind of SMP config may be less
     * trustworthy, simply because the SMP table may have been
     * stomped on during early boot. These loaders are buggy and
     * should be fixed.
     *
     * MP1.4 SPEC states to only scan first 1K of 4K EBDA.
     */

    address = get_bios_ebda();
    if (address)
        smp_scan_config(address, 0x400);
}

\linux-3.15.5\arch\x86\include\asm\bios_ebda.h

/*
 * Returns physical address of EBDA.  Returns 0 if there is no EBDA.
 */
static inline unsigned int get_bios_ebda(void)
{
    /*
     * There is a real-mode segmented pointer pointing to the
     * 4K EBDA area at 0x40E.
     */
    unsigned int address = *(unsigned short *)phys_to_virt(0x40E);
    address <<= 4;
    return address;    /* 0 means none */
}

BIOS会提供一个Intel中规定的MP table,它的头4个字节为_MP_,smp_scan_config()就是根据在BIOS数据区中寻找这个表,如果找到就打印:Scan for SMP in [mem %#010lx-%#010lx]

/source/arch/x86/kernel/irqinit.c

static void __init apic_intr_init(void)
{
    smp_intr_init();

#ifdef CONFIG_X86_THERMAL_VECTOR
    alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
#ifdef CONFIG_X86_MCE_THRESHOLD
    alloc_intr_gate(THRESHOLD_APIC_VECTOR, threshold_interrupt);
#endif

#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
    /* self generated IPI for local APIC timer */
    alloc_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt);

    /* IPI for X86 platform specific use */
    alloc_intr_gate(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi);
#ifdef CONFIG_HAVE_KVM
    /* IPI for KVM to deliver posted interrupt */
    alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif

    /* IPI vectors for APIC spurious and error interrupts */
    alloc_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);
    alloc_intr_gate(ERROR_APIC_VECTOR, error_interrupt);

    /* IRQ work interrupts: */
# ifdef CONFIG_IRQ_WORK
    alloc_intr_gate(IRQ_WORK_VECTOR, irq_work_interrupt);
# endif

#endif
}

在apic_intr_init()中,内核为Local APIC设置了一些新的中断处理函数,对Local APIC中的配置寄存器进行配置后,这样当Local APIC产生中断时,就会调用对应的中断处理函数了

 

7. CPU在关中断状态下编程要注意的事项

0x1: 关中断下内核内存申请的限制

典型的在kprobe的CPU关中断情况下,使用kmalloc、vmalloc需要注意一些限制

1. 不能使用vmalloc
    1) vmalloc可能导致缺页中断
    2) vmalloc在内部使用了kmalloc(.., PAGE_KERNEL),这可能导致在申请内存的时候的睡眠,因为这个使用CPU无法响应中断,随机导致kernel panic
2. 如果使用kmalloc
    1) 不能使用PAGE_KERNEL,原因和vmalloc的"2)"是相同的
    2) 必须使用GFP_ATOMIC

关于kmalloc、vmalloc的相关知识,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/4113830.html

0x2: 关中断下内核中禁止使用信号量这种锁

由于信号量可能引起进程切换,但是在中断环境中是不允许进程切换的,否则会引起Kernel Panic

为了解决这个问题,应该采用自旋锁(spinlock)

关于Linux内核中锁的相关知识,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/4116368.html

0x3: 关中断下内核中禁止使用copy_from_user、strncpy_from_user进行KernelSpace <-> UserSpace之间复制内存数据

在中断状态下,禁止使用copy_from_user、strncpy_from_user进行与用户空间地址之间来回复制内存数据的行为,因为

1. copy_from_user、strncpy_from_user都是在进行从用户态到内核态的内存复制
2. 内核无法确定等待数据的应用程序此时是否正在运行(是否获得CPU调度资源)
3. 在copy_from_user、strncpy_from_user运行中,有一定概率发生当前正在复制的UserSpace内存被Page Out,这个时候进行内存复制就会触发缺页中断(Page Fault),缺页中断会引发一个CPU中断
4. 在Kprobe关中断情况下,回调Handler函数中引发的CPU中断是无法得到响应的
5. copy_from_user、strncpy_from_user在复制过程中引发的Page Fault,不能得到响应,只会被自身汇编代码中的.fixup函数段捕获,并打印:  do_page_fault....,但其实这个时候已经是不正常现象了
6. Linux在发生了Page Fault之后,不会立刻Kernel Panic,而是继续运行,但是此时系统已经处于不正常状态了,如果继续运行,最终可能导致Panic

copy_from_user的内核代码分析

1. http://lxr.free-electrons.com/source/arch/x86/include/asm/uaccess.h#L688
2. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L681
3. http://lxr.free-electrons.com/source/arch/x86/include/asm/uaccess.h#L88
4. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L583
5. http://lxr.free-electrons.com/source/arch/x86/lib/usercopy_32.c#L531

strncpy_from_user的内核源代码分析

http://www.cs.fsu.edu/~baker/devices/lxr/http/source/2.6.25.8/linux/arch/x86/lib/usercopy_64.c?v=.
http://www.verydemo.com/demo_c378_i61436.html
http://www.tuicool.com/articles/uYzAzy
http://www.hep.by/gnu/kernel/kernel-api/API-strncpy-from-user.html
http://blog.csdn.net/justlinux2010/article/details/8972754
https://www.kernel.org/doc/htmldocs/device-drivers/API-might-sleep.html

为了重现这种观点,可以尝试编译并运行以下代码

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
static char buf[] = "hello";
static char buf1[10];

int fileOp(void)
{
struct file *fp;
mm_segment_t fs;
loff_t pos;
printk("hello enter\n");
fp = filp_open("test", O_RDWR | O_CREAT, 0644);
if (IS_ERR(fp)) 
{
printk("create file error\n");
return -1;
}
fs = get_fs();
set_fs(KERNEL_DS);
pos = 0;
vfs_write(fp, buf, sizeof(buf), &pos);
pos = 0;
vfs_read(fp, buf1, sizeof(buf), &pos);
printk("read: %s\n", buf1);
filp_close(fp, NULL);
set_fs(fs);
}

int __init hello_init(void)
{
__asm__("cli");

fileOp();
return 0;
}
void __exit hello_exit(void)
{ 
printk("hello exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
//在关中断情况下触发中断会导致Kernel Panic

代码做了如下的事情

1. 代码关闭了CPU的硬件中断EFLAGS开关,此时CPU屏蔽一切可屏蔽中断
2. 向磁盘写文件,这是个绝对是触发硬件中断的操作,这个时候CPU无法响应这个硬件中断
3. 此时引发的PAGE FAULT缺页中断处于无人应答状态
4. LINUX内核选择忽略这个PAGE FAULT继续运行,但是这个内存是一个Bad Address
5. 处于"异常状态"的Linux内核最终在某个时刻PANIC奔溃

Relevant Link:

http://blog.csdn.net/yangdelong/article/details/5491097
http://oss.org.cn/kernel-book/ldd3/ch06s02.html
http://blog.csdn.net/eroswang/article/details/3529750
http://blog.csdn.net/ce123_zhouwei/article/details/8454226

 

Copyright (c) 2014 LittleHann All rights reserved

 

posted @ 2014-11-24 19:14  郑瀚Andrew  阅读(2426)  评论(0编辑  收藏  举报