# 第十講:中斷處理和現代架構考量 > 本筆記僅為個人紀錄,相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有 * 直播: * ==[Linux 核心設計:中斷處理和現代架構考量 (上) - 2019/1/14](https://youtu.be/EjPWklezNrU)== * ==[Linux 核心設計:中斷處理和現代架構考量 (下) - 2019/2/13](https://youtu.be/8ejnYR7A2Nc)== * 詳細共筆:[Linux 核心設計: 中斷處理和現代架構考量](https://hackmd.io/s/S1WKTCFM4) * 主要參考資料: * [Interrupts in Linux](http://www.cs.columbia.edu/~krj/os/lectures/L07-LinuxEvents.pdf) * [Linux Interrupt Processing and Kernel Thread](https://slideplayer.com/slide/8944476/) * [Making Linux do Hard Real-time](https://www.slideshare.net/jserv/making-linux-do-hard-realtime) * [ARM Interrupt Virtualization](http://events17.linuxfoundation.org/sites/events/files/slides/ARM_Interrupt_Virtualization_Przywara.pdf) * [I/O in Linux Hypervisors and Virtual Machines](https://github.com/fanjinfei/docs/blob/master/vmio_may9_2016.pdf) --- ## 引言 本課深入探討 Linux 核心中最為關鍵且複雜的機制之一:**中斷處理 (Interrupt Handling)**。中斷,這個看似基礎的計算機概論名詞,在現代作業系統核心中,其實涉及了從硬體特性、周邊 I/O、中斷控制器,到排程、任務調度、延遲處理等一系列複雜議題。 隨著處理器架構的飛速演進,特別是多核心處理器 (Multi-core Processor)、虛擬化技術 (Virtualization Technology),以及為了資訊安全而生的隔離執行環境 (如 Arm TrustZone、Intel SGX) 的普及,Linux 的中斷處理機制也經歷了巨大的變革。 本課將從硬體特性出發,解析 Intel 與 Arm 架構的中斷機制,進而探討 Linux 核心如何透過 `softirq`、`tasklet` 與 `workqueue` 實現延遲處理。同時,本課也會分析 `request_threaded_irq` API 的引入,及其與即時核心 (PREEMPT_RT) 的深刻關聯。最終將視野擴展至多核、虛擬化與安全架構下的中斷處理,分析其帶來的全新技術挑戰。 --- ## 中斷處理的核心概念 ### 為何需要中斷? 在電腦系統中,CPU 的運算速度遠遠超過了大部分的周邊設備,例如網路卡、硬碟或鍵盤。如果讓高速的 CPU 持續等待 (輪詢 Polling) 慢速設備完成其工作,將會是巨大的效能浪費。 為了解決此問題,引入了 **Interrupt (中斷)** 機制。 > [!Tip]**核心思想**: > CPU 可以繼續執行自己的任務,而當慢速的周邊設備完成其工作後,主動發出一個信號通知 CPU。CPU 在接收到信號後,可以暫停當前的工作,轉而去處理該周邊設備的事件,處理完畢後再返回原先的任務。 這種機制讓 CPU 的利用率大幅提升,如同一個高效的事件驅動系統。 ### 中斷、例外與陷阱 在探討中斷時,經常會遇到幾個相關詞彙:**Interrupt**、**Exception** 與 **Trap**。 | | Interrupt (中斷) | Exception (例外) | Trap (陷阱) | | :---:| -------- | -------- | -------- | | **常見**<br>**定義** |來自硬體的 **非同步 (Asynchronous)** 事件,這些事件的發生時間點與 CPU 當前執行的指令無關。|由 CPU 執行指令時內部產生的 **同步 (Synchronous)** 事件。|一種特殊的 **Exception**,通常是刻意設計的,用於觸發作業系統的服務 | |**例子** |使用者按下鍵盤、網路卡收到封包 |除以零、存取無效的記憶體位址| 系統呼叫 (System Call)| 不同的處理器架構對這些詞彙的定義和劃分可能有所不同。例如: * Intel x86:有較明確的區分。 * ARM:這些概念趨於一致,Interrupt 常被視為一種特定類型的 Exception。 :::info 本課將重點放在其運作原理,而非嚴格的術語辨析。 ::: ### Linux 核心中的中斷處理流程概覽 當一個硬體中斷發生時,Linux 核心並非一步到位就完成處理,而是遵循一個精密的、分層的流程。這條路徑比想像中要長得多,確保了系統的穩定性與效能。 一個典型的流程如下: 1. **硬體觸發**:外部裝置 (如網卡) 透過 **Interrupt Request Line (IRQ)** 向 **Interrupt Controller (中斷控制器)** 發出信號。 2. **中斷控制器 (Interrupt Controller)**:控制器將信號派送給某個 CPU 核心。 3. **CPU 響應**:CPU 偵測到中斷信號後,會立即改變其處理狀態,切換到專門處理中斷的模式。 4. **進入點 (`entry.s`)**:CPU 會跳轉到一個預先定義好的記憶體位址,這個位址是特定於該 Interrupt Vector 的 ISR (中斷處理常式) 進入點。在 Linux 中,這通常是一段特定於架構的組合語言程式碼,位於 `entry.s` 檔案中。此階段僅僅是整個處理流程的開端。 5. **通用處理層 (`do_irq`)**:`entry.s` 中的程式碼會呼叫一個通用的 C 語言函式 `do_irq()`。此函式負責辨識中斷來源,並進行初步的派送。 6. **事件處理 (`handle_irq_event`)**:`do_irq()` 接著會呼叫 `handle_irq_event()`,這個函式會進一步根據中斷的類型 (例如網路、儲存) 去查找並執行由特定設備驅動程式註冊的 `Interrupt Service Routine (ISR)`。 7. **返回 (`ret_from_intr`)**:在 ISR 執行完畢後,系統會執行另一段組合語言程式碼 `ret_from_intr()`,負責還原 CPU 的狀態,並讓系統返回到被中斷前的執行點。 ![](https://i.imgur.com/YaxH1O1.png) 這個流程確保了中斷處理的模組化與可擴展性,但同時也顯示了其內在的複雜性。 --- ## 硬體層面的中斷機制 (以 Intel x86 為例) 要理解軟體如何處理中斷,必須先了解其硬體基礎。 ### PIC (Programmable Interrupt Controller, 可程式化中斷控制器) 在 **早期的單核心** PC 架構中,Programmable Interrupt Controller (PIC) 扮演了核心角色。典型的設計是使用兩片 Intel 8259 晶片,以主從模式 (Master/Slave) 級聯,管理來自不同硬體設備的 IRQ。 * **功能**:PIC 的主要職責是將來自多個 IRQ 線路的信號進行匯總與優先級排序,然後轉換成一個 CPU 能識別的 Interrupt Vector (中斷向量) 編號,最後再向 CPU 發出單一的中斷信號。 * **通訊**:當 CPU 準備好處理中斷時,它會回覆一個確認 (Acknowledge) 信號給 PIC,PIC 接著會透過資料匯流排將 Interrupt Vector 編號傳送給 CPU。 * **遮罩 (Masking)**:PIC 允許透過程式設計來暫時忽略或「遮罩」特定的 IRQ,使其無法觸發中斷。 ![image](https://hackmd.io/_uploads/SyPwViGrlx.png) ### APIC (Advanced Programmable Interrupt Controller, 進階可程式化中斷控制器) 隨著 **對稱多處理 (Symmetric Multi-Processing, SMP)** 架構的出現,傳統的 PIC 已不敷使用。Intel 引入了 Advanced Programmable Interrupt Controller (APIC) 系統來應對多核心環境的挑戰。 APIC 系統主要由兩部分組成: 1. **I/O APIC**:負責接收來自外部硬體設備的中斷信號。 2. **Local APIC (LAPIC)**:每個 CPU 核心內部都整合了一個 LAPIC。 ![image](https://hackmd.io/_uploads/BJ5dEiMrex.png) APIC 系統的核心功能是 **Interrupt Routing (中斷派送)**: * **作用**:能夠根據設定,將來自 I/O APIC 的中斷請求精準地派送到指定的某個 CPU 核心、某一群核心、甚至是所有的核心。 * **優勢**:在多核心系統中,可以更有效地分配中斷處理負載。 此外,APIC 系統也帶來了 **Inter-Processor Interrupts (IPI)** 機制: * **作用**:CPU 核心之間可以透過 APIC 發送 IPI,允許一個 CPU 核心向另一個或多個核心發送中斷訊號。 * **優勢**:在多核心協同工作 (如任務排程、快取同步) 中至關重要。 ### 可遮罩中斷 (Maskable Interrupts) 與 非可遮罩中斷 (Non-Maskable Interrupts, NMI) 除了來源與派送方式,中斷本身根據其 **可否被暫時忽略** 的特性,可分為兩大類: * **可遮罩中斷 (Maskable Interrupts)**:系統中最常見的中斷類型,幾乎所有來自周邊裝置的 IRQ 都屬於此類。 * **做法**:CPU 可以透過執行特定指令 (如 x86 的 `cli` 指令) 來暫時 **遮罩/關閉** 或忽略這些中斷。 * **目的**:保護核心中的 **Critical Section**,確保在執行不可分割的關鍵操作時,不會被其他中斷打斷,從而避免資料競爭與不一致的狀態。 * **不可遮罩中斷 (Non-Maskable Interrupts, NMI)**:一種無法被 CPU 遮罩的最高優先級中斷。 * **目的**:通常保留給最緊急的系統事件,例如嚴重的硬體故障 (如記憶體同位檢查錯誤 Parity Error)。 * **實務案例**:許多伺服器主機板上會配備一個 **NMI 按鈕**。當系統完全死當、無法回應任何操作時,管理者可以手動觸發 NMI。對應的 NMI 處理常式被設計用來將當前的記憶體狀態傾印 (Dump) 到磁碟中,以便事後進行根本原因分析 (Root Cause Analysis),這對於診斷系統崩潰至關重要。 ### IRQ (Interrupt Request Line, 中斷請求) 與 Interrupt vector (中斷向量) * **IRQ**:硬體設備用來發出中斷請求的 **物理或邏輯線路**。在現代系統中,多個設備可能共享同一個 IRQ。 * **Interrupt vector**:是一個範圍在 0-255 的數字,作為 **Interrupt Descriptor Table (IDT)** 的索引。CPU 透過這個向量來找到對應的 **Interrupt Service Routine (ISR, 中斷處理常式)**。在 Linux 中,為了與 **Exception** 區分,硬體 IRQ 通常會被對應到 32 以上的向量編號,因為前 32 個向量保留給了 CPU內部的 **Exception** 和 **Trap**。 ### IDT (Interrupt Descriptor Table, 中斷描述符表) **IDT** 是 x86 架構中一個至關重要的 **資料結構**。它是一個由 256 個條目組成的表格,每個條目都對應一個 Interrupt Vector。當 CPU 接收到一個 Interrupt Vector 時,它會使用這個向量作為索引在 IDT 中查找對應的條目。 <center><img src="https://hackmd.io/_uploads/rkA54jMrgg.png" alt="image" width="80%" /></center> 每個 IDT 條目 (稱為門描述符,Gate Descriptor) 包含了以下關鍵資訊: * **處理常式位址**:指向該 ISR 在記憶體中的進入點位址。 * **權限等級 (Privilege Level)**:定義了允許觸發此中斷所需的權限等級。 * **類型**:區分是中斷門 (Interrupt Gate)、陷阱門 (Trap Gate) 還是任務門 (Task Gate)。例如,透過 Interrupt Gate 觸發的 ISR 在執行期間會自動禁止其他中斷,而 Trap Gate 則不會。 作業系統在啟動階段必須初始化 IDT,將每個需要處理的中斷向量都填上對應的 **處理常式位址** 和 **屬性**。 --- ## Linux 的中斷處理分層機制 為了兼顧 **回應速度** 與 **系統整體效能**,Linux 將中斷處理巧妙地拆分為兩個部分:上半部與下半部,這個劃分是 Linux 中斷處理的精髓所在。 ### 上半部 (Top-half) 與下半部 (Bottom-half) * **上半部 (Top-half) / 硬中斷 (Hard IRQ)**: * **職責**: * 直接回應硬體中斷事件。 * 執行最緊急、具有高度時間敏感性的任務 (例如,讀取暫存器、確認中斷 / 回應中斷控制器、重設硬體)。 * 將需要後續處理的耗時工作「排程」到 Bottom-half。 * **特性**: * **執行速度**:必須 **盡可能快速完成**,以最短時間釋放 CPU。 * **中斷遮蔽**:執行時通常會 **禁用 (disable / mask)** 部分或全部其他中斷,以避免 Nested Interrupt (巢狀中斷) 導致的複雜性,因此執行速度極快。 * **執行環境**:在 **中斷上下文 (Interrupt Context)** 中執行。這意味著它 **不能休眠 (sleep) 或阻塞 (block)** 等任何可能導致排程的操作,因為它不與任何特定的 user process 相關聯。 * **下半部 (Bottom-half) / 軟中斷 (Soft IRQ)**: * **職責**:執行那些可以被延後處理的、較為耗時的任務 (例如,處理網路卡收到的完整封包、將鍵盤輸入的資料複製到 user space 的緩衝區等)。 * **特性**: * **可延遲執行 (Deferred Execution)**:可以 **晚一點再做**。上半部完成後,會將下半部的任務排入佇列,等待核心在適當時機 (例如:從中斷返回前、系統較不繁忙時) 執行。 * **可排程**:由於執行時間可以延後,因此可以被重新排程,並與系統中的其他任務競爭 CPU 時間。 > [!Tip] > 所謂 **軟中斷 (Soft IRQ)**,soft 在核心中通常代表時間點上具有彈性、可被排程。 * **允許中斷**:下半部執行時,中斷通常是開啟的,允許系統回應其他更高優先級的硬體事件 (包括新的 Hard IRQ)。 ![image](https://hackmd.io/_uploads/Bys34ofreg.png) #### 實務案例:鍵盤輸入處理流程 1. **使用者按下按鍵**:硬體觸發一個中斷。 2. **Top-Half (上半部) 執行**: * ISR 被觸發,立即在「中斷上下文」中執行。 * 單純、快速的任務:從鍵盤控制器讀取 **掃描碼 (Scan Code)**,以識別是哪個按鍵被按下。 * 讀取完畢後,上半部的工作就結束了,並會排定一個下半部任務。 3. **Bottom-Half (下半部) 執行**: * 在稍後的某個時間點,核心排程並執行這個下半部任務。 * 處理更複雜邏輯的任務,例如: * 解析 Scan Code,判斷是單一按鍵還是組合鍵 (如 Ctrl+C)。 * 將對應的字元複製到終端機的 buffer。 * 喚醒正在等待輸入的 user process。 透過這種方式,Linux 核心可以快速地響應硬體事件 (在 Top-half 中讀取 Scan Code),同時將大部分的處理負擔 (解析、複製、喚醒行程) 轉移到 Bottom-half,從而 **將中斷延遲 (Interrupt Latency) 降至最低**,提升了系統的整體吞吐量和響應性。 > [!Note] 延伸說明:中斷 (Interrupt) 與 搶佔式上下文切換 (Preemptive Context Switch) > 上述案例中「喚醒 user process」這個動作,解釋了 **Interrupt** 與 **Preemptive Context Switch** 的密切關係。可以說,硬體中斷 (原因) 是觸發搶佔行為 (結果) 最主要的機制。在這個過程中: > * **Interrupt** 扮演了 **催化劑** 的角色,強制中斷了當前任務。 > * **ISR** 扮演了 **狀態改變者** 的角色,讓更高優先級的任務準備就緒。 > * **排程器** 扮演了 **決策者** 的角色,最終執行了搶佔。 > > 這個機制確保了系統能及時響應重要事件,讓高優先級的任務得以插隊執行,是現代作業系統響應性的基石。 > ![image](https://hackmd.io/_uploads/rJjMHjfSll.png) ### Bottom-half 的實作機制 早期的 Linux kernel 只有一個 bottom half 機制,簡單好用,但性能不佳。現在的 Linux kernel 提供了三種 bottom half 的機制:**Softirq**、**Tasklet**、**Workqueue**,來應對不同的需求。 ### 1. Softirq **Softirq (軟中斷)** 是 Bottom-half 機制中最底層、效能最高的實作。 * **靜態定義**:系統中 Softirq 的類型在編譯時期就已靜態定義好,數量有限。 `include/linux/interrupt.h` ```c=547 enum { HI_SOFTIRQ=0, // Tasklet 高優先級 TIMER_SOFTIRQ, // 用於 Timer NET_TX_SOFTIRQ, // 用於網路發送 NET_RX_SOFTIRQ, // (同上) BLOCK_SOFTIRQ, // 用於 block device IRQ_POLL_SOFTIRQ, // (同上) TASKLET_SOFTIRQ, // Tasklet 普通優先級 SCHED_SOFTIRQ, // 用於 scheduler HRTIMER_SOFTIRQ, // 用於 高解析度 Timer RCU_SOFTIRQ, // 用於 read-copy-update (RCU) NR_SOFTIRQS // = 10 }; ``` * **並行執行**:同一種類型的 Softirq 可以在多個 CPU 核心上並行執行,這要求其處理函式必須是 **可重入的 (re-entrant)**,並且需要自行處理鎖定 (locking) 以保護共享資料。 `include/linux/interrupt.h` ```c=587 struct softirq_action { void (*action)(void); }; ``` `kernel/softirq.c` ```c=60 static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; ``` ```c=536 static void handle_softirqs(bool ksirqd) { ... struct softirq_action *h; ... restart: ... h = softirq_vec; while (...) { ... h->action(); ... ``` * **觸發與執行**:Softirq 通常由 Top-half 透過 `raise_softirq()` 觸發。核心會在多個時間點檢查並執行待處理的 Softirq,最常見的時間點是在 Hard IRQ 處理完畢返回前。 * **ksoftirqd**: * **目的**:為了防止 Softirq 獨佔 CPU (使 user process 飢餓),如果 Softirq 處理負擔過重,核心會喚醒一個名為 `ksoftirqd` 的 Kernel Thread。 * **作法**:每個 CPU 核心都有一個對應的 `ksoftirqd` 執行緒,該 thread 會以 **普通行程的優先級** 被排程,來處理剩餘的 Softirq,從而保證系統的公平性。 :::info 從 **高優先的 Interrupt Context** 變成 **低優先的 Process Context**。 ::: `kernel/softirq.c` ```c=960 static void run_ksoftirqd(unsigned int cpu) { ksoftirqd_run_begin(); // local_irq_disable(),禁用本地中斷 if (local_softirq_pending()) { // 檢查當前 CPU 是否有待處理的軟中斷 /** * 處理一批待處理的軟中斷。 * 此函式有內建的「剎車機制」(時間/次數限制 + 檢查重新排程的需求), * 在高負載下會提前返回,不會執行完「所有」工作。 */ handle_softirqs(true); ksoftirqd_run_end(); // local_irq_enable(),重新啟用本地中斷 cond_resched(); // 檢查是否需要排程,避免 ksoftirqd 霸佔 CPU return; } /* 沒有 pending 的軟中斷 */ ksoftirqd_run_end(); // local_irq_enable(),恢復本地中斷並結束 } ``` ### 2. Tasklet Tasklet 是基於 Softirq 實現的一種更為常用和易用的 Bottom-half 機制。 * **動態建立**:Tasklet 可以動態地建立和銷毀。 `include/linux/interrupt.h` * **建立**: ```c=701 #define DECLARE_TASKLET(name, _callback) \ struct tasklet_struct name = { \ .count = ATOMIC_INIT(0), \ .callback = _callback, \ .use_callback = true, \ } ``` * **排程**: ```c=755 static inline void tasklet_schedule(struct tasklet_struct *t) {...} ... static inline void tasklet_hi_schedule(struct tasklet_struct *t) {...} ``` * **Enable / Disable**: ```c=786 static inline void tasklet_disable(struct tasklet_struct *t) {...} ... static inline void tasklet_enable(struct tasklet_struct *t) {...} ``` * **串行執行**:與 Softirq 不同,**同一種類型的 Tasklet** 在任何時候都 **只能在一個 CPU 核心上執行**,不能並行。不同類型的 Tasklet 則可以在不同 CPU 核心上並行執行。 :::success **優勢**:大大簡化了驅動程式的撰寫,因為開發者不需要擔心 Tasklet 函式的可重入問題。 ::: * **實現**:內部上,Tasklet 使用了兩種 Softirq:`TASKLET_SOFTIRQ` (普通優先級) 和 `HI_SOFTIRQ` (高優先級)。 ![image](https://hackmd.io/_uploads/ryRXBiMrxg.png) `kernel/softirq.c` ```c=940 void __init softirq_init(void) { int cpu; for_each_possible_cpu(cpu) { ... } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); } ``` > 延伸閱讀: > * [linux kernel 的中斷子系統之 (八) :softirq](http://www.wowotech.net/linux_kenrel/soft-irq.html) > * [linux kernel 的中斷子系統之 (九) :tasklet](http://www.wowotech.net/irq_subsystem/tasklet.html) ### 3. Workqueue Workqueue 是最強大也最複雜的 Bottom-half 機制。它將延遲的工作交由一個通用的 kernel thread pool 來執行。 * **屬於 Process Context**:與 Softirq 和 Tasklet 在 Interrupt Context 中執行不同,Workqueue 的處理函式在 **Process Context (per-CPU kernel thread)** 中執行。 `include/linux/workqueue.h` * **編譯時期,靜態宣告 work**: ```c=250 #define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f) ``` * **Run time,動態初始化 work**: ```c=291 #define INIT_WORK(_work, _func) \ __INIT_WORK((_work), (_func), 0) ``` * **分配 workqueue 空間**: ```c=567 #define create_workqueue(name) \ alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name)) ``` * **將 work 加入 workqueue (scheduling)**: ```c=586 static inline bool queue_work(struct workqueue_struct *wq, struct work_struct *work) { return queue_work_on(WORK_CPU_UNBOUND, wq, work); } ... static inline bool schedule_work(struct work_struct *work) { return queue_work(system_wq, work); } ``` > `WORK_CPU_UNBOUND` 表示該 kernel thread 不限定在哪個 CPU 中被執行 * **清空 workqueue**:安全地清空 work queue,所有目前待處理的 work 都會先完成。用於 **模組卸載** 或 **關閉裝置** 之前 (在編譯時偵測,在 runtime 時警告)。 ```c=769 #define flush_workqueue(wq) \ ({ \ struct workqueue_struct *_wq = (wq); \ if (...) \ __warn_flushing_systemwide_wq(); \ __flush_workqueue(_wq); \ }) ``` * **銷毀 workqueue**:安全地清空 work queue 並釋放資源,所有目前待處理的 work 都會先完成 (會呼叫 `__flush_workqueue()`)。用於 **不再需要這個 workqueue** 時。 ```c=578 extern void destroy_workqueue(struct workqueue_struct *wq); ``` * **可以休眠**:這是 Workqueue 最顯著的優勢。由於在 Process Context 中執行,它的 **處理函式可以被排程**,因此允許執行 **阻塞操作 (blocking operations)**,例如請求記憶體分配、獲取 `semaphore` 或進行耗時的 I/O。 `include/linux/workqueue.h` ```c=146 struct workqueue_attrs { int nice; // Nice 值,用於排程 ... ``` * **使用場景**:當延遲處理的任務非常複雜,特別是需要休眠時,Workqueue 是唯一的選擇。 > 參照: > * [第六講:不僅是個執行單元的 Process - Work Queue (工作佇列)](https://hackmd.io/@Jaychao2099/Linux-kernel-6#Work-Queue-%E5%B7%A5%E4%BD%9C%E4%BD%87%E5%88%97) > * [Details of the workqueue interface - LWN](https://lwn.net/Articles/11360/) > * [Linux Interrupt Processing and Kernel Thread](http://rts.lab.asu.edu/web_438/CSE438_598_slides_yhlee/438_7_Linux_ISR.pdf) ### 三者的比較 | 特性 | ISR<br>(Hard IRQ<br>/ Top-half) | Softirq | Tasklet | Workqueue<br>/ KThread | | :---: | :---: | :---: | :---: | :---: | | **執行上下文** | 中斷上下文 | 中斷上下文 | 中斷上下文 | **行程上下文** | | **是否可休眠 (Sleepable)** | ❌ | ❌ | ❌ | ✅ | | **單一 CPU 上的行為**<br>(同一類型是否可重入) | ❌<br>**序列化** | ❌<br>**序列化** | ❌<br>**序列化** | ✅<br>**可搶佔** | | **跨 CPU 的並行性**<br>(同一類型是否可並行) | ─ | ✅<br>**跨 CPU 可並行** | ❌<br>**跨 CPU 亦序列化** | ✅<br>**跨 CPU 可並行** | | **主要應用場景** | 立即、極速的<br>硬體響應 | 高吞吐量的<br>延遲任務<br>(如:網路、block device) | 大多數驅動中的<br>延遲任務<br>(易用性優先) | 需休眠<br>或<br>與 user space 互動的<br>複雜延遲任務 | --- ## 現代架構考量 (一):多核心 與 即時性 (Real-Time) 傳統的 Linux 核心設計優先考慮的是 **吞吐量 (Throughput)**,而非 **即時性 (Real-Time)**。然而,隨著應用場景的擴展 (如電信、工業控制),對低延遲和可預測性的要求越來越高。 ### 延遲 (Latency) 的來源 在即時系統中,**Latency** 是指從事件發生 (如硬體中斷) 到系統對其做出反應 (如高優先級任務開始執行) 所經過的時間。Linux 核心中主要的延遲來源如下: 1. **中斷延遲 (Interrupt Latency)** * **定義**:從硬體發出中斷訊號,到核心開始執行對應的中斷服務常式 (ISR) 的時間差。 * **來源**: * **核心程式碼關閉中斷**:為了保護臨界區 (Critical Section),核心的某些部分 (例如在獲取 Spinlock 時) 會暫時關閉中斷 (`local_irq_disable()`),這會延遲後續所有中斷的處理。這是造成延遲最主要且最難預測的因素之一。 * **更高優先級的中斷正在執行**:如果一個更高優先級的中斷正在被處理,那麼較低優先級的中斷就必須等待。 2. **中斷處理器執行時間 (Interrupt Handler Duration)** * **定義**:ISR 本身的執行時間,即 **Top-half** 與 **Bottom-half**。 3. **排程器延遲與執行時間 (Scheduler Latency & Duration)** * **定義**:從一個高優先級任務變為可執行狀態 (runnable),到排程器真正將它放到 CPU 上執行之間的時間。 * **來源**: * **返回 Interrupt Context 的時機**:如果中斷發生時,系統正在執行 user space 的程式,排程器會立即執行。若正在執行 syscall,則必須等待該 syscall 執行完畢才能進行排程。 * **排程器本身的執行時間**:選擇最佳任務並執行 Context Switch。 4. **其他系統層級的延遲來源** * **核心可搶佔性 (Kernel Preemptibility)**:在傳統或未經即時化配置的核心中,一旦程式碼進入核心模式 (例如執行 syscall),它就不能被其他任務搶佔,直到它主動放棄 CPU 或返回 user space。一個執行時間很長的 syscall 會嚴重影響高優先級任務的響應時間。 * **記憶體管理 (Memory Management)**: * **請求分頁 (Demand Paging)**:當程式存取一個尚未載入到 RAM 的 page 時,會觸發 Page Fault,核心需要從速度較慢的 disk 中讀取該 page,造成不可預期的顯著延遲。 * **動態記憶體配置 (Dynamic Memory Allocation)**:核心的記憶體配置器 (如 `kmalloc()`) 雖然速度快,但並不保證在固定的時間內完成,也可能成為延遲的來源。 ![image](https://hackmd.io/_uploads/ByJrBjMBee.png) ### 完全可搶佔核心 (PREEMPT_RT) 為了將 Linux 轉變為一個具備 **硬即時 (Hard Real-Time)** 能力的系統,社群發展了 `PREEMPT_RT` 修補程式集。其核心目標是讓核心在 **幾乎任何地方** 都是可搶佔的,從而將延遲降至最低。 `PREEMPT_RT` 帶來了幾個根本性的改變: * **可休眠的 Spinlock**:將大部分的 **Spinlock 替換為可休眠的 Mutex**。這意味著當一個 thread 試圖獲取一個已被佔用的鎖時,它不會再空轉等待,而是會進入休眠,讓出 CPU 給其他任務。 * **Threaded IRQs**:將絕大多數的 Hard IRQ 處理常式 (上半部) 和所有的 Softirq (下半部) 都轉變為 **獨立的 kernel thread**。 ### Threaded IRQs 這是 `PREEMPT_RT` 最重要的變革。將 ISR 變為 kernel thread 後,它們就納入了核心的統一排程管理。 * **可排程與可搶佔**:每個中斷處理 thread 都有自己的優先級。一個高優先級的使用者任務可以搶佔一個正在執行的低優先級中斷處理 thread。 * **延遲降低**:Interrupt Context 的優先級不再是絕對高於所有 Process Context。一個硬體中斷觸發後,核心僅在一個極短的、不可搶佔的 Top-half 中喚醒對應的 Interrupt thread,然後就可以立即進行排程。如果此時有更高優先級的任務存在,該任務會先被執行,**中斷處理反而會被延後**。 這種設計顛覆了傳統的中斷處理模型,極大地降低了高優先級任務的排程延遲,使其具備了可預測性。 ![image](https://hackmd.io/_uploads/By4roQbSgx.png) ### API 的演進:request_irq 與 request_threaded_irq 為了支持 `Threaded IRQ`,核心引入了新的 API `request_threaded_irq()`。 * 傳統的中斷註冊 API ```c int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); ``` * 支援 Threaded IRQs 的新 API `kernel/irq/manage.c` ```c int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id); ``` 使用 `request_threaded_irq()` 時,可以分別提供一個 `handler` 函式 (作為快速的 Top-half) 和一個 `thread_fn` 函式 (在獨立的 kernel thread 中執行)。在 `PREEMPT_RT` 核心中,即使是呼叫舊的 `request_irq()`,其底層也大多被轉換為 `Threaded IRQ` 來執行。 > 延伸閱讀: > * [第十八講:PREEMPT_RT 作為邁向硬即時作業系統的機制](https://hackmd.io/@Jaychao2099/Linux-kernel-18) > * [Making Linux do Hard Real-time](https://www.slideshare.net/jserv/making-linux-do-hard-realtime) --- ## 現代架構考量 (二):Tickless Kernel 傳統作業系統依賴一個固定週期的 **計時器中斷 (Timer Interrupt)** 來驅動排程、計時等活動。這個固定的時間間隔被稱為 **Tick**。 例如,一個被設定為每 10ms 觸發一次中斷的系統,會利用這個固定的 Tick 來更新系統時間、檢查是否有任務的時間配額 (Time Slice) 已用完,進而觸發排程器 (Scheduler) 的運作。 然而,在系統閒置時,這種週期性的喚醒會造成不必要的能源消耗,並在高效能運算 (High-Performance Computing, HPC) 場景中引入不必要的干擾 (`Jitter`)。 ### Tickless Kernel (`CONFIG_NO_HZ`) * **核心思想**:當系統閒置時,取消週期性的 Timer Interrupt。 Tickless 並非完全沒有 Tick,而是 **不再依賴一個固定週期的 Tick**。 * **運作方式**:系統會計算下一個需要觸發的事件 (如下一個計時器到期) 在何時發生,並動態地將下一次 Timer Interrupt 設定在那個確切的時間點。 在此之前,CPU 可以進入更深度的省電狀態,不被無意義的 Tick 喚醒。 這種運作模式是**事件驅動 (Event-driven)** 的,只有當真正有事件(如外部中斷、計時器到期)需要處理時,核心才會被喚醒。 > **比喻**:傳統的 Tick 機制就像一個人即使想睡覺,也設定了每一分鐘響一次的鬧鐘,每次被叫醒後發現無事可做又馬上睡回去,非常耗費精力。 Tickless 機制則像是這個人只在真正需要起床的時間點設定一次鬧鐘,在那之前都能安心睡眠。 ### Tickless 的優勢與應用場景 1. **省電 (適用於行動與物聯網裝置)** * 對於手機、平板、物聯網 (IoT) 等消費性電子產品,Tickless 機制可以顯著減少 CPU 在閒置狀態下的喚醒次數,從而降低功耗,延長電池續航力。 * **限制**:雖然 Tickless 在理論上能省電,但由於 Linux 核心內部還有其他複雜的考量,**實際節省的電力可能相對有限**,但依然能降低一部分功耗。 2. **提升效能與降低延遲 (適用於伺服器與 HPC)** * **減少中斷干擾**:在伺服器和高效能運算環境中,Tickless 的主要好處是 **降低中斷的總量**,避免了不必要的 Timer Interrupt。 這使得需要大量運算的任務 (如科學運算、資料庫查詢) 可以更長時間地獨佔 CPU,不會頻繁被中斷打擾,進而避免了快取被污染 (Cache Pollution) 和 Context Switch 的開銷。 * **避免中斷碰撞**:透過減少 Timer Interrupt 的發生,可以降低它與其他重要 I/O 中斷 (如網路、儲存) 發生碰撞的機率。 * **降低虛擬化成本**:Tickless 同時也能降低虛擬化環境下的效能開銷。 > 延伸閱讀:[第十一講:Timer 及其管理機制 - Tickless Kernel (動態 Tick)](https://hackmd.io/@Jaychao2099/Linux-kernel-11#Tickless-Kernel-%E5%8B%95%E6%85%8B-Tick) --- ## 現代架構考量 (三):虛擬化的影響 虛擬化技術允許在單一物理硬體上同時運行多個作業系統 (稱為 Guest OS)。這對中斷處理帶來了全新的挑戰,因為物理中斷源只有一套,但卻需要被派送給多個不同的 Guest OS。 ### 為何 I/O 虛擬化是個難題? * **設備共享**:像網卡、磁碟控制器這樣的物理設備是唯一的,Hypervisor (或 Virtual Machine Monitor, VMM) 必須攔截 Guest OS 的所有 I/O 請求,並進行 **裁決、模擬和轉發**。 * **中斷虛擬化**:當一個物理中斷發生時,Hypervisor 必須決定這個中斷應該被投遞給哪一個 Guest OS,甚至可能需要同時通知多個 Guest。這個過程引入了顯著的 VM-Exit (從 Guest 切換到 Hypervisor) 開銷,嚴重影響效能。 ![image](https://hackmd.io/_uploads/rk7J0N-Sll.png) ### 硬體輔助 * **ARM 的通用中斷控制器 (GIC)** ARM Generic Interrupt Controller (GIC) 是 ARM 體系結構中用於管理中斷的標準組件,特別是 `GICv2` 及之後的版本,為了多核心和虛擬化進行了專門設計。 * **Distributor**:GIC 的核心組件,負責從所有來源接收中斷,並根據設定將它們路由到一個或多個 CPU 核心。 * **CPU Interface**:每個 CPU 核心的介面,用於與 Distributor 通訊。<center><img src="https://hackmd.io/_uploads/HkNaYVWHxe.png" alt="image" width="70%" /></center> > [!Note] GIC 與安全架構:Arm TrustZone 的角色 > GIC 的 Interrupt Routing 能力不僅用於多核心與虛擬化,在 **安全架構** 中也至關重要。Arm TrustZone 技術將處理器劃分為兩個世界:**安全世界 (Secure World)** 和 **普通世界 (Normal World)**。 > > * **中斷隔離**:GIC 必須確保來自安全周邊 (如指紋感測器、加密加速器) 的 **安全中斷 (Secure Interrupts)**,只能被 routing 到在安全世界中運行的程式碼進行處理,而普通世界的作業系統 (如 Linux) 完全無法窺探或干擾這些中斷。 > * **安全保障**:這種 **硬體層級** 的隔離,確保了如行動支付、數位版權管理 (DRM) 等敏感操作的中斷處理流程不會被惡意軟體洩漏或竄改,是建構可信執行環境 (TEE) 的基礎。 * **GIC 中的虛擬化支援** 從 `GICv2` 開始,引入了對中斷虛擬化的硬體支援。 * **Virtual CPU Interface**:GIC 為 Hypervisor 提供了一個虛擬 CPU 介面。Hypervisor 可以攔截物理中斷,然後透過寫入特定的暫存器,將一個 **虛擬中斷** 注入到指定的 Guest OS 中。 * **List Registers**:Hypervisor 可以將待處理的虛擬中斷排入一個由硬體維護的列表中。 * **降低 VM-Exit**:Guest OS 在處理虛擬中斷時,可以直接與虛擬 CPU 介面互動,完成大部分操作,而無需每次都 VM-Exit 到 Hypervisor,從而大幅提升了效能。`GICv3` 和 `GICv4` 在此基礎上做了進一步的增強。 > 延伸閱讀: > * [ARM Interrupt Virtualization](http://events17.linuxfoundation.org/sites/events/files/slides/ARM_Interrupt_Virtualization_Przywara.pdf) > * [簡介 GIC-400 (GIC v2)](https://justinchunotes.blogspot.com/2017/12/gic-400.html) ### KVM 與 VirtIO * **KVM (Kernel-based Virtual Machine)**:是 Linux 核心內建的 Hypervisor。它利用了處理器的硬體虛擬化擴展 (如 Intel VT-x, AMD-V),並 **將每個 Guest OS 作為一個標準的 Linux 行程來管理**。 <center><img src="https://hackmd.io/_uploads/B1hMa4-Hlx.png" alt="image" width="60%" /></center> * **VirtIO**:為了解決 I/O 虛擬化的效能瓶頸,開發了一套名為 VirtIO 的標準化 **半虛擬化 (Para-virtualization)** 驅動框架。它定義了一套通用的前端驅動 (在 Guest 中) 和後端驅動 (在 Host 的 Hypervisor 中,如 QEMU) **之間的通訊協議**。透過 VirtIO,Guest 和 Host 可以透過高效的共享記憶體環形緩衝區 (`vring`) 直接交換 I/O 請求,繞過了大部分耗時的設備模擬過程,從而獲得接近原生的 I/O 效能。 <center><img src="https://hackmd.io/_uploads/BkxFREZSll.png" alt="image" width="80%" /></center> <center><img src="https://hackmd.io/_uploads/SkWrkH-Blg.png" alt="image" width="80%" /></center> > 延伸閱讀: > * [I/O in Linux Hypervisors and Virtual Machines](https://github.com/fanjinfei/docs/blob/master/vmio_may9_2016.pdf) > * [第十七講:KVM: 虛擬化基礎建設](https://hackmd.io/@Jaychao2099/Linux-kernel-17) --- ## 實驗與動態分析 理論知識需要透過實踐來驗證。 ### 觀察中斷: Linux 核心透過 `/proc` 這個虛擬檔案系統向 user space 暴露了大量的狀態資訊。 * **`/proc/interrupts`**:這個檔案顯示了系統中每個 IRQ 線路被觸發的次數,以及它們在每個 CPU 核心上的分佈情況。這是觀察 **中斷負載均衡** 和 **IRQ affinity** 的關鍵工具。 * **負載不均是正常現象**:某些核心 (如 CPU0) 可能會因為處理較多系統服務而有頻繁的 Timer Interrupt,其計數值會快速增長。 * **閒置核心計數低**:其他閒置的核心,因為進入了 Tickless 狀態,其 Timer Interrupt 計數值會增長得非常緩慢,甚至長時間不變。 > [!Tip] > 此現象體現了 Tickless 的核心價值:讓系統可以將特定核心 **隔離 (Partitioning)** 出來,專門執行需要長時間、不受干擾的運算任務,這對於 CPU Affinity (處理器親和性) 的應用場景尤其重要。 ```bash $ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 0: 16 0 0 0 IO-APIC 2-edge timer 1: 0 1234 0 0 IO-APIC 1-edge i8042 ... NMI: 0 0 0 0 Non-maskable interrupts LOC: 5648943 5648830 5648798 5648812 Local timer interrupts IPI: ... ``` * **`/proc/stat`**:提供了更為全面的 CPU 活動統計,其中 `intr` 行顯示了自系統啟動以來處理的中斷總數。 * **`mpstat` 工具**:一個更強大的多核心處理器監控工具。透過 `mpstat`,可以觀察到更細緻的即時數據。 * **指令**:執行 `mpstat -P ALL 1` 可以每秒更新一次所有 CPU 核心的狀態。 * **觀察重點**:在輸出的 `intr/s` 欄位中,可以看到每個核心 **每秒處理的中斷次數**。這對於分析高負載下的中斷風暴 (Interrupt Storm) 或觀察特定核心的中斷處理壓力非常有用。 ```bash $ mpstat -P ALL 1 Linux 6.6.87.1 ... Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle Average: all 0.00 0.00 0.08 0.00 0.00 0.00 0.00 0.00 0.00 99.92 Average: 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 2 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 Average: 4 0.00 0.00 0.33 0.00 0.00 0.00 0.00 0.00 0.00 99.67 ... ``` ### 撰寫一個簡單的字元驅動程式來處理中斷 * [ ] todo: 自己操作一次 理解中斷處理最好的方法就是親手編寫一個設備驅動程式。 1. **註冊字元設備**:使用 `register_chrdev` 或較新的 `alloc_chrdev_region` API 來向核心註冊一個字元設備,並提供一個 `file_operations` 結構來定義讀、寫、打開等操作。 2. **請求 IRQ**:使用 `request_irq` 或 `request_threaded_irq` 函式來為設備註冊一個 ISR。 ```c // 範例:在模組初始化函式中請求 IRQ static irqreturn_t my_interrupt_handler(int irq, void *dev_id) { // 快速處理上半部邏輯 printk(KERN_INFO "Interrupt %d occurred!\n", irq); // 返回 IRQ_HANDLED 表示中斷已被處理 return IRQ_HANDLED; } static int __init my_driver_init(void) { // 假設設備使用 IRQ 12 if (request_irq(12, my_interrupt_handler, IRQF_SHARED, "my_device", THIS_MODULE)) { pr_err("Failed to request IRQ 12\n"); return -EBUSY; } return 0; } ``` 3. **釋放 IRQ**:在模組卸載時,務必使用 `free_irq` 函式來釋放之前請求的中斷線路。 透過編譯、載入這個模組,並觸發對應的硬體事件,就可以透過 `dmesg` 命令觀察到 `printk` 的輸出,從而驗證 ISR 是否被成功調用。 > 延伸閱讀: > * [I/O access and Interrupts 實驗](https://linux-kernel-labs.github.io/refs/heads/master/labs/interrupts.html) > * [Kernel 4.10 Examples of linux drivers](https://github.com/rrmhearts/linux-driver-examples) ### eBPF (extended Berkeley Packet Filter): 一種革命性的核心內追蹤技術。它允許使用者將一小段安全的、事件驅動的程式碼注入到運作中的核心裡,以極低的效能開銷收集詳細的執行數據。搭配 `kprobes`(動態追蹤核心函式)等工具,可以精準地觀察中斷處理路徑上的每一個細節,而無需重新編譯核心。 > 延伸閱讀: > * [IRQs: the Hard, the Soft, the Threaded and the Preemptible](https://she-devel.com/Chaiken_ELCE2016.pdf) 後段 > * [第四講:透過 eBPF 觀察作業系統行為](https://hackmd.io/@Jaychao2099/Linux-kernel-4) ## 總結 Linux 的中斷處理是一個從硬體到軟體、從底層到高層的複雜協同系統。它隨著 CPU 架構的發展而不斷演進,以應對多核心、即時性和虛擬化帶來的挑戰。理解其分層設計 (上半部/下半部) 、延遲處理機制 (softirq, tasklet, workqueue),以及現代架構下的重大變革 (如 `PREEMPT_RT` 和中斷虛擬化),是深入掌握 Linux 核心運作原理的必經之路。 > 延伸閱讀: > * [Interrupts and Interrupt Handling](https://0xax.gitbooks.io/linux-insides/content/Interrupts/) part 1 ~ 10 > * [linux kernel 的中斷子系統](http://www.wowotech.net/irq_subsystem/interrupt_subsystem_architecture.html) (一) ~ (九) --- 回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel)