linux内核-中断的响应和服务

搞清了i386 CPU的中断机制和内核中有关的初始化以后,我们就可以从中断请求的发生到CPU的响应,再到中断服务程序的调用与返回,沿着CPU所经历的路线走一遍。这样,既可以弄清和理解linux内核对中断响应和服务的总体的格局和安排,还可以顺着这个过程介绍内核中的一些相关的基础设施。对此二者的了解和理解,有助于读者对整个内核的理解。

这里,我们假定外设的驱动程序都已经完成了初始化,并且把相应的中断服务程序挂入到特定的中断请求队列中,系统正在用户空间正常运行(所以中断必然是开着的),并且某个外设已经产生了一次中断请求。该请求通过中断控制器i8259A到达了CPU的中断请求引线INTR。由于中断是开着的,所以CPU在执行完当前指令后就来响应该次中断请求。

CPU从中断控制器取得中断向量,然后根据具体的中断向量从中断向量表IDT中找到相应的表项,而该表项应该是一个中断门。这样,CPU就根据中断门的设置而到达了该通道的总服务程序的入口,假定为IRQ0x03_interrupt。由于中断是当CPU在用户空间中运行时发生的,当前的运行级别CPL为3;而中断服务程序属于内核,其运行级别DPL为0,二者不同。所以,CPU要从寄存器TR所指的当前TSS中取出用于内核(0级)的堆栈指针,并把堆栈切换到内核堆栈,即当前进程的系统空间堆栈。应该指出,CPU每次使用内核堆栈时对堆栈所做的操作总是均衡的,所以每次从系统空间返回到用户空间时堆栈指针一定回到其原点,或曰堆栈底部。也就是说,当CPU从TSS中取出内核堆栈指针并切换到内核堆栈时,这个堆栈一定是空的。这样,当CPU进入IRQ0x013_interrupt时,堆栈中除寄存器EFLAGS的内容以及返回地址外就一无所有了。另外,由于所穿过的是中断门(而不是陷阱门),所以中断已被关闭,在重新开启中断之前再没有其他的中断可以发生了。

中断服务的总入口IRQ0xYY_interrupt的代码以前已经见到过了,但为方便起见再把它列出在这里。再说,我们现在的认识也可以更深入一些了。

如前所述,所有公用中断请求的服务程序总入口是由gcc的预处理阶段生成的,全部都具有相同的模式:

