Linux内核中断系统笔记(转)

1中断和异常

1.1中断的由来及实质

Linux内核要管理计算机上的硬件设备,首先要和他们通信。而处理器的速度跟外围硬件设备的速度往往不在一个数量级上,因此,如果内核采取让处理器向硬件发出一个请求,然后专门等待回应的办法,显然差强人意。既然硬件的响应这么慢,那么内核就应该在此期间处理其他事务,等到硬件真正完成了请求的操作之后,再回过头来对它进行处理。想要实现这种功能,轮询(polling)可能会是一种解决办法。可以让内核定期对设备的状态进行查询,然后做出相应的处理。不过这种方法很可能会让内核做不少无用功,因为无论硬件设备是正在忙碌着完成任务还是已经大功告成,轮询总会周期性地重复执行。更好的办法是由我们来提供一种机制,让硬件在需要的时候再向内核发出信号(变内核主动为硬件主动)。这就是中断机制。

由中断或异常处理程序执行的代码不是一个进程,它是一个内核控制路径,代表中断发生时正在运行的进程执行,内核控制路径比一个进程要“轻“。

1.2异常和中断

中断(interrupt)通常被定为一个事件,该事件改变处理器执行的指令顺序。

中断通常分为同步中断异步中断:

l   同步中断是当指令执行时由CPU控制单元产生的称为同步,是因为只有在一条指令执行完成后CPU 才会发生中断

l   异步中断是由其他硬件设备依照CPU始终信号随机产生的

同步中断又称为异常,异常是由程序的错误产生的(例如除0),或者是由内核必须处理的异常条件(例如缺页)产生的。前一种情况下,必须通知应用程序出现了异常,内核通过发送一个每个Unix程序员熟悉的信号来处理异常,后一种必须借助于内核才能修复,内核执行恢复异常需要的所有步骤。

异常和异步中断的相同点:如果CPU当前不处于核心态,则发起从用户态到内核态的转变,接下来。内核中执行一个专门的中断服务例程(ISR interrupt service routine)。

异常和异步中断的不同点:产生源的区别,产生原因的区别,及发生时间的区别。还有一方面,许多中断可以禁用,但有些不行。此后本文所说中断默认指的都是异步中断。

(注意,这里是Intel划分的,并不是LinuxLinux有自己的划分方法)更进一步,Intel文档中又把中断和异常继续进行了细分:

l   中断:

可屏蔽中断: I/O设备发出的所有的中断请求(IRQ)产生可屏蔽中断。可屏蔽中断产生两种状态:屏蔽的(masked)或非屏蔽的(unmasked);当中断被屏蔽,则CPU控制单元就忽略它。

非可屏蔽中断:总是由CPU辨认。只有几个危急事件引起非屏蔽中断。

l   异常:

1.处理器探测到得异常:

1)故障(fault)

通常可以纠正;一旦纠正,程序可以在不失连贯性的情况下重新开始。 保存在eip中的值是引起故障的指令地址。因此,当异常处理程序停止时,那条指令会重新执行

2)陷阱(trap)
在陷阱指令执行后立即报告;内核把控制权返回给程序后就可以继续它的执行而不失连贯性。保存在 eip中的值是一个随后要执行的指令地址。只有当没有必要重新执行已中止的指令时,才触发陷阱。陷阱的主要用途是为了调试程序。

3)异常终止(abort)
发生一个严重错误;CPU控制单元出了问题,不能在eip寄存器中保存引起异常指令所在的确切位置。这个异常中止处理程序除了强制中止受影响的进程中止外,没有别的选择。

 2.编程异常(programmed exception)

在编程者发出请求时发生。是由int或int3指令触发的。控制单元把编程异常作为陷阱来处理。编程异常也叫软中断。用途:执行系统调用及给调试程序通报一个特定的事件。

综上所述,从广义上讲,中断可分为四类:中断、故障、陷阱、终止。这些类别之间的同点请参看表 1。

表 1 中断的类别(此表摘自《深入理解计算机系统》)

类别

原因

异步/同步

返回行为

中断

来自 I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

返回到当前指令

终止

不可恢复的错误

同步

不会返回

在可能的情况下,内核试图避免禁用中断,因为禁用会损害系统性能(比如禁用键盘,就交互不友好了),但有些场合禁用中断是必要的,在处理第一个中断时,如果发生第二个中断,内核会发生严重的问题。如果内核在禁用中断的情况下,花费过多时间处理一个ISR(中断服务例程),会丢失一些系统正确运作不可必要的中断。并且因为中断随时可能发生,即中断处理程序随时可能执行,必须保证中断处理程序快速执行,这样才能保证尽可能快的恢复中断代码的执行,中断处理很重要,但对系统其它部分而言,中断处理程序尽可能短时间内处理完同样重要。为了解决这些问题,一般把中断处理且为两个部分。中断处理程序是上半部,接收到中断立即开始执行,但只做有严格时限的工作,例如对中断的应答,这些工作是在所有中断被禁止的情况下完成的,能够被允许稍后完成的会推迟到下半部去。此后,在合适的时机,下半部会被开中断执行。

1.2中断控制器

每个能发出中断请求的硬件设备控制器都有一条IRQ(Interrupt Request)输出线,它连接到可编程中断控制器(PIC),是PIC将中断请求转发到CPU的中断输入,外部设备不能直接发出中断,是通过PIC请求中断,所以中断更正确的叫法是IRQ,中断请求。IRQ线是从0开始顺序编号,第一条IRQ线表示成IRQ0,与IRQn关联度额Intel的缺省向量是n+32,这是因为0~31号是Intel保留为异常使用的。X86计算机的CPU为中断只提供了两条外接引脚:NMIINTR。其中NMI是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号,这些信号由中断控制器传递给 CPU。

常见的中断控制器有两种:

  1. 可编程中断控制器8259A

传统的PIC是由两片8259A风格的外部芯片以“级联”的方式连接在一起。每个芯片可处理多达8个不同的IRQ输入线。因为从PIC的INT输出线连接到主PIC的IRQ2引脚,所以可用IRQ线的个数限制为15 。

  1. 高级可编程中断控制器(APIC)

8259A只适合单CPU的情况,为了充分挖掘SMP体系结构的并行性,能够把中断传递给系统中的每个CPU至关重要。基于此理由,Intel引入了一种名位I/O高级可编程控制器的新组件,用以替代老式的8259A可编程中断控制器。此外,Intel当前所有的CPU都含有一个本地 APIC。每个本地APIC 都有32位的寄存器,一个内部时钟,一个本地定时设备及为本地中断保留的两条额外的IRQ线 LINT0 和 LINT1。所有本的APIC都连接到一个外部 I/O APIC,形成一个多APIC的系统。

目前大部分单处理器系统都包含一个I/O APIC芯片,可以通过以下两种方式来对这种芯片进行配置:

1)  作为一种标准 8259A方式的外部 PIC 连接到 CPU。本地 APIC 被禁止,两条 LINT0和 LINT1 分别连接到 INTR 和 NMI引脚。

2)  作为一种标准外部I/O APIC。本地APIC被激活,且所有的外部中断都通过I/O APIC接收。

