《深入理解Linux内核》笔记3:中断

本文基于2.6.11内核简单介绍了中断处理的过程。本文是一个概述性质的整理,可能没有对每段代码有详细的分析,但希望读者看完之后对整个过程有大致了解。

详细的文档请参考这篇论文以及这篇精华帖子,还有这里

整个中断大致的过程(注:本处并不仅仅指中断处理程序)可以描述如下:

硬件中断==>

CPU在指令周期的最后检测到有中断==>

中断应答时序提供8位矢量(中断号)==>

根据IDTR找到IDT表(idt_table)==>

再根据中断号在IDT表中找到对应的描述符(irq_desc_t)==>

根据描述符,调用对应的IRQ0xNN_interrupt,实际上就是调用common_interrupt,实际上就是调用do_IRQ函数。

下面是详细的步骤:

(1)中断向量表的第一次初始化

内核初始化时,用arch/i386/kernel/head.S中的setup_idt宏用同样的中断门(中断门包含段选择符和中断处理程序的段内偏移量,即可以简单认为就是中断处理程序的地址。在系统第一次初始化时,这个中断门是一个宏ignore_int的地址)初始化中断向量表idt_table。初始化完成后,256个表项都一样。在2.6.29内核中该文件为arch/x86/kernel/head_32.S。

(2)产生interrupt[]数组

在arch/i386/kernel/entry.S中建立一个跳转表:interrupt数组,该数组有NR_IRQS个元素。

void (*interrupt[NR_IRQS])(void) = {
     IRQLIST_16(0x0
),

#ifdef CONFIG_X86_IO_APIC
              IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3
),
     IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7
),
     IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb
),
     IRQLIST_16(0xc), IRQLIST_16(0xd
)
#endif

#define IRQ(x,y) \
IRQ##x##y##_interrupt

#define IRQLIST_16(x) \
IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)

从定义可以看出:IRQLIST_16是一个含有24个中断号的表(16进制下的16实际上是24)。如果定义了高级的中断控制器APIC,那么会有224个中断号(0xd);如果没有定义,那么只有24个中断号。

数组中元素定义如下:经过宏变换以后,interrupt[ i ]实际上就是IRQ0xNN_interrupt,其中NN代表十六进制数,如IRQ0x1_interrupt,IRQ0x2_interrupt等。

数组实际上是由一个ENRTY(name)宏定义的(ENTRY宏表明name是全局变量):

ENTRY(interrupt)
.previous
vector=0

ENTRY(irq_entries_start)
.rept NR_IRQS //表示循环NR_IRQS 到.endr截止
      ALIGN


1:      pushl $vector-256
      jmp common_interrupt
.data
      .long 1b
.text
vector=vector+1
.endr

每个IRQ0xNN_interrupt所做的事情就是把向量号压入栈,然后跳转到common_interrupt(通用中断处理程序,第4小节中详细解释)。

(3)中断向量表的第二次初始化

interrupt数组生成之后,内核进行第二次中断向量表idt_table的初始化。在/init/main.c中调用了init_IRQ函数(定义在arch\i386\kernel\i8259.c中),函数中有如下的循环:

    for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++ ) {
        int vector = FIRST_EXTERNAL_VECTOR +
i;
        if (i >=
NR_IRQS)
            break
;
        if (vector !=
SYSCALL_VECTOR)
             set_intr_gate(vector, interrupt[i]); //使用interrupt数组初始化中断门
     }

而set_intr_gate函数实际上就是初始化idt_table的函数,其定义如下:

void set_intr_gate(unsigned int n, void *addr)
{
_set_gate( idt_table+n,14,0,addr);
}

因此最终效果就是将interrupt数组对应的每一项地址放到了idt_table中。所以interrupt[]数组只是一个中间变量。最后CPU查找时仍然是查找idt_table。

(4)通用中断处理程序

当每一个中断到来时,都会执行下面的通用中断处理程序(在interrupt数组中的粗体):

common_interrupt:   //通用中断处理程序
     SAVE_ALL            // 1
     movl %esp,%
eax    //
     call do_IRQ           // 2+3
     jmp ret_from_intr   // 4

不管引起中断的设备是什么,所有的I/O中断处理程序都执行四个相同的基本操作:

1,在内核态堆栈保存IRQ的值和寄存器的内容
2,为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断
3,执行共享这个IRQ的所有设备的中断服务例程(ISR)
4,跳到ret_from_intr()的地址