asmlinkage void IRQ0x00_interrupt(void); \
__asm__( \
"\n" \
IRQ0x00_interrupt:\n\t" \
	"pushl $0x03-256\n\t" \
	"jmp common_interrupt");   

这段程序的目的在于将一个与中断请求号相关的数值压入堆栈,使得common_interrupt中可以通过这个数值来确定该次中断的来源。可是为什么要从中断请求号0x03中减去256使其变成负数呢?就用数值0x03不是更直截了当吗?这是因为,系统堆栈中的这个位置在因系统调用而进入内核时要用来存放系统调用号,而系统调用又与中断服务公用一部分子程序。这样,就要有个手段来加以区分。当然,要区分系统调用号和中断请求号并不非得把其中之一变成负数不可。例如,在中断请求号上加上一个常数,比方说0x1000,也可以达到目的。但是,如果考虑到运行时的效率,那么把其中之一变成负数无疑是效率最高的。将一个整数装入到一个通用寄存器之后,要判断它是否大于等于0是很方便的,只要一条寄存器指令就可以了,如orl %%eax,%%eax或testl %%eax,%%eax都可以达到目的。而如果要与另一个常数相比,那就至少要多访问一次内存。从这个例子也可以看出,内核中的有些代码看似简单,好像只是作者随意的决定,但实际上却是经过精心推敲的。

公共的跳转目标common_interrupt的定义:

IRQ0x03_interrupt=>common_interrupt

#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,就是所谓的保存现场,把中断发生前夕所有寄存器的内容都保存在堆栈中,待中断服务完毕要返回之前再来恢复现场。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;

这里要指出两点:第一是标志位寄存器EFLAGS的内容并不是在SAVE_ALL中保存的,这是因为CPU在进入中断服务时已经把它的内容连同返回地址一起压入堆栈了。第二是段寄存器DS和ES原来的内容被保存在堆栈中,然后就被改成指向用于内核的__KERNEL_DS。我们在内存管理博客中讲过,__KERNEL_DS和__USER_DS都指向从0开始的空间,所不同的只是运行级别DPL一个为0级,另一个为3级。至于原来的堆栈段寄存器SS和堆栈指针SP的内容,则或者已被压入堆栈(如果更换堆栈),或者继续使用而无需保存(如果不更换堆栈)。这样,在SAVE_ALL以后,堆栈中的内容就成为下图形式。

此时系统堆栈中各项相对于堆栈指针的位置如上图所示,而arch/i386/kernel/entry.S中也根据这些关系定义了一些常数:

EBX		= 0x00
ECX		= 0x04
EDX		= 0x08
ESI		= 0x0C
EDI		= 0x10
EBP		= 0x14
EAX		= 0x18
DS		= 0x1C
ES		= 0x20
ORIG_EAX	= 0x24
EIP		= 0x28
CS		= 0x2C
EFLAGS		= 0x30
OLDESP		= 0x34
OLDSS		= 0x38

 这里的EAX,举例来说,当出现在entry.S的代码中时并不是表示寄存器%%eax,而是表示该寄存器的内容在系统堆栈中的位置相对于此时的堆栈指针的位移。前面在转入common_interrupt之前压入堆栈的(中断调用号-256)所在位置称为ORIG_EAX,对中断服务程序而言它代表着中断请求号。

回到common_interrupt的代码。在SAVE_ALL以后,又将一个程序标号(入口)ret_from_intr压入堆栈,并通过jmp指令转入另一段程序do_IRQ。读者可能已注意到,IRQ0x03_interrupt和common_interrupt本质上都不是函数,它们都没有return相当的指令,所以从common_interrupt不能返回到IRQ0x03_interrupt,而从IRQ0x03_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中定义的,我们先看开头几行:

IRQ0x03_interrupt=>common_interrupt=>do_IRQ

/*
 * 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)
{	
	/* 
	 * We ack quickly, we don't want the irq controller
	 * thinking we're snobs just because some other CPU has
	 * disabled global interrupts (we have already done the
	 * INT_ACK cycles, it's too late to try to pretend to the
	 * controller that we aren't taking the interrupt).
	 *
	 * 0 return value means that this irq is already being
	 * handled by some other CPU. (or is disabled)
	 */
	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;

函数的调用参数是一个pt_regs数据结构。注意,这是一个数据结构,而不是指向数据结构的指针。也就是说,在堆栈中的返回地址以上的位置上应该是一个数据结构的映像。数据结构struct pt_regs是在include/asm-i386/ptrace.h中定义的:


/* this struct defines the way the registers are stored on the 
   stack during a system call. */

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;
};

相信读者一定会联想到前面讲过的系统堆栈的内容并且恍然大悟:原来前面所做的一切,包括CPU在进入中断时自动做的,实际上都是在为do_IRQ建立一个模拟的子程序调用环境,使得在do_IRQ中既可以方便地知道进入中断前夕各个寄存器的内容,又可以在执行完毕以后返回到ret_from_intr,并且从那里执行中断返回。可想而知,当do_IRQ调用具体的中断服务程序时也一定会把pt_regs数据结构的内容传下去,不过那时只要传递一个指针就够了。读者不妨回顾一下我们在内存管理中讲过的页面异常服务程序do_page_fault,其调用参数表为:

asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code);

第一个参数就是指向struct pt_regs的指针,实际上就是指向系统堆栈中的那个地方。当时我们无法将这一点讲清楚,所以略了过去。而现在结合进入中断的过程一看就清楚了。不过,页面异常并不属于通用的中断请求,而是为CPU保留专用的,所以中断发生时并不经过do_IRQ这条线路,但是对于系统堆栈的这种安排基本上是一致的。

以后读者还会看到,对系统堆栈的这种安排不光用于中断,还用于系统调用。

