Linux05:中断机制

1.中断概念

      中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。

      在实地址模式中,CPU把内存中从0开始的1KB空间作为一个中断向量表。表中的每一项占4个字节。但是在保护模式中,有这4个字节的表项构成的中断向量表不满足实际需求,于是根据反映模式切换的信息和偏移量的足够使得中断向量表的表项由8个字节组成,而中断向量表也叫做了中断描述符表(IDT)。在CPU中增加了一个用来描述中断描述符表寄存器(IDTR),用来保存中断描述符表的起始地址。

2. Linux中断处理

  2.1 系统中断号

  由上述中断定义可知,系统中断向量表中共可保存256个中断向量入口,即IDT中包含的256个中断描述符(对应256个中断向量)。

  而0-31号中断向量被intel公司保留用来处理异常事件,不能另作它用。对这 0-31号中断向量,操作系统只需提供异常的处理程序,当产生一个异常时,处理机就会自动把控制转移到相应的处理程序的入口,运行相应的处理程序;而事实 上,对于这32个处理异常的中断向量,2.6版本的 Linux只提供了0-17号中断向量的处理程序,其对应处理程序参见下表、中断向量和异常事件对应表;也就是说,17-31号中断向量是空着未用的。

中断向量号 异常事件 Linux的处理程序
0 除法错误 Divide_error
1 调试异常 Debug
2 NMI中断 Nmi
3 单字节,int 3 Int3
4 溢出 Overflow
5 边界监测中断 Bounds
6 无效操作码 Invalid_op
7 设备不可用 Device_not_available
8 双重故障 Double_fault
9 协处理器段溢出 Coprocessor_segment_overrun
10 无效TSS Incalid_tss
11 缺段中断 Segment_not_present
12 堆栈异常 Stack_segment
13 一般保护异常 General_protection
14 页异常 Page_fault
15  (intel保留) Spurious_interrupt_bug
16 协处理器出错 Coprocessor_error
17 对齐检查中断 Alignment_check

  0-31号中断向量已被保留,那么剩下32-255共224个中断向量可用。 这224个中断向量又是怎么分配的呢?2.6版本的Linux中,除了0x80(SYSCALL_VECTOR)用作系统调用总入口之外,其他都用在外部硬件中断源上,其中包括可编程中断控制器8259A的15个irq;事实上,当 没有定义CONFIG_X86_IO_APIC时,其他223(除0x80外)个中断向量,只利用了从32号开始的15个,其它208个空着未用。

 2.2 中断请求

  2.2.1 中断请求概述

  外部设备当需要操作系统做相关的事情的时候,会产生相应的中断。

  设备通过相应的中断线向中断控制器发送高电平以产生中断信号,而操作系统则会从中断控制器的状态位取得那根中断线上产生的中断。而且只有在设备在对某一条中断线拥有控制权,才可以向这条中断线上发送信号。也由于现在的外设越来越多,中断线又是很宝贵的资源不可能被一一对应。因此在使用中断线前,就得对相应的中断线进行申请。无论采用共享中断方式还是独占一个中断,申请过程都是先讲所有的中断线进行扫描,得出哪些没有别占用,从其中选择一个作为该设备的IRQ。其次,通过中断申请函数申请相应的IRQ。最后,根据申请结果查看中断是否能够被执行。

  2.2.2 中断相关结构

  中断中核心处理数据结构为irq_desc,它完整的描述了一条中断线,Linux 2.6。22.6中源码如下。

  irq_desc定义在include/linux/irq.h中

     irq_desc

     其相关联的几个结构体如下:

  定义在include/linux/ interrupt.h中的中断行动结构体:struct irqaction

struct irqaction {
    irq_handler_t handler;
    unsigned long flags;
    cpumask_t mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
};

  定义在include/linux 中的:irq_chip 芯片相关的处理函数集合

/**
 * struct irq_chip - hardware interrupt chip descriptor
 *
 * @name:        name for /proc/interrupts
 * @startup:        start up the interrupt (defaults to ->enable if NULL)
 * @shutdown:        shut down the interrupt (defaults to ->disable if NULL)
 * @enable:        enable the interrupt (defaults to chip->unmask if NULL)
 * @disable:        disable the interrupt (defaults to chip->mask if NULL)
 * @ack:        start of a new interrupt
 * @mask:        mask an interrupt source
 * @mask_ack:        ack and mask an interrupt source
 * @unmask:        unmask an interrupt source
 * @eoi:        end of interrupt - chip level
 * @end:        end of interrupt - flow level
 * @set_affinity:    set the CPU affinity on SMP machines
 * @retrigger:        resend an IRQ to the CPU
 * @set_type:        set the flow type (IRQ_TYPE_LEVEL/etc.) of an IRQ
 * @set_wake:        enable/disable power-management wake-on of an IRQ
 *
 * @release:        release function solely used by UML
 * @typename:        obsoleted by name, kept as migration helper
 */
