linux内核-中断向量表IDT的初始化

linux内核在初始化阶段完成了对页式虚存管理的初始化以后,便调用trap_init和init_IRQ两个函数进行中断机制的初始化。其中trap_init主要是对一些系统保留的中断向量的初始化,而init_IRQ则主要用于外设的中断。

函数trap_init是在include/i386/kernel/traps.c中定义的:

void __init trap_init(void)
{
#ifdef CONFIG_EISA
	if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24))
		EISA_bus = 1;
#endif

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

	/*
	 * Should be a barrier for any external CPU state.
	 */
	cpu_init();

#ifdef CONFIG_X86_VISWS_APIC
	superio_init();
	lithium_init();
	cobalt_init();
#endif
}

程序中先设置中断向量表开头的19个陷阱门,这些中断向量表都是CPU保留用于异常处理的。例如,中断向量14就是为页面异常保留的,CPU硬件在页面映射及访问过程中发生问题(比如缺页),就会产生一次以14(0xe)为中断向量的异常。操作系统的设计和实现必须遵守这些规定。

然后是对系统调用向量的初始化,但是有些Unix变种已经用了调用门来实现系统调用,如注释中所说的iBCS和Solaris /X86。为了与这些系统上编译的应用程序可执行代码相兼容,linux内核也相应设置了两个调用门,983行和984行就是对这两个调用门的初始化。由于我们在这里并不关心SGI公司的特殊工作站显示设备,所以就略去了从991行开始的几行条件编译代码。

从程序中可以看到,这里用了三个函数来进行这些表项的初始化,那就是set_trap_gate、set_system_gate以及set_call_gate。还有一个用于外设中断的set_intr_gate,这里虽然没有用到,但是也属于同一组函数。这些函数都是在文件arch/i386/kernel/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,设置中段描述表idt_table中的第n项,所不同的是参数表中的第二个第三个参数。第二个参数对应于中断门或陷阱门格式中的D标志位加上类型位段。参数14(第二个参数)表示D标志位为1而类型为110,所以set_intr_gate设置的是中断门。第三个参数则对应于DPL位段。中断门的DPL一律设置成0是有讲究的。当中断是由外部产生或是CPU异常产生时,中断门的DPL是被忽略的,所以总能穿过该中断门。可是,要是用户进程在用户空间试着用一条INT2来进入不可屏蔽中断的服务程序时,由于用户状态的运行级别为3,而中断门的DPL为0(级别最高),由软件产生的中断就会被拒之门外(CPU会产生一个异常),因此不能得逞。同样,set_trap_gate也是将DPL设成0,所不同的是调用_set_gate时的第二个参数是15,也即类型111,表示所设置的是陷阱门。我们在前面已经讲过,陷阱门与中断门的不同仅在于通过中断门进入服务程序时自动关闭中断,而通过陷阱门进入服务程序时则维持不变。所以,例如说,因CPU的页面异常而进入服务程序时,中断多半是开着的,我们在内存管理中看到的那些程序,如handle_mm_fault等等,都是可中断的。此外set_system_gate所设置的是也是陷阱门,所以系统调用也是可中断的。但是DPL为3,因为系统调用是在用户空间通过INT 0x80进行的,只有将该陷阱门的DPL设成3才能让系统调用顺利通过,否则就会把系统调用拒之门外了。

进一步看看,这些IDT表项到底怎么设置。_set_gate也是在同一个文件中定义的:

#define _set_gate(gate_addr,type,dpl,addr) \
do { \
  int __d0, __d1; \
  __asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
	"movw %4,%%dx\n\t" \
	"movl %%eax,%0\n\t" \
	"movl %%edx,%1" \
	:"=m" (*((long *) (gate_addr))), \
	 "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
	:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	 "3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
} while (0)

首先,do{}while{0);决定了它的循环体,也就是从790到798行,一定会被执行一遍,并且只执行一遍。特别是在编译时不管在什么情况下都不会有问题。从795行的第一个:到797行的第二个:之间为输出部,其中说明了有四个变量会被改变,分别于%0、%1、%2、%3相结合。其中%0与参数gate_addr结合,%1与(gate_addr+1)结合,二者都是内存单元;%2与局部变量__d0结合,存放在寄存器%%eax中,而%3与局部变量__d1结合,存放在寄存器%%edx中。从797行至798行则为输入部。由于输出部已经定义了%0-%3,输入部的第一个变量便是%4,而后面还有两个变量分别等价于输出部中的%3和%2。输入部中说明的各输入变量的值,包括%3和%2的值,都会在引用这些变量之前设置好。