前面讲过,在IRQ0x03_interrupt中把数值(0x03-256)压入堆栈的目的是使得在公共的中断处理程序中可知道中断的来源,现在进入do_IRQ以后的第一件事情就是要弄清楚这一点。以IRQ3为例,压入堆栈的数值为0xffffff03,现在通过regs.orig_eax & 0xff读回来并且把高位屏蔽掉,就又得到0x03。由于do_IRQ仅用于中断服务,所以不需要顾及系统调用时的情况。

代码中561行的smp_processor_id是为多处理器SMP结构而设的,在单处理器系统中总是返回0。现在,既然中断请求号已经恢复,从数组irq_desc中找到相应的中断请求队列当然是轻而易举的了(562行)。下面就是对具体中断请求队列的操作了。我们继续在do_IRQ中往下看:

IRQ0x03_interrupt=>common_interrupt=>do_IRQ

	kstat.irqs[cpu][irq]++;
	spin_lock(&desc->lock);
	desc->handler->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
	   */
	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;
	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;

当通过中断门进入中断服务时,CPU的中断响应机制就自动被关闭了。既然已经关闭中断,为什么567行还要调用spin_lock加锁呢?这是为多处理的情况而设置的,我们将在多处理器SMP系统结构系列博客中讲述,这里暂时只考虑单处理器结构。

中断处理器(如i8259A)将中断请求上报到CPU以后,期待CPU给它一个确认(ACK),表示我已经在处理,这里的568行就是做这件事。对函数指针desc->handler->ack的设置前面已经讲过。从569行至586行主要是对desc->status,即中断通道状态的处理和设置,关键在于将其IRQ_INPROGRESS标志位设成1,而IRQ_PENDING标志位清0。其中IRQ_INPROGRESS主要是为多处理器设置的,而IRQ_PENDING的作用则下面就会看到:

