Linux硬中断
Linux 中断和其他操作系统的中断处理一样,要求有硬件和软件的支持。Linux的好处就是可以看到核心处理中断的一举一动,以下对linux的中断机制做详细的分析。
首先对linux中能处理的中断分类:
1.物理硬件设备产生的中断,这些设备与主板上的i8259A中断控制器相连,具体的连接可以找本《计算机组成原理》看看。linux中可以处理的有16个中断号,但这并不意味linux只能处理16个外设中断请求,实际上许多外设是可以共享中断号,这个要求操作系统的软件支持,在后面可以看到linux是如果处理。
2.异常,异常是无法预测的意外。如被0除、缺页、
set_trap_gate(0,÷_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所处理的异常。从上面的注释中可以看到异常3-5,就是中断指令int3,int4,int5可以被所有的进程调用,这三个实际上是调试程序所用到的,所以当然能被你的程序调用。而其他的异常只能被处于特权级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的资料,在这里就理解为进程权限改变时必须通过的一道检查门。Intel为80386设置了4种门:任务门、中断门、陷阱门、调用门。后三者基本上差不多。任务门看不出有什么用处,在linux的注释中说明为了在linux上运行iBCS和Solaris/X86设置了两个任务门。80386的中断向量描述也和8086完全不一样了,中断向量有64位长,给出的是中断处理程序的段选择符(16位)、程序的偏移地址(32位)、类型码。因此在这里可以描述一下80386处理中断的场景了:(1)CPU根据中断矢量在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_gate和call_gate用户态进程能调用,intr_gate和trap_gate只有 核心态进程能调用了。这里要补充一点就是intr_gate和trap_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_irqs,set_intr_gate和一个全局数组变量interrupt。宏FIRST_EXTERNAL_VECTOR=20,也就是硬件中断向量>=20,这里说明一下中断号和中断向量的区别,中断号实际上是硬件连接i8259a是的线号,是物理的;中断向量是逻辑的,代表中断服务在IDT中的下标。宏SYSCALL_VECTOR是0x80,为系统调用保留。
先看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 irqaction和hw_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<16时irq_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定义了16个BI,每一个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, ®s, 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, ®s, 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函数中处理。硬件中断就写这么多了,还有软中断和系统调用没有写,等看完后再说。