由于Intel公司保留 0~31 号中断向量用来处理异常事件。因此,硬中断必须设在 31 以后,Linux则在实模式下初始化时把8259AIRQ0~IRQ15 设在 0x20~0x2fINT32~INT47)。既然0~31号中断向量被保留,就剩下32~255共224个中断向量可用。这224个中断向量又是怎么分配的呢?除了0x80(SYSCALL_VECTOR)(INT128)用作系统调用总入口外,其他都用在外部硬件中断源上,如可编程中断控制器 8259A的15个IRQ。事实上,当没有定义CONFIG_X86_IO_APIC时,其他223个(除 0x80 外)中断向量,只利用了从32号开始的15个(与 8259A中的15个IRQ相对应),其他208个都空着,具体分布情况请参看下表:

向量范围

用途

0~19(0x0~0x13)

非屏蔽中断和异常

21~31(0x14~0x1f)

Intel 保留

32~127(0x20~0x7f)

外部中断(IRQ)

128(0x80)

系统调用(重点)

129~238(0x81~0xee)

外部中断(IRQ)

239(0xef)

本地 APIC 时钟中断(重点)

240~250(0xf0~0xfa)

由 Linux 留做将来使用

251~255(0xfb~0xff)

处理器间中断(必须是SMP机器

1.3中断门

Intel在实现保护模式时,对CPU中断响应机制作了很大修改,中断向量表的表项变成了类似于入口地址加PSW(能够切换CPU运行模式及优先级)并且更复杂的项,称为“门”。只要想切换CPU的运行状态,即优先级别,就需要通过一道门,中断处理也是。X86 CPU中一共有四种门,即任务门、中断门、陷阱门以及调用门。其中调用门不与中断向量表相联系,即中断向量表上只有前三种门

Linux使用不同的分类:

关于陷阱的:

1)   系统门(system gate)

用户态的进程可以访问的一个Intel陷阱门(门的DPL字段是3)。通过系统门激活三个Linux异常处理程序,向量分别是4,5,128(int 0x80)。

2)   陷阱门(trap gate)

用户态的进程不能访问的一个Intel陷阱门(门的DPL字段是0)。

关于中断的:

1)   系统中断门:

能够被用户态进程访问的Intel中断门(门的DPL字段是3)。向量是3

2)   中断门:

用户态进程不能访问的Intel中断门(门的DPL字段是0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。

任务门(task gate):

不能被用户态进程访问的任务门(门的DPL字段是0)。Linux对”Double fault”异常处理程序是通过任务门激活的。

2中断处理内幕

在此,在2.6以后,Linux抽象出一个与平台无关的中断系统,代码放在kernel中。而与各平台相关的部分分散在各部分架构中。先来看一下抽象部分,kernel/irq/Makefile如下:

obj-y := handle.o manage.o spurious.o resend.o chip.o

obj-$(CONFIG_GENERIC_IRQ_PROBE) += autoprobe.o

obj-$(CONFIG_PROC_FS) += proc.o

obj-$(CONFIG_GENERIC_PENDING_IRQ) += migration.o

其中spurious.c是处理伪中断的,resend.c是重发中断的,在此我们关注handle.c manage.c和chip.c。在IA-32上与平台相关的内核代码文件有arch/i386/kernel/irq.c、arch/i386/kernel/apic.c、arch/i386/kernel/entry.S、arch/i386/kernel/i8259.c 以及include/asm-i386/hw_irq.c等。

2.1数据结构

2.1.1中断处理程序描述符irqaction(include/linux/interrupt.h)

struct irqaction {
    irqreturn_t (*handler)(int, void *, struct pt_regs *);
    unsigned long flags;
    cpumask_t mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
};

handler:函数指针。指向设备的中断响应函数,它在系统初始化时被置入,当中断发生时,系统将自动调用该函数。注意,handler函数的原型是特定的—它接收三个参数,并有一个类型为irqreturn_t的返回值。

flags:标志。指明中断类型,如正常中断、快速中断等

mask: 中断屏蔽字

name:中断设备名。这个宁子会被/proc/irq和proc/interrupt文件使用

dev_id: 中断设备id,主要用于共享中断线。当一条中断线是被多个中断设备共享的时候。在中断处理时,内核会按照dev_id逐个调用处理程序,并检查时不时本设备发出的中断。在中断处理程序释放时,dev_id提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的一个。

irq:表示要分配的终端号,对某些设备,如传统PC设备上的系统时钟或键盘是预定死的。对于大多数设备来说,要么是通过探测(probe)获取,要不通过编程动态确定。

next:指向下一个irqaction。

static irqreturn_t (*handler)(int, void *dev_id, struct pt_regs *regs);

第一个参数irq是中断线号,dev_id是一个通用指针,用来区分共享同一中断处理程序的多个设备,比如两个一样的硬盘。对于设备而言,设备结构是唯一的,通常把设备结构传递给dev_id。第三个参数regs是一个指向结构的指针,包含处理中断之前处理器的寄存器和状态。返回值的类型是reqreturn_t。中断处理程序可能返回两个特殊值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备不是注册处理函数时指定的源时,返回IRQ_NONE,正确调用,返回IRQ_HANDLED。使用宏IRQ_RETVAL(x),x为非0,宏返回IRQ_HANDLED;否则,返回IRQ_NONE。利用这些值,内核可以知道设备发出的是否是一种虚假的中断。中断处理程序一般是static,因为它不被别的文件中代码直接调用,static表明只在本代码文件可用。

2.1.2IRQ控制器抽象irq_chip(include/linux/irq.h)

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);
#ifdef CONFIG_IRQ_RELEASE_METHOD
    void        (*release)(unsigned int irq, void *dev_id);
#endif
    const char  *typename;
};

name:用于标识硬件控制器,在IA-32系统上可能的值是“XTPIC”和“IO-APIC”。

startup:指向一个函数,用于第一次初始化一个IRQ。大多情况下,初始化工作仅限于启用该IRQ,因而实际上就是将工作转给enable。

shutdown:完全关闭一个中断源。如果是NULL的话默认为 disable

enable:激活一个IRQ。执行IRQ由禁用状态到启用状态的转换。

disable:禁用IRQ,而shutdown是完全关闭一个中断源

ack:响应一个中断,与中断控制器硬件密切相关。在某些模型中,IRQ请求的到达必须显示的确认,后续的请求才能进行处理。

mask:屏蔽中断源。

mask_ack:确认一个中断,并在接下来屏蔽该中断

unmask:unmask中断源

eoi:在处理中断时需要一个到硬件的回调,由eoi提供。eoi表示end of interrupt,即中断结束

end:调用end标记中断处理在电流层次的约束。如果一个中断在中断处理期间被禁用,那么该函数负责重新启用此类中断

set_type:设置中断触发方式IRQ_TYPE_LEVEL,在x86上没有该方法

大多数控制方法都是重复的,基本上只要有中断响应、中断屏蔽、中断开启、中断触发类型设置等方法就可以满足要求了。其他各种方法基本上和这些相同。

2.1.3IRQ描述符irq_desc(include/linux/irq.h)

对于每个IRQ中断线,Linux都用一个irq_desc_t数据结构来描述,我们把它叫做IRQ描述符,NR_IRQS个IRQ形成一个全局数组irq_desc[],其定义在/include/linux/irq.h中:

struct irq_desc {
    void fastcall       (*handle_irq)(unsigned int irq,
                          struct irq_desc *desc,
                          struct pt_regs *regs);
    struct irq_chip     *chip;
    void            *handler_data;
    void            *chip_data;
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status;     /* IRQ status */
 
