Linux核心筆記009 - 中斷、異常、陷阱、Bottom half、訊號
記得剛學習C語言時,只要找個包含if語句的程式,然後通過理解整個程式執行到這條語句時,發生了什麼,自然就明白if語句的作用了。同樣,為了理解"中斷"的含義,我特別建議站在可以看見整個系統的角度,去看出現中斷時,整個系統這個"大程式"是如何執行的。
時鐘中斷(硬體觸發,對於軟體是被動的)、異常(軟體缺頁、除0bug等情況無意觸發)、陷阱(軟體顯式執行int指令觸發)出現時,都會穿過一道"門",跳轉到核心在"門"中設定的指令地址處執行,所以它們本質上和執行jmp、call、rte等跳轉指令一樣,都是打斷"大程式"的順序執行,跳轉到指定的指令處執行,只不過在跳轉前,CPU硬體層還會做一些額外的操作。
中斷、異常、陷阱相互之間,只在兩點上稍有區別(根據本篇筆記稍後的內容,可以明白為什麼需要這些區別):
① 緊接著中斷的發生,硬體層是否關閉該型別的中斷;
② 穿過"門"的時候,CPL/RPL許可權檢查的邏輯。
2. 外設通用中斷(使用中斷門)
- 中斷過程分析
BUILD_COMMON_IRQ() // 展開得到:common_interrupt程式碼塊定義(見[code3]) // ③ 展開得到16個程式碼塊的定義:IRQ0x00_interrupt~IRQ0x0f_interrupt(見[code2]) #define BI(x,y) \ BUILD_IRQ(x##y) // ② 展開得到:BUILD_IRQ(0x00)~BUILD_RIQ(0x0f) #define BUILD_16_IRQS(x) \ BI(x,0) BI(x,1) BI(x,2) BI(x,3) \ BI(x,4) BI(x,5) BI(x,6) BI(x,7) \ BI(x,8) BI(x,9) BI(x,a) BI(x,b) \ BI(x,c) BI(x,d) BI(x,e) BI(x,f) // ① 展開得到:BI(0x0,0)~BI(0x,f) /* * ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts: * (these are usually mapped to vectors 0x20-0x2f) */ BUILD_16_IRQS(0x0)
// 所有的IRQ0xXX_interrupt,都將(xx-256)壓入棧中,然後跳轉到common_interrupt處執行 #define BUILD_IRQ(nr) \ asmlinkage void IRQ_NAME(nr); \ __asm__( \ "\n"__ALIGN_STR"\n" \ SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \ "pushl $"#nr"-256\n\t" \ "jmp common_interrupt");
/* * ① SAVE_ALL:向棧中壓入一個struct pt_regs結構資料 * ② pushl $ret_from_intr,向棧中壓入ret_from_intr指令地址 * ③ jmp到do_IRQ()函式,注意不是call,所以步驟②壓入的指令地址,對於do_IRQ()函式來說,是返回地址 !! * do_IRQ()函式原型:asmlinkage unsigned int do_IRQ(struct pt_regs regs),注意引數型別是一個完整的結構,而不是指標,所以正好是步驟①壓入引數 !! */ #define BUILD_COMMON_IRQ() \ asmlinkage void call_do_IRQ(void); \ __asm__( \ "\n" __ALIGN_STR"\n" \ "common_interrupt:\n\t" \ SAVE_ALL \ "pushl $ret_from_intr\n\t" \ SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \ "jmp "SYMBOL_NAME_STR(do_IRQ));
根據[code1]、[code2]、[code3]三處程式碼,可歸納中斷髮生時的跳轉過程:
IRQ0x00_interrupt // 程式碼塊 |- jmp common_interrupt // 程式碼塊 |- jmp do_IRQ() // 函式
進入do_IRQ()函式後,根據IRQ0xXX_interrupt向棧的ORIG_EAX位置壓入的通用中斷號,並遍歷執行irq_desc[regs.orig_eax].action連結串列中的函式:
do_IRQ() |- irq = regs.orig_eax & 0xff |- spin_lock(&desc->lock) |- IRQ_INPROGRESS |- for(;;) | |- IRQ_PENDING | |- handle_IRQ_event() |- do_softirq()
③ handle_IRQ_event()函式開中斷執行
EFLAGS暫存器並不能精確控制每道"門"的開關,只能通過一個"I"標誌位,整體開啟/關閉所有"門",所以從CPU穿過某道中斷門到再次將"I"標誌位置1期間,任何中斷源傳送的中斷訊號,都會丟失,雖然中斷源沒有收到CPU的響應訊號,一般會再次傳送,Linux核心還是通過軟體層的設計,儘量緩解中斷訊號的丟失。其實,不同irq_desc[X].action連結串列中的函式,既然是處理不同的外設中斷,所以訪問的資源一般是相互獨立的,很少會出現競爭的情況,所以Linux核心將是否允許重入handle_IRQ_event()(即正在該函式內部執行時接收到中斷訊號,又要重新從IRQ0xXX_interrupt開始,執行到該函式),留給action開發者選擇。
比如,如果開發者可以保證,irq_desc[0x00].action連結串列上的函式,與其它action連結串列上的函式,不存在資源競爭,那麼,就可以通過設定actions->flags的SA_INTERRUPT標誌,讓handle_IRQ_event()函式在入口處執行sti指令,快速恢復當前CPU的中斷功能。這樣,由於do_IRQ()在呼叫handle_IRQ_event()前,執行了unlock,所以再次進入do_IRQ(),不會發生死鎖;另外,由於沒有資源競爭,所以交叉執行也不會有任何問題(irq_desc[0x00].action未執行完 -> 執行irq_desc[0x01].action -> 根據中斷時儲存的現場,恢復執行irq_desc[0x00].action)。
還有另外一種場景:CPU正在執行0號通用中斷的處理函式,這時又產生了0號通用中斷。其實,這就跟多CPU同時執行同一中斷通道的場景相同, handle_IRQ_event()會被IRQ_INPROGRESS、IRQ_PENDING"序列化"執行,所以也沒有問題:
可以看出,選擇中斷門和使用"序列化"設計,對外設中斷進行管理,大大減小了action開發者的負擔,相應也減少了產生bug的根源。
- Bottom Half
softirq_init() |- tasklet_init() // 初始化bh_task_vec[32],func成員都指向bh_action() |- open_softirq() // 初始化softirq_vec[32] |- softirq_vec[HI_SOFTIRQ].action = tasklet_hi_action() // HI_SOFTIRQ用於相容老的bottom hafl機制 |- softirq_vec[TASKLET_SOFTIRQ].action = tasklet_action() // TASKLET_SOFTIRQ用於新擴充套件的bottom hafl機制 |- irq_stat[所有CPU].__softirq_mask的HI_SOFTIRQ、TASKLET_SOFTIRQ位置1,從而每次執行do_softirq()時,就會呼叫tasklet_hi_action()、tasklet_action() sched_init() // 別的模組也會根據需要註冊其它bh函式 |- init_bh(TIMER_BH, timer_bh) |- init_bh(TQUEUE_BH, tqueue_bh) |- init_bh(IMMEDIATE_BH, immediate_bh)
訊號
3. 缺頁異常(使用陷阱門,DPL為0)
跟IRQ0xXX_interrupt型別,直接看程式碼。
arch/i386/kernel/entry.S,410~412:
ENTRY(page_fault) pushl $ SYMBOL_NAME(do_page_fault) jmp error_code
error_code: pushl %ds pushl %eax xorl %eax,%eax pushl %ebp pushl %edi pushl %esi pushl %edx decl %eax # eax = -1 pushl %ecx pushl %ebx cld movl %es,%ecx movl ORIG_EAX(%esp), %esi # get the error code(硬體自動壓入) movl ES(%esp), %edi # get the function address(執行page_fault時壓入) movl %eax, ORIG_EAX(%esp) movl %ecx, ES(%esp) movl %esp,%edx pushl %esi # push the error code(do_page_fault()的error_code引數) pushl %edx # push the pt_regs pointer(do_page_fault()的regs引數) movl $(__KERNEL_DS),%edx movl %edx,%ds movl %edx,%es GET_CURRENT(%ebx) call *%edi # 呼叫do_page_fault() addl $8,%esp jmp ret_from_exception
ENTRY(coprocessor_error) pushl $0 pushl $ SYMBOL_NAME(do_coprocessor_error) jmp error_code
void do_page_fault(struct pt_regs *regs, unsigned long error_code)
time_init() // arch/i386/kernel/time.c, 626~706 |- setup_irq(0, &irq0) // 向irq_desc[0]註冊action // arch/i386/kernel/time.c, 547 static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
5. 系統呼叫(使用陷阱門,DPL為3)
跟CPU異常一樣,系統呼叫也是用陷阱門實現:
static void __init set_trap_gate(unsigned int n, void *addr) { _set_gate(idt_table+n,15,0,addr); // 15: D:1,type:111(陷阱門),DPL: 0 } static void __init set_system_gate(unsigned int n, void *addr) { _set_gate(idt_table+n,15,3,addr); // 15: D:1,type:111(陷阱門),DPL: 3 }
// int sethostname(cost char *name, size_t len); 00000000 <sethostname>: 0: 89 da mov %ebx,%edx 2: 8b 4c 24 08 mov 0x8(%esp,1),%ecx # len 引數 6: 8b 5c 24 04 mov 0x4(%esp,1),%ebx # name引數 a: b8 4a 00 00 00 mov $0x4a,%eax # sethostname()函式對應的系統呼叫號 f: cd 80 int $0x80 11: 89 d3 mov %edx,%ebx 13: 3d 01 f0 ff ff cmp $0xfffff001,%eax # eax暫存器為核心介面的返回值,負數表示出錯 18: 0f 83 fc ff ff ff jae 1a <sethostname+0x1a> # 重定位後,為__syscall_error()函式地址(將exa絕對值儲存到errno,並將eax修改為-1,表示向上層程式返回-1) 1e: c3 ret
ENTRY(system_call) pushl %eax # save orig_eax,將eax暫存器中的系統呼叫號,壓入系統棧的ORIG_EAX位置(終於看到這個名稱的來歷,外設中斷時儲存中斷號,異常時儲存錯誤碼) SAVE_ALL # SAVE_ALL最後壓入棧中的ecx、ebx,正好為long sys_sethostname(char *name, int len)的引數,跟外設中斷和異常的處理函式不同,引數不再是struct pt_regs結構 GET_CURRENT(%ebx) # 將當前程式的task_struct管理結構的地址,儲存到ebx暫存器(第四章) cmpl $(NR_syscalls),%eax jae badsys testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS,如果當前程式被strace除錯工具跟蹤,跳轉到tracesys()執行(暫不關心) jne tracesys call *SYMBOL_NAME(sys_call_table)(,%eax,4) # 跳轉到系統呼叫號對應的函式執行,即sys_sethostname() movl %eax,EAX(%esp) # save the return value ENTRY(ret_from_sys_call) #ifdef CONFIG_SMP movl processor(%ebx),%eax shll $CONFIG_X86_L1_CACHE_SHIFT,%eax movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask #else movl SYMBOL_NAME(irq_stat),%ecx # softirq_active testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask #endif jne handle_softirq ################# 以下部分,暫時瞭解即可 ################# ret_with_reschedule: cmpl $0,need_resched(%ebx) jne reschedule # 程式排程(第四章) cmpl $0,sigpending(%ebx) jne signal_return # 訊號(第六章) restore_all: RESTORE_ALL ALIGN signal_return: sti # we can get here from an interrupt handler testl $(VM_MASK),EFLAGS(%esp) movl %esp,%eax jne v86_signal_return xorl %edx,%edx call SYMBOL_NAME(do_signal) # 執行應用程式中的訊號處理函式 jmp restore_all
sys_sethostname() |- copy_from_user() // 將主機名修改到核心空間,從而所有程式可以看到新的主機名 |- .. |- __copy_user_zeroing() // 彙編程式碼,建議仔細品一品
// do_page_fault()函式片段 no_context: /* Are we prepared to handle this kernel fault? */ if ((fixup = search_exception_table(regs->eip)) != 0) { // 在"異常表"中,查詢導致異常的那條指令的地址(__copy_user_zeroing()的後面部分程式碼,就是向該表中加入"出錯指令地址-修復地址"對應關係) regs->eip = fixup; // 如果找到了,修改異常程式的eip,讓它跳轉到修復地址執行(否則回到原指令,又會觸發缺頁異常) return; } ... do_sigbus: ... /* Kernel mode? Handle exceptions or die */ if (!(error_code & 4)) // 在核心態發生的缺頁異常 goto no_context; return;
相關文章
- 硬中斷,軟中斷,訊號,異常2020-10-17
- Java核心技術筆記 異常、斷言和日誌2019-01-19Java筆記
- 《Java核心技術(卷1)》筆記:第7章 異常、斷言和日誌2020-06-23Java筆記
- 異常和中斷2024-03-26
- Java 筆記《異常》2019-04-03Java筆記
- Linux核心軟中斷2021-05-04Linux
- 記Linux使用異常22024-11-06Linux
- Linux 核心配置筆記2024-07-05Linux筆記
- iOS Mach異常和signal訊號2020-06-17iOSMac
- Java中的異常處理(隨堂筆記)2020-10-08Java筆記
- java異常處理筆記2020-04-06Java筆記
- java學習筆記(異常)2020-12-12Java筆記
- X86中斷/異常與APIC2020-11-29API
- Linux核心自旋鎖使用筆記2019-05-14Linux筆記
- Linux核心中斷2024-08-29Linux
- PHP 訊號中斷系統2019-08-27PHP
- 異常篇——異常記錄2022-02-26
- Linux核心筆記004 - 從記憶體管理開始,認識Linux核心2020-05-28Linux筆記記憶體
- Linux系統中對中斷的處理(學習筆記)2024-03-26Linux筆記
- Linux系統程式設計之訊號中斷處理(下)2019-09-05Linux程式設計
- Linux系統程式設計之訊號中斷處理(上)2019-09-05Linux程式設計
- PHP 多程式與訊號中斷實現多工常駐記憶體管理【Master/Worker 模型】2019-09-26PHP記憶體AST模型
- PHP 核心 - 異常處理2019-11-22PHP
- Linux核心筆記005 - 越界訪問記憶體,Linux核心處理過程2020-06-06Linux筆記記憶體
- [異常筆記] zookeeper叢集啟動異常: Cannot open channel to 2 at election address ……2020-11-01筆記
- Golang 學習筆記八 錯誤異常2019-01-20Golang筆記
- swoft 學習筆記之異常處理2019-08-13筆記
- 筆記:異常處理之report與render2021-09-04筆記
- 009-時間不同步導致Sentinel監控異常2019-03-07
- Linux(核心剖析):19---中斷總體概述2020-01-13Linux
- Linux 核心處理中斷全過程解析2021-01-12Linux
- 記一次訂單號重複的異常2019-12-05
- 異常監控和判斷2024-07-30
- 中斷的學習筆記2024-10-16筆記
- Java異常處理最佳實踐及陷阱防範2019-04-15Java
- python異常的一些程式碼筆記2024-05-10Python筆記
- Java編譯異常捕捉與上報筆記2023-02-15Java編譯筆記
- SpringMVC學習筆記10-異常處理2020-12-16SpringMVC筆記