IRQ0x03_interrupt=>common_interrupt=>do_IRQ

	/*
	 * 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.
	 */
	if (!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 (;;) {
		spin_unlock(&desc->lock);
		handle_IRQ_event(irq, &regs, action);
		spin_lock(&desc->lock);
		
		if (!(desc->status & IRQ_PENDING))
			break;
		desc->status &= ~IRQ_PENDING;
	}
	desc->status &= ~IRQ_INPROGRESS;
out:
	/*
	 * The ->end() handler has to deal with interrupts which got
	 * disabled while the handler was running.
	 */
	desc->handler->end(irq);
	spin_unlock(&desc->lock);

如果某一个中断请求队列的服务是关闭的(IRQ_DISABLED标志位为1),或者IRQ_INPROGRESS标志位为1,或者队列是空的,那么指针action为NULL(见580和582行),无法往下执行了,所以只好返回。但是,在这几种情况下desc->status中的IRQ_PENDING标志位为1(见574和583行)。这样,以后当CPU(在多处理器系统结构中有可能是另一个CPU)开启该队列的服务时,会看到这个标志位而补上一次中断服务,称为IRQ_REPLAY。而如果队列是空的,那么整个通道也必然是关着的,因为这是在将第一个服务程序挂入队列时才开启的。所以,这两种情形实际上相同。最后一种情况是服务已经开启,队列也不是空的,可是IRQ_INPROGRESS标志位为1。这只是在两种情形下才会发生。一种情形是在多处理器SMP系统结构中,一个CPU正在中断服务,而另一个CPU又进入了do_IRQ,这时候由于队列的IRQ_INPROGRESS标志位1而经595行返回,此时desc->status中的IRQ_PENDING标志位也是1。第二种情形是在单处理器系统中CPU已经在中断服务程序中,但是因某种原因又将中断开启了,而且在同一个中断通道中又产生了一次中断。在这种情形下后面发生的那个中断也会因为IRQ_INPROGRESS标志位为1,而经595行返回,但也是将desc->status的IRQ_PENDING置成为1。总之,这两种情形下最后的结果也是一样的,即desc->status中的IRQ_PENDING标志位为1。

那么,IRQ_PENDING标志位到底是怎样起作用的呢?请看612行和613行。这是在一个无限for循环中,具体的中断服务是在609行的handle_IRQ_event中进行的。在进入609行时,desc->status中的IRQ_PENDING标志位必然为0。当CPU完成了具体的中断服务返回到610行以后,如果这个标志位仍然为0,那么循环就在613行结束了。而如果变成了1,那就说明已经发生过前述的某种情况,所以又循环回到609行再服务一次。这样,就把本来可能发生的在同一通道上(甚至可能来自同一个中断源)的中断嵌套化解成一个循环。

这样,同一个中断通道上的中断处理就得到了严格的串行化。也就是说,对于同一个CPU而言不允许中断服务嵌套,而对于不同的CPU则不允许并发地进入同一个中断服务程序。如果不是这样处理的话,那就要求所有的中断服务程序都必须是可重入的纯代码,那样就使中断服务程序的设计和实现复杂化了。这么一套机制的设计和实现,不能不说是非常周到、非常巧妙的。而linux的稳定性和可靠性也正是植根与这种从Unix时代继承下来、并经过时间考验的设计中。当然,在极端的情况下,也有可能会发生这样的情景:中断服务程序总是把中断打开,而中断源又不断地产生中断请求,使得CPU每次从handle_IRQ_event返回时IRQ_PENDING标志位永远为1,从而使607行的for循环变成一个真正的无限循环,如果真的发生这种情况而得不到纠正的话,那么该中断服务程序的作者应该另请高就了。

还要指出,对desc->status的任何改变都是在加锁的情况下进行的,这也是出于对多处理器SMP系统结构的考虑。

最后,在循环结束以后,只要本队列的中断服务还是开着的,就要对中断控制器执行一次结束中断服务操作(622行),具体取决于中断控制器硬件的要求,所调用的函数也是在队列初始化时设置好的。

再看上面for循环中调用的handle_IRQ_event,这个函数依次执行队列中的各个中断服务程序,让它们辨认本次中断请求是否来自各自的服务对象,即中断源,如果是就进而提供相应的服务。其代码如下:

IRQ0x03_interrupt=>common_interrupt=>do_IRQ=>handle_IRQ_event


/*
 * This should really return information about whether
 * we should do bottom half handling etc. Right now we
 * end up _always_ checking the bottom half, which is a
 * waste of time and is not what some drivers would
 * prefer.
 */
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;
}

其中430行的irq_enter和446行的irq_exit只是对一个计数器进行操作,二者均定义于include/asm-i386/hardirq.h:

#define irq_enter(cpu, irq)	(local_irq_count(cpu)++)
#define irq_exit(cpu, irq)	(local_irq_count(cpu)--)

当这个计数器的值非0时就表示CPU正处于具体的中断服务程序中,以后读者会看到有些操作是不允许在此期间进行的。

一般来说,中断服务程序都是在关闭中断(不包括不可屏蔽中断NMI)的条件下执行的,这也是CPU在穿越中断门时自动关闭中断的原因。但是,关闭中断是个既不可不用,又不可滥用的手段,特别是当中断服务程序较长,操作比较复杂时,就有可能因关中断的时间持续太长而丢失其他的中断。经验表明,允许中断在同一个中断源或同一个中断通道嵌套是应该避免的,因此内核在do_IRQ中通过IRQ_PENDING标志位的运用来保证了这一点。可是,允许中断在不同的通道上嵌套,则只要处理得当就还是可行的。当然,必须十分小心。所以,在调用request_irq将一个中断服务程序挂入某个中断服务队列时,允许将参数irqflags中的一个标志位SA_SHIRQ置成0,表示该服务程序应该在开启中断的情况下执行。这里的434-435行和444行就是为此而设的(__sti为开中断,__cli为关中断)。

然后,从437行至441行的do while循环就是实质性的操作了。它依次调用队列中的每一个中断服务程序。调用的参数有三:irq为中断请求号;action->dev_id是一个void指针,由具体的服务程序自行解释和运用,这是由设备驱动程序在调用request_irq时自己规定的;最后一个就是前述的pt_regs数据结构指针regs了。至于具体的中断服务程序,那是设备驱动范畴内的东西,这里就不讨论了。

读者或许会问,如果中断请求队列中有多个服务程序存在,每次有来自这个通道的中断请求时就要依次把队列中所有的服务程序依次都执行一遍,岂非使效率大降?回答是:确实会有所下降,但不会严重。首先,在每个具体的中断服务程序中都应该(通常都确实是)一开始就检查各自的中断源,一般是读相应设备(接口卡上)的中断状态寄存器,看是否有来自该设备的中断请求,如没有就马上返回了,这个过程一般只需要几条机器指令;其次,每个队列中服务程序的数量一般也不会太大。所以,实际上不会有显著的影响。

最后,在442-443行,如果队列中的某个服务程序要为系统引入一些随机性的话,就调用add_interrupt_randomness来实现。有关详情在设备驱动系列博客中还会讲到。

从handle_IRQ_event返回的status的最低位必然为1,这是在432行设置的,代码中还为此加了些注解(418-424行),其作用在看了下面这一段以后就会明白,我们随着CPU回到do_IRQ中继续往下看:

IRQ0x03_interrupt=>common_interrupt=>do_IRQ


	retval = setup_irq(irq, action);
	if (retval)
		kfree(action);
	return retval;
}

到624行以后,从逻辑的角度对中断请求的服务似乎已经完毕,可以返回了。可是linux内核在这里有个特殊的考虑,这就是所谓softirq,即(在时间上)软性的中断请求,以前称为bottom half。在linux中,设备驱动程序的设计人员可以将中断服务分成两半,其实是两部分,而并不一定是两半。第一部分是必须立即执行,一般是在关中断条件下执行的,并且必须是对每次请求都单独执行的。而另一部分,即后半部分,是可以稍后在开中断条件下执行的,并且往往可以将若干次中断服务中剩下来的部分合并起来执行。这些操作往往是比较费时的,因而不适宜在关中断条件下执行,或者不适宜一次占据CPU时间太长而影响对其他中断请求的服务。这就是所谓的后半(bottom half),在内核代码中常常称为bh。作为一个比喻,读者不妨想象在cooked mode下从键盘输入字符串的过程(详见设备驱动),每当按下一个键的时候,首先要把字符读进来,这要放在前半中执行;而进一步检查所按的是否回车键,从而决定是否完成了一个字符串的输入,并进一步把睡眠中的进程唤醒,则可以放在后半中执行。

执行bh的机制是内核中的一项基础设施,所以我们在下一篇博客中单独加以介绍。这里,读者暂且只要知道有这么回事就行了。

在do_softirq中执行完相关的bh函数(如果有的话)以后,就到了从do_IRQ返回的时候了。返回到哪里?entry.S中的标号ret_from_intr处,这是内核中处心积虑安排好了的。其代码如下:

IRQ0x03_interrupt=>common_interrupt=>...=>ret_from_intr

ENTRY(ret_from_intr)
	GET_CURRENT(%ebx)
	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_with_reschedule
	jmp restore_all

这里的GET_CURRENT(%ebx)将指向当前进程的task_struct结构的指针置入寄存器ebx。275行和276行则在寄存器eax中拼凑起由中断前夕寄存器EFLAGS的高16位和代码段寄存器cs的(16位)内核构成的32位长整数。其目的是要检验:

  • 中断前夕CPU是否运行与VM86模式。
  • 中断前夕CPU运行于用户空间还是系统空间。

VM86模式是i386保护模式下模拟运行DOS软件而设置的。在寄存器EFLAGS的高16位中有个标志位表示CPU正在VM86模式中运行,我们对VM86模式不感兴趣,所以不予深究。而cs的最低两位,那就有文章了。这两位代表着中断发生时CPU的运行级别CPL。我们知道linux只采用两种运行级别,系统为0,用户为3。所以,若是CS的最低两位为非0,那就说明中断发生于用户空间。