    unsigned int        depth;      /* nested irq disables */
    unsigned int        wake_depth; /* nested wake enables */
    unsigned int        irq_count;  /* For detecting broken IRQs */
    unsigned int        irqs_unhandled;
    spinlock_t      lock;
#ifdef CONFIG_SMP
    cpumask_t       affinity;
    unsigned int        cpu;
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
    cpumask_t       pending_mask;
    unsigned int        move_irq;   /* need to re-target IRQ dest */
#endif
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry *dir;
#endif
} ____cacheline_aligned;
 
extern struct irq_desc irq_desc[NR_IRQS];

handle_irq:上层的通用中断处理函数指针,如果未设置则默认为__do_IRQ()。通常针对电平触发或者边沿触发有不同的处理函数。每个中断线可分别设置,该函数负责使用chip中提供的特定于控制器的方法,进行处理终端所必须的一些底层操作。

chip:指向irq_chip的指针,初始化默认是no_irq_chip;

handler_data:附加参数,用于handle_irq,特定于处理程序;

chip_data:平台相关的附加参数,用于chip;

action:指向 struct irqaction 结构组成的队列的头,正常情况下每个irq只有一个操作,因此链表的正常长度是1或0。但是,如果IRQ被两个或多个设备所共享,那么这个队列就有多个操作了

status:中断线状态;

depth:如果启用这条IRQ中断线,depth则为0;如果禁用这条IRQ中断线不止一次,则为一个正数。如果depth等于0,每当调用一次disable_irq( ),该函数就对这个域的值加1,同时该函数就禁用这条IRQ中断线。相反,每当调用enable_irq( )函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。

lock:用于串行访问IRQ描述符和PIC的自旋锁

irq_count: 统计IRQ线上发生中断的次数(诊断时使用)

irq_unhandled:对在IRQ线上无法处理的中断进行计数(仅在诊断时使用)。当100000次中断产生时,如果意外中断次数超过99900,内核禁用这条IRQ线。

“____cacheline_aligned”表示这个数据结构的存放按32字节(高速缓存行的大小)进行对齐,以便于将来存放在高速缓存并容易存取

2.1.4数据结构的定义

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned = {
    [0 ... NR_IRQS-1] = {
        .status = IRQ_DISABLED,
        .chip = &no_irq_chip,
        .handle_irq = handle_bad_irq,
        .depth = 1,
        .lock = SPIN_LOCK_UNLOCKED,
#ifdef CONFIG_SMP
        .affinity = CPU_MASK_ALL
#endif
    }
};

此处,IRQ线默认是禁用的,depth为1,即启用一次即可激活,irq_chip *chip默认是没有中断控制器。handle_bad_irq()函数处理意外和伪中断。no_irq_chip如下:

struct irq_chip no_irq_chip = {
    .name       = "none",
    .startup    = noop_ret,
    .shutdown   = noop,
    .enable     = noop,
    .disable    = noop,
    .ack        = ack_bad,
    .end        = noop,
};

2.2中断初始化

2.2.1保护模式下的初始化

在调用 start_kernel()函数进行内核初始化时,将会调用 trap_init()和 init_IRQ()函数对中断进行第二次初始化,其中 trap_init()只初始化系统将用到的 CPU 异常处理程序。这部分已经运行于保护模式下。

文件名:init/main.c

asmlinkage void __init start_kernel(void)
{
    ……
    trap_init();//异常初始化
    rcu_init();
init_IRQ();//外部中断初始化
}

  为了设计方便,为了完成门描述符的设定,Linux提供了两层函数,底层调用_set_gate()来完成共有的操作,高层函数set_intr_gate()(中断门)、set_trap_gate()(陷阱门)、set_system_gate()(系统门)、set_task_gate()(任务门)和 set_system_intr_gate()(系统中断门)函数均调用_set_gate()来完成门描述符的设定。其中_set_gate()完成的动作与在保护模式下的 setup_idt汇编代码设定的任务完全相同。

void __init trap_init(void)
{
#ifdef CONFIG_EISA           //如果配置了 EISA
    void __iomem *p = ioremap(0x0FFFD9, 4);
    if (readl(p) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) {
        EISA_bus = 1;
    }
    iounmap(p);
#endif
 
#ifdef CONFIG_X86_LOCAL_APIC //如果配置了本地APIC,就要对其初始化
    init_apic_mappings();
#endif
//0~19号中断向量。
    set_trap_gate(0,&divide_error);
    set_intr_gate(1,&debug);
    set_intr_gate(2,&nmi);
    set_system_intr_gate(3, &int3); /3号,看上面的系统中断门哦,用户态的
    set_system_gate(4,&overflow);
    set_trap_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);
    if (cpu_has_fxsr) {
        struct fxsrAlignAssert {
            int _:!(offsetof(struct task_struct,
                    thread.i387.fxsave) & 15);
        };
        printk(KERN_INFO "Enabling fast FPU save and restore... ");
        set_in_cr4(X86_CR4_OSFXSR);
        printk("done.\n");
    }
    if (cpu_has_xmm) {
        printk(KERN_INFO "Enabling unmasked SIMD FPU exception "
                "support... ");
        set_in_cr4(X86_CR4_OSXMMEXCPT);
        printk("done.\n");
    }
    set_system_gate(SYSCALL_VECTOR,&system_call);//设置系统调用,其中SYSCALL定、、//义在include/asm-i386/mach-default/irq_vetors.h里面 #define SYSCALL_VECTOR      //0x80
     cpu_init();
    trap_init_hook();
}

从以上代码可以看出,trap_init()函数只初始化了 0~19 号中断向量及 128(0x80)号中断(20~31 号是系统保留的),那么其他的中断是怎么被初始化的呢?答案就是 init_IRQ()。

arch/i386/kernel/i8259.c

void __init init_IRQ(void)
{
    int i;
 
    /* all the set up before the call gates are initialised */
    pre_intr_init_hook();
    /*
     * Cover the whole vector space, no vector can escape
     * us. (some of these will be overridden and become
     * 'special' SMP interrupts)
     */
//其中include/asm-i386/mach-default/irq_vectors.h  此文件包含i386架构使用所有关于中断向量(不是中断请求)的统计量,比如FIRST_EXTERNAL_VECTOR(0x20),SYSCALL_VECTOR(0x80)和NR_IRQS,FIRST_SYSTEM_VECTOR=0xef(239),NR_VECTORS=256,NR_IRQS的定义在include/asm-i386/mach-default/irq_vectors_limits.h 配置了apic 的话,NR_IRQS=224,如果没有,则NR_IRQS=16,就是8259A的级联。其实在LINUX中物理需要的IRQ为238-32=206 ,其中0x20~0x2f属于8259A管理,0x30~0xee是属于APIC管理。224是OS系统能用到的最大的中断向量个数,除掉了INTEL CPU保留的异常部分。这224个中断向量中有一些是不需要IRQ的(0xef后都不需要,例如0xef是本地APIC时钟中断),不过用224就起到了最大值,没漏掉一个中断向量,对于不需要IRQ的,到时候再来另外处理,也难说将来不用。 NR_IRQ_VECTORS:就是将IRQ分配到中断向量,注意,不用IRQ的到时再作处理,这里取个224最大值,防止了将来的变动,而无须大改。 
 
    for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
        int vector = FIRST_EXTERNAL_VECTOR + i;
        if (i >= NR_IRQS)
            break;
//排除系统调用,因为在 trap_init()函数中已经初始化了
        if (vector != SYSCALL_VECTOR) 
//设置中断门,其中 interrupt[i]为中断处理函数,
            set_intr_gate(vector, interrupt[i]);
    }
    /* setup after call gates are initialised (usually add in
     * the architecture specific gates)
     */
