Linux源代码阅读笔记-硬件中断

来源:互联网 发布:唯品会用什么抢购软件 编辑:程序博客网 时间:2024/05/16 11:46

Linux硬中断

Linux 中断和其他操作系统的中断处理一样,要求有硬件和软件的支持。Linux的好处就是可以看到核心处理中断的一举一动,以下对linux的中断机制做详细的分析。

首先对linux中能处理的中断分类:

1.物理硬件设备产生的中断,这些设备与主板上的i8259A中断控制器相连,具体的连接可以找本《计算机组成原理》看看。linux中可以处理的有16个中断号,但这并不意味linux只能处理16个外设中断请求,实际上许多外设是可以共享中断号,这个要求操作系统的软件支持,在后面可以看到linux是如果处理。

2.异常,异常是无法预测的意外。如被0除、缺页、

set_trap_gate(0,&divide_error);

set_trap_gate(1,&debug);

set_intr_gate(2,&nmi);

set_system_gate(3,&int3);   /* int3-5 can be called from all */

set_system_gate(4,&overflow);

set_system_gate(5,&bounds);

set_trap_gate(6,&invalid_op);

set_trap_gate(7,&device_not_available);

set_trap_gate(8,&double_fault);

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_trap_gate(14,&page_fault);

set_trap_gate(15,&spurious_interrupt_bug);

set_trap_gate(16,&coprocessor_error);

set_trap_gate(17,&alignment_check);

set_trap_gate(18,&machine_check);

set_trap_gate(19,&simd_coprocessor_error);

 

set_system_gate(SYSCALL_VECTOR,&system_call);

 

/*

 * default LDT is a single-entry callgate to lcall7 for iBCS

 * and a callgate to lcall27 for Solaris/x86 binaries

 */

set_call_gate(&default_ldt[0],lcall7);

set_call_gate(&default_ldt[4],lcall27);

 

以上摘自i386/kernel/Traps.c-> trap_init()函数片断,trap_init()函数又被操作系统的初始化工作start_kernel()函数调用,从列表中可以看出linux所处理的异常。从上面的注释中可以看到异常35,就是中断指令int3int4int5可以被所有的进程调用,这三个实际上是调试程序所用到的,所以当然能被你的程序调用。而其他的异常只能被处于特权级0的进程调用,也就是说这些异常只能被内核所处理,道理非常明显,但是实现起来就不是那么直接了。