为了方便,我们把所要求的的中断门(或陷阱门)的格式再表示在下图中:

由于791行要用到的%%dx和%%ax,所以编译(以及汇编)以后的代码会按输入部的说明先将%%dx设成addr,而%%ax设成(__KERNEL_CS << 16)。而791行将%%dx的低16位移入%%ax的低16位(注意%%dx与%%ax的区别)。这样在eax中就形成了所需要的中断门的第一个长整数,其高16位为__KERNEL_CS,而低16位为addr的低16位。接着,在792行中将(0x8000+(dpl<<13)+(type<<8))装入edx的低16位。这样,edx的高16位为addr的高16位,而低16位的P位为1(因为是0x8000),DPL位段为dpl(因为dpl<<3),而D位加上类型位段则为type(因为type<<8),其余各位皆为0。这就形成了中断门中的第二个长整数。然后,793行将%%eax写入*gate_addr,而794行则将%%edx写入*(gate_addr+1)。读者不妨试试,看看能否写出效率更高的代码!当然,这种高效率是以牺牲可读性为代价的。对于像设置IDT表项一类并不是频繁发生的操作,这样做是否值得,大可商榷。不过,这毕竟是在内核中,而且是很底层的东西,一般也不会有很多人去读、去维护的。

系统初始化,在trap_init中设置了一些为CPU保留专用的IDT表项以及系统调用所用的陷阱门以后,就要进入init_IRQ设置大量用于外设的通用中断门了。函数init_IRQ的代码在arch/i386/kernel/i8259.c中。我们分段来看:

void __init init_IRQ(void)
{
	int i;

#ifndef CONFIG_X86_VISWS_APIC
	init_ISA_irqs();
#else
	init_VISWS_APIC_irqs();
#endif
	/*
	 * Cover the whole vector space, no vector can escape
	 * us. (some of these will be overridden and become
	 * 'special' SMP interrupts)
	 */
	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中对PC的中断控制器8259A进行初始化,并且初始化一个结构数组irq_desc。为什么要有这么一个结构数组呢?我们知道,i386的系统结构支持256个中断向量,还要扣除一些CPU本身保留的向量。但是,作为一个通用的操作系统,很难说剩下的这些中断向量是否够用。而且,很多外部设备由于种种原因可能本来就不得不共用中断向量。所以,在像linux这样的系统中,限制每个中断源都必须独占使用一个中断向量是不现实的。解决的方法是为共用中断向量提供一种手段。因此,系统中为每个中断向量设置一个队列,而根据每个中断源所使用(产生)的中断向量,将其中断服务程序挂到相应的队列中去,而数组irq_desc中的每个元素则是这样一个队列的头部以及控制结构。当中断发生时,首先执行与中断向量相对应的一段总服务程序,根据具体中断源的设备号在其所属队列中找到特定的服务程序加以执行。这个过程我们将在以后详细介绍,这里只要知道需要有这么一个结构数组就行了。

接着,从FIRST_EXTERNAL_VECTOR开始,设立NR_IRQS个中断向量的IDT表项。常数FIRST_EXTERNAL_VECTOR定义为0x20,而NR_IRQS则为224。不过,要跳过用于系统调用的向量0x80,那已经在前面设置好了。这里设置的服务程序入口地址都来自一个函数指针数组interrupt。

函数指针数组interrupt的内容也是在这个文件中:

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

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

#undef IRQ
#undef IRQLIST_16

数组的第一部分内容定义于107行,顺着IRQLIST_16和IRQ的定义到98行,可知关于函数指针的文字是由gcc的预处理自动产生的,因为符号##的作用是将字符串连接在一起。例如,当108行以参数0x0(作为字符串)调用IRQLIST_16时,102行中的IRQ(x,0)就会在预处理阶段被替换成IRQ-0x00_interrupt。后面依次为IRQ-0x01_interrupt、IRQ-0x02_interrupt。。。一直到IRQ-0x0f_interrupt。这样,就利用gcc的预处理自动生成了所需的文字,而避免了枯燥繁琐的文字输入和编辑。所以,这一部分给出了interrupt中开头16个函数指针。对于单CPU系统结构,后面的指针就都是NULL了。如果是多处理器SMP结构,则后面还有IRQ-0x10至IRQ-0xdf等208个函数指针。

那么,从IRQ-0x00_interrupt到IRQ-0x0f_interrupt这16个函数本身是在哪里定义的呢?请看i8259.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)