这里的4个操作就对应上面common_interrupt宏中的四条指令:SAVE_ALL宏就是将大部分的寄存器的值保存到内核中断栈中(中断都是在内核态发生的,这个宏的具体代码可以很容易的找到);然后下一行将参数入栈供do_IRQ函数调用;do_IRQ函数则执行了大部分的中断处理工作,包括给PIC发送应答以及执行本IRQ号的中断服务例程;最后一条则跳到ret_from_intr()的地址。

(5)中断处理函数:do_IRQ/__do_IRQ/handle_IRQ_event

我们知道,CPU要根据得到的IRQ号到中断向量表中查找中断描述符,以便调用相应的中断处理函数。因此,中断描述符irq_desc_t(读者可以回忆一下进程描述符task_struct)也一一对应的形成了一个数组irq_desc[]。描述符中最重要的两部分是handler对象(代表硬件PIC)和action对象(代表中断服务例程ISR)。请注意:中断服务例程ISR不同于上面说到的通用中断处理程序和中断处理函数。中断服务例程是指由发起中断的设备的驱动程序注册到系统中,当发生中断时由CPU调用的一个程序,是由设备或者用户定义的;而通用中断处理程序和中断处理函数都是Linux内核所默认执行的。

中断处理程序do_IRQ除了做一些准备工作以及切换到内核栈以外,最主要的工作调用__do_IRQ来完成,需要把IRQ号作为参数(irq,保存在eax寄存器中)传递给它。

__do_IRQ函数的主要工作有:

// 加锁
spin_lock(&(irq_desc[irq].lock ));
// 向PIC发出应答信号

irq_desc[irq].handler-> ack(irq);

//
当前状态既不是IRQ_REPLAY:The IRQ line has been disabled but the previous IRQ occurrence has not yet been acknowledged to the PIC
// 也不是IRQ_WAITING:The kernel is using the IRQ line while performing a hardware device probe; moreover, the corresponding interrupt has not been raised

irq_desc[irq].status &= ~(IRQ_REPLAY | IRQ_WAITING);

// 当前状态为IRQ_PENDING: An IRQ has occurred on the line; its occurrence has been acknowledged to the PIC, but it has not yet been serviced by the kernel

irq_desc[irq].status |= IRQ_PENDING;

if (!(irq_desc[irq].status & (IRQ_DISABLED | IRQ_INPROGRESS)) // 如果当前状态不是IRQ_DISABLED或者 IRQ_INPROGRESS

            && irq_desc[irq].action) {                            // 有对应的处理函数
         irq_desc[irq].status |= IRQ_INPROGRESS;// 设置当前当前状态为IRQ_INPROGRESS : A handler for the IRQ is being executed
        do {
             irq_desc[irq].status &= ~IRQ_PENDING;// 设置当前状态不是IRQ_PENDING,因为下面要开始处理了

             spin_unlock(&(irq_desc[irq].lock ));
             handle_IRQ_event(irq, regs, irq_desc[irq].action);    // 处理事件:执行其action函数指针指向的函数

             spin_lock(&(irq_desc[irq].lock ));
         } while (irq_desc[irq].status & IRQ_PENDING);            // 如果当前状态还是IRQ_PENDING循环继续

         irq_desc[irq].status &= ~IRQ_INPROGRESS;                // 设置当前状态不是IRQ_INPROGRESS
}

irq_desc[irq].handler->
end(irq);
spin_unlock(&(irq_desc[irq].lock
));

在循环处理IRQ请求的时候,最开始要设置状态为IRQ_INPROGRESS同时不是IRQ_PENDING,这个循环处理IRQ请求的过程在当前状态是IRQ_PENDING则一直进行下去,当该循环处理完毕之后, 再将状态设置为IRQ_INPROGRESS。

其中真正执行ISR的是上面粗体标出的handle_irq_event函数,定义如下:

fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
struct irqaction *action)
{
int ret, retval = 0, status = 0;

if (!(action->flags & SA_INTERRUPT)) //如果穿越中断门,而又没设置SA_INTERRUPT标志,这里需要允许中断
local_irq_enable();

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 & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();

return retval;
}

由于系统中可能有很多个设备共享同一个IRQ号,它们在注册到系统中时,每个IRQ号的ISR(下图中的irqaction)就形成了一个链表。

《深入理解Linux内核》笔记3:中断

所以handle_irq_event函数中使用了一个do/while循环遍历了这个链表,并执行每一个注册的ISR:action->handler()。其中此处的handler函数是一个函数指针,指向设备注册的那个函数。

你可能感兴趣的:(linux)