// include/asm-i386/mach-default/setup.c中定义,#ifdef CONFIG_X86_LOCAL_APIC
    apic_intr_init();if (!acpi_ioapic)setup_irq(2, &irq2); 8259A必须连接到IRQ2线,用做PIC级联。
    intr_init_hook();
    /*
     * Set the clock to HZ Hz, we already have a valid
     * vector now:
     */
//初始化8253 芯片
    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()函数来初始化 irq_desc_t结构体。具体代码如下

文件名:arch/i386/mach-default/setup.c

void __init pre_intr_init_hook(void)
{
    init_ISA_irqs();//初始化irq_desc_t
}
文件(arch\i386\kernel\i8259.c)
void __init init_ISA_irqs (void)
{
    int i;
 
#ifdef CONFIG_X86_LOCAL_APIC
    init_bsp_APIC();
#endif
//初始化8259A芯片
    init_8259A(0);
//初始化 irq_desc_t 结构体
    for (i = 0; i < NR_IRQS; i++) {
        irq_desc[i].status = IRQ_DISABLED;
        irq_desc[i].action = NULL;
        irq_desc[i].depth = 1;
//头16 个(即 IRQ0~IRQ15)中的hw_interrupt_type(即irq_chip)结构体初始化为 i8259A_irq_type,其余的初始化为no_irq_type
        if (i < 16) {
            irq_desc[i].chip = &i8259A_irq_type;
        } else {
            irq_desc[i].chip = &no_irq_type;
        }
    }
}

其中,i8259A_irq_type 的值为(arch\i386\kernel\i8259.c):

static struct hw_interrupt_type i8259A_irq_type = {
    .typename = "XT-PIC",
    .startup = startup_8259A_irq,
    .shutdown = shutdown_8259A_irq,
    .enable = enable_8259A_irq,
    .disable = disable_8259A_irq,
    .ack = mask_and_ack_8259A,
    .end = end_8259A_irq,
};

no_irq_type 值为(kernel\irq\handle.c),已经在上面出现过,如下

struct irq_chip no_irq_chip = {
    .name       = "none",
    .startup    = noop_ret,
    .shutdown   = noop,
    .enable     = noop,
    .disable    = noop,
    .ack        = ack_bad,
    .end        = noop,
};

  从以上分析可以看出,hw_interrupt_type、irq_desc_t、irqaction 三个结构具有如下的关系:

在 init_IRQ()函数中,将32~256 号的中断服务例程设置为 interrupt[i]的偏移地址,那么这个 interrupt[i]为何物,其实 interrupt[i]不是中断服务函数,而是所有中断的一个共同操作,具体的中断服务例程是由硬件的驱动程序所设定的。interrupt[i]是在 entry.S中所设定的,由汇编代码实现。它使得从31号以后的中断都跳到了 common_interrupt,到这里怎么区分具体的中断处理程序,难道都执行同一个吗?上面所讲述的 irqaction 三个结构体,与具体的中断有什么联系?请读者继续往下看

2.2.2注册中断处理程序

在 IDT 表的初始化完成之初,每个中断服务队列(irqaction)都是空的。即使开了中断,并且产生了中断,也只不过是让它在 common_interrupt 中空跑一趟。所以,真正的中断服务要到具体硬件设备的初始化程序将其中断服务程序通过 request_irq()向系统“登记”,挂入某个中断服务队列(irqaction)以后才会发生。

中断函数注册信息就保留在irq_desc_t结构中,系统所有的中断信息构成了一个由224个 irq_desc_t结构组成的全局描述符结构数组irq_desc[]。request_irq()的作用就是注册一个中断并启用。如下图:是注册中断处理程序的流程图:

文件名:kernel/irq/manage.c

//irq 为中断请求号,handler 为中断处理程序,irqflags 为中断类型标志,dev_id 用来共享中断号

int request_irq(unsigned int irq,
        irqreturn_t (*handler)(int, void *, struct pt_regs *),
        unsigned long irqflags, const char *devname, void *dev_id)
{
    struct irqaction *action;
    int retval;
 
#ifdef CONFIG_LOCKDEP//希望是原子操作,则设置SA_INTERRUPT标志。
    /*
     * Lockdep wants atomic interrupt handlers:
     */
    irqflags |= SA_INTERRUPT;
#endif
    /*
     * Sanity-check: shared interrupts must pass in a real dev-ID,
     * otherwise we'll have trouble later trying to figure out
     * which interrupt is which (messes up the interrupt freeing
     * logic etc).
     */
//如果设置了共享标志,但 dev_id 为空,则出错
    if ((irqflags & IRQF_SHARED) && !dev_id)
        return -EINVAL;
//中断请求号超过 224,出错
    if (irq >= NR_IRQS)
        return -EINVAL;
    if (irq_desc[irq].status & IRQ_NOREQUEST)
        return -EINVAL;
//中断处理函数为空,出错
    if (!handler)
        return -EINVAL;
//为 irqaction结构体申请空间
    action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
    if (!action)
        return -ENOMEM;
//设置参数
    action->handler = handler;
    action->flags = irqflags;
    cpus_clear(action->mask);
    action->name = devname;
    action->next = NULL;
    action->dev_id = dev_id;
 
    select_smp_affinity(irq);
//将该 irqaction 结构体挂入中断服务队列中
    retval = setup_irq(irq, action);
    if (retval)//如果成功会返回0,不成功,则释放内存
        kfree(action);
 
    return retval;
}

2.2.2.1 irqaction->irqflags和 irq_desc->status

在此先总结一下irqflags和status,以及这两个的区别

因为irqflags属于中断处理程序描述符irqaction的标志位,所以它属于中断标志位,代码在/include/linux/interrupt.h中,其中IRQF表示IRQ的flags。

#define IRQF_TRIGGER_NONE   0x00000000//IRQF表示中断请求Flags
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING    0x00000002
#define IRQF_TRIGGER_HIGH   0x00000004
#define IRQF_TRIGGER_LOW    0x00000008
#define IRQF_TRIGGER_MASK   (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
                 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE  0x00000010
 
/*
 * These flags used only by the kernel as part of the
 * irq handling routines.
 *
 * IRQF_DISABLED - keep irqs disabled when calling the action handler
 * IRQF_SAMPLE_RANDOM - irq is used to feed the random generator
 * IRQF_SHARED - allow sharing the irq among several devices
 * IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
 * IRQF_TIMER - Flag to mark this interrupt as timer interrupt
 */
#define IRQF_DISABLED       0x00000020//调用此中断处理程序的时候禁止中断
#define IRQF_SAMPLE_RANDOM  0x00000040//作为随机数的来源
#define IRQF_SHARED     0x00000080//允许共享
#define IRQF_PROBE_SHARED   0x00000100
#define IRQF_TIMER      0x00000200//时钟中断
#define IRQF_PERCPU     0x00000400//是SMP标志
 
/*
 * Migration helpers. Scheduled for removal in 1/2007
 * Do not use for new code !
 */