struct irq_chip {
    const char    *name;
    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        (*mask)(unsigned int irq);
    void        (*mask_ack)(unsigned int irq);
    void        (*unmask)(unsigned int irq);
    void        (*eoi)(unsigned int irq);

    void        (*end)(unsigned int irq);
    void        (*set_affinity)(unsigned int irq, cpumask_t dest);
    int        (*retrigger)(unsigned int irq);
    int        (*set_type)(unsigned int irq, unsigned int flow_type);
    int        (*set_wake)(unsigned int irq, unsigned int on);

    /* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
    void        (*release)(unsigned int irq, void *dev_id);
#endif
    /*
     * For compatibility, ->typename is copied into ->name.
     * Will disappear.
     */
    const char    *typename;
};

  2.2.3 中断请求实现

         上下半部机制

    我们期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,如何解决——上下半部机制。

    我们把中断处理切为两半。中断处理程序是上半部——接受中断,他就立即开始执行,但只有做严格时限的工作。能够被允许稍后完成的工作会推迟到下半部去,此后,在合适的时机,下半部会被开终端执行。上半部简单快速,执行时禁止一些或者全部中断。

    下半部稍后执行,而且执行期间可以响应所有的中断。这种设计可以使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。上半部只有中断处理程序机制,而下半部的实现有软中断实现,tasklet实现和工作队列实现。

    我们用网卡来解释一下这两半。当网卡接受到数据包时,通知内核,触发中断,所谓的上半部就是,及时读取数据包到内存,防止因为延迟导致丢失,这是很急迫的工作。读到内存后,对这些数据的处理不再紧迫,此时内核可以去执行中断前运行的程序,而对网络数据包的处理则交给下半部处理。

上下半部划分原则

  1) 如果一个任务对时间非常敏感,将其放在中断处理程序中执行;

  2) 如果一个任务和硬件有关,将其放在中断处理程序中执行;

  3) 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行;

  4) 其他所有任务,考虑放置在下半部执行。

下半部实现机制之软中断

  软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,软中断执行中断处理程序留给它去完成的剩余任务,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。它的特性包括:

  a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。

  b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。

下半部实现机制之tasklet

  tasklet是通过软中断实现的,所以它本身也是软中断。

  软中断用轮询的方式处理。假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。

  为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。

  Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。Tasklet作为一种新机制,显然可以承担更多的优点。正好这时候SMP越来越火了,因此又在tasklet中加入了SMP机制,保证同种中断只能在一个cpu上执行。在软中断时代,显然没有这种考虑。因此同一种软中断可以在两个cpu上同时执行,很可能造成冲突。

  总结下tasklet的优点:

  (1)无类型数量限制;

  (2)效率高,无需循环查表;

  (3)支持SMP机制;

  它的特性如下:

  1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。

  2)多个不同类型的tasklet可以并行在多个CPU上。

  3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。

下半部实现机制之工作队列(work queue)

  上面我们介绍的可延迟函数运行在中断上下文中(软中断的一个检查点就是do_IRQ退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。

  上面我们介绍的可延迟函数运行在中断上下文中,于是导致了一些问题,说明它们不可挂起,也就是说软中断不能睡眠、不能阻塞,原因是由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列,它能够在不同的进程间切换,以完成不同的工作。

  如果推后执行的任务需要睡眠,那么就选择工作队列,如果不需要睡眠,那么就选择软中断或tasklet。工作队列能运行在进程上下文,它将工作托付给一个内核线程。工作队列说白了就是一组内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。我们用结构体workqueue_struct表示工作者线程,工作者线程是用内核线程实现的。而工作者线程是如何执行被推后的工作——有这样一个链表,它由结构体work_struct组成,而这个work_struct则描述了一个工作,一旦这个工作被执行完,相应的work_struct对象就从链表上移去,当链表上不再有对象时,工作者线程就会继续休眠。因为工作队列是线程,所以我们可以使用所有可以在线程中使用的方法。

Linux软中断和工作队列的作用是什么

  Linux中的软中断和工作队列是中断上下部机制中的下半部实现机制。

  1.软中断一般是“可延迟函数”的总称,它不能睡眠,不能阻塞,它处于中断上下文,不能进城切换,软中断不能被自己打断,只能被硬件中断打断(上半部),可以并发的运行在多个CPU上。所以软中断必须设计成可重入的函数,因此也需要自旋锁来保护其数据结构。

  2.工作队列中的函数处在进程上下文中,它可以睡眠,也能被阻塞,能够在不同的进程间切换,以完成不同的工作。

可延迟函数和工作队列都不能访问用户的进程空间,可延时函数在执行时不可能有任何正在运行的进程,工作队列的函数有内核进程执行,他不能访问用户空间地址。

猜你喜欢

转载自blog.csdn.net/xuanying_china/article/details/81194080