3.陷阱(trap

陷阱又称为主动异常,就是在你的程序中直接出现int n指令,实际上在你的程序中能出现的这样的指令也不多,除非你的程序是已模块的方式联入内核中了。

4.系统调用int80

之所以特地提出就是为了把系统调用说明白,系统调用是操作系统实现的,给用户提供的功能接口。很多人会对之有一种神秘感,它的代码在系统初始化时载入内存的低端,被映射入内核空间(3G)以上,对所有的调用进程来说,它们的地址是一样的。

以上的分类很多人并不一定赞同,但有胜于无。

 

Linux的中断的处理

80386以前的cpu中,中断是通过中断向量表IDT处理。这个处理大家可以看看有关的书。就是操作系统把中断处理的人口地址放在内存0开始的地方,每一个中断向量占4个字节,2个为了段基地址,另外2个为了偏移。一共是256个向量,刚好是1k。在80386中,CPU允许在任意地址存放IDT,这样和处理段一样,CPU多了一个寄存器IDTR,存放IDT在内存的起址。80386中为了在保护模式中处理中断,引入了一个新的术语“门”,对于这个术语大家可以参考intel的资料,在这里就理解为进程权限改变时必须通过的一道检查门。Intel80386设置了4种门:任务门、中断门、陷阱门、调用门。后三者基本上差不多。任务门看不出有什么用处,在linux的注释中说明为了在linux上运行iBCSSolaris/X86设置了两个任务门。80386的中断向量描述也和8086完全不一样了,中断向量有64位长,给出的是中断处理程序的段选择符(16位)、程序的偏移地址(32位)、类型码。因此在这里可以描述一下80386处理中断的场景了:(1CPU根据中断矢量在IDT中找到对应的中断项;(2)根据中断向量中的段选择符,在GDT中找到段的起址,结合偏移量,就得到中断服务的线性地址,这个地址肯定在3G以上的,也就是说是映射到核心区域中的。

80386中对中断设置了门(中断向量),它对进程的权限有严格的要求,具体是:

1.进入门之前,进程的权限要高于门的权限(这个权限在中断向量的类型描述中),

2.进入门后,进程的权限要高于中断服务程序所在的段的运行权限(GDT)。

3.硬件中断可以绕过上述过程。

4.其它类型的中断,异常,如果不满足1就会引发一个保护异常。

这种权限设置在初始化的时候就完成了,我们看函数trap_init()中,共有四种设置函数,分别是

       set_trap_gate

       set_system_gate

       set_intr_gate

       set_call_gate

在源代码traps.c中找到它们的定义

void set_intr_gate(unsigned int n, void *addr)

{

       _set_gate(idt_table+n,14,0,addr);

}

 

static void __init set_trap_gate(unsigned int n, void *addr)

{

       _set_gate(idt_table+n,15,0,addr);

}

 

static void __init set_system_gate(unsigned int n, void *addr)

{

       _set_gate(idt_table+n,15,3,addr);

}

 

static void __init set_call_gate(void *a, void *addr)

{

       _set_gate(a,12,3,addr);

}

实际上它们都是由一个内部函数完成的,_set_gate(),就是参数不同,这里就关注第三个参数,这个参数就是门的权限。如果这个参数等于3,就是说用户态进程也能调用。这样就得出system_gatecall_gate用户态进程能调用,intr_gatetrap_gate只有 核心态进程能调用了。这里要补充一点就是intr_gatetrap_gate系统处理是基本一致的,唯一的不同点就是intr_gate处理关了中断,而trap_gate在处理时不关中断。

       对异常的处理具体可以看处理函数的定义,这里就不详细解释了。Linux对硬件的中断进行了统一的处理。这里就要看看具体的代码了。在源文件arch/i386/kernel/i8259.c中,先看一下函数init_IRQ,这个函数是硬件中断的初始化函数,也被start_kernel调用。去掉了SMP宏等条件编译的代码,简化一下就得到如下代码

void __init init_IRQ(void)

{

       ……….

       init_ISA_irqs();

       ……….

       for (i = 0; i < NR_IRQS; i++) {

              int vector = FIRST_EXTERNAL_VECTOR + i;

              if (vector != SYSCALL_VECTOR)

                     set_intr_gate(vector, interrupt[i]);

       }

       ……..

}

核心是两个函数init_ISA_irqsset_intr_gate和一个全局数组变量interrupt。宏FIRST_EXTERNAL_VECTOR=20,也就是硬件中断向量>=20,这里说明一下中断号和中断向量的区别,中断号实际上是硬件连接i8259a是的线号,是物理的;中断向量是逻辑的,代表中断服务在IDT中的下标。宏SYSCALL_VECTOR0x80,为系统调用保留。

先看init_ISA_irqs的定义

void __init init_ISA_irqs (void)

{

       int i;

 

       init_8259A(0);

 

       for (i = 0; i < NR_IRQS; i++) {

              irq_desc[i].status = IRQ_DISABLED;

              irq_desc[i].action = 0;

              irq_desc[i].depth = 1;

 

              if (i < 16) {

                     /*

                      * 16 old-style INTA-cycle interrupts:

                      */

                     irq_desc[i].handler = &i8259A_irq_type;

              } else {

                     /*

                      * 'high' PCI IRQs filled in on demand

                      */

                     irq_desc[i].handler = &no_irq_type;

              }

       }

}

首先要解释的是全局irq_desc数组。

/*

 * This is the "IRQ descriptor", which contains various information

 * about the irq, including what kind of hardware handling it has,

 * whether it is disabled etc etc.

 *

 * Pad this out to 32 bytes for cache and indexing reasons.

 */

typedef struct {

       unsigned int status;              /* IRQ status */

       hw_irq_controller *handler;

       struct irqaction *action;  /* IRQ action list */

       unsigned int depth;              /* nested irq disables */

       spinlock_t lock;

} ____cacheline_aligned irq_desc_t;

 

extern irq_desc_t irq_desc [NR_IRQS];

 

从以上的声明中看出,irq_desc"IRQ descriptor"。不明白的是结构中的类型struct irqactionhw_irq_controller,再找出它们的声明:

struct hw_interrupt_type {

       const char * typename;

       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 (*end)(unsigned int irq);

       void (*set_affinity)(unsigned int irq, unsigned long mask);

};

 

typedef struct hw_interrupt_type  hw_irq_controller;

 

struct irqaction {

       void (*handler)(int, void *, struct pt_regs *);

       unsigned long flags;

       unsigned long mask;

       const char *name;

       void *dev_id;

       struct irqaction *next;

};

hw_irq_controller就是控制中断控制芯片的,它的功能从数据类型上可以看出。所以当I<16irq_desc[i].handler = &i8259A_irq_type;这里的I对应着中断号。struct irqaction是一个链表,前面说过硬件可以共享中断号,因此一个中断号来了后,就在一个链表中找处理程序,这个处理程序就是irqaction->handler,这个链表是irq_desc[i]-> irqaction。可见init_ISA_irqs就是初始化irq_desc数组用的,irq_desc数组记录了各个中断号的服务程序的列表,在后面的系统统一中断处理程序中还会看到它。

第二个函数set_intr_gate前面已经见过了,这里为了方便再粘贴一下

void set_intr_gate(unsigned int n, void *addr)

{

       _set_gate(idt_table+n,14,0,addr);

}

它是写IDT表格中断向量的,关键是全局数组interrupt

在同文件中找到

void (*interrupt[NR_IRQS])(void) = {

       IRQLIST_16(0x0),

…….

}

是一个函数指针数组,IRQLIST_16(0x0)是一个宏定义

#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(x)定义了16个函数,名称是IRQ0x0n_interrupt,比如3号中断的函数就是void IRQ0x03_interrupt(void)。这个函数的定义在linux的源文件I8259a.c中,就是通过一串的宏定义掩盖了,仔细看

#define BI(x,y) /

       BUILD_IRQ(x##y)

 

#define BUILD_16_IRQS(x) /

       BI(x,0) BI(x,1) BI(x,2) BI(x,3) /

       BI(x,4) BI(x,5) BI(x,6) BI(x,7) /

       BI(x,8) BI(x,9) BI(x,a) BI(x,b) /

       BI(x,c) BI(x,d) BI(x,e) BI(x,f)

 

/*

 * ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts:

 * (these are usually mapped to vectors 0x20-0x2f)

 */

BUILD_16_IRQS(0x0)

 

BUILD_16_IRQS定义了16BI,每一个BI都通过宏BUILD_IRQ(x##y)展开,

 

找出BUILD_IRQ(x##y)的宏定义:

 

#define BUILD_IRQ(nr) /

asmlinkage void IRQ_NAME(nr); /

__asm__( /

"/n"__ALIGN_STR"/n" /

SYMBOL_NAME_STR(IRQ) #nr "_interrupt:/n/t" /

       "pushl $"#nr"-256/n/t" /

       "jmp common_interrupt");

 

再找出IRQ_NAME的宏定义

#define IRQ_NAME2(nr) nr##_interrupt(void)

#define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)

 

终于找出IRQ0x03_interrupt的定义,就是

__asm__( /

"/n"__ALIGN_STR"/n" /

SYMBOL_NAME_STR(IRQ) #nr "_interrupt:/n/t" /

       "pushl $"#nr"-256/n/t" /

       "jmp common_interrupt");

翻译为c语言就是

{

       IRQ#nr_interrupt:

       Pushl nr-256

       Jmp common_interrupt

}

这个一连串的宏定义就是为了简写源代码,否则要写16编相同的代码。如果有人看过《深入浅出MFC》,对这个应该比较熟悉,在MFC中的类定义里面也用到了很多的宏,同样让你看傻了眼。这些函数都要跳到common_interrupt下面的代码执行,那么这个又在哪里?在

一个头文件里面,这个文件是Hw_irq.h,定义如下:

#define BUILD_COMMON_IRQ() /

asmlinkage void call_do_IRQ(void); /

__asm__( /

       "/n" __ALIGN_STR"/n" /

       "common_interrupt:/n/t" /

       SAVE_ALL /

       "pushl $ret_from_intr/n/t" /

       SYMBOL_NAME_STR(call_do_IRQ)":/n/t" /

       "jmp "SYMBOL_NAME_STR(do_IRQ));

跳到了一个函数call_do_IRQ里面,这个函数用c复写一遍就是

void call_do_IRQ(void)

{

       common_interrupt:

              SAVE_ALL

pushl $ret_from_intr

call_do_IRQ:

       jmp do_IRQ

}

SAVE_ALL是一个宏,

       #define SAVE_ALL /

       "cld/n/t" /

       "pushl %es/n/t" /

       "pushl %ds/n/t" /

       "pushl %eax/n/t" /

       "pushl %ebp/n/t" /

       "pushl %edi/n/t" /

       "pushl %esi/n/t" /

       "pushl %edx/n/t" /

       "pushl %ecx/n/t" /

       "pushl %ebx/n/t" /

       "movl $" STR(__KERNEL_DS) ",%edx/n/t" /

       "movl %edx,%ds/n/t" /

       "movl %edx,%es/n/t"

一看就明白,保存当前进程的寄存器,保存在当前进程的系统堆栈里。ret_from_intr估计可以猜出了,就是处理中断返回的函数,但是为什么要进堆栈呢?这个就和do_IRQ函数的调用方式有关了,do_IRQ函数是通过jmp跳过去执行的,也就是没有把函数后的指令地址入栈,这样函数返回后就找不到下一条指令了,pushl $ret_from_intr就解决了这个问题,函数do_IRQ返回后,指令ret就把ret_from_intr出栈,同时装入到ip中,就到ret_from_intr执行了,这是一个汇编程序设计的技巧,被用到了这里,也难怪会把人看得要抓狂了。

绕了老半天,明白了处理硬件中断服务的是函数do_IRQ。看一下这个函数,在irq.c里面

绕过一些错误处理和SMP的处理,简单地看一下。

/*

 * do_IRQ handles all normal device IRQ's (the special

 * SMP cross-CPU interrupts have their own specific

 * handlers).

 */

asmlinkage unsigned int do_IRQ(struct pt_regs regs)

{

int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code  */

       int cpu = smp_processor_id();

       irq_desc_t *desc = irq_desc + irq;

       struct irqaction * action;

       unsigned int status;

 

       kstat.irqs[cpu][irq]++;

       spin_lock(&desc->lock);

       desc->handler->ack(irq);

       …….

action = NULL;

       if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {

              action = desc->action;

              status &= ~IRQ_PENDING; /* we commit to handling */

              status |= IRQ_INPROGRESS; /* we are handling it */

       }

       desc->status = status;

 

for (;;) {

              spin_unlock(&desc->lock);

              handle_IRQ_event(irq, &regs, action);

              spin_lock(&desc->lock);

             

              if (!(desc->status & IRQ_PENDING))

                     break;

              desc->status &= ~IRQ_PENDING;

       }

…….

if (softirq_active(cpu) & softirq_mask(cpu))

              do_softirq();

       return 1;

}

关键在函数ndle_IRQ_event(irq, &regs, action)中,

int handle_IRQ_event(unsigned int irq, struct pt_regs * regs, struct irqaction * action)

{

       int status;

       int cpu = smp_processor_id();

 

       irq_enter(cpu, irq);

 

       status = 1;       /* Force the "do bottom halves" bit */

 

       if (!(action->flags & SA_INTERRUPT))

              __sti();

 

       do {

              status |= action->flags;

              action->handler(irq, action->dev_id, regs);

              action = action->next;

       } while (action);

       if (status & SA_SAMPLE_RANDOM)

              add_interrupt_randomness(irq);

       __cli();

 

       irq_exit(cpu, irq);

 

       return status;

}

andle_IRQ_event函数的do-while循环中,终于看到了action = action->next,系统在沿着链表处理中断,和原先的设想是一致的。看到这里,再回到前面,明白了interrupt数组的作用。

现在再回顾一下linux处理中断的整个过程,硬件产生一个中断,cpu得到中断号,系统在IDT中找到和这个中断号对应的中断向量,从中断向量中得到IRQ0x0n_interupt的函数地址,

执行这个函数,跳到common_interrupt,到call_do_IRQ,到do_IRQ,最后到handle_IRQ_event

从整个中断的处理过程来说,以上也就是涉及到了中断的初始,处理的方面,还有用户是如何设置中断处理函数,这个在request_IRQ函数中处理。硬件中断就写这么多了,还有软中断和系统调用没有写,等看完后再说。