#define SA_INTERRUPT        IRQF_DISABLED
#define SA_SAMPLE_RANDOM    IRQF_SAMPLE_RANDOM
#define SA_SHIRQ        IRQF_SHARED
#define SA_PROBEIRQ     IRQF_PROBE_SHARED
#define SA_PERCPU       IRQF_PERCPU
 
#define SA_TRIGGER_LOW      IRQF_TRIGGER_LOW
#define SA_TRIGGER_HIGH     IRQF_TRIGGER_HIGH
#define SA_TRIGGER_FALLING  IRQF_TRIGGER_FALLING
#define SA_TRIGGER_RISING   IRQF_TRIGGER_RISING
#define SA_TRIGGER_MASK     IRQF_TRIGGER_MASK

SA_INTERRUPT:此标志表明给定的中断处理程序是一个快速中断处理程序。在本地处理器上,快速中断处理程序在禁止所有终端的情况下运行。而默认情况(未设置)情况下,除了正在运行的中断处理程序对应的中断线被屏蔽外,其余所有中断都是激活的。

SA_SHIRQ:此标志表明可以在多个中断处理程序之间共享中断线。(每个中断都有一个编号,若中断号n分配给一个网卡而不是SCSI控制器,那么内核可以区分两个设备。但是遗憾的是,由于特别设计,只有少数的编号可用于硬件中断,所以必须及格设备共享一个编号,在IA-32的处理器上,硬件中断的最大数目是15,这个叫中断共享)这个标志表明可以在多个处理程序之间共享中断线,在同一条线上注册的每个处理程序必须制定这个标志,否则,每条线上只有一个处理程序。

共享的处理程序与非共享的中断处理程序的不同

l   request_irq()的参数flags必须设置SA_SHIRQ标志。

l   对每个注册的中断处理程序来说,dev_id参数必须唯一

l   中断处理程序必须能够区分它的设备是否真的产生了中断,这既需要硬件的支持,也需要中断处理程序中有相应的处理逻辑。

所有共享中断线的驱动程序都必须满足以上要求,只要有任何一个设备没有按规则进行共享,那么中断线就无法共享了。指定SA_SHIRQ标志以调用request_irq()时,只有以下两种情况才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了SA_SHIRQ。

/*
 * IRQ line status.
 * Bits 0-16 are reserved for the IRQF_* bits in linux/interrupt.h
 * IRQ types
 */这些都是与中断电流触发方式有关
#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      0x00010000  /* IRQ handler active - do not enter! */
#define IRQ_DISABLED        0x00020000  /* IRQ disabled - do not enter! */
#define IRQ_PENDING     0x00040000  /* IRQ pending - replay on enable */
#define IRQ_REPLAY      0x00080000  /* IRQ has been replayed but not acked yet */
#define IRQ_AUTODETECT      0x00100000  /* IRQ is being autodetected */
#define IRQ_WAITING     0x00200000  /* IRQ not yet seen - for autodetection */
#define IRQ_LEVEL       0x00400000  /* IRQ level triggered */
#define IRQ_MASKED      0x00800000  /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU     0x01000000  /* IRQ is per CPU */
#ifdef CONFIG_IRQ_PER_CPU
# define CHECK_IRQ_PER_CPU(var) ((var) & IRQ_PER_CPU)
#else
# define CHECK_IRQ_PER_CPU(var) 0
#endif
 
#define IRQ_NOPROBE     0x02000000  /* IRQ is not valid for probing */
#define IRQ_NOREQUEST       0x04000000  /* IRQ cannot be requested */
#define IRQ_NOAUTOEN        0x08000000  /* IRQ will not be enabled on request irq 
#define IRQ_DELAYED_DISABLE 0x10000000  /* IRQ disable (masking) happens delayed. 
#define IRQ_WAKEUP      0x20000000  /* IRQ triggers system wakeup */

IRQ_INPROGRESS  IRQ的一个处理程序正在执行

IRQ_DISABLED    禁用IRQ线

IRQ_PENDING    悬挂,其实就是有中断到达,已对PIC做出应答,但由于某种情况未处理,先标记上

IRQ_REPLAY      IRQ线已被禁用,但是前一个出现的IRQ还没对PIC做出应答

IRQ_AUTODETECT 内核在执行硬件设备探测时使用IRQ线

IRQ_WAITING     内核在执行硬件设备探测时使用IRQ线,此外,相应的中断还没有产生

IRQ_MASKED     屏蔽此中断线上的中断请求

2.2.2.1 setup( )函数

在request_irq()中调用函数setup_irq()来完成建立中断的具体操作。在全局中断描述符结构数组(irq_destc[])中,如果该中断号被使用,并且新旧中断都设置了共享,就把新中断加载就中断的后面;如果中断号没有被使用,就直接加在中断描述符结构数组中,并设置中断使能,具体函数分析如下:

/*
 * Internal function to register an irqaction - typically used to
 * allocate special interrupts that are part of the architecture.
 */
int setup_irq(unsigned int irq, struct irqaction *new)
{
//从全局中断描述符结构数组中得到对应中断号的中断描述结构
    struct irq_desc *desc = irq_desc + irq;
    struct irqaction *old, **p;
    unsigned long flags;
    int shared = 0;
 
    if (irq >= NR_IRQS)
        return -EINVAL;
 
    if (desc->chip == &no_irq_chip)
        return -ENOSYS;
    /*
     * Some drivers like serial.c use request_irq() heavily,
     * so we have to be careful not to interfere with a
     * running system.
     */
    if (new->flags & IRQF_SAMPLE_RANDOM) {
 
        rand_initialize_irq(irq);
    }
 
  
 //下面这段代码必须原子执行,不能被打断   
    spin_lock_irqsave(&desc->lock, flags);
    p = &desc->action;
    old = *p;
if (old) {
/*双方声明为共享中断且触发类型一致*/
        if (!((old->flags & new->flags) & IRQF_SHARED) ||
            ((old->flags ^ new->flags) & IRQF_TRIGGER_MASK))
            goto mismatch;
 
#if defined(CONFIG_IRQ_PER_CPU)
        /* All handlers must agree on per-cpuness */
        if ((old->flags & IRQF_PERCPU) !=
            (new->flags & IRQF_PERCPU))
            goto mismatch;
#endif
 
        /* 找到IRQ队列的尾部,并取得最后一个 irqaction 的指针赋值给 p
        do {
            p = &old->next;
            old = *p;
        } while (old);
        shared = 1;
    }
//将新中断挂在 p 上
    *p = new;
#if defined(CONFIG_IRQ_PER_CPU)
    if (new->flags & IRQF_PERCPU)
        desc->status |= IRQ_PER_CPU;
#endif
//如果该中断没有设置共享,即第一此给该中断号设置中断处理程序,则设置该 irq_desc_t 
//结构体的值
    if (!shared) {
        irq_chip_set_defaults(desc->chip);
 
            /* 若指定了触发类型则进行配置*/在i386 架构下没有设置
        if (new->flags & IRQF_TRIGGER_MASK) {
            if (desc->chip && desc->chip->set_type)
                desc->chip->set_type(irq,
                        new->flags & IRQF_TRIGGER_MASK);
            else
                printk(KERN_WARNING "No IRQF_TRIGGER set_type "
                       "function for IRQ %d (%s)\n", irq,
                       desc->chip ? desc->chip->name :
                       "unknown");
        } else
            compat_irq_chip_set_default_handler(desc);//只是检测电流处理程序是否被重写
 
        desc->status &= ~(IRQ_AUTODETECT | IRQ_WAITING |
                  IRQ_INPROGRESS);
 
        if (!(desc->status & IRQ_NOAUTOEN)) {
            desc->depth = 0;
            desc->status &= ~IRQ_DISABLED;
//中断使能
            if (desc->chip->startup)
                desc->chip->startup(irq);
            else
                desc->chip->enable(irq);
        } else
            /* Undo nested disables: */
            desc->depth = 1;
    }
    spin_unlock_irqrestore(&desc->lock, flags);
//以下两个函数在/proc/irq文件创建一个与中断相对应的项  
    new->irq = irq;
    register_irq_proc(irq); //更新该中断的proc记录
    new->dir = NULL;
    register_handler_proc(irq, new);
 
    return 0;
 
mismatch:
    spin_unlock_irqrestore(&desc->lock, flags);
    if (!(new->flags & IRQF_PROBE_SHARED)) {
        printk(KERN_ERR "IRQ handler type mismatch for IRQ %d\n", irq);
        dump_stack();
    }
    return -EBUSY;
}

  在内核中,设备驱动程序一般都要通过 request_irq()向系统登记其中断服务程序。有一点很重要,初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。注意 request_irq()函数可能睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。

  当所有的硬件设备初始化完成后,IDT 表也就完成了所有的初始化动作,现在就可以开始响应中断,并进行中断处理了。

