CPU响应中断
CPU从中断控制器取得中断向量,然后根据具体的中断向量从中断向量表IDT中找到对应的表项,而该表项应该是一个中断门。这样,CPU就根据中断门的设置而到达了该通道的总服务程序入口。
由于中断是当CPU在用户空间运行时发生的,运行级别CPL为3,而中断服务程序属于内核,其运行级别CPL为0。所以CPU要从寄存器TR所指向的当前TSS中取出用于内核0级的堆栈指针,并把堆栈切换到内核堆栈,即当前进程的系统空间堆栈。
每次从系统空间返回到用户空间时堆栈指针一定回到其原点,“堆栈底部”。也就是说,当CPU从TSS中取出内核堆栈指针并切换到内核堆栈时,这个堆栈一定是空的。
所有公用中断请求的服务程序总入口是由gcc在预处理阶段生成的。它将一个中断请求号相关的数值压入堆栈(中断请求号减去256使其变成负数!)。
系统堆栈中的这个位置在因系统调用而进入内核时要用来存放系统调用号,而系统调用又与中断服务共用一部分子程序。这样,要有个手段来加以区分。
将一个整数装入一个通用寄存器之后,要判断它是否大于等于0很方便的,只要一条寄存器指令就可以了。而如果要与另一个常数相比较,就至少要多访问一次内存。
公共得跳转目标common_interrupt()是在include/asm-i386/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));
其中SAVE_ALL,是保存现场,见arch/i386/kernel/entry.S中
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__KERNEL_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;
在SAVE_ALL以后,又将一个程序标号入口ret_from_intr压入堆栈,并通过jmp指令转入另一段程序do_IRQ()。common_interrupt本质上不是函数,它们都没有与return相关得指令,所以从common_interrupt不能方回到某一个interrupt,可是,do_IRQ()却是一个函数。在通过jmp指令转入do_IRQ()之前将返回地址ret_from_intr压入堆栈就模拟了一次函数调用,仿佛对do_IRQ()的调用就发生在CPU进入ret_from_intr的的一条指令前夕一样。这样,当从do_IRQ()返回时就会“返回”到ret_from_intr继续执行。
do_IRQ()是在arch/i386/kernel/irq.c中定义的,
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
调用参数是一个pt_regs数据结构,主要这是一个数据结构,不是指向数据结构得指针!也就是说,在堆栈中的返回地址以上的位置应该是一个数据结构的映象。
数据结构struct pt_regs是在include/asm-i386/ptrace.h中定义的:
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
(对照前面的SAVE_ALL来看这个结构!)
前面所做得一切,包括CPU在进入中断时自动做的,实际上都是在为do_IRQ()建立一个模拟得子程序调用环境,使得在do_IRQ()中既可以方便地知道进入中断前夕各个寄存器得内容,又可以在执行完毕后返回到ret_from_intr,并且从那里执行中断放回。
对系统堆栈的这种安排不光用于中断,还用于系统调用。
在IRQ0xXX_interrupt中把中断请求号相关的数值减去256压入堆栈的目的是使得在公共的中断处理程序中可以知道中断的来源。以IRQ3为例,3-265
= -253 =
0xffffff03,通过regs.orig_eax读出来并且把高位屏蔽掉,又得到0x03。由于do_IRQ()仅用于中断服务,所以不需要顾及系统调用时的情况。
do_IRQ()中调用spin_lock()加锁,是为了多处理器的情况而设置的。
desc->status的标志位IRQ_PENDING和IRQ_INPROGRESS是为了同一个CPU不允许中断服务套嵌,不同CPU不允许并发地进入同一个中断服务程序而设计的。
handle_IRQ_event()函数依次执行对列中的各个中断服务程序,让它们辨认本次中断请求是否来自各自的服务对象,即中断源,如果是就提供相应的服务。
action->flags标志位SA_INTERRUPT表示这个中断服务程序应该在开启中断的情况下执行。_sti()为开中断,_cli为关中断。标志位SA_SAMPLE_RANDOM表示服务程序要为系统引入一些随机性。
handle_IRQ_event()返回值status最低位必须为1。Force the "do bottom
halves" bit.
当do_IRQ()返回时,返回到ret_from_intr处,见
arch/i386/kernel/entry.S
ENTRY(ret_from_intr)
GET_CURRENT(%ebx)
ret_from_exception:
movl EFLAGS(%esp),%eax # mix EFLAGS and CS
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax # return to VM86 mode or
non-supervisor?
jne ret_from_sys_call
jmp restore_all
即使跳转到ret_from_sys_call,最终还是会到restore_all
#define RESTORE_ALL \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax; \
1: popl %ds; \
2: popl %es; \
addl $4,%esp; \
3: iret; \
这与SAVE_ALL是相互对应的。addl
$4,%esp将堆栈指针的当前值加4,是为了跳过ORIG_EAX,那是在进入中断之初压入堆栈的经过变形的中断请求号。
当CPU到达iret指令时,系统堆栈又恢复到刚进入中断门时的状态,而iret则使CPU从中断返回。