本文为原创,转载请注明:http://www.cnblogs.com/tolimit/
本篇文章主要讲述源码中是如何对中断进行一系列的初始化的。
回顾
在上一篇概述中,介绍了几个对于中断来说非常重要的数据结构,分别是:中断描述符表,中断描述符数组,中断描述符,中断控制器描述符,中断服务例程。可以说这几个结构组成了整个内核中断框架主体,所以内核对整个中断的初始化工作大多集中在了这几个结构上。
在系统中,当一个中断产生时,首先CPU会从中断描述符表中获取相应的中断向量,并根据中断向量的权限位判断是否处于该权限,之后跳转至中断处理函数,在中断处理函数中会根据中断向量号获取中断描述符,并通过中断描述符获取此中断对应的中断控制器描述符,然后对中断控制器执行应答操作,最后执行此中断描述符中的中断服务例程链表,最后执行软中断。
而整个初始化的过程与中断处理过程相应,首先先初始化中断描述符表,再初始化中断描述符数组和中断描述符。中断控制器描述符是系统预定编写好的静态变量,如i8259A中断控制器对应的变量就是i8259A_chip。这时一个中断已经初始化完毕,之后驱动需要使用此中断时系统会将驱动中的中断处理加入到该中断的中断服务例程链表中。如下图
初始化中断向量
虽然称之为中断描述符表,其实对于CPU来说只是一个起始地址,此地址开始每向上9个字节为一个中断向量。我们的CPU上有一个idtr寄存器,它专门用于保存中断描述符表地址,当产生一个中断时,CPU会自动从idtr寄存器保存的中断描述符表地址处获取相应的中断向量,然后判断权限并跳转至中断处理函数。当计算机刚启动时,首先会启动引导程序(BIOS),在BIOS中会把中断描述符表存放在内存开始位置(0x00000000)。BIOS会有自己的一些默认中断处理函数,而当BIOS处理完后,会将计算机控制器转交给linux,而linux会在使用BIOS的中断描述符表的同时重新设置新的中断描述符表(新的地址保存在配置中的CONFIG_VECTORS_BASE),之后会完全使用新的中断描述符表。
一般的,我们也把中断描述符表中的中断向量称为门描述符,其大小为64位,其主要保存了段选择符、权限位和中断处理程序入口地址。CPU主要将门分为三种:任务门,中断门,陷阱门。虽然CPU把门描述符分为了三种,但是linux为了处理更多种情况,把门描述符分为了五种,分别为中断门,系统门,系统中断门,陷阱门,任务门;但其存储结构与CPU定义的门不变。结构如下:
在一个门描述符中:
- P:代表的是段是否处于内存中,因为linux从不把整个段交换的硬盘上,所以P都被置为1。
- DPL:代表的是权限,用于限制对这个段的存取,当其为0时,只有CPL=0(内核态)才能够访问这个段,当其为3时,任何等级的CPL(用户态及内核态)都可以访问。
- 段选择符:除了任务门设置为TSS段,陷阱门和中断门都设置为__KERNER_CS(内核代码段)。
- 偏移量:就是中断处理程序入口地址。
门描述符的初始化主要分为两部分,我们知道,中断描述符表中保存的是中断和异常,所以整个中断描述符的初始化需要分为中断初始化和异常初始化。而中断描述符表的初始化情况是,第一部分是经过一段汇编代码对整个中断描述符表进行初始化,第二部分是在系统进入start_kernel()函数后分别对异常和中断进行初始化。在linux中,中断描述符表用idt_table[NR_VECTORS]数组进行描述,中断向量(门描述符)在系统中用struct desc_struct结构表示,具体我们可以往下看。
第一部分 - 汇编代码(arch/x86/kernel/head_32.S):
/*
* setup_once
*
* The setup work we only want to run on the BSP.
*
* Warning: %esi is live across this function.
*/
__INIT
setup_once:
movl $idt_table,%edi # idt_table就是中断描述符表,地址保存到edi中
movl $early_idt_handlers,%eax # early_idt_handlers地址保存到eax中,early_idt_handlers是二维数组,每行9个字符
movl $NUM_EXCEPTION_VECTORS,%ecx # NUM_EXCEPTION_VECTORS地址保存到ecx中,ecx用于循环,NUM_EXCEPTION_VECTORS为32
1:
movl %eax,(%edi) # 将eax的值保存到edi保存的地址中
movl %eax,4(%edi) # 将eax的值保存到edi保存的地址+4中
/* interrupt gate, dpl=0, present */
movl $(0x8E000000 + __KERNEL_CS),2(%edi) # 将(0x8E000000 + __KERNEL_CS)一共4个字节保存到edi保存的地址+2的位置中
addl $9,%eax # eax += 9,指向early_idt_handlers数组下一列
addl $8,%edi # edi += 8,就是下一个门描述符地址
loop 1b # 根据ecx是否为0进行循环
# 前32个中断向量初始化结果:
# |63 48|47 32|31 16|15 0|
# |early_idt_handlers[i](高16位)| 0x8E00 | __KERNEL_CS |early_idt_handlers[i](低16位)|
movl $256 - NUM_EXCEPTION_VECTORS,%ecx # 256 - 32 保存到ecx,进行新一轮的循环
movl $ignore_int,%edx # ignore_int保存到edx
movl $(__KERNEL_CS << 16),%eax # (__KERNEL_CS << 16)保存到eax
movw %dx,%ax
movw $0x8E00,%dx 2:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi # edi += 8,就是下一个门描述符地址
loop 2b
# 其他中断向量初始化结果:
# |63 48|47 32|31 16|15 0|
# | ignore_int(高16位) | 0x8E00 | __KERNEL_CS | ignore_int(低16位) |
如果CPU是486,之后会通过 lidt idt_descr 命令将中断描述符表(idt_descr)地址放入idtr寄存器;如果不是,则暂时不会将idt_descr放入idtr寄存器(在trap_init()函数再执行这步操作)。idtr寄存器一共是48位,低16位保存的是中断描述符表长度,高32位保存的是中断描述符表基地址。我们可以看看idt_descr的形式,如下:
idt_descr:
.word IDT_ENTRIES*8-1 # 这里放入的是表长度, 256 * 8 - 1
.long idt_table # idt_table地址放在这,idt_table定义在/arch/x86/kernel/trap.h中
/* 我们再看看 idt_table 是怎么定义的,idt_table代表的就是中断描述符表 */
/* 代码地址:arch/x86/kernel/Traps.c */
gate_desc idt_table[NR_VECTORS] __page_aligned_bss;
/* 继续,看看 gate_desc ,用于描述一个中断向量 */
#ifdef CONFIG_X86_64
typedef struct gate_struct64 gate_desc;
#else
typedef struct desc_struct gate_desc;
#endif
/* 我们看看32位下的 struct desc_struct,此结构就是一个中断向量(门描述符) */
struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
};
};
} __attribute__((packed));
可以看出,在汇编代码初始化部分,所有的门描述符的DPL权限位都设置为0(用户态不可访问),段选择符设置为__KERNEL_CS内核代码段。而对于中断处理函数设置则不同,前32个门描述符的中断处理函数为early_idt_handlers,之后的门描述符的中断处理函数为ignore_int。而在linux中,0~19的中断向量是用于异常和陷阱。20~31的中断向量是intel保留使用的。
初始化异常向量
异常向量作为在中断描述符表中的前20个向量(0~19),在汇编代码中已经将其的处理函数设置为early_idt_handlers,而进入start_kernel()函数后,系统会在trap_init()函数中重新设置它们的处理函数,由于异常和陷阱的特殊性,它们并没有像中断这样复杂的数据结构,单纯的,每个异常和陷阱有它们自己的中断处理函数,系统只是简单地把中断处理函数放入异常和陷阱的门描述符中。在了解trap_init()函数之前,我们需要先了解如下几个函数:
/* 设置一个中断门
* n:中断号
* addr:中断处理程序入口地址
*/
#define set_intr_gate(n, addr) \
do { \
BUG_ON((unsigned)n > 0xFF); \
_set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0, \
__KERNEL_CS); \
_trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,\
0, 0, __KERNEL_CS); \
} while (0)
/* 设置一个系统中断门 */
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0x3, 0, __KERNEL_CS);
}
/* 设置一个系统门 */
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}
/* 设置一个陷阱门 */
static inline void set_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS);
}
/* 设置一个任务门 */
static inline void set_task_gate(unsigned int n, unsigned int gdt_entry)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TASK, (void *)0, 0, 0, (gdt_entry<<3));
}
这几个函数用于设置不同门的API函数,他们的参数n都为中断号,而他们都会调用_set_gate()函数,只是参数不同,_set_gate()函数如下:
/* 设置一个门描述符,并写入中断描述符表
* gate: 中断号
* type: 门类型
* addr: 中断处理程序入口
* dpl: 权限位
* ist: 64位系统才使用
* seg: 段选择符
*/
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
/* 生成一个门描述符 */
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
/*
* does not need to be atomic because it is only done once at
* setup time
*/
/* 将新的门描述符写入中断描述符表中的gate项,使用memcpy进行写入 */
write_idt_entry(idt_table, gate, &s);
/* 用于跟踪? 暂时还不清楚这个 trace_idt_table 的用途 */
write_trace_idt_entry(gate, &s);
}
了解了以上的设置门描述符的函数,我们再看看trap_init()函数:
1 void __init trap_init(void)
2 {
3 int i;
4
5 /* 使用了EISA总线 */
6 #ifdef CONFIG_EISA
7 void __iomem *p = early_ioremap(0x0FFFD9, 4);
8
9 if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
10 EISA_bus = 1;
11 early_iounmap(p, 4);
12 #endif
13
14 /* Interrupts/Exceptions */
15 //enum {
16 // X86_TRAP_DE = 0, /* 0, 除0操作 Divide-by-zero */
17 // X86_TRAP_DB, /* 1, 调试使用 Debug */
18 // X86_TRAP_NMI, /* 2, 非屏蔽中断 Non-maskable Interrupt */
19 // X86_TRAP_BP, /* 3, 断点 Breakpoint */
20 // X86_TRAP_OF, /* 4, 溢出 Overflow */
21 // X86_TRAP_BR, /* 5, 越界异常 Bound Range Exceeded */
22 // X86_TRAP_UD, /* 6, 无效操作码 Invalid Opcode */
23 // X86_TRAP_NM, /* 7, 无效设备 Device Not Available */
24 // X86_TRAP_DF, /* 8, 双重故障 Double Fault */
25 // X86_TRAP_OLD_MF, /* 9, 协处理器段超限 Coprocessor Segment Overrun */
26 // X86_TRAP_TS, /* 10, 无效任务状态段(TSS) Invalid TSS */
27 // X86_TRAP_NP, /* 11, 段不存在 Segment Not Present */
28 // X86_TRAP_SS, /* 12, 栈段错误 Stack Segment Fault */
29 // X86_TRAP_GP, /* 13, 保护错误 General Protection Fault */
30 // X86_TRAP_PF, /* 14, 页错误 Page Fault */
31 // X86_TRAP_SPURIOUS, /* 15, 欺骗性中断 Spurious Interrupt */
32 // X86_TRAP_MF, /* 16, X87 浮点数异常 Floating-Point Exception */
33 // X86_TRAP_AC, /* 17, 对齐检查 Alignment Check */
34 // X86_TRAP_MC, /* 18, 设备检查 Machine Check */
35 // X86_TRAP_XF, /* 19, SIMD 浮点数异常 Floating-Point Exception */
36 // X86_TRAP_IRET = 32, /* 32, 汇编指令异常 IRET Exception */
37 //};
38
39 set_intr_gate(X86_TRAP_DE, divide_error);
40 /* 在32位系统上其效果等同于 set_intr_gate */
41 set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
42 /* int4 can be called from all */
43 set_system_intr_gate(X86_TRAP_OF, &overflow);
44 set_intr_gate(X86_TRAP_BR, bounds);
45 set_intr_gate(X86_TRAP_UD, invalid_op);
46 set_intr_gate(X86_TRAP_NM, device_not_available);
47 #ifdef CONFIG_X86_32
48 set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS);
49 #else
50 set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
51 #endif
52 set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun);
53 set_intr_gate(X86_TRAP_TS, invalid_TSS);
54 set_intr_gate(X86_TRAP_NP, segment_not_present);
55 set_intr_gate(X86_TRAP_SS, stack_segment);
56 set_intr_gate(X86_TRAP_GP, general_protection);
57 set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug);
58 set_intr_gate(X86_TRAP_MF, coprocessor_error);
59 set_intr_gate(X86_TRAP_AC, alignment_check);
60 #ifdef CONFIG_X86_MCE
61 set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
62 #endif
63 set_intr_gate(X86_TRAP_XF, simd_coprocessor_error);
64
65 /* 将前32个中断号都设置为已使用状态 */
66 for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
67 set_bit(i, used_vectors);
68
69 #ifdef CONFIG_IA32_EMULATION
70 /* 设置0x80系统调用的系统中断门 */
71 set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
72 set_bit(IA32_SYSCALL_VECTOR, used_vectors);
73 #endif
74
75 #ifdef CONFIG_X86_32
76 /* 设置0x80系统调用的系统门 */
77 set_system_trap_gate(SYSCALL_VECTOR, &system_call);
78 set_bit(SYSCALL_VECTOR, used_vectors);
79 #endif
80
81 /*
82 * Set the IDT descriptor to a fixed read-only location, so that the
83 * "sidt" instruction will not leak the location of the kernel, and
84 * to defend the IDT against arbitrary memory write vulnerabilities.
85 * It will be reloaded in cpu_init() */
86 /* 将中断描述符表设置在一个固定的只读的位置,以便“sidt”指令不会泄漏内核的位置,和保护中断描述符表可以处于任意内存写的漏洞。它将会在 cpu_init() 中被加载到idtr寄存器 */
87 __set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);
88 idt_descr.address = fix_to_virt(FIX_RO_IDT);
89
90 /* 执行CPU的初始化,对于中断而言,在 cpu_init() 中主要是将 idt_descr 放入idtr寄存器中 */
91 cpu_init();
92
93 /* x86_init是一个定义了很多x86体系上的初始化操作,这里执行的另一个trap_init()函数为空函数,什么都不做 */
94 x86_init.irqs.trap_init();
95
96 #ifdef CONFIG_X86_64
97 /* 64位操作 */
98 /* 将 idt_table 复制到 debug_idt_table 中 */
99 memcpy(&debug_idt_table, &idt_table, IDT_ENTRIES * 16);
100 set_nmi_gate(X86_TRAP_DB, &debug);
101 set_nmi_gate(X86_TRAP_BP, &int3);
102 #endif
103 }
在代码中,used_vectors变量是一个bitmap,它用于记录中断描述符表中哪些中断已经被系统注册和使用,哪些未被注册使用。trap_init()已经完成了异常和陷阱的初始化。对于linux而言,中断号0~19是专门用于陷阱和故障使用的,以上代码也表明了这一点,而20~31一般是intel用于保留的。而我们的外部IRQ线使用的中断为32~255(代码中32号中断被用作汇编指令异常中断)。所以,在trap_init()代码中,专门对0~19号中断的门描述符进行了初始化,最后将新的中断描述符表起始地址放入idtr寄存器中。在trap_init()中我们看到每个异常和陷阱都有他们自己的处理函数,不过它们的处理函数的处理方式都大同小异,如下:
#代码地址:arch/x86/kernel/entry_32.S
# 11号异常处理函数入口
ENTRY(segment_not_present)
RING0_EC_FRAME
ASM_CLAC
pushl_cfi $do_segment_not_present
jmp error_code
CFI_ENDPROC
END(segment_not_present)
# 12号异常处理函数入口
ENTRY(stack_segment)
RING0_EC_FRAME
ASM_CLAC
pushl_cfi $do_stack_segment
jmp error_code
CFI_ENDPROC
END(stack_segment)
# 17号异常处理函数入口
ENTRY(alignment_check)
RING0_EC_FRAME
ASM_CLAC
pushl_cfi $do_alignment_check
jmp error_code
CFI_ENDPROC
END(alignment_check)
# 0号异常处理函数入口
ENTRY(divide_error)
RING0_INT_FRAME
ASM_CLAC
pushl_cfi $0 # no error code
pushl_cfi $do_divide_error
jmp error_code
CFI_ENDPROC
END(divide_error)
这些函数具体细节我们下篇文章分析。
在trap_init()函数中调用了cpu_init()函数,在此函数中会将新的中断描述符表地址放入idtr寄存器中,而具体内核是如何实现的呢,之前已经说明,idtr寄存器的低16位保存的是中断描述符表长度,高32位保存的是中断描述符表基地址,相对于的,内核定义了一个struct desc_ptr结构专门用于保存idtr寄存器内容,其如下:
/* 代码地址:arch/x86/include/asm/Desc_defs.h */
struct desc_ptr {
unsigned short size;
unsigned long address;
} __attribute__((packed)) ;
/* 代码地址:arch/x86/kernel/cpu/Common.c */
/* 专门用于保存需要写入idtr寄存器值的变量,这里可以看出,中断描述符表长度为256 * 16 - 1,地址为idt_table */
struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table };
在cpu_init()中,会调用load_current_idt()函数进行写入,如下:
static inline void load_current_idt(void)
{
if (is_debug_idt_enabled())
/* 开启了中断调试,用的是 debug_idt_descr 和 debug_idt_table */
load_debug_idt();
else if (is_trace_idt_enabled())
/* 开启了中断跟踪,用的是 trace_idt_descr 和 trace_idt_table */
load_trace_idt();
else
/* 普通情况,用的是 idt_descr 和 idt_table */
load_idt((const struct desc_ptr *)&idt_descr);
}
/* load_idt()的定义 */
#define load_idt(dtr) native_load_idt(dtr)
/* native_load_idt()的定义 */
static inline void native_load_idt(const struct desc_ptr *dtr)
{
asm volatile("lidt %0"::"m" (*dtr));
}
到这,异常和陷阱已经初始化完毕,内核也已经开始使用新的中断描述符表了,BIOS的中断描述符表就已经遗弃,不再使用了。
初始化中断
内核是在异常和陷阱初始化完成的情况下才会进行中断的初始化,中断的初始化也是处于start_kernel()函数中,分为两个部分,分别是early_irq_init()和init_IRQ()。early_irq_init()是第一步的初始化,其工作主要是跟硬件无关的一些初始化,比如一些变量的初始化,分配必要的内存等。init_IRQ()是第二步,其主要就是关于硬件部分的初始化了。
首先我们先看看中断描述符数组irq_desc[NR_IRQS]:
/* 中断描述符数组 */
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
可以看到,irq_desc数组有NR_IRQS个元素,NR_IRQS并不是256-32,实际上,虽然中断描述符表中一共有256项(前32项用作异常和intel保留),但并不是所有中断向量都会使用到,所以中断描述符数组也不一定是256-32项,CPU可以使用多少个中断是由中断控制器(PIC、APIC)或者内核配置决定的,我们看看NR_IRQS的定义:
/* IOAPIC为外部中断控制器 */
#ifdef CONFIG_X86_IO_APIC
#define CPU_VECTOR_LIMIT (64 * NR_CPUS)
#define NR_IRQS \
(CPU_VECTOR_LIMIT > IO_APIC_VECTOR_LIMIT ? \
(NR_VECTORS + CPU_VECTOR_LIMIT) : \
(NR_VECTORS + IO_APIC_VECTOR_LIMIT))
#else /* !CONFIG_X86_IO_APIC: NR_IRQS_LEGACY = 16 */
#define NR_IRQS NR_IRQS_LEGACY
#endif
这时我们可以先看看early_irq_init()函数:
int __init early_irq_init(void)
{
int count, i, node = first_online_node;
struct irq_desc *desc;
/* 初始化irq_default_affinity变量,此变量用于设置中断默认的CPU亲和力 */
init_irq_default_affinity();
printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);
/* 指向中断描述符数组irq_desc */
desc = irq_desc;
/* 获取中断描述符数组长度 */
count = ARRAY_SIZE(irq_desc);
for (i = 0; i < count; i++) {
/* 为kstat_irqs分配内存,每个CPU有自己独有的kstat_irqs数据,此数据用于统计 */
desc[i].kstat_irqs = alloc_percpu(unsigned int);
/* 为 desc->irq_data.affinity 和 desc->pending_mask 分配内存 */
alloc_masks(&desc[i], GFP_KERNEL, node);
/* 初始化中断描述符的锁 */
raw_spin_lock_init(&desc[i].lock);
/* 设置中断描述符的锁所属的类,此类用于防止死锁 */
lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
/* 一些变量的初始化 */
desc_set_defaults(i, &desc[i], node, NULL);
}
return arch_early_irq_init();
}
更多的初始化在desc_set_defaults()函数中:
static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node,
struct module *owner)
{
int cpu;
/* 中断号 */
desc->irq_data.irq = irq;
/* 中断描述符的中断控制器芯片为 no_irq_chip */
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;
/* 设置中断状态 desc->status_use_accessors 为初始化状态_IRQ_DEFAULT_INIT_FLAGS */
irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
/* 中断默认被禁止,设置 desc->irq_data->state_use_accessors = IRQD_IRQ_DISABLED */
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
/* 设置中断处理回调函数为 handle_bad_irq,handle_bad_irq作为默认的回调函数,此函数中基本上不做什么处理,就是在屏幕上打印此中断信息,并且desc->kstat_irqs++ */
desc->handle_irq = handle_bad_irq;
/* 嵌套深度为1,表示被禁止1次 */
desc->depth = 1;
/* 初始化此中断发送次数为0 */
desc->irq_count = 0;
/* 无法处理的中断次数为0 */
desc->irqs_unhandled = 0;
/* 在/proc/interrupts所显名字为空 */
desc->name = NULL;
/* owner为空 */
desc->owner = owner;
/* 初始化kstat_irqs中每个CPU项都为0 */
for_each_possible_cpu(cpu)
*per_cpu_ptr(desc->kstat_irqs, cpu) = 0;
/* SMP系统才使用的初始化,设置
* desc->irq_data.node = first_online_node
* desc->irq_data.affinity = irq_default_affinity
* 清除desc->pending_mask
*/
desc_smp_init(desc, node);
}
整个early_irq_init()在这里就初始化完毕了,相对来说比较简单,可以说early_irq_init()只是初始化了中断描述符数组中的所有元素。
在看init_IRQ()前需要看看legacy_pic这个变量,它其实就是CPU内部的中断控制器i8259A,定义了与i8259A相关的一些处理函数和中断数量,如下:
struct legacy_pic default_legacy_pic = {
.nr_legacy_irqs = NR_IRQS_LEGACY,
.chip = &i8259A_chip,
.mask = mask_8259A_irq,
.unmask = unmask_8259A_irq,
.mask_all = mask_8259A,
.restore_mask = unmask_8259A,
.init = init_8259A,
.irq_pending = i8259A_irq_pending,
.make_irq = make_8259A_irq,
};
struct legacy_pic *legacy_pic = &default_legacy_pic;
在X86体系下,CPU使用的内部中断控制器是i8259A,内核就定义了这个变量进行使用,在init_IRQ()中会将所有的中断描述符的中断控制器芯片指向i8259A,具体我们先看看init_IRQ()代码:
void __init init_IRQ(void)
{
int i;
/*
* On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15.
* If these IRQ's are handled by legacy interrupt-controllers like PIC,
* then this configuration will likely be static after the boot. If
* these IRQ's are handled by more mordern controllers like IO-APIC,
* then this vector space can be freed and re-used dynamically as the
* irq's migrate etc.
*/
/* nr_legacy_irqs() 返回 legacy_pic->nr_legacy_irqs,为16
* vector_irq是一个int型的数组,长度为中断描述符表长,其保存的是中断向量对应的中断号(如果中断向量是异常则没有中断号)
* i8259A中断控制器使用IRQ0~IRQ15这16个中断号,这里将这16个中断号设置到CPU0的vector_irq数组的0x30~0x3f上。
*/
for (i = 0; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
/* x86_init是一个结构体,里面定义了一组X86体系下的初始化函数 */
x86_init.irqs.intr_init();
}
x86_init.irqs.intr_init()是一个函数指针,其指向native_init_IRQ(),我们可以直接看看native_init_IRQ():
void __init native_init_IRQ(void)
{
int i;
/* Execute any quirks before the call gates are initialised: */
/* 这里又是执行x86_init结构中的初始化函数,pre_vector_init()指向 init_ISA_irqs */
x86_init.irqs.pre_vector_init();
/* 初始化中断描述符表中的中断控制器中默认的一些中断门初始化 */
apic_intr_init();
/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
/* 第一个外部中断,默认是32 */
i = FIRST_EXTERNAL_VECTOR;
/* 在used_vectors变量中找出所有没有置位的中断向量,我们知道,在trap_init()中对所有异常和陷阱和系统调用中断都置位了used_vectors,没有置位的都为中断
* 这里就是对所有中断设置门描述符
*/
for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
/* IA32_SYSCALL_VECTOR could be used in trap_init already. */
/* interrupt[]数组保存的是外部中断的中断门信息
* 这里将中断描述符表中空闲的中断向量设置为中断门,interrupt是一个函数指针数组,其将31~255数组元素指向interrupt[i]函数
*/
set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
}
/* 如果外部中断控制器需要,则安装一个中断处理例程irq2到中断IRQ2上 */
if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
setup_irq(2, &irq2);
#ifdef CONFIG_X86_32
/* 在x86_32模式下,会为当前CPU分配一个中断使用的栈空间 */
irq_ctx_init(smp_processor_id());
#endif
}
在native_init_IRQ()中,又使用了x86_init变量中的pre_vector_init函数指针,其指向init_ISA_irqs()函数:
void __init init_ISA_irqs(void)
{
/* CHIP默认是i8259A_chip */
struct irq_chip *chip = legacy_pic->chip;
int i;
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
/* 使用了CPU本地中断控制器 */
/* 开启virtual wire mode */
init_bsp_APIC();
#endif
/* 其实就是调用init_8259A(),进行8259A硬件的初始化 */
legacy_pic->init(0);
for (i = 0; i < nr_legacy_irqs(); i++)
/* i为中断号,chip是irq_chip结构,最后是中断回调函数
* 设置了中断号i的中断描述符的irq_data.irq_chip = i8259A_chip
* 设置了中断回调函数为handle_level_irq
*/
irq_set_chip_and_handler(i, chip, handle_level_irq);
}
在init_ISA_irqs()函数中,最主要的就是将内核使用的外部中断的中断描述符的中断控制器设置为i8259A_chip,中断回调函数为handle_level_irq。
回到native_init_IRQ()函数,当执行完x86_init.irqs.pre_vector_init()之后,会执行apic_initr_init()函数,这个函数中会初始化一些中断控制器特定的中断函数(这些中断游离于之前描述的中断体系中,它们没有自己的中断描述符,中断向量中直接保存它们自己的中断处理函数,类似于异常与陷阱的调用情况),具体我们看看:
static void __init apic_intr_init(void)
{
smp_intr_init();
#ifdef CONFIG_X86_THERMAL_VECTOR
/* 中断号为: 0xfa,处理函数为: thermal_interrupt */
alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
#ifdef CONFIG_X86_MCE_THRESHOLD
alloc_intr_gate(THRESHOLD_APIC_VECTOR, threshold_interrupt);
#endif
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
/* self generated IPI for local APIC timer */
alloc_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt);
/* IPI for X86 platform specific use */
alloc_intr_gate(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi);
#ifdef CONFIG_HAVE_KVM
/* IPI for KVM to deliver posted interrupt */
alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif
/* IPI vectors for APIC spurious and error interrupts */
alloc_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);
alloc_intr_gate(ERROR_APIC_VECTOR, error_interrupt);
/* IRQ work interrupts: */
# ifdef CONFIG_IRQ_WORK
alloc_intr_gate(IRQ_WORK_VECTOR, irq_work_interrupt);
# endif
#endif
}
在apic_intr_init()函数中,使用了alloc_intr_gate()函数进行处理,这个函数的处理也很简单,置位该中断号所处used_vectors位置,调用set_intr_gate()设置一个中断门描述符。
到这里整个中断及异常都已经初始化完成了。
总结
整篇文章代码有点多,又有点乱,这里我们总结一下。
- 在linux系统中,中断一共有256个,0~19主要用于异常与陷阱,20~31是intel保留,未使用。32~255作为外部中断进行使用。特别的,0x80中断用于系统调用。
- 机器上电时,BIOS会初始化一个中断描述符表,当交接给linux内核后,内核会自己新建立一个中断描述符表,之后完全使用自己的中断描述符表,舍弃BIOS的中断描述符表。
- 在x86上系统默认使用的中断控制器为i8259A。
- 中断描述符的初始化过程中,内核会将中断描述符的默认中断控制器设置为i8259A,中断处理回调函数为handle_level_irq()。
- 外部中断的门描述的中断处理函数都为interrupt[i]。
中断的初始化大体上分为两个部分,第一个部分为汇编代码的中断描述符表的初次初始化,第二部分为C语言代码,其又分为异常与陷阱的初始化和中断的初始化。如图:
在汇编的中断描述符表初始化过中,其主要对整个中断描述符表进行了初始化,其主要工作是:
- 所有的门描述符的权限位为0;
- 所有的门描述符的段选择符为__KERNEL_CS;
- 0~31的门描述符的中断处理程序为early_idt_handlers[i](0 <= i <= 31);
- 其他的门描述符的中断处理程序为ignore_int;
而trap_init()所做的异常与陷阱初始化,就是修改中断描述符表的前19项(异常和中断),主要修改他们的中断处理函数入口和权限位,特殊的如任务门还会设置它们的段选择符。在trap_init()中就已经把所有的异常和陷阱都初始化完成了,并会把新的中断描述符表地址放入idtr寄存器,开始使用新的中断描述符表。
在early_irq_init()中,主要工作是初始化整个中断描述符数组,将数组中的每个中断描述符中的必要变量进行初始化。
最后在init_IRQ()中,主要工作是初始化中断描述符表中的所有中断门描述符,对于一般的中断,内核将它们的中断处理函数入口设置为interrupt[i],而一些特殊的中断会在apic_intr_init()中进行设置。之后,init_IRQ()会初始化内部和外部的中断控制器,最后将一般的中断使用的中断控制器设置为i8259A,中断处理函数为handle_level_irq(电平触发)。