32获取外部中断状态_Linux中断一网打尽(1) — 中断及其初始化
中斷是什么
既然叫中斷, 那我們首先就會(huì)想到這個(gè)中斷是中斷誰?想一想計(jì)算機(jī)最核心的部分是什么?沒錯(cuò), CPU, 計(jì)算機(jī)上絕大部分的計(jì)算都在CPU中完成,因此這個(gè)中斷也就是中斷CPU當(dāng)前的運(yùn)行,讓CPU轉(zhuǎn)而先處理這個(gè)引起中斷的事件,通常來說這個(gè)中斷的事件比較緊急,處理完畢后再繼續(xù)執(zhí)行之前被中斷的task。比如,我們敲擊鍵盤,CPU就必須立即響應(yīng)這個(gè)操作,不然我們打字就全變成了慢動(dòng)作~說白了中斷其實(shí)就是一種主動(dòng)通知機(jī)制,如果中斷源不主動(dòng)通知,那想知道其發(fā)生了什么事情,只能一次次地輪詢了,白白耗費(fèi)CPU。
2中斷的分類
大的方向上一般分為兩大類:同步中斷和異步中斷,按Intel的說法,將異步中斷稱為中斷,將同步中斷稱為異常。
異步中斷
主要是指由CPU以外的硬件產(chǎn)生的中斷,比如鼠標(biāo),鍵盤等。它的特點(diǎn)是相對(duì)CPU來說隨時(shí)隨機(jī)發(fā)生,事先完全沒有預(yù)兆,不可預(yù)期的。異步中斷發(fā)生時(shí),CPU基本上都正在執(zhí)行某條指令。
異步中斷可分為可屏蔽和不可屏蔽兩種,字如其義不用多解釋。
同步中斷
主要是指由CPU在執(zhí)行命令過程中產(chǎn)生的異常,它一定是在CPU執(zhí)行完一條命令后才會(huì)發(fā)出,產(chǎn)生于CPU內(nèi)部。按其被CPU處理后返回位置的不同,我們將同步中斷分為故障(fault), 陷阱(trap)和終止(abort)三類。我們通過一個(gè)表格來作下對(duì)比區(qū)分:
兩點(diǎn)說明:
處理完畢后的返回位置:發(fā)生異常時(shí),CPU最終會(huì)進(jìn)入到相應(yīng)的異常處理程序中(簡(jiǎn)單說就是CPU需要執(zhí)行一次跳轉(zhuǎn))在執(zhí)行具體操作前會(huì)設(shè)置好的異常處理完成后跳轉(zhuǎn)回的CS:IP, 即代碼段寄存器和程序指針寄存器,不同類型的異常其設(shè)置的CS:IP不同而已;
有些分類方法還會(huì)有一種叫可編程異常的,比如說把系統(tǒng)調(diào)用算作這一類,也可以。但是如果按處理完畢后的返回位置來說系統(tǒng)調(diào)用是可以歸入陷阱這一類的。
面對(duì)如此眾多的服務(wù)器
我們都知道CPU上只有有限多的腳針,負(fù)責(zé)與外部通訊,比如有數(shù)據(jù)線,地址線等,也有中斷線,但一般只有兩條NMI(不可屏蔽中斷線)和INTR(可屏蔽中斷線), 新的CPU有LINT0和LINT1腳針。那您會(huì)問了,電腦上有那么多外設(shè),CPU就這兩根線,怎么接收這么多外設(shè)的中斷信號(hào)呢?確實(shí),因此CPU找了一個(gè)管理這些眾多中斷的代理人——中斷控制器。
就目前我們使用的SMP多核架構(gòu)里,我們經(jīng)常使用高級(jí)可編程中斷控制器APIC, 老式的 8259A 可編程中斷控制器大家有興趣可自行搜索。
APIC分為兩部分,IO APIC和Local APIC,從名字上我們就可略知一二。
IO APIC: 用來連接各種外設(shè)的硬件控制器,接收其發(fā)送的中斷請(qǐng)求信號(hào),然后將其傳送到Local APIC, 這個(gè)IO APIC一般會(huì)封裝在主板南板芯片上;
Local APIC: 基本上集成在了CPU里, 向CPU通知中斷發(fā)生。
放張網(wǎng)上的圖:
4中斷的初始化
Linux的啟動(dòng)流程
中斷的初始化是穿插在Linux本身啟動(dòng)和初始化過程中的,因此我們?cè)谶@里簡(jiǎn)要說一下Linux本身的初始化。
64位Linux啟動(dòng)大的方向上需要經(jīng)過?實(shí)模式 -> 保護(hù)模式 -> 長模式?第三種模式的轉(zhuǎn)換;
電源接通,CPU啟動(dòng)并重置各寄存器后運(yùn)行于實(shí)模式下,CS:IP加載存儲(chǔ)于ROM中的一跳轉(zhuǎn)指令,跳轉(zhuǎn)到BIOS中;
BIOS啟動(dòng),硬件自測(cè),讀取MRB;
BIOS運(yùn)行第一階段引導(dǎo)程序,第一階段引導(dǎo)程序運(yùn)行第二階段引導(dǎo)程序,通常是 grub;
Grub開始引導(dǎo)內(nèi)核運(yùn)行;
相關(guān)初始化后進(jìn)行保護(hù)模式,再進(jìn)入長模式,內(nèi)核解壓縮;
體系無關(guān)初始化部分;
體系相關(guān)初始化部分;
總結(jié)了一張圖,僅供參考:
中斷描述符表
外設(shè)千萬種,CPU統(tǒng)統(tǒng)不知道。所有的中斷到了CPU這里就只是一個(gè)中斷號(hào),然后初始化階段設(shè)置好中斷號(hào)到中斷處理程序的對(duì)應(yīng)關(guān)系,CPU獲取到一個(gè)中斷號(hào)后,查到對(duì)應(yīng)的中斷處理程序調(diào)用就好了。
這兩者的對(duì)應(yīng)關(guān)系最后會(huì)抽象成了中斷向量表, 現(xiàn)在叫 IDT中斷描述符表。
中斷的第一次初始化
實(shí)模式下的初始化
上面那張Linux啟動(dòng)流程圖如果你仔細(xì)看的話會(huì)發(fā)現(xiàn)在BIOS程序加載運(yùn)行時(shí),在實(shí)模式下也有一個(gè)BIOS的中斷向量表,這個(gè)中斷向量表提供了一些類似于BIOS的系統(tǒng)調(diào)用一樣的方法。比如Linux在初始化時(shí)需要獲取物理內(nèi)存的詳情,就 是調(diào)用了BIOS的相應(yīng)中斷來獲取的。見下圖:
中斷的第二次初始化
在進(jìn)入到保護(hù)模式后,會(huì)全新初始化一個(gè)空的中斷描述符表 IDT, 供 kernel 使用;
Linux Kernel提供256個(gè)大小的中斷描述符表
中斷的第三次初始化
在進(jìn)入到長模式后,在x86_64_start_kernel先初始化前32個(gè)異常類型的中斷(即上面定義的 idt_table 的前32項(xiàng));
void __init idt_setup_early_handler(void){ int i; for (i = 0; i < NUM_EXCEPTION_VECTORS; i++) set_intr_gate(i, early_idt_handler_array[i]); load_idt(&idt_descr);}其中 early_idt_handler_array這個(gè)數(shù)組放置了32個(gè)異常類型的中斷處理程序,我們先看一下它的定義:
const char early_idt_handler_array[32][9];二維數(shù)組,每一個(gè)early_idt_handler_array[i]有9個(gè)字節(jié)。
這個(gè) early_idt_handler_array的初始化很有意思,它用AT&T的匯編代碼完成,在文件arch/x86/kernel/head_64.S中:
ENTRY(early_idt_handler_array) i = 0 .rept NUM_EXCEPTION_VECTORS .if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0 UNWIND_HINT_IRET_REGS pushq $0 # Dummy error code, to make stack frame uniform .else UNWIND_HINT_IRET_REGS offset=8 .endif pushq $i # 72(%rsp) Vector number jmp early_idt_handler_common UNWIND_HINT_IRET_REGS i = i + 1 .fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc .endr UNWIND_HINT_IRET_REGS offset=16END(early_idt_handler_array)這段匯編循環(huán)遍歷32次來初始化每一個(gè)early_idt_handler_array[i], 也就是填充它的9個(gè)字節(jié):其中2個(gè)字節(jié)是壓棧錯(cuò)誤碼指令,2個(gè)字節(jié)是壓棧向量號(hào)指令,余下的5個(gè)字節(jié)是函數(shù)跳轉(zhuǎn)指令(jmp early_idt_handler_common)。由此我們可以看出,這前32個(gè)異常類型的中斷處理函數(shù)最終都會(huì)調(diào)用到early_idt_handler_common, 這個(gè)函數(shù)這里就不貼它的代碼了,我們說下它的大致流程:
a. 先將各寄存器的值壓棧保存;b. 如果是 缺頁異常,就調(diào)用 `early_make_patable`; c. 如果是 其他異常,就調(diào)用 `early_fixup_exception`;體系結(jié)構(gòu)相關(guān)的中斷初始化:
這也是一次部分初始化,它發(fā)生在 start_kernel的setup_arch中,即發(fā)生在 Linux 啟動(dòng)流程中的體系結(jié)構(gòu)初始化部分。這部分實(shí)際上是更新上面已初始化的32個(gè)異常類中的X86_TRAP_DB(1號(hào), 用于debug)和X86_TRAP_BP(3號(hào), 用于debug時(shí)的斷點(diǎn));
static const __initconst struct idt_data early_idts[] = { INTG(X86_TRAP_DB, debug), SYSG(X86_TRAP_BP, int3),};void __init idt_setup_early_traps(void){ idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts), true); load_idt(&idt_descr);}debug和int3這兩個(gè)匯編實(shí)現(xiàn)的中斷處理程序這里我們就不詳述了。
更新?X86_TRAP_PF 缺頁異常的中斷處理程序:
void __init idt_setup_early_pf(void){ idt_setup_from_table(idt_table, early_pf_idts, ARRAY_SIZE(early_pf_idts), true);}static const __initconst struct idt_data early_pf_idts[] = { INTG(X86_TRAP_PF, page_fault),};在trap_init中調(diào)用 idt_setup_traps更新部分異常的中斷處理程序:
void __init idt_setup_traps(void){ idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);}static const __initconst struct idt_data def_idts[] = { INTG(X86_TRAP_DE, divide_error), INTG(X86_TRAP_NMI, nmi), INTG(X86_TRAP_BR, bounds), INTG(X86_TRAP_UD, invalid_op), INTG(X86_TRAP_NM, device_not_available), INTG(X86_TRAP_OLD_MF, coprocessor_segment_overrun), INTG(X86_TRAP_TS, invalid_TSS), INTG(X86_TRAP_NP, segment_not_present), INTG(X86_TRAP_SS, stack_segment), INTG(X86_TRAP_GP, general_protection), INTG(X86_TRAP_SPURIOUS, spurious_interrupt_bug), INTG(X86_TRAP_MF, coprocessor_error), INTG(X86_TRAP_AC, alignment_check), INTG(X86_TRAP_XF, simd_coprocessor_error),#ifdef CONFIG_X86_32 TSKG(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS),#else INTG(X86_TRAP_DF, double_fault),#endif INTG(X86_TRAP_DB, debug),#ifdef CONFIG_X86_MCE INTG(X86_TRAP_MC, &machine_check),#endif SYSG(X86_TRAP_OF, overflow),#if defined(CONFIG_IA32_EMULATION) SYSG(IA32_SYSCALL_VECTOR, entry_INT80_compat),#elif defined(CONFIG_X86_32) SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),#endif};在trap_init中調(diào)用?idt_setup_ist_traps更新部分異常的中斷處理程序
看到這里您可能問,上面不是調(diào)用了idt_setup_traps,怎么這時(shí)又調(diào)用idt_setup_ist_traps? 這兩者有什么區(qū)別?說起來話有點(diǎn)長,我們盡量從流程上給大家講清楚,但不深入到具體的細(xì)節(jié)。
想說明這個(gè)問題,我們先來講下棧這個(gè)東西:
a. ?首先每個(gè)進(jìn)程都有自己的用戶態(tài)棧,對(duì)應(yīng)進(jìn)程虛擬地址空間內(nèi)的stack部分,用于進(jìn)程在用戶態(tài)變量申請(qǐng),函數(shù)調(diào)用等操作;
b. 除了用戶態(tài)棧,每個(gè)進(jìn)程在創(chuàng)建時(shí)(內(nèi)核對(duì)應(yīng)創(chuàng)建 task_struct結(jié)構(gòu))同時(shí)會(huì)創(chuàng)建對(duì)應(yīng)的內(nèi)核棧,這里進(jìn)程由用戶態(tài)進(jìn)入到內(nèi)核態(tài)執(zhí)行函數(shù)時(shí),相應(yīng)的所用的棧也會(huì)切換到內(nèi)核棧;
c. 如果內(nèi)核進(jìn)入到中斷處理程序,早期的kernel針對(duì)中斷處理程序的執(zhí)行會(huì)使用當(dāng)前中斷task的內(nèi)核棧,這里有存在一定的問題,存在棧溢出的風(fēng)險(xiǎn)。舉個(gè)例子,如果在中斷處理程序里又發(fā)生了異常中斷,此時(shí)會(huì)觸發(fā)double fault,但其在處理過程中依然要使用當(dāng)前task的內(nèi)核棧,并且當(dāng)前task內(nèi)核棧已滿,double fault無法被正確處理。為了解決這樣的內(nèi)部,linux kernel引出了獨(dú)立的內(nèi)核棧,針對(duì)SMP系統(tǒng),它還是pre-cpu的。我們來看一下其初始化,還特別貼心地為softirq也開辟了單獨(dú)的棧:
2. 在x86_64位系統(tǒng)中,還引入了一種新的棧配置:IST(Interrupt Stack Table)。目前Linux kernel中每個(gè)cpu最多支持7個(gè)IST,可以通過tss.ist[]來訪問。
3. 現(xiàn)在我們?cè)賮砜磇dt_setup_ist_traps,其實(shí)就是重新初始化一個(gè)異常處理,讓這些異常處理使用IST作為中斷棧。其中 IST_INDEX_DB IST_INDEX_NMI IST_INDEX_DF IST_INDEX_MCE就是要使用的ist[]的索引。
void __init idt_setup_ist_traps(void){ idt_setup_from_table(idt_table, ist_idts, ARRAY_SIZE(ist_idts), true);}static const __initconst struct idt_data ist_idts[] = { ISTG(X86_TRAP_DB, debug, IST_INDEX_DB), ISTG(X86_TRAP_NMI, nmi, IST_INDEX_NMI), ISTG(X86_TRAP_DF, double_fault, IST_INDEX_DF),#ifdef CONFIG_X86_MCE ISTG(X86_TRAP_MC, &machine_check, IST_INDEX_MCE),#endif};#define ISTG(_vector, _addr, _ist) \ G(_vector, _addr, _ist + 1, GATE_INTERRUPT, DPL0, __KERNEL_CS)剩下的最后一部分就是硬件中斷的初始化了,它同樣在start_kernel中執(zhí)行:
early_irq_init();init_IRQ();這部分具體細(xì)節(jié)我們?cè)贚inux中斷一網(wǎng)打盡(2) - IDT及中斷處理的實(shí)現(xiàn)介紹。
本文轉(zhuǎn)載自云計(jì)算
往期精彩回顧
2019年度精選文章
httprunner使用總結(jié)
全國新型肺炎實(shí)時(shí)動(dòng)態(tài)360技術(shù)公眾號(hào)
技術(shù)干貨|一手資訊|精彩活動(dòng)
掃碼關(guān)注我們總結(jié)
以上是生活随笔為你收集整理的32获取外部中断状态_Linux中断一网打尽(1) — 中断及其初始化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java单例模式的七种写法
- 下一篇: linux原理与应用期末考试,武汉大学计