可见,51行的宏定义BUILD_16_IRQS在预处理节点会被展开成从BUILD_IRQ(0x00)至BUILD_IRQ(0x0f)共16项宏定义的引用。而BUILD_IRQ则是在include/asm-i386/hw_irq.h中定义的:

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

经过gcc的预处理以后,便会展开成一系列如下式样的代码:

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

由此可以看出,实际上由外设产生的中断处理全部进入一段公共程序common_interrupt中,而在此之前分别跑到IRQ0x00_interrupt等的目的,只在于由此得到一个与中断向量相关的数值(压入堆栈中)。对应IRQ0x00_interrupt到IRQ0x0f_interrupt该数值分别为0xffffff00至0xffffff0f,余类推。至于common_interrupt,那也是由gcc的预处理展开一个宏定义BUILD_COMMON_IRQ而生成的,这段程序我们在后面到的情景中还要讲,这里先从略。

回到init_IRQ中断继续往下看:

#ifdef CONFIG_SMP
	/*
	 * IRQ0 must be given a fixed assignment and initialized,
	 * because it's used before the IO-APIC is set up.
	 */
	set_intr_gate(FIRST_DEVICE_VECTOR, interrupt[0]);

	/*
	 * The reschedule interrupt is a CPU-to-CPU reschedule-helper
	 * IPI, driven by wakeup.
	 */
	set_intr_gate(RESCHEDULE_VECTOR, reschedule_interrupt);

	/* IPI for invalidation */
	set_intr_gate(INVALIDATE_TLB_VECTOR, invalidate_interrupt);

	/* IPI for generic function call */
	set_intr_gate(CALL_FUNCTION_VECTOR, call_function_interrupt);
#endif	

#ifdef CONFIG_X86_LOCAL_APIC
	/* self generated IPI for local APIC timer */
	set_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt);

	/* IPI vectors for APIC spurious and error interrupts */
	set_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);
	set_intr_gate(ERROR_APIC_VECTOR, error_interrupt);
#endif

	/*
	 * Set the clock to HZ Hz, we already have a valid
	 * vector now:
	 */
	outb_p(0x34,0x43);		/* binary, mode 2, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */

#ifndef CONFIG_VISWS
	setup_irq(2, &irq2);
#endif

	/*
	 * External FPU? Set up irq13 if so, for
	 * original braindamaged IBM FERR coupling.
	 */
	if (boot_cpu_data.hard_math && !cpu_has_fpu)
		setup_irq(13, &irq13);

由于我们这里既不关心多处理器SMP结构,也不考虑SGI工作站的特殊处理,剩下的就只是对系统时钟的初始化了。代码中有个注释,说我们已经有了个中断向量,实际上指的是IRQ0x00_interrupt。但是要注意,虽然该中断服务的入口地址已设置到中断向量中,但实际上我们还没有把具体的时钟中断服务程序挂到IRQ0的队列中去。这个时候,这些irq队列都还是空的,所以即使开了中断,并产生了时钟中断,也只不过是让它在common_interrupt中空跑一趟。读者以后将看到,时钟中断对时钟中断的服务,就好像是动物的心跳、脉搏。而现在内核的脉搏尚未开始。为什么还不让它开始呢?这是因为系统在这个时候还没有完成对进程调度机制的初始化,而一旦时钟中断开始,进程调度也就要随之开始。所以,一定要等完成了对进程调度的初始化,做好了准备以后才能让脉搏开始跳动。

由此可见,设计一个真正实用的操作系统,有多少事情需要周到精细的考虑!

猜你喜欢

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