本文為原創,轉載請注明: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(電平觸發)。