2.2.2.1 free_irq( )函数

kernel/irq/manage.c

void free_irq(unsigned int irq, void *dev_id)
{
    struct irq_desc *desc;
    struct irqaction **p;
    unsigned long flags;
 
    WARN_ON(in_interrupt());
    if (irq >= NR_IRQS)
        return;
//找到中断号对应的irq_desc_t
    desc = irq_desc + irq;
    spin_lock_irqsave(&desc->lock, flags);
    p = &desc->action;//irqaction链表头
    for (;;) {//直到找到对应的dev_id才结束
        struct irqaction *action = *p;
//如果还没到irqaction链表的结尾
        if (action) {
            struct irqaction **pp = p;
 
            p = &action->next;
            if (action->dev_id != dev_id)//如果还不是对应的dev_id,那么继续循环
                continue;
 
            /* Found it - now remove it from the list of entries */
            *pp = action->next;
 
            /* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
            if (desc->chip->release)
                desc->chip->release(irq, dev_id);//释放对应的中断处理程序
#endif
//如果链表里面已经没有了irqaction的话,那么禁用并关闭中断线
            if (!desc->action) {
                desc->status |= IRQ_DISABLED;
                if (desc->chip->shutdown)
                    desc->chip->shutdown(irq);
                else
                    desc->chip->disable(irq);
            }
            spin_unlock_irqrestore(&desc->lock, flags);
            unregister_handler_proc(irq, action);
 
            /* Make sure it's not being used on another CPU */
            synchronize_irq(irq);
            kfree(action);
            return;
        }
        printk(KERN_ERR "Trying to free already-free IRQ %d\n", irq);
        spin_unlock_irqrestore(&desc->lock, flags);
        return;
    }
}

free_irq()函数做的就是找到对应的中断处理程序,然后删掉它的中断处理程序,如果在这条中断线上已经没有中断的话,那么禁用这条中断线。

2.3中断电流处理

2.3.1设置控制器硬件

文件include/linux/irq.h中

int set_irq_chip(unsigned int irq, struct irq_chip *chip);

void set_irq_handler(unsigned int irq, irq_flow_handler_t handle);

void set_irq_chained_handler(unsigned int irq, irq_flow_handler_t handle)

void set_irq_chip_and_handler(unsigned int irq, struct irq_chip *chip,irq_flow_handler_t handle);

void set_irq_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,irq_flow_handler_t handle, const char *name);

static inline void
set_irq_handler(unsigned int irq,
        void fastcall (*handle)(unsigned int, struct irq_desc *,
                    struct pt_regs *))
{
    __set_irq_handler(irq, handle, 0);
}
static inline void
set_irq_chained_handler(unsigned int irq,
            void fastcall (*handle)(unsigned int, struct irq_desc *,
                        struct pt_regs *))
{
    __set_irq_handler(irq, handle, 1);
}

set_irq_chip将一个IRQ芯片以irq_chip实例的形式关联到某个特定的中断,除了从irq_desc选取适当的成员并设置chip指针之外,如果没有提供特定于芯片的实现,该函数将设置默认的处理程序,如果chip指针为NULL,将使用通用的irq_chip实例no_irq_chip,该实现只提供了空操作。源码如下:

int set_irq_chip(unsigned int irq, struct irq_chip *chip)
{
    struct irq_desc *desc;
    unsigned long flags;
    if (irq >= NR_IRQS) {
        printk(KERN_ERR "Trying to install chip for IRQ%d\n", irq);
        WARN_ON(1);
        return -EINVAL;
    }
 
    if (!chip)//如果chip为NULL,那么使用通用的no_irq_chip
        chip = &no_irq_chip;
 
    desc = irq_desc + irq;
    spin_lock_irqsave(&desc->lock, flags);
    irq_chip_set_defaults(chip);
    desc->chip = chip;
    spin_unlock_irqrestore(&desc->lock, flags);
    return 0;
}

kernel/irq/chip.c中

void
__set_irq_handler(unsigned int irq,
          void fastcall (*handle)(unsigned int, irq_desc_t *,
                      struct pt_regs *),
          int is_chained)
{
    struct irq_desc *desc;
    unsigned long flags;
 
    if (irq >= NR_IRQS) {
        printk(KERN_ERR
               "Trying to install type control for IRQ%d\n", irq);
        return;
    }
//找出对应的irq_desc_t
    desc = irq_desc + irq;
//如果handle为NULL
    if (!handle)
        handle = handle_bad_irq;
//如果chip未指定,还是默认的no_irq_chip
    if (desc->chip == &no_irq_chip) {
        printk(KERN_WARNING "Trying to install %sinterrupt handler "
               "for IRQ%d\n", is_chained ? "chained " : " ", irq);
        desc->chip = &dummy_irq_chip;
    }
 
    spin_lock_irqsave(&desc->lock, flags);
//下面的需要原子操作
//如果handle未指定,还是默认的handle_bad_irq,禁用它
    if (handle == handle_bad_irq) {
        if (desc->chip != &no_irq_chip) {
            desc->chip->mask(irq);
            desc->chip->ack(irq);
        }
        desc->status |= IRQ_DISABLED;
        desc->depth = 1;
    }
    desc->handle_irq = handle;//设置irq_desc­_t的handle选项
//如果这个是非共享的中断线
    if (handle != handle_bad_irq && is_chained) {
        desc->status &= ~IRQ_DISABLED;
        desc->status |= IRQ_NOREQUEST | IRQ_NOPROBE;
        desc->depth = 0;
        desc->chip->unmask(irq);
    }
    spin_unlock_irqrestore(&desc->lock, flags);
}

2.3.1电流处理

不同的硬件需要不同的电流处理方式,例如,边沿触发和电平触发需要不同的处理。内核对各种类型提供了几个默认的电流处理程序,它们有一个共同点,每个电流处理程序在工作结束后,都要负责调用高层ISR。Handle_IRQ_event()负责激活高层的处理程序。