顺便说一下,275行的EFLAGS(%esp)表示地址为堆栈指针%esp的当前值加上常数EFLAGS处的内容,这就是保存在堆栈中的中断前夕寄存器%eflags的内容。常数EFLAGS我们已经在前面介绍过,其值为0x30。276行中的CS(%esp)也是一样。

如果中断发生于系统空间,控制就直接转移到restore_all,而如果发生于用户空间(或VM86模式)则转移到ret_with_reschedule。这里我们假定中断发生于用户空间,因为从ret_with_reschedule最终还会到达restore_all。这段程序在同一文件中:

IRQ0x03_interrupt=>common_interrupt=>...=>ret_from_intr=>ret_with_reschedule

ret_with_reschedule:
	cmpl $0,need_resched(%ebx)
	jne reschedule
	cmpl $0,sigpending(%ebx)
	jne signal_return
restore_all:
	RESTORE_ALL

	ALIGN
signal_return:
	sti				# we can get here from an interrupt handler
	testl $(VM_MASK),EFLAGS(%esp)
	movl %esp,%eax
	jne v86_signal_return
	xorl %edx,%edx
	call SYMBOL_NAME(do_signal)
	jmp restore_all

这里,首先检查是否需要进行一次进程调度。上面我们已经看到,寄存器ebx中的内容就是当前进程的task_struct结构指针,而need_resched(%ebx)就表示该task_struct结构中位移为need_resched处的内容。220行的sigpending(%ebx)也是一样。常数need_resched和sigpending的定义为:


/*
 * these are offsets into the task-struct.
 */
state		=  0
flags		=  4
sigpending	=  8
addr_limit	= 12
exec_domain	= 16
need_resched	= 20
tsk_ptrace	= 24
processor	= 52

如果当前进程的task_struct结构中的need_resched字段非0,即表示需要进行调度,reschedule也在该文件中:

IRQ0x03_interrupt=>common_interrupt=>...=>ret_from_intr=>ret_with_reschedule=>reschedule

reschedule:
	call SYMBOL_NAME(schedule)    # test
	jmp ret_from_sys_call

程序在这里调用一个函数schedule进行调度,然后又转移到ret_from_sys_call。我们将在系统调用博客中再加讨论。至于schedule则在进程的博客中介绍,这里我们暂且假定不需要调度。读者以后会看到,如果要调度的话,从ret_from_sys_call处经过一段略为曲折的道路最终也会到达restore_all。

同样,如果当前进程的task_struct结构中的sigpending字段非0,就表示该进程有信号等待处理,要先处理了这些待处理的信号才最后从中断返回,所以先转移到226行。在228行处先区分是否VM86模式,然后将寄存器edx的内容清0(231行)再调用do_signal。信号signal基本上是一种进程间通信的手段,我们将在进程间通信博客中加以介绍。处理完信号以后,控制还是回到222行的restore_all。实际上,ret_from_sys_call最后还是回到ret_from_intr,最终殊途同归都会到达restore_all,并从那里执行中断返回。宏操作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遥相呼应的。为方便读者加以对照,我们再把SAVE_ALL列出在这里:

#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;

为什么RESTORE_ALL的111行要将堆栈指针的当前值加4?这是为了跳过ORIG_EAX,那是在进入中断之初压入堆栈的中断请求号(经过变形)。我们已经看到在do_IRQ中的第一件事就是从中取出其最低8位,然后以此为下标从irq_desc中找到相应的中断服务描述结构。以后在讲述系统调用和异常时读者会进一步看到其作用。读者也许会问:那为什么不像堆栈中的其他内容一样也使用popl指令呢?是的,在正常的情况下却是应该使用popl指令,但是popl指令一定是与一个寄存器相联系的,现在所有的寄存器都已经占满了,还能popl到哪里去呢?

这样,当CPU到达112行的iret指令时,系统堆栈又恢复到刚进入中断门的状态,而iret则使CPU从中断返回。跟进入中断时相对应,如果是从系统态返回到用户态就会将当前堆栈切换到用户堆栈。

猜你喜欢

转载自blog.csdn.net/guoguangwu/article/details/121065798