I/O中断处理

from:I/O中断处理


一般而言I/O中断处理程序必须足够灵活以给多个设备同时提供服务,例如在PCI总线的体系结构中,几个设备可以共享同一个IRQ线,这就意味着仅仅中断向量并不能说明所有问题。中断处理程序的灵活性是以两种不同的方式实现的:

IRQ共享

中断处理程序执行多个中断服务例程(interrupt service routine,ISR),每个ISR是一个与单独设备相关的函数,因为不可能预先直到哪个特定的设备产生IRQ,因此,每个ISR都被执行,以验证它的设备是否需要关注,如果是,当设备产生中断时,就执行需要执行的所有操作。

IRQ动态分配

一条IRQ线在可能的最后时刻才与一个设备驱动程序关联,例如光驱里的IRQ线只有在用户需要访问的时候才被分配,这样,即使几个硬件设备并不共享IRQ线,同一个IRQ向量页可以由这几个设备在不同时刻使用。

当一个中断发生时,并不是所有的操作都具有相同的紧迫性,所以,把所有的操作都放进中断处理程序本身并不合适,需要时间长的非常重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ线上发出的信号就暂时被忽略。

更重要的时,中断处理程序时代表进程执行的,它代表的进程的状态总是应该处于TASK_RUNNING的状态,否则就可能出现系统僵死的情况,所以中断处理程序不能执行任何阻塞的操作,例如磁盘I/O操作,因此,Linxu把中断马上要执行的操作分为三类:

  1. 紧急的(Critical):这样的操作如PIC应答中断,对PIC或设备控制器重编程或者修改由设备和处理器同时访问的数据结构都应该尽可能快额度执行。紧急操作要在一个中断处理程序内立即执行,而且必须在禁止可屏蔽中断的情况下。

  2. 非紧急的(Noncritical):这样的操作如修改那些只有处理才会访问的数据结构,这些操作页要很快地完成,因此它们由中断处理程序立即执行,但必须时在开中断的情况下。

  3. 非紧急可延迟的(Noncritical deferrable),这样的操作例如把缓冲区的内容拷贝到某个进程的地址空间,例如把键盘缓冲区的内容发送到终端处理程序,这些操作可能被延迟较长的时间间隔而不影响内核操作。非紧急可延迟中断由单独的程序执行。

不管引起中断的种类如何,所有I/O中断处理程序都执行四个相同的步骤:

  1. 在内核态堆栈中保存IRQ的值和寄存器的内容。

  2. 为正在给IRQ线服务的PIC发送一个应答,这允许PIC进一步发出中断。

  3. 执行共享这个IRQ的所有设备的中断服务例程ISR。

  4. 跳到ret_from_intr()的地址后终止。

如下图:
I/O IRQ

I/O中断处理


中断向量


物理IRQ可以分配给32~238范围内的任何向量,不过Linux使用向量128实现系统调用。由于IBM PC兼容的体系结构要求,一些设备必须被静态地连接到指定的IRQ线,如下:

  1. 间隔定时设备必须连到IRQ0线1

  2. 从8259A PIC必须与IRQ2线相连2

  3. 必须把外部数学协处理器连接到IRQ13线,虽然最近的80x86处理器不再使用这样的设备。

  4. 一般而言,一个I/O设备可以连接到有限个IRQ线。

PIC的概念可以点此连接。Linux中的中断向量如下:

向量范围用途
0~19非屏蔽中断和异常
20~31Intel保留
32~127外部中断IRQ
128Linux用于系统调用的可编程异常
129~238外部中断IRQ
239本地APIC时钟中断
240本地APIC高温中断
241~250Linux保留
251~253处理器间中断
254本地APIC错误中断
255本地APIC伪中断

其中本地APIC伪中断由CPU屏蔽某个中断时产生。

为IRQ可配置设备选择一条线有三种方式:

  1. 设置一些硬件跳线,但只适用于旧式设备卡。

  2. 安装设备时执行一个使用程序,这样的程序可以让用户选择一个可用的IRQ号,或者探测系统自身以确定一个可用的IRQ号。

  3. 在系统启动时执行一个硬件协议,外设宣布它们准备使用哪些中断线,然后协商一个最终的值以尽可能的减少冲突。

内核必须在启动中断前发现IRQ号与I/O设备之间的对应,否则,内核在不知道哪个向量对应哪个设备的情况下,无法处理来自这个设备的信号。IRQ号与I/O设备之间的对应是在初始化每个设备驱动程序时建立的。

IRQ数据结构


每个中断向量都有自己的irq_desc描述符,代码如下:

<include/linux/irq.h>



struct irq_desc {
    unsigned int        irq;
    unsigned int            *kstat_irqs;
#ifdef CONFIG_INTR_REMAP    struct irq_2_iommu      *irq_2_iommu;
#endif    irq_flow_handler_t  handle_irq;
    struct irq_chip     *chip;
    struct msi_desc     *msi_desc;
    void            *handler_data;
    void            *chip_data;
    struct irqaction    *action;
    unsigned int        status;

    unsigned int        depth;
    unsigned int        wake_depth;
    unsigned int        irq_count;
    unsigned long       last_unhandled;
    unsigned int        irqs_unhandled;
    spinlock_t      lock;
#ifdef CONFIG_SMP    cpumask_var_t       affinity;
    unsigned int        node;
#ifdef CONFIG_GENERIC_PENDING_IRQ    cpumask_var_t       pending_mask;
#endif
#endif    atomic_t        threads_active;
    wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FS    struct proc_dir_entry   *dir;
#endif    const char      *name;
} ____cacheline_internodealigned_in_smp;


其中重要字段的意义如下:

字段说明
irqIRQ线
time_rand_statetimer和state的指针
kstat_irqsper-CPU irq状态
handle_irqIRQ线上的事件处理,如果为空,则调用_do_IRQ()
msi_descMSI描述符
handler_dataIRQ线上的数据
actionIRQ线上的动作
status状态信息
depth如果IRQ线被激活,则显示为0,如果IRQ线被禁止了不止一次,则显示一个正数
wake_depth嵌套IRQ调用
irq_countIRQ线上发生的中断数
last_unhandled最后一次没有处理的中断的时间
irqs_unhandled意外中断的总次数
lockSMP系统的锁
node为了均衡使用的索引

如果一个中断内核没有处理,那么这个中断就是意外中断,也就是说,与某个IRQ线县官的中断处理例程ISR不存在,或者与某个中断线相关的所有例程都识别不出是否时自己硬件发出的中断。通常,内核检查从IRQ线接收以外中断的数量,当这条IRQ线连接的有故障的设备没完没了的发出中断,就禁用这条IRQ线。

由于多个设备会共有一条IRQ线,所以内核不会在每次检测到一个意外中断就立即禁止IRQ线,内核把中断和意外中断的总次数分别放在irq_desc描述符的irq_countirqs_unhandled字段中,当产生100000次中断时,意外中断超过99900,内核才禁用这条IRQ线。

IRQ的状态标志如下:

字段说明
IRQ_INPROGRESSIRQ的一个处理程序正在执行
IRQ_DISABLED由一个设备驱动程序故意的禁用IRQ
IRQ_PENDING一个IRQ已经出现在线上,它的出现页对PIC做出应答
IRQ_REPLAYIRQ线被禁用,但是前一个出现的IRQ还没有对PIC做出应答
IRQ_AUTODETECT内核在执行硬件设备探测时使用IRQ线
IRQ_WAITING内核在执行硬件设备探测时使用IRQ线但相应的中断还没有产生
IRQ_LEVEL80x86结构上未使用
IRQ_MASKED未使用
IRQ_PER_CPU80x86结构上未使用
IRQ_NOPROBEIRQ线无法进行探测
IRQ_NOREQUESTIRQ线无法被请求
IRQ_NOAUTOENIRQ线无法被激活以请求

多个设备共享一个单独的IRQ,所以内核要维护多个irqaction描述符,也就是上面的action,其中每个描述符涉及一个特定的硬件设备和特定的中断,irqaciton代码如下所示:

<include/linux/interrupt.h>



struct irqaction {
    irq_handler_t handler;
    unsigned long flags;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
    irq_handler_t thread_fn;
    struct task_struct *thread;
    unsigned long thread_flags;
};


其中重要字段的意义如下:

字段说明
handler指向一个I/O设备的中断服务例程,这是允许多个设备共享同一个IRQ的关键字段
flags描述IRQ与I/O设备之间的关系
mask未使用
nameI/O设备名
dev_idI/O设备私有字段
next链表的下一个irqaction元素
irqIRQ线
dir指向与IRQn相关的/proc/irq/n目录的描述符

  1. 我们以后可以看到本地时钟产生的中断的IRQ代码里设定的是irq0。

  2. 虽然现在有更先进的PIC,但Linux还是支持8259风格的PIC。