现在硬件大部分采用的是边沿触发中断默认处理程序是handle_edge_irq()。

在处理边沿触发中断的IRQ时无须屏蔽,这使得SMP系统:在一个CPU上处理一个IRQ时,另一个同样编号的IRQ可以出现在另一个CPU上,使得两个CPU可能同时调用一个IRQ中断处理程序。内核想避免这种情况:在handle_edge_irq()中,如果设置了IRQ_INPROGRESS标志,说明该IRQ中断处理程序正在被调用,通过设置IRQ_PENDING标志,内核记录还有一个IRQ需要处理。屏蔽该IRQ并通过mask_and_ack()向控制器发送一个确认后,处理过程放弃。注意:如果IRQ被禁用,或没有可用的ISR处理程序,都会放弃处理。

代码流程图如下:

 

void fastcall
handle_edge_irq(unsigned int irq, struct irq_desc *desc, struct pt_regs *regs)
{
    const unsigned int cpu = smp_processor_id();
 
    spin_lock(&desc->lock);
//设置一些标志
    desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
    /*
    如果没有中断处理程序,或者当前irq在运行或者被禁用,那么屏蔽并设置IRQ_PENEDING标志位,并取消处理。
     */
    if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||
            !desc->action)) {
        desc->status |= (IRQ_PENDING | IRQ_MASKED);
        mask_ack_irq(desc, irq);
        goto out_unlock;
    }
 
    kstat_cpu(cpu).irqs[irq]++;
 
//ack应答中断
    desc->chip->ack(irq);
 
//标识中断在处理中
    desc->status |= IRQ_INPROGRESS;
 
    do {
        struct irqaction *action = desc->action;
        irqreturn_t action_ret;
 
        if (unlikely(!action)) {//unlickly只是GCC对代码进行优化,没什么含义
            desc->chip->mask(irq);
            goto out_unlock;
        }
 
//如果当我们处理中断的时候,另一个中断请求到了,我们需要屏蔽它。现在要接触对irq //的屏蔽,如果它在此期间没有被禁用的话。
        if (unlikely((desc->status &
                   (IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
                  (IRQ_PENDING | IRQ_MASKED))) {
            desc->chip->unmask(irq);
            desc->status &= ~IRQ_MASKED;
        }
 
        desc->status &= ~IRQ_PENDING;
        spin_unlock(&desc->lock);
        action_ret = handle_IRQ_event(irq, regs, action);//调用高层ISR
        if (!noirqdebug)
            note_interrupt(irq, desc, action_ret, regs);
        spin_lock(&desc->lock);
 
    } while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);
 
    desc->status &= ~IRQ_INPROGRESS;
out_unlock:
    spin_unlock(&desc->lock);
}

IRQ的处理时在一个循环中的,假定我们刚好出于handle_IRQ_event()之后的位置上,在第一个IRQ的ISR处理程序运行时,可能同时有第二个IRQ请求发送过来,会通过设置IRQ_PENDING表示,如果设置了该标志,那么有另一个IRQ正在等待处理。循环将从头再次开始。但这种情况下IRQ已经被屏蔽,那么必须用chip->unmask接触IRQ的屏蔽,并清除IRQ_MASKED标志。这确保在handle_IRQ_event执行期间只能发生一个中断。

2.4 IRQ处理

在注册了IRQ处理程序后,每次发生中断时将执行处理程序例程。每次先切换到核心态,它是基于每个中断之后由处理器自动执行的汇编语言代码,实现在arch/i386/kernel/entry.s中,只有必要的操作会直接在汇编语言代码中执行,内核试图尽快到额返回到C代码。在C语言中调用函数时,大多数平台上,控制流会传递到C函数do_IRQ。arch/i386/kernel/irq.c中,do_IRQ做了两件事,一是切换内核栈,二是调用了__do_IRQ()。

fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
//得到全局描述符结构数组的下标
    struct irq_desc *desc = irq_desc + irq;
    struct irqaction *action;
    unsigned int status;
 
kstat_this_cpu.irqs[irq]++;
//因为 irq_desc[]数组中,每个 CPU 占一个元素,这里的 desc 就是本 CPU  数据,所以此处不需要加锁。
    if (CHECK_IRQ_PER_CPU(desc->status)) {
        irqreturn_t action_ret;
 
        /*
         * No locking required for CPU-local interrupts:
         */
//中断处理器在将中断请求“上报”到 CPU后,期待 CPU给它一个确认(ACK) ,表示“我
//已经在处理”,也就是给8259A或 APIC 芯片一个应答信号
        if (desc->chip->ack)
            desc->chip->ack(irq);
//该函数将会调用中断处理程序
        action_ret = handle_IRQ_event(irq, regs, desc->action);
//对中断控制器执行一次“结束中断服务操作”
        desc->chip->end(irq);
        return 1;
    }
//对于多 CPU的情况,进行加锁处理
    spin_lock(&desc->lock);
    if (desc->chip->ack)
        desc->chip->ack(irq);
    /*
     * REPLAY is when Linux resends an IRQ that was dropped earlier
     * WAITING is used by probe to mark irqs that are being tested
     */
//从desc->status中清除清IRQ_REPLAY和 IRQ_WAITING 位,同时设置 IRQ_PENDING 位
    status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
    status |= IRQ_PENDING; /* we _want_ to handle it */
 
    /*
     * If the IRQ is disabled for whatever reason, we cannot
     * use the action we have.
     */
action = NULL;
//如果设置了IRQ_DISABLED位  ,或者 IRQ_INPROGRESS 位,即当前中断被关闭 
//(IRQ_DISABLED 为 1 表示关闭),或者已经在其他 CPU 上运行(IRQ_INPROGRESS 为
// 1 表示运行),则退出(此时 action = NULL)。反之执行 if语句内的内容。
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
//取得中断服务程序队列头的指针
        action = desc->action;
//清除 IRQ_PENDING 位,即设置 IRQ_PENDING 位为 0,同时设置 IRQ_INPROGRESS 位为1。//其中IRQ_INPROGRESS 位是为多处理器设置的,表示我正在执行,你就不要执行了。对于 //IRQ_PENDING 位的作用下面就可以看到 
        status &= ~IRQ_PENDING; /* we commit to handling */
        status |= IRQ_INPROGRESS; /* we are handling it */
}
//保存当前中断号的状态
    desc->status = status;
 
    /*
     * If there is no IRQ handler or it was disabled, exit early.
     * Since we set PENDING, if another processor is handling
     * a different instance of this same irq, the other processor
     * will take care of it.
     */
//如果 action队列为空(还包含上面的两种情况),做相关处理后,将退出
    if (unlikely(!action))
        goto out;
 
    /*
     * Edge triggered interrupts need to remember
     * pending events.
     * This applies to any hw interrupts that allow a second
     * instance of the same irq to arrive while we are in do_IRQ
     * or in the handler. But the code here only handles the _second_
     * instance of the irq, not the third or fourth. So it is mostly
     * useful for irq hardware that does not mask cleanly in an
     * SMP environment.
     */
    for (;;) {
        irqreturn_t action_ret;
 
        spin_unlock(&desc->lock);
//执行中断处理程序
        action_ret = handle_IRQ_event(irq, regs, action);
 
        spin_lock(&desc->lock);
        if (!noirqdebug)
            note_interrupt(irq, desc, action_ret, regs);
//在进入 for循环时,IRQ_PENDING 位已经为 0。如果该位此时还为 0,结束 for循环。//如 果为 1,表示该中断又发生了一次,回到循环再调用中断服务程序一次。这样就将本//来可能发生在同一通道上的中断嵌套化解为一个循环。
        if (likely(!(desc->status & IRQ_PENDING)))
            break;
    //清除 IRQ_PENDING 位,即设置 IRQ_PENDING 位为 0。     
         desc->status &= ~IRQ_PENDING;
}
//清 IRQ_INPROGRESS 位,表示可以继续响应下一次中断服务
    desc->status &= ~IRQ_INPROGRESS;
 
