在《深入理解Linux内核》第四章中提到,Intel文档提出了两个 概念。
第一个概念是把同步和异步中断分别称为异常(exception)和中断(interrupt);
第二个概念是进一步的,对于中断和异常,Intel又再细分了很多类,将中断分为可屏蔽中断和不可屏蔽中断;将异常分为故障、陷阱、异常中止和编程异常。
以上这些概念及其逻辑当然会体现在x86的中断向量表的设计中。特别重要的:每个中断和异常是由0~255之间的一个数来标识。在Intel中,把这个8位的无符号整数叫做一个向量(vector),所以,x86中中断和异常都有自己的向量。其中,异常和非屏蔽中断两者的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变。
上文已经说到,所有的中断和异常都会占用0~255这256个中断向量中的一个。
其中异常和非屏蔽中断占用的向量是固定的。在各种书籍中,都介绍x86体系结构的异常和非屏蔽中断占用了0~19这20个中断向量。且其中仅有2号向量用于非屏蔽中断,其余19个全部分类为异常。
这20个向量的设置或初始化会在trap_init()函数中完成到可用状态,在最新的3.0内核中,通过调用set_intr_gate()、set_system_intr_gate()、set_task_gate()等一系列函数设置这20个向量。也即是说,在x86体系下,这20个向量的用处是定死的,是死规定,不可改变,而且在trap_init()中直接设置到位,所谓的设置到位,主要是指对这20个向量的门进行设置。
需要强调的是,由此我们可以发现,x86体系结构下的trap_init()实际上设置的就是idt_table表,也即是给各个门赋值。在3.4.35版本内核源码中,这个idt_table表是在traps.c文件下定义的一个全局变量。
-----------------------arch\x86\include\asm\irq_vectors.h
#define NR_VECTORS 256
-----------------------------------arch\x86\kernel\traps.c
/*
* The IDT has to be page-aligned to simplify the Pentium
* F0 0F bug workaround.
* 一些旧的Pentium模式有声名狼藉的“F00F”bug,能让用户态程序冻结系统
*/
gate_desc idt_table[NR_VECTORS] __page_aligned_data =
{ { { { 0, 0 } } }, };
128号向量被系统调用占用。
在x86体系中,外部可屏蔽中断,也就是物理IRQ所对应的中断向量号,占用了32~238这个范围(128系统调用除外)。
这里涉及一个设备的IRQ号分配(间接隐含设备的中断向量号分配)的问题,根据《深入理解Linux内核》一书介绍,IBM兼容机(其实就是x86体系)的体系结构要求,某些设备必须被静态地分配指定的IRQ线,给出的例子包括:
1. 间隔定时器必须分配IRQ0,也即是32号中断向量;
2. 从8259A PIC(如果有的话)必须分配IRQ2,也即是34号中断向量;
3. 外部协处理器(如果有的话)必须分配IRQ13,也即是45号中断向量。
中断相关的初始化差不多是从start_kernel()函数开始的,包括两个层次。
其实setup_arch()不仅仅是为中断或异常完成一些设置,但其中涉及到和中断相关的有两步。
----- arch/x86/kernel/traps.c ----->
1 /* Set of traps needed for early debugging. */
2 void __init early_trap_init(void)
3 {
4 set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
5 /* int3 can be called from all */
6 set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
7 set_intr_gate(X86_TRAP_PF, &page_fault);
8 load_idt(&idt_descr);
9 }
trap_init()主要是对一些系统保留的中断向量进行初始化,也就是设置中断向量表开头的0~19这20个陷阱门,当然,其中1号Debug调试向量、3号Breakpoint断点向量和14号Page Fault缺页异常已经在setup_arch()中设置好了。另外,trap_init()还会对系统调用的向量做初始化。
分两步,第一步是与硬件和平台无关的通用逻辑层处理,其本质就是一个for循环执行一个函数desc_set_defaults(),为所有的irq_desc填充默认值。其关键点是为这些irq_desc注册的中断控制器全部先填充为一个所谓no_irq_chip。
------kernel/irq/irqdesc.c------->
static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node, struct module *owner)
{
int cpu;
desc->irq_data.irq = irq;
desc->irq_data.chip = &no_irq_chip;
desc->irq_data.chip_data = NULL;
desc->irq_data.handler_data = NULL;
desc->irq_data.msi_desc = NULL;
irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
desc->handle_irq = handle_bad_irq;
desc->depth = 1;
desc->irq_count = 0;
desc->irqs_unhandled = 0;
desc->name = NULL;
desc->owner = owner;
for_each_possible_cpu(cpu)
*per_cpu_ptr(desc->kstat_irqs, cpu) = 0;
desc_smp_init(desc, node);
}
第二步就与硬件平台相关了,调用arch_early_irq_init()进行一些平台相关的irq系统前期初始化。在x86体系中,arch_early_irq_init()主要是处理的8259和IOAPC的差异相关,这导致对前16个固定IRQ的处理会有些区别,此处不做深究。
init_IRQ()主要用于对外设中断的初始化,可以总结为5步过程。
第一步是调用init_ISA_irqs()对前16个被所谓的legacy_pic控制的legacy_irqs进行一些相关的初始化。包括:
第二步就是setup_irq()两个IRQ,分别是: