linux中斷原始碼分析
本篇文章主要講述原始碼中是如何對中斷進行一系列的初始化的。
回顧
在上一篇概述中,介紹了幾個對於中斷來說非常重要的資料結構,分別是:中斷描述符表,中斷描述符陣列,中斷描述符,中斷控制器描述符,中斷服務例程。可以說這幾個結構組成了整個核心中斷框架主體,所以核心對整個中斷的初始化工作大多集中在了這幾個結構上。
在系統中,當一箇中斷產生時,首先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中斷用於系統呼叫。