《深入理解Linux内核》第四章中断和异常(一)

最近鼓足勇气拿起吃灰多年的linux红宝书《深入理解Linux内核》,希望可以提升和巩固Linux内核理论知识。在此记录自己对书本内容和源代码理解,望各位大虾批评指正。

一、中断描述符(4.2.3. Interrupt Descriptor Table)

目的:关联中断或者异常向量和对应中断或异常处理程序地址

大小:64bit(8个byte)

寄存器:idtr

格式:

其中40---43bit用来识别中断描述符类型。

二、硬件上对中断或者异常处理过程(4.2.4. Hardware Handling of Interrupts and Exceptions)

当一个中断或者异常通过中断控制器送到CPU中断管脚时,CPU会开始处理中断或者异常,其处理过程如下:

1、获得中断向量 i (0<=i<=255)

2、从idtr寄存器找到IDT(中断描述符表),然后找到第 i 项

3、从gdtr寄存器找到GDT(全局描述符表)基地址,根据IDT中第 i 项内容指定的段选择符,获得中断或者异常处理程序所在段的基地址。

4、检查中断源合理性

5、检查是否需要切换CPU特权级,以决定是否需要切换stack,如果需要,则需要保存旧stack的ss和esp寄存器值到新stack

6、如果是一个fault中断,则将cs和eip寄存器值(默认已指向下一条指令地址)重新指向刚执行的指令地址,以便CPU可以再次执行该条指令

7、在stack中保存eflags,cs,和 eip寄存器值

8、如果异常有对应异常码,则将异常对应的异常码保存到stack

9、根据中断处理程序地址(所在段基地址(参考第3步) + IDT中offset字段),来设置cs和eip,使程序跳转到中断处理程序。

三、重要数据结构

1、struct irq_desc

2、struct hw_interrupt_type

typedef struct hw_interrupt_type  hw_irq_controller

四、源代码分析

1、首次建立IDT_TABLE

代码路径:arch/i386/kernel/head.S

将IDT中256项中断处理程序地址均填充为ignore_int,ignore_int是默认中断处理程序,下面是其中重点代码解释:

364行:将ignore_int标号地址加载到edx寄存器

365行:将中断描述符表中中断描述符的段选择符设置为KERNEL_CS,并将其作为eax的高16bit

366行:将ignore_int标号地址低16bit放到eax的低16bit

367行:设置中断描述符【32:47】bit为0x8E00,对应中断门描述符(因为bit[43:40]=1110),DPL=0,present,并将其作为edx的低16bit

至此中断描述符就填充完成,edx高16bit对应offset【16:31】,edx低16bit对应8E00,eax高16bit对应Segment Selector,eax低16bit对应offset【0:15】

369行:将idt_table所在地址加载到edi寄存器

372行:将eax寄存器值赋值到edi所标识的地址单元

373行:将edx寄存器值赋值到edi + 4所标识的地址单元

循环256次,就设置好了idt_table内容。

2、更新IDT_TABLE()

start_kernel()

init_IRQ()

     set_intr_gate(vector, interrupt[i])  (32 <= vector <= 256)

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

#define _set_gate(gate_addr,type,dpl,addr,seg) \
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" ((seg) << 16)); \
} while (0)

上面嵌入式汇编代码可以参考setup_idt 和中断门描述符结构进行理解。

这里interrupt[i]是从下面得来的,但是这个写得比较隐晦,arch/i386/kernel/entry.S

/*
 * Build the entry stubs and pointer table with
 * some assembler magic.
 */
.data
ENTRY(interrupt)
.text

vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
    ALIGN
1:    pushl $vector-256
    jmp common_interrupt
.data
    .long 1b
.text
vector=vector+1
.endr

至此,idt_table中中断描述符中offset,就变为标号1所在地址啦

3、软件上中断处理过程(4.6. Interrupt Handling)

注意:这里以I/O中断为例

从上面“硬件上对中断或者异常处理过程”部分可知,软件执行的第一个指令就是由idt_table中offset字段标识的中断处理程序

arch/i386/kernel/entry.S

    pushl $vector-256

    jmp common_interrupt

common_interrupt:
    SAVE_ALL
    movl %esp,%eax
    call do_IRQ
    jmp ret_from_intr

说明:$vector - 256的值就是orig_eax寄存器值

#define SAVE_ALL \
	cld; \
	pushl %es; \
	pushl %ds; \
	pushl %eax; \
	pushl %ebp; \
	pushl %edi; \
	pushl %esi; \
	pushl %edx; \
	pushl %ecx; \
	pushl %ebx; \
	movl $(__USER_DS), %edx; \
	movl %edx, %ds; \
	movl %edx, %es;

至此,寄存器按照如下的格式全部保存至stack中,

     0(%esp) - %ebx
     4(%esp) - %ecx
     8(%esp) - %edx
     C(%esp) - %esi
     10(%esp) - %edi
     14(%esp) - %ebp
     18(%esp) - %eax
     1C(%esp) - %ds
     20(%esp) - %es
     24(%esp) - orig_eax
     28(%esp) - %eip
     2C(%esp) - %cs
     30(%esp) - %eflags
     34(%esp) - %oldesp
     38(%esp) - %oldss

然后调用do_irq()

1)irq_enter(),增加preempt_count

2)  如果使能CONFIG_4KSTACKS宏,需要判断是否需要切换IRQ stack,如果之前已处于中断上下文,则无需要切换IRQ stack,具体请参考4.6.1.4. Multiple Kernel Mode stacks

先看下irq_ctx是如何初始化的:

start_kernel()

init_IRQ()

     irq_ctx_init(smp_processor_id())

#ifdef CONFIG_4KSTACK

/*
 * These should really be __section__(".bss.page_aligned") as well, but
 * gcc's 3.0 and earlier don't handle that correctly.
 */
static char softirq_stack[NR_CPUS * THREAD_SIZE]
		__attribute__((__aligned__(THREAD_SIZE)));

static char hardirq_stack[NR_CPUS * THREAD_SIZE]
		__attribute__((__aligned__(THREAD_SIZE)));

/*
 * allocate per-cpu stacks for hardirq and for softirq processing
 */
void irq_ctx_init(int cpu)
{
	union irq_ctx *irqctx;

	if (hardirq_ctx[cpu])
		return;

	irqctx = (union irq_ctx*) &hardirq_stack[cpu*THREAD_SIZE];
	irqctx->tinfo.task              = NULL;
	irqctx->tinfo.exec_domain       = NULL;
	irqctx->tinfo.cpu               = cpu;
	irqctx->tinfo.preempt_count     = HARDIRQ_OFFSET;
	irqctx->tinfo.addr_limit        = MAKE_MM_SEG(0);

	hardirq_ctx[cpu] = irqctx;

	irqctx = (union irq_ctx*) &softirq_stack[cpu*THREAD_SIZE];
	irqctx->tinfo.task              = NULL;
	irqctx->tinfo.exec_domain       = NULL;
	irqctx->tinfo.cpu               = cpu;
	irqctx->tinfo.preempt_count     = SOFTIRQ_OFFSET;
	irqctx->tinfo.addr_limit        = MAKE_MM_SEG(0);

	softirq_ctx[cpu] = irqctx;

	printk("CPU %u irqstacks, hard=%p soft=%p\n",
		cpu,hardirq_ctx[cpu],softirq_ctx[cpu]);
}

从上面可以看出,如果使能CONFIG_4KSTACKS宏,则会为每一个CPU分配一个hardirq stack,并且对相应字段进行简单初始化。有了这些配置之后,我们再来看do_irq()

#ifdef CONFIG_4KSTACKS

	curctx = (union irq_ctx *) current_thread_info();
	irqctx = hardirq_ctx[smp_processor_id()];

	/*
	 * this is where we switch to the IRQ stack. However, if we are
	 * already using the IRQ stack (because we interrupted a hardirq
	 * handler) we can't do that and just have to keep using the
	 * current stack (which is the irq stack already after all)
	 */
	if (curctx != irqctx) {
		int arg1, arg2, ebx;

		/* build the stack frame on the IRQ stack */
		isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
		irqctx->tinfo.task = curctx->tinfo.task;
		irqctx->tinfo.previous_esp = current_stack_pointer;

		asm volatile(
			"       xchgl   %%ebx,%%esp      \n"
			"       call    __do_IRQ         \n"
			"       movl   %%ebx,%%esp      \n"
			: "=a" (arg1), "=d" (arg2), "=b" (ebx)
			:  "0" (irq),   "1" (regs),  "2" (isp)
			: "memory", "cc", "ecx"
		);
	} else
#endi

假如是第一次进入中断,也就是说之前处于进程上下文,curtx不会等于irqtx,则进入if语句,在嵌入式汇编代码中" xchgl   %%ebx,%%esp\n"就会将isp赋值给esp寄存器,这样等下次再来I/O中断时,curtx就会等于irqtx

3)调用__do_irq(irq, regs)

4)irq_exit() ,减少preempt_count

古人云:欲知下文如何,且见下文分解!

 

参考资料:《深入理解Linux内核》

                  《understanding the linux kernel》

                    Linux2.6.11源代码


版权声明:本文为yxw0609131056原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。