out:
    /*
     * The ->end() handler has to deal with interrupts which got
     * disabled while the handler was running.
     */
//对中断控制器执行一次“结束中断服务操作”
    desc->chip->end(irq);
    spin_unlock(&desc->lock);
 
    return 1;
}

  从以上分析可知,如果中断关闭(IRQ_DISABLED,含义请参看表 5),或者中断处理程序正在执行(IRQ_INPROGRESS) ,或者中断服务程序队列(action)为空,都将无法往下执行。但此时 IRQ_PENDING 是置位的。所以在以下两种情况下,程序看到此标志位置位时,将补上一次中断服务。

1.  如果 action队列为空

2.  在多 CPU 中,一个 CPU 正在执行中断服务,而另一个 CPU 又进入了 do_IRQ(),这时候由于 IRQ_INPROGRESS 位为 1,程序退出。

在这两种情况下,IRQ_PENDING 标志位都为 1,都将会使程序再次执行相应的中断处理程序。大家可能会感到疑问的是对于第二种情况,不是进入__do_irq()时,已经采用了自旋锁吗?怎么会出现第二种情况呢?请仔细看代码,在执行中断处理程序前,已经释放自旋锁了,所以出现了第二种情况。

我们不难看出 IRQ_PENDING 标志的作用,就是用于判断中断是否又发生了一次。从以上代码可以看出,修改 desc->status的值是在加锁的情况下进行的,主要是基于 SMP 的考虑。 

代码中的 handle_IRQ_event()函数才是真正的执行中断处理程序代码。

文件名:kernel\irq\handle.c

irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
                 struct irqaction *action)
{
    irqreturn_t ret, retval = IRQ_NONE;
    unsigned int status = 0;
 
    handle_dynamic_tick(action);
//一般来说,中断服务程序都是在关闭中断(排除 NMI 情况)的条件下执行的,这也是 CPU
//在穿越中断门时自动关闭中断的原因。但是在调用 request_irq()函数注册中断服务程//序时,允许设置 SA_INTERRUPT 位为 0,表示该服务程序应该在开中断的情况下执行。
    if (!(action->flags & IRQF_DISABLED))
        local_irq_enable_in_hardirq();
//依次调用 action 队列中所有的中断服务子程序。其实这个过程并不需要很长的时间。因//为每个具体的中断服务程序中都会一开始检查各自的中断源,一般是读相应设备的中断状//态寄存器,看是否有来自该设备的中断请求,如果没有则马上退出,这个过程一般只需要//几个指令;其次,每个 action 队列的中断服务程序的数量一般也不会很大。所以,不会//有显著的影响。
 
    do {
        ret = action->handler(irq, action->dev_id, regs);
        if (ret == IRQ_HANDLED)
            status |= action->flags;
        retval |= ret;
        action = action->next;
    } while (action);
 
    if (status & IRQF_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    local_irq_disable();
 
    return retval;
}

  此时,执行完成后,将返回到 do_IRQ()函数,执行 irq_exit()里面的代码。这个函数主要是还原 preempt_count 的值,并执行软中断。执行完软中断后,将会退栈处理(对应前面的切换内核栈)。

3 附录

3.1中断控制

Linux内核提供了一组接口用于操作机器上的中断状态,这些接口为我们提供了能够尽职当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。

一般来说,控制中断系统的原因归根结底是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码,禁止中断还可以禁止内核抢占。然而,不管是禁止中断还是禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。Linux支持多处理器。因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问。获取这些锁的同时伴随着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。

禁止本地CPU中断是确保一组内核语句被当作一个临界区处理的主要机制。这个机制的意义是:即使当硬件设备产生了一个IRQ信号时,中断禁止也让内核控制路径继续执行,因此,这就提供了一种有效的方式,确保内核控制路径中的一些中断处理程序能访问的数据结构也受到保护。

宏local_irq_disable()使用cli汇编语言指令关闭本地CPU上的中断(x86体系的禁止本地中断位于include/linux/Irqflags.h):

#define local_irq_disable() \
    do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
include/asm-i386/Irqflags.h
static inline void raw_local_irq_disable(void)
{

    __asm__ __volatile__("cli" : : : "memory");
}

宏local_irq_enable()使用sti汇编语言指令打开被关闭的中断(同样在include/linux/Irqflags.h):
#define local_irq_enable() \
    do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
include/asm-i386/Irqflags.h

static inline void raw_local_irq_enable(void)
{

    __asm__ __volatile__("sti" : : : "memory");
}

汇编语言指令cli和sti分别清除和设置eflags控制寄存器的IF标志。如果eflags寄存器的IF标志被清0,宏irqs_disabled()产生等于1的值;如果IF标志被设置,该宏也产生为1的值。

有时候,需要防止中断处理程序对eflags的寄存器内容进行破坏,需要另一种机制来保护临界资源。保存和恢复eflags的内容是分别通过宏 local_irq_save和local_irq_restore来实现的。local_irq_save宏把eflags寄存器的内容拷贝到一个局部 变量中,随后用cli汇编语言指令把IF标志清0(同样在include/linux/Irqflags.h):
#define local_irq_save(flags) \

  do { raw_local_irq_save(flags); trace_hardirqs_off(); } while (0)
include/asm-i386/Irqflags.h

#define raw_local_irq_save(flags) \

   do { (flags) = __raw_local_irq_save(); } while (0)

static inline unsigned long __raw_local_irq_save(void)

{

unsigned long flags = __raw_local_save_flags();

 raw_local_irq_disable();

return flags;

}

static inline unsigned long __raw_local_save_flags(void)
{

    unsigned long flags;

    __asm__ __volatile__(
        "pushfl ; popl %0"
        : "=g" (flags)
        : /* no input */
    );

    return flags;
}

在临界区的末尾,宏local_irq_restore恢复eflags原来的内容:

#define local_irq_restore(flags)                \
    do {                            \
        if (raw_irqs_disabled_flags(flags)) {        \
            raw_local_irq_restore(flags);        \
            trace_hardirqs_off();            \
        } else {                    \
            trace_hardirqs_on();            \
            raw_local_irq_restore(flags);        \
        }                        \
    } while (0)
static inline int raw_irqs_disabled_flags(unsigned long flags)

{

return !(flags & (1 << 9));
}

static inline void raw_local_irq_restore(unsigned long flags)

{

    __asm__ __volatile__(

     "pushl %0 ; popfl"

      : /* no output */

      :"g" (flags)

     :"memory", "cc"

   );

}

因此,只是在这个控制路径发出cli汇编语言指令之前,即raw_local_irq_disable()之前中断被激活的情况下,中断才处于打开状态。

posted @ 2012-07-30 13:09  Drobobls  阅读(287)  评论(0编辑  收藏  举报