本文轉自博客:https://www.cnblogs.com/sky-heaven/p/5640961.html
目錄
- 爲什麼要有中斷
- 中斷的作用
- 中斷的處理原則
- Linux 中斷機制
- 中斷控制器
- 中斷描述符
- 中斷數據結構
- 中斷的初始化
- 內核接口
- 中斷處理過程
- CPU 的中斷處理流程
- 保存中斷信息
- 處理中斷
- 從中斷中返回
- 編寫中斷處理程序
- 軟中斷、tasklet與工作隊列
- 上半部與下半部
- 軟中斷
- tasklet
- 工作隊列
1 爲什麼要有中斷
1.1 中斷的作用
處理器的運算速度一般要比外部硬件快很多。以讀取硬盤爲例,如果是簡單的順序執行,CPU 必須等待很長時間,不停地輪詢硬盤是否讀取完畢,這會浪費很多 CPU 時間。中斷提供了這樣一種機制,使得讀取硬盤這樣的操作可以交給硬件來完成,CPU 掛起當前進程,將控制權轉交給其他進程,待硬件處理完畢後通知 CPU,操作系統把當前進程設爲活動的,從而允許該進程繼續執行,處理讀取硬盤的結果。
另一方面,有些事件不是程序本身可預見的,需要硬件以某種方式告訴進程。例如時鐘中斷爲定時器提供了基礎,如果沒有時鐘中斷,程序只能每執行幾條指令就檢查一下當前系統時間,這在效率上是不可接受的。
從廣義上說,中斷是改變 CPU 處理指令順序的硬件信號。分爲兩類:
- 異步的:在程序執行的任何時刻都可能產生,如時鐘中斷
- 同步的:在特殊或錯誤指令執行時由 CPU 控制單元產生,稱爲異常
1.2 中斷的處理原則
中斷處理的基本原則就是“快”。如果反應慢了,數據可能丟失或被覆蓋。例如鍵盤按鍵中斷,所按下的鍵的 keycode 放在 KBDR 寄存器中,如果在中斷被處理之前用戶又按了一個鍵,則 KBDR 的值被新按下的鍵的 keycode 覆蓋,早先按下的鍵對應的數據就丟失了。
當一箇中斷信號到達時,CPU 必須停止當前所做的事,轉而處理中斷信號。爲了儘快處理中斷併爲接收下一個中斷做好準備,內核應儘快處理完一箇中斷,將更多的處理向後推遲。
爲達到“快”這一目標,內核允許不同類型的中斷嵌套發生,即在中斷處理的臨界區之外可以接受新的中斷。這樣,更多的 I/O 設備將處於忙狀態。
2 Linux 中斷機制
2.1 中斷控制器
中斷控制器是連接設備和 CPU 的橋樑,一個設備產生中斷後,需要經過中斷控制器的轉發,才能最終到達 CPU。時代發展至今,中斷控制器經歷了 PIC(Programmable Interrupt Controller,可編程中斷控制器) 和 APIC (Advanced Programmable Interrupt Controller,高級可編程中斷控制器) 兩個階段。前者在 UP(Uni-processor,單處理器) 上威震四方,隨着 SMP (Symmetric Multiple Processor,對稱多處理器) 的流行,APIC 已廣爲流行並將最終取代 PIC。
8259A (PIC) 管腳圖
上圖中的管腳說明:
- IR0~IR7 (Interrupt Request0~7,用於連接設備)
- INT (連接 CPU,當有中斷請求時,拉高該管腳以通知 CPU 中斷的到來)
- INTA (連接 CPU,CPU 通過該管腳應答中斷請求,並通知 PIC 提交中斷的 vector 到數據線)
- CS (片選,用於將兩個 8259A 串聯成可連接 15 個設備的 PIC)
8259A 中的寄存器:
- ICW: Initialization Command Word,初始化命令寄存器,用於初始化 8259A
- OCW: Operation Command Word,操作命令字,用於控制 8259A
- IRR: Interrupt Request Register,中斷請求寄存器,共 8bit,對應 IR0~IR7 八個中斷管腳。當某個管腳的中斷請求到來後,若該管腳沒有被屏蔽,IRR 中對應的 bit 被置1。表示 PIC 已經收到設備的中斷請求,但還未提交給 CPU。
- ISR: In Service Register,服務中寄存器,共 8bit,每 bit 意義同上。當 IRR 中的某個中斷請求被髮送給 CPU 後,ISR 中對應的 bit 被置1。表示中斷已發送給 CPU,但 CPU 還未處理完。
- IMR: Interrupt Mask Register,中斷屏蔽寄存器,共 8bit,每 bit 意義同上。用於屏蔽中斷。當某 bit 置1時,對應的中斷管腳被屏蔽。
arch/x86/kernel/i8259_32.c 中通過位運算來開啓和關閉中斷。
63 void disable_8259A_irq(unsigned int irq) 64 { 65 unsigned int mask = 1 << irq; 66 unsigned long flags; 67 68 spin_lock_irqsave(&i8259A_lock, flags); // 用 spinlock 鎖住 69 cached_irq_mask |= mask; // 將 IRQ 的相應位置1,屏蔽中斷 70 if (irq & 8) 71 outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管腳負責 8259A 的級聯(見下圖),爲0時使用主片,爲1時使用從片 72 else 73 outb(cached_master_mask, PIC_MASTER_IMR); 74 spin_unlock_irqrestore(&i8259A_lock, flags); // 解開自旋鎖 75 }
77 void enable_8259A_irq(unsigned int irq) 78 { 79 unsigned int mask = ~(1 << irq); 80 unsigned long flags; 81 82 spin_lock_irqsave(&i8259A_lock, flags); // 用 spinlock 鎖住 83 cached_irq_mask &= mask; // 將 IRQ 的相應位置0,開啓中斷 84 if (irq & 8) 85 outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管腳負責 8259A 的級聯(見下圖),爲0時使用主片,爲1時使用從片 86 else 87 outb(cached_master_mask, PIC_MASTER_IMR); 88 spin_unlock_irqrestore(&i8259A_lock, flags); // 解開自旋鎖 89 }
PIC 的每個管腳具有優先級,連接號碼較小的設備具有較高的中斷優先級。
在 PIC 默認的 Full Nested 模式下,通過 PIC 發起中斷的流程如下:
- 一個或多個 IR 管腳上產生電平信號,若對應的中斷沒有被屏蔽,IRR 中相應的 bit 被置1。
- PIC 拉高 INT 管腳通知 CPU 中斷髮生。
- CPU 通過 INTA 管腳應答 PIC,表示中斷請求收到。
- PIC 收到 INTA 應答後,將 IRR 中具有最高優先級的 bit 清零,並設置 ISR 中對應的 bit。
- CPU 通過 INTA 管腳第二次發出脈衝,PIC 收到後計算最高優先級中斷的 vector,並將它提交到數據線上。
- 等待 CPU 寫 EOI (End of Interrupt)。收到 EOI 後,ISR 中最高優先級的 bit 被清零。如果 PIC 處於 AEOI 模式,當第二個 INTA 脈衝收到後,ISR 中最高優先級的 bit 自動清零。
PIC 還有優先級輪轉模式,即 PIC 在服務完一個管腳之後將其優先級臨時降低,並升高未服務管腳的優先級,以實現類似輪詢的模式,避免一個管腳持續發出中斷導致其他設備“餓死”。
下圖是一個典型的 PIC 中斷分配,管腳基本上都被古董級設備佔據了。
arch/x86/kernel/i8259_32.c 中 8259A 引腳的分配(function init_8259A)
292 outb_pic(0x11, PIC_MASTER_CMD); /* ICW1: select 8259A-1 init */ 293 outb_pic(0x20 + 0, PIC_MASTER_IMR); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */ 294 outb_pic(1U << PIC_CASCADE_IR, PIC_MASTER_IMR); /* 8259A-1 (the master) has a slave on IR2 */ 295 if (auto_eoi) /* master does Auto EOI */ 296 outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR); 297 else /* master expects normal EOI */ 298 outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR); 299 300 outb_pic(0x11, PIC_SLAVE_CMD); /* ICW1: select 8259A-2 init */ 301 outb_pic(0x20 + 8, PIC_SLAVE_IMR); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */ 302 outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR); /* 8259A-2 is a slave on master's IR2 */ 303 outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR); /* (slave's support for AEOI in flat mode is to be investigated) */
從上圖可見,PIC 能接的設備數量實在太少了,而且不支持多處理器。
爲了使用 8259A 級聯連接較多的設備,可以採用兩種方式:
- IRQ 共享:中斷處理程序執行多箇中斷服務程序(ISR),每個 ISR 是一個與共享 IRQ 線相關的函數。
IRQ 共享需要滿足兩個條件:
- 每個 ISR 都願意共享 IRQ,即 request_irq() 時指定了 IRQF_SHARED
- 所有 ISR 具有相同的觸發條件(電平觸發或邊沿觸發、高低電平或上下邊沿)
-
IRQ 動態分配:在可能的最後時刻,才把 IRQ 線分配給一個設備。
當然,APIC 是現代的解決方案。即使是 APIC,也需要使用 IRQ 共享。
I/O APIC 的組成爲:一組 24 條 IRQ 線,一張 24 項的中斷重定向表,可編程寄存器,通過 APIC 總線發送和接收 APIC 信息的一個信息單元。
與 8259A 不同,中斷優先級不與引腳號相關聯,中斷重定向表中的每一項都可以被單獨編程以指明中斷向量和優先級、目標處理器和選擇處理器的方式。
來自外部硬件設備的中斷以兩種方式在可用 CPU 之間分發:
- 靜態分發
- 動態分發
2.2 中斷描述符
Intel 提供了三種類型的中斷描述符:任務門、中斷門及陷阱門描述符。
Linux 使用與 Intel 稍有不同的分類,把中斷描述符分爲五類:
- 中斷門(interrupt gate):用戶態的進程不能訪問Intel中斷門(門的DPL字段爲0)。所有的Linux中斷處理程序都通過中斷門激活,並全部限制在內核態。
set_intr_gate(n,addr)
上述系統調用在 IDT 的第 n 個表項插入一箇中斷門。門中的段選擇符設置成內核代碼的段選擇符,偏移量設置爲中斷處理程序的地址 addr,DPL 字段設置爲0。
- 系統門(system gate):用戶態的進程可以訪問Intel陷阱門(門的DPL字段爲3)。通過系統門來激活三個Linux異常處理程序,它們的向量是4,5及128,因此,在用戶態下,可以發佈into、bound及int $0x80三條彙編語言指令。
set_system_gate(n,addr)
- 系統中斷門(system interrupt gate):能夠被用戶態進程訪問的Intel中斷門(門的DPL字段爲3)。與向量3相關的異常處理程序是由系統中斷門激活的,因此,在用戶態可以使用彙編語言指令int3。
set_system_intr_gate(n,addr)
- 陷阱門(trap gate):用戶態的進程不能訪問的一個Intel陷阱門(門的DPL字段爲0)。大部分Linux異常處理程序都通過陷阱門來激活。
set_trap_gate(n,addr)
- 任務門(task gate):不能被用戶態進程訪問的Intel任務門(門的DPL字段爲0)。Linux對“Double fault”異常的處理程序是由任務門激活的。
set_task_gate(n,gdt)
門中的段選擇符中存放一個TSS的全局描述符表的指針,該TSS中包含要被激活的函數。
在 IDT 中插入門的函數定義在 include/asm-x86/desc.h 中。
這些函數以不同的參數調用內部函數 _set_gate()。_set_gate 調用兩個內部函數
- pack_gate: 設置門的數據結構:中斷號、門類型、處理函數地址、DPL、ist、目錄段寄存器
38 static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func, 39 unsigned dpl, unsigned ist, unsigned seg) 40 { 41 gate->offset_low = PTR_LOW(func); // 處理函數低內存偏移 42 gate->segment = __KERNEL_CS; // 內核代碼段 43 gate->ist = ist; // ist 44 gate->p = 1; 45 gate->dpl = dpl; // DPL 46 gate->zero0 = 0; 47 gate->zero1 = 0; 48 gate->type = type; // 門類型(宏定義) 49 gate->offset_middle = PTR_MIDDLE(func); // 處理函數中內存偏移 50 gate->offset_high = PTR_HIGH(func); // 處理函數高內存偏移 51 }
- write_idt_entry: 宏定義爲 native_write_idt_entry,用 memcpy 將設置好的門寫入 IDT。
2.3 中斷數據結構
在 Linux 中,中斷描述符的核心數據結構是 include/linux/irq.h 中的 irq_desc 結構體。每個 irq_desc 實例描述一條中斷線。
153 struct irq_desc { 154 irq_flow_handler_t handle_irq; // 中斷事件處理函數,下面會介紹 155 struct irq_chip *chip; // irq_chip 指針,描述了一些硬件信息,下面會介紹 156 struct msi_desc *msi_desc; 157 void *handler_data; // chip 中使用的數據 158 void *chip_data; // chip 中使用的數據 159 struct irqaction *action; /* IRQ action list */ // irqaction 指針,下面會介紹 160 unsigned int status; /* IRQ status */ // IRQ 線狀態標誌 161 162 unsigned int depth; /* nested irq disables */ 163 unsigned int wake_depth; /* nested wake enables */ 164 unsigned int irq_count; /* For detecting broken IRQs */ // 中斷計數 165 unsigned int irqs_unhandled; // 無法處理的中斷計數 166 unsigned long last_unhandled; /* Aging timer for unhandled count */ 167 spinlock_t lock; // 自旋鎖 168 #ifdef CONFIG_SMP 169 cpumask_t affinity; // 多處理器中的處理器親和性 170 unsigned int cpu; 171 #endif 172 #if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE) 173 cpumask_t pending_mask; 174 #endif 175 #ifdef CONFIG_PROC_FS 176 struct proc_dir_entry *dir; // 在 /proc 文件系統中的目錄 177 #endif 178 const char *name; // 中斷名稱 179 } ____cacheline_internodealigned_in_smp;
irq_desc 在 kernel/irq/handle.c 中被使用,此文件是 IRQ 機制的核心入口,描述了各中斷線。
50 struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = { 51 [0 ... NR_IRQS-1] = { 52 .status = IRQ_DISABLED, // 默認屏蔽中斷 53 .chip = &no_irq_chip, // 沒有與 chip 相關聯 // 未知(壞的)IRQ 處理程序,輸出 IRQ 信息供調試,更新 CPU IRQ 次數計數器,迴應 IRQ。 54 .handle_irq = handle_bad_irq, 55 .depth = 1, // 默認是第一層(沒有嵌套中斷) 56 .lock = __SPIN_LOCK_UNLOCKED(irq_desc->lock), // 還沒有自旋鎖 57 #ifdef CONFIG_SMP 58 .affinity = CPU_MASK_ALL // 處理器親和性未定義 59 #endif 60 } 61 };
下面介紹 irq_desc 中的主要數據成員。
handle_irq
handle_irq 是函數指針,指向 kernel/irq/chip.c 中的中斷事件處理函數。
- handle_simple_irq
- handle_level_irq
- handle_fasteoi_irq
- handle_edge_irq
- handle_percpu_irq
這個函數指針是由 kernel/irq/chip.c 中的 __set_irq_handler() 設置的。
chip
chip 是 irq_chip 結構體指針,include/linux/irq.h 中的 irq_chip 結構體定義了對每根中斷線的底層硬件操作:
99 struct irq_chip { 100 const char *name; // 中斷線名稱 101 unsigned int (*startup)(unsigned int irq); // 初始化中斷的函數指針 102 void (*shutdown)(unsigned int irq); // 停止中斷的函數指針 103 void (*enable)(unsigned int irq); // 啓用中斷的函數指針 104 void (*disable)(unsigned int irq); // 關閉中斷的函數指針 105 106 void (*ack)(unsigned int irq); // 確認中斷的函數指針 107 void (*mask)(unsigned int irq); // 屏蔽中斷的函數指針 108 void (*mask_ack)(unsigned int irq); // 確認並屏蔽中斷的函數指針 109 void (*unmask)(unsigned int irq); // 取消屏蔽中斷的函數指針 110 void (*eoi)(unsigned int irq); // 中斷處理結束的函數指針 111 112 void (*end)(unsigned int irq); 113 void (*set_affinity)(unsigned int irq, cpumask_t dest); // 設置處理器親和性 114 int (*retrigger)(unsigned int irq); // 重新出發中斷 // 設置中斷觸發類型,根據 IRQ_TYPE 宏定義,包括上邊沿、下邊沿、邊沿、高電平、低電平等 115 int (*set_type)(unsigned int irq, unsigned int flow_type); 116 int (*set_wake)(unsigned int irq, unsigned int on); // 喚醒中斷 117 118 /* Currently used only by UML, might disappear one day.*/ 119 #ifdef CONFIG_IRQ_RELEASE_METHOD 120 void (*release)(unsigned int irq, void *dev_id); 121 #endif 122 /* 123 * For compatibility, ->typename is copied into ->name. 124 * Will disappear. 125 */ 126 const char *typename; 127 };
action
action 是 irqaction 結構體指針,指向一個 irqaction 鏈表。irqaction 在 include/linux/interrupt.h 中定義,每個結構體描述一箇中斷處理程序。
60 struct irqaction { 61 irq_handler_t handler; // 中斷處理程序的函數指針 62 unsigned long flags; 63 cpumask_t mask; // 處理器親和性 64 const char *name; // 中斷處理程序名稱,顯示在 /proc/interrupts 中 65 void *dev_id; // 設備 ID 66 struct irqaction *next; // 指向鏈表中的下一個 irqaction 結構體 67 int irq; // 中斷通道號 68 struct proc_dir_entry *dir; // 在 /proc 文件系統中的目錄 69 };
status
status 是描述 IRQ 線狀態的一組標誌。在同一文件中宏定義:
49 #define IRQ_INPROGRESS 0x00000100 /* IRQ handler active - do not enter! */ 50 #define IRQ_DISABLED 0x00000200 /* IRQ disabled - do not enter! */ 51 #define IRQ_PENDING 0x00000400 /* IRQ pending - replay on enable */ 52 #define IRQ_REPLAY 0x00000800 /* IRQ has been replayed but not acked yet */ 53 #define IRQ_AUTODETECT 0x00001000 /* IRQ is being autodetected */ 54 #define IRQ_WAITING 0x00002000 /* IRQ not yet seen - for autodetection */ 55 #define IRQ_LEVEL 0x00004000 /* IRQ level triggered */ 56 #define IRQ_MASKED 0x00008000 /* IRQ masked - shouldn't be seen again */ 57 #define IRQ_PER_CPU 0x00010000 /* IRQ is per CPU */
綜上所述,內核中的中斷描述符表是一個 irq_desc 數組,數組的每一項描述一根中斷線的信息,包括芯片中斷處理程序、底層硬件操作函數、註冊的中斷處理程序鏈表等。
中斷向量表可以通過 /proc/interrupts 查看:
[boj@~]$ cat /proc/interrupts CPU0 CPU1 0: 3652701 2 IO-APIC-edge timer 1: 34517 0 IO-APIC-edge i8042 8: 1 0 IO-APIC-edge rtc0 9: 48512 19 IO-APIC-fasteoi acpi 12: 12 0 IO-APIC-edge i8042 14: 29337 0 IO-APIC-edge ata_piix 15: 38002 0 IO-APIC-edge ata_piix 16: 263352 1 IO-APIC-fasteoi uhci_hcd:usb5, yenta, i915 18: 0 0 IO-APIC-fasteoi uhci_hcd:usb4 19: 105769 0 IO-APIC-fasteoi uhci_hcd:usb3 21: 34677 0 IO-APIC-fasteoi eth0 22: 151 0 IO-APIC-fasteoi firewire_ohci 23: 2 0 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, mmc0 42: 360215 0 PCI-MSI-edge iwl3945 43: 656 0 PCI-MSI-edge hda_intel NMI: 0 0 Non-maskable interrupts LOC: 253429 2025163 Local timer interrupts SPU: 0 0 Spurious interrupts PMI: 0 0 Performance monitoring interrupts IWI: 0 0 IRQ work interrupts RES: 1063515 1286501 Rescheduling interrupts CAL: 3762 2967 Function call interrupts TLB: 13274 13115 TLB shootdowns TRM: 0 0 Thermal event interrupts THR: 0 0 Threshold APIC interrupts MCE: 0 0 Machine check exceptions MCP: 32 32 Machine check polls ERR: 0 MIS: 0
負責打印 /proc/interrupts 的代碼位於 arch/x86/kernel/irq_32.c。
242 int show_interrupts(struct seq_file *p, void *v)
2.4 中斷的初始化
中斷機制的初始化分爲三步:
- arch/x86/kernel/head_32.S 中 setup IDT,在內核引導分析報告中已經闡述。
- init/main.c 的 start_kernel() 中的 trap_init()
- init/main.c 的 start_kernel() 中的 init_IRQ()
trap_init()
trap_init() 定義於 arch/x86/kernel/traps_32.c,作用是設置中斷向量。
- 初始化 APIC 映射表
- 調用 set_trap_gate、set_intr_gate、set_task_gate、set_system_gate 等,初始化中斷描述符表。
- 調用 set_system_gate,初始化系統調用
- 將已設置的中斷向量置保留位
- 將已設置的系統調用置保留位
- 初始化 CPU 作爲屏障
- 執行 trap_init 的鉤子函數
init_IRQ
init_IRQ() 定義於 arch/x86/kernel/paravirt.c,由 paravirt_ops.init_IRQ() 和 native_init_IRQ() 二者組成。
native_init_IRQ() 定義於 arch/x86/kernel/i8259.c。該函數主要將 IDT 未初始化的各項初始化爲中斷門。
-
pre_intr_init_hook() 調用 init_ISA_irqs(),初始化 irq_desc 數組、status、action、depth。
-
在循環中,對於所有在 FIRST_EXTERNAL_VECTOR(0x20) 與 NR_VECTOR(0x100)之間的、不是系統中斷的 256 - 32 - 1 = 223 項,調用 set_intr_gate(),初始化爲中斷門。
-
現在我們關心的是,這些中斷門的中斷處理程序是什麼?在 x86 體系結構下沒找到 interrupt 數組的定義,因此使用 64 位體系結構的做說明:
// arch/x86/kernel/i8259_64.c 80 static void (*__initdata interrupt[NR_VECTORS - FIRST_EXTERNAL_VECTOR])(void) = { 81 IRQLIST_16(0x2), IRQLIST_16(0x3), 82 IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7), 83 IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb), 84 IRQLIST_16(0xc), IRQLIST_16(0xd), IRQLIST_16(0xe), IRQLIST_16(0xf) 85 };
- 以上是 interrupt 數組的定義。在下面的代碼中,## 是將字符串連接起來,這樣宏定義的函數 IRQ(0x4,6) 就是 IRQ0x46_interrupt,生成 224 個這樣的函數填入數組。
// arch/x86/kernel/i8259_64.c 70 #define IRQ(x,y) \ 71 IRQ##x##y##_interrupt 72 73 #define IRQLIST_16(x) \ 74 IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \ 75 IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \ 76 IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \ 77 IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
- 那麼這 224 個函數在哪裏呢?通過下面的宏可以生成一個彙編函數,它調用了 common_interrupt 函數:
// include/asm/hw_irq_64.h 155 #define IRQ_NAME2(nr) nr##_interrupt(void) 156 #define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr) 162 #define BUILD_IRQ(nr) \ 163 asmlinkage void IRQ_NAME(nr); \ 164 asm("\n.p2align\n" \ 165 "IRQ" #nr "_interrupt:\n\t" \ 166 "push $~(" #nr ") ; " \ 167 "jmp common_interrupt");
- common_interrupt 是彙編函數,這個函數最終調用了 do_IRQ,這是我們下章要介紹的核心中斷處理函數。
// arch/x86/kernel/entry_32.S 613 common_interrupt: 614 SAVE_ALL 615 TRACE_IRQS_OFF 616 movl %esp,%eax 617 call do_IRQ 618 jmp ret_from_intr 619 ENDPROC(common_interrupt)
- 看來,只需要調用 BUILD_IRQ 就能生成中斷處理函數了。Linux Kernel 正是這樣做的:
// arch/x86/kernel/i8259_64.c 37 #define BI(x,y) \ 38 BUILD_IRQ(x##y) 39 40 #define BUILD_16_IRQS(x) \ 41 BI(x,0) BI(x,1) BI(x,2) BI(x,3) \ 42 BI(x,4) BI(x,5) BI(x,6) BI(x,7) \ 43 BI(x,8) BI(x,9) BI(x,a) BI(x,b) \ 44 BI(x,c) BI(x,d) BI(x,e) BI(x,f) .................................................... 61 BUILD_16_IRQS(0x2) BUILD_16_IRQS(0x3) 62 BUILD_16_IRQS(0x4) BUILD_16_IRQS(0x5) BUILD_16_IRQS(0x6) BUILD_16_IRQS(0x7) 63 BUILD_16_IRQS(0x8) BUILD_16_IRQS(0x9) BUILD_16_IRQS(0xa) BUILD_16_IRQS(0xb) 64 BUILD_16_IRQS(0xc) BUILD_16_IRQS(0xd) BUILD_16_IRQS(0xe) BUILD_16_IRQS(0xf)
- 不得不發表一下感慨,Linux Kernel 對 C 語言宏的運用真是爐火純青。如果是我寫代碼,很可能不得不先寫個代碼生成器,用它生成 224 個函數的源碼。但 Linux Kernel Source 裏出現幾百個幾乎一模一樣的函數太不優雅了,於是利用 C 語言的預處理機制,採用二級宏定義,儘可能降低代碼量。
2.5 內核接口
中斷處理程序不是編譯內核時就完全確定的,因此要爲開發者留下編程接口。
2.6.17 內核引入了 generic IRQ 機制,支持 i386、x86-64 和 ARM 三個體系結構。generic IRQ 層的引入,是爲了剝離 IRQ flow 和 IRQ chip 過於緊密的耦合。爲驅動開發者提供通用的 API 來 request/enable/disable/free 中斷,而不需要知道任何底層的中斷控制器細節。
這些中斷 API 是在內核中用 EXPORT_SYMBOL 導出的。
請求中斷
kernel/irq/manage.c 中的 request_irq:
536 int request_irq(unsigned int irq, irq_handler_t handler, 537 unsigned long irqflags, const char *devname, void *dev_id)
參數
- irq: 中斷通道號,無符號整數
- handler:中斷處理程序的函數指針 (irq_return_t isr_func(int irq, void *dev_id))
- irq_flags:標誌位
- IRQF_SHARED: 共享中斷通道
- IRQF_DISABLED: 中斷處理程序執行時關中斷
- IRQF_SAMPLE_RANDOM: 隨機發生中斷,可用於產生隨機數
- IRQF_TRIGGER_LOW:2.6.26 中沒有,低電平有效
- IRQF_TRIGGER_HIGH: 2.6.26 中沒有,高電平有效
- IRQF_TRIGGER_RISING: 2.6.26 中沒有,上升沿有效
- IRQF_TRIGGER_FALLING: 2.6.26 中沒有,下降沿有效
- dev_name:名稱,顯示在 /proc/interrupts 中
- dev_id:設備 ID,區分不同的設備
內部機制:
- 檢查輸入數據的合法性
- 爲臨時變量 irqaction 分配內存空間,初始化 irqaction 數據結構
- 如果是調試模式,測試是否運行正常
- 進入工作函數 setup_irq(unsigned int irq, struct irqaction *new)
- 如果是 IRQF_SAMPLE_RANDOM 模式,隨機初始化 irq
- 上自旋鎖
- 如果希望共享中斷通道,所有中斷處理程序需要有相同的觸發特性標識、PERCPU 特性
- 把新 irqaction 結構體掛在鏈表尾部
- 如果設置了 IRQF_TRIGGER_MASK,初始化觸發特性
- 初始化 irq 狀態、嵌套深度
- 啓動(enable)此 IRQ
- 釋放自旋鎖
- 調用 /kernel/irq/proc.c 中的 register_irq_proc() 和 register_handler_proc(),建立 /proc 文件系統中的相關數據結構
- 返回成功(0)
- 如果出錯,輸出內核調試信息,釋放自旋鎖,返回錯誤
- 釋放 irqaction 的內存空間,返回 setup_irq 的返回值
清除中斷
kernel/irq/manage.c 中的 free_irq:
435 void free_irq(unsigned int irq, void *dev_id)
參數
- irq: 中斷通道號,無符號整數
- dev_id: 請求中斷時指定的設備 ID
內部機制:
- 不能在中斷上下文中調用
- 上自旋鎖
- 循環,沿鏈表查找要刪除的中斷處理程序
- 如果發現是已經釋放的,則輸出內核調試信息,釋放自旋鎖
- 如果 dev_id 不對,沿着 irqaction 鏈表繼續向下尋找
- 如果找到了,從鏈表中移除這個 irqaction
- 關閉此 IRQ,關閉硬件,釋放自旋鎖,從 /proc 文件系統中刪除對應目錄
- 同步 IRQ 以防正在其他 CPU 上運行
- 如果是調試模式,測試驅動程序是否知道此共享 IRQ 已移除
- 釋放內存空間,返回
啓用中斷
kernel/irq/manage.c 中的 enable_irq:
153 static void __enable_irq(struct irq_desc *desc, unsigned int irq)
內部調用了 __enable_irq,首先上自旋鎖,找到 irq_desc 結構體指針,判斷嵌套深度,刷新 IRQ 狀態,釋放自旋鎖。
參數
- desc: 指向 irq_desc 結構體的指針
- irq: 中斷通道號
關閉中斷
kernel/irq/manage.c 中的 disable_irq:
140 void disable_irq(unsigned int irq)
參數
- irq: 中斷通道號
關閉中斷 (無等待)
disable_irq 會保證存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即關中斷並返回。事實上,disable_irq 首先調用 disable_irq_nosync,然後調用 synchronize_irq 同步。
111 void disable_irq_nosync(unsigned int irq)
同步中斷 (多處理器)
30 void synchronize_irq(unsigned int irq)
設置 IRQ 芯片
kernel/irq/chip.c: set_irq_chip()
93 int set_irq_chip(unsigned int irq, struct irq_chip *chip)
設置 IRQ 類型
kernel/irq/chip.c: set_irq_type()
122 int set_irq_type(unsigned int irq, unsigned int type)
設置 IRQ 數據
kernel/irq/chip.c: set_irq_data()
150 int set_irq_data(unsigned int irq, void *data)
設置 IRQ 芯片數據
kernel/irq/chip.c: set_irq_chip_data()
202 int set_irq_chip_data(unsigned int irq, void *data)
3 中斷處理流程
3.1 CPU的中斷處理流程
本節摘自參考文獻之 中斷的硬件環境
每個能夠發出中斷請求的硬件設備控制器都有一條名爲 IRQ 的輸出線。所有現有的 IRQ 線都與一個名爲可編程中斷控制器(PIC)的硬件電路的輸入引腳相連,可編程中斷控制器執行下列動作:
- 監視 IRQ 線,檢查產生的信號。如果有兩條以上的 IRQ 線上產生信號,就選擇引腳編號較小的 IRQ 線。
- 如果一個引發信號出現在 IRQ 線上:
- 把接收到的引發信號轉換成對應的向量號
- 把這個向量存放在中斷控制器的一個 I/O 端口(0x20、0x21),從而允許 CPU 通過數據總線讀此向量。
- 把引發信號發送到處理器的 INTR 引腳,即產生一箇中斷。
- 等待,直到 CPU 通過把這個中斷信號寫進可編程中斷控制器的一個 I/O 端口來確認它;當這種情況發生時,清 INTR 線。
- 返回第1步。
當執行了一條指令後,CS和eip這對寄存器包含下一條將要執行的指令的邏輯地址。在處理那條指令之前,控制單元會檢查在運行前一條指令時是否已經發生了一箇中斷或異常。如果發生了一箇中斷或異常,那麼控制單元執行下列操作:
- 確定與中斷或異常關聯的向量i (0 ≤ i ≤ 255)。
- 讀由idtr寄存器指向的 IDT表中的第i項(在下面的分析中,我們假定IDT表項中包含的是一箇中斷門或一個陷阱門)。
- 從gdtr寄存器獲得GDT的基地址,並在GDT中查找,以讀取IDT表項中的選擇符所標識的段描述符。這個描述符指定中斷或異常處理程序所在段的基地址。
- 確信中斷是由授權的(中斷)發生源發出的。首先將當前特權級CPL(存放在cs寄存器的低兩位)與段描述符(存放在GDT中)的描述符特權級DPL比較,如果CPL小於DPL,就產生一個“General protection”異常,因爲中斷處理程序的特權不能低於引起中斷的程序的特權。對於編程異常,則做進一步的安全檢查:比較CPL與處於IDT中的門描述符的DPL,如果DPL小於CPL,就產生一個“General protection”異常。這最後一個檢查可以避免用戶應用程序訪問特殊的陷阱門或中斷門。
- 檢查是否發生了特權級的變化,也就是說,CPL是否不同於所選擇的段描述符的DPL。如果是,控制單元必須開始使用與新的特權級相關的棧。通過執行以下步驟來做到這點:
- 讀tr寄存器,以訪問運行進程的TSS段。
- 用與新特權級相關的棧段和棧指針的正確值裝載ss和esp寄存器。這些值可以在TSS中找到(參見第三章的“任務狀態段”一節)
- 在新的棧中保存ss和esp以前的值,這些值定義了與舊特權級相關的棧的邏輯地址。
- 如果故障已發生,用引起異常的指令地址裝載CS和eip寄存器,從而使得這條指令能再次被執行。
- 在棧中保存eflags、CS及eip的內容。
- 如果異常產生了一個硬件出錯碼,則將它保存在棧中。
- 裝載cs和eip寄存器,其值分別是IDT表中第i項門描述符的段選擇符和偏移量字段。這些值給出了中斷或者異常處理程序的第一條指令的邏輯地址。
控制單元所執行的最後一步就是跳轉到中斷或者異常處理程序。換句話說,處理完中斷信號後,控制單元所執行的指令就是被選中處理程序的第一條指令。
中斷或異常被處理完後,相應的處理程序必須產生一條iret指令,把控制權轉交給被中斷的進程,這將迫使控制單元:
- 用保存在棧中的值裝載CS、eip或eflags寄存器。如果一個硬件出錯碼曾被壓入棧中,並且在eip內容的上面,那麼,執行iret指令前必須先彈出這個硬件出錯碼。
- 檢查處理程序的CPL是否等於CS中最低兩位的值(這意味着被中斷的進程與處理程序運行在同一特權級)。如果是,iret終止執行;否則,轉入下一步。
- 從棧中裝載ss和esp寄存器,因此,返回到與舊特權級相關的棧。
- 檢查ds、es、fs及gs段寄存器的內容,如果其中一個寄存器包含的選擇符是一個段描述符,並且其DPL值小於CPL,那麼,清相應的段寄存器。控制單元這麼做是爲了禁止用戶態的程序(CPL=3)利用內核以前所用的段寄存器(DPL=0)。如果不清這些寄存器,懷有惡意的用戶態程序就可能利用它們來訪問內核地址空間。
3.2 保存中斷信息
Linux 內核的中斷處理機制自始至終貫穿着 “重要的事馬上做,不重要的事推後做” 的思想。
中斷處理程序首先要做:
- 將中斷號壓入棧中,以便找到對應的中斷服務程序
- 將當前寄存器信息壓入棧中,以便中斷退出時恢復上下文
顯然, 這兩步都是不可重入的。因此在進入中斷服務程序時,CPU 已經自動禁止了本 CPU 上的中斷響應。
上章中斷初始化過程的分析中,已經介紹了 interrupt 數組的生成過程,其中索引爲 n 的元素中存放着下列指令的地址:
pushl n-256 jmp common_interrupt
執行結果是將中斷號 - 256 保存在棧中,這樣棧中的中斷都是負數,而正數用來表示系統調用。這樣,系統調用和中斷可以用一個有符號整數統一表示。
現在重述一下 common_interrupt 的定義:
// arch/x86/kernel/entry_32.S 613 common_interrupt: 614 SAVE_ALL 615 TRACE_IRQS_OFF 616 movl %esp,%eax # 將棧頂地址放入 eax,這樣 do_IRQ 返回時控制轉到 ret_from_intr() 617 call do_IRQ # 核心中斷處理函數 618 jmp ret_from_intr # 跳轉到 ret_from_intr()
其中 SAVE_ALL 宏將被展開成:
cld push %es # 保存除 eflags、cs、eip、ss、esp (已被 CPU 自動保存) 外的其他寄存器 push %ds pushl %eax pushl %ebp pushl %edi pushl %edx pushl %ecx pushl %ebx movl $ _ _USER_DS, %edx movl %edx, %ds # 將用戶數據段選擇符載入 ds、es movl %edx, %es
3.3 處理中斷
前面彙編代碼的實質是,以中斷髮生時寄存器的信息爲參數,調用 arch/x86/kernel/irq32.c 中的 do_IRQ 函數。
我們注意到 unlikely 和 unlikely 宏定義,它們的含義是
#define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0)
__builtin_expect 是 GCC 的內部機制,意思是告訴編譯器哪個分支條件更有可能發生。這使得編譯器把更可能發生的分支條件與前面的代碼順序串接起來,更有效地利用 CPU 的指令流水線。
do_IRQ 函數流程:
- 保存寄存器上下文
- 調用 irq_enter:
// kernel/softirq.c 281 void irq_enter(void) 282 { 283 #ifdef CONFIG_NO_HZ // 無滴答內核,它將在需要調度新任務時執行計算並在這個時間設置一個時鐘中斷,允許處理器在更長的時間內(幾秒鐘)保持在最低功耗狀態,從而減少了電能消耗。 284 int cpu = smp_processor_id(); 285 if (idle_cpu(cpu) && !in_interrupt()) 286 tick_nohz_stop_idle(cpu); // 如果空閒且不在中斷中,則停止空閒,開始工作 287 #endif 288 __irq_enter(); 289 #ifdef CONFIG_NO_HZ 290 if (idle_cpu(cpu)) 291 tick_nohz_update_jiffies(); // 更新 jiffies 292 #endif 293 }
// include/linux/hardirq.h 135 #define __irq_enter() \ /* 在宏定義函數中,do { ... } while(0) 結構可以把語句塊作爲一個整體,就像函數調用,避免宏展開後出現問題 */ 136 do { \ 137 rcu_irq_enter(); \ 138 account_system_vtime(current); \ 139 add_preempt_count(HARDIRQ_OFFSET); \ /* 程序嵌套數量計數器遞增1 */ 140 trace_hardirq_enter(); \ 141 } while (0)
- 如果可用空間不足 1KB,可能會引發棧溢出,輸出內核錯誤信息
- 如果 thread_union 是 4KB 的,進行一些特殊處理
- 調用 desc->handle_irq(irq, desc),調用 __do_IRQ() (kernel/irq/handle.c)
- 取得中斷號,獲取對應的 irq_desc
- 如果是 CPU 內部中斷,不需要上鎖,簡單處理完就返回了
- 上自旋鎖
- 應答中斷芯片,這樣中斷芯片就能開始接受新的中斷了。
- 更新中斷狀態。
IRQ_REPLAY:如果被禁止的中斷管腳上產生了中斷,這個中斷是不會被處理的。當這個中斷號被允許產生中斷時,會將這個未被處理的中斷轉爲 IRQ_REPLAY。
IRQ_WAITING:探測用,探測時會將所有沒有中斷處理函數的中斷號設爲 IRQ_WAITING,只要這個中斷管腳上有中斷產生,就把這個狀態去掉,從而知道哪些中斷管腳上產生過中斷。
IRQ_PENDING、IRQ_INPROGRESS 是爲了確保同一個中斷號的處理程序不能重入,且不能丟失這個中斷的下一個處理程序。具體地說,當內核在運行某個中斷號對應的處理程序時,狀態會設置成 IRQ_INPROGRESS。如果發現已經有另一實例在運行了,就將這下一個中斷標註爲 IRQ_PENDING 並返回。這個已在運行的實例結束的時候,會查看是否期間有同一中斷髮生了,是則再次執行一遍。
- 如果鏈表上沒有中斷處理程序,或者中斷被禁止,或者已經有另一實例在運行,則進行收尾工作。
- 循環:
- 釋放自旋鎖
- 執行函數鏈:handle_IRQ_event()。其中主要是一個循環,依次執行中斷處理程序鏈表上的函數,並根據返回值更新中斷狀態。如果願意,可以參與隨機數採樣。中斷處理程序執行期間,打開本地中斷。
- 上自旋鎖
- 如果當前中斷已經處理完,則退出;不然取消中斷的 PENDING 標誌,繼續循環。
- 取消中斷的 INPROGRESS 標誌
- 收尾工作:有的中斷在處理過程中被關閉了,->end() 處理這種情況;釋放自旋鎖。
- 執行 irq_exit(),在 kernel/softirq.c 中:
- 遞減中斷計數器
- 檢查是否有軟中斷在等待執行,若有則執行軟中斷。
- 如果使用了無滴答內核看是不是該休息了。
- 恢復寄存器上下文,跳轉到 ret_from_intr (跳轉點早在 common_interrupt 中就被指定了)
在中斷處理過程中,我們反覆看到對自旋鎖的操作。在單處理器系統上,spinlock 是沒有作用的;在多處理器系統上,由於同種類型的中斷可能連續產生,同時被幾個 CPU 處理(注意,應答中斷芯片是緊接着獲得自旋鎖後,位於整個中斷處理流程的前部,因此在中斷處理流程的其餘部分,中斷芯片可以觸發新的中斷並被另一個 CPU 開始處理),如果沒有自旋鎖,多個 CPU 可能同時訪問 IRQ 描述符,造成混亂。因此在訪問 IRQ 描述符的過程中需要有 spinlock 保護。
3.4 從中斷中返回
上面的中斷處理流程中隱含了一個問題:整個處理過程是持續佔有CPU的(除開中斷情況下可能被新的中斷打斷外),這樣
- 連續的低優先的中斷可能持續佔有 CPU, 而高優先的某些進程則無法獲得 CPU
- 中斷處理的這幾個階段中不能調用可能導致睡眠的函數
對於第一個問題,較新的 linux 內核增加了 ksoftirqd 內核線程,如果持續處理的軟中斷超過一定數量,則結束中斷處理過程,喚醒 ksoftirqd,由它來繼續處理。
對於第二個問題,linux 內核提供了 workqueue(工作隊列)機制,定義一個 work 結構(包含了處理函數),然後在上述的中斷處理的幾個階段的某一步中調用 schedule_work 函數,work 便被添加到 workqueue 中,等待處理。
工作隊列有着自己的處理線程, 這些 work 被推遲到這些線程中去處理。處理過程只可能發生在這些工作線程中,不會發生在內核中斷處理路徑中,所以可以睡眠。下章將簡要介紹這些中斷機制。
3.5 編寫中斷處理程序
本節編寫一個簡單的中斷處理程序 (catchirq) 作爲內核模塊,演示捕獲網卡中斷。
- catchirq.c
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/interrupt.h> #include <linux/timer.h> #define DEBUG #ifdef DEBUG #define MSG(message, args...) printk(KERN_DEBUG "catchirq: " message, ##args) #else #define MSG(message, args...) #endif MODULE_LICENSE("GPL"); MODULE_AUTHOR("boj"); int irq; char *interface; // module_param(name, type, perm) module_param(irq, int, 0644); module_param(interface, charp, 0644); int irq_handle_function(int irq, void *device_id) { static int count = 1; MSG("[%d] Receive IRQ at %ld\n", count, jiffies); count++; return IRQ_NONE; } int init_module() { if (request_irq(irq, irq_handle_function, IRQF_SHARED, interface, (void *)&irq)) { MSG("[FAILED] IRQ register failure.\n"); return -EIO; } MSG("[OK] Interface=%s IRQ=%d\n", interface, irq); return 0; } void cleanup_module() { free_irq(irq, &irq); MSG("IRQ is freed.\n"); }
- Makefile(編寫說明參見 Documentation/kbuild/)
obj-m := catchirq.o KERNELDIR := /lib/modules/$(shell uname -r)/build default: make -C $(KERNELDIR) M=$(shell pwd) clean: make -C $(KERNELDIR) M=$(shell pwd) clean
- 命令:make
-
[boj@~/int]$ ls built-in.o catchirq.c catchirq.ko catchirq.mod.c catchirq.mod.o catchirq.o Makefile modules.order Module.symvers
- 查看 /proc/interrupts(前面章節已經貼出來了),獲知我們想截獲的網卡(eth0)是 21 號中斷。通過 insmod 的 interface 和 irq 指定模塊加載參數(源文件中的 module_params 指定的)
sudo insmod catchirq.ko interface=eth1 irq=21
- 成功插入一個內核模塊:
[boj@~]$ lsmod | grep catchirq catchirq 12636 0
- 我們看到,/proc/interrupts 的 21 號中斷增加了一箇中斷處理程序:eth1
[boj@~/int]$ cat /proc/interrupts CPU0 CPU1 0: 23443709 27 IO-APIC-edge timer 1: 205319 0 IO-APIC-edge i8042 8: 1 0 IO-APIC-edge rtc0 9: 170665 80 IO-APIC-fasteoi acpi 12: 12 0 IO-APIC-edge i8042 14: 135310 0 IO-APIC-edge ata_piix 15: 205712 1 IO-APIC-edge ata_piix 16: 1488409 29 IO-APIC-fasteoi uhci_hcd:usb5, yenta, i915 18: 0 0 IO-APIC-fasteoi uhci_hcd:usb4 19: 477290 5 IO-APIC-fasteoi uhci_hcd:usb3 21: 107049 0 IO-APIC-fasteoi eth0, eth1 22: 806 0 IO-APIC-fasteoi firewire_ohci 23: 2 0 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, mmc0 42: 1803270 2 PCI-MSI-edge iwl3945 43: 11783 0 PCI-MSI-edge hda_intel NMI: 0 0 Non-maskable interrupts LOC: 2013602 12644870 Local timer interrupts SPU: 0 0 Spurious interrupts PMI: 0 0 Performance monitoring interrupts IWI: 0 0 IRQ work interrupts RES: 6046340 7106551 Rescheduling interrupts CAL: 20110 14839 Function call interrupts TLB: 33385 36028 TLB shootdowns TRM: 0 0 Thermal event interrupts THR: 0 0 Threshold APIC interrupts MCE: 0 0 Machine check exceptions MCP: 172 172 Machine check polls ERR: 0 MIS: 0
- dmesg 中可以看到大量如下形式的內核信息。這恰好是我們在源碼中的 DEBUG 模式通過 printk 輸出的。
// [Time] module_name: [count] Receive IRQ at jiffies [51837.231505] catchirq: [499] Receive IRQ at 12884307 [51837.232803] catchirq: [500] Receive IRQ at 12884308 [51837.232849] catchirq: [501] Receive IRQ at 12884308 [51837.269587] catchirq: [502] Receive IRQ at 12884317 [51844.585799] catchirq: [503] Receive IRQ at 12886146 [51844.586724] catchirq: [504] Receive IRQ at 12886146
- 演示完畢,卸載內核模塊:
sudo rmmod catchirq
- 根據 dmesg,catchirq 模塊輸出了最後一句話,被正常卸載。從 /proc/interrupts 看到,中斷處理程序表恢復原狀。
[52413.797952] catchirq: [2245] Receive IRQ at 13028449 [52413.815899] catchirq: [2246] Receive IRQ at 13028453 [52413.815990] catchirq: [2247] Receive IRQ at 13028453 [52413.841763] catchirq: IRQ is freed.
4 軟中斷、tasklet與工作隊列
4.1 上半部與下半部
軟中斷、tasklet和工作隊列並不是Linux內核中一直存在的機制,而是由更早版本的內核中的“下半部”(bottom half)演變而來。下半部的機制實際上包括五種,但2.6版本的內核中,下半部和任務隊列的函數都消失了,只剩下了前三者。
上半部指的是中斷處理程序,下半部則指的是一些雖然與中斷有相關性但是可以延後執行的任務。舉個例子:在網絡傳輸中,網卡接收到數據包這個事件不一定需要馬上被處理,適合用下半部去實現;但是用戶敲擊鍵盤這樣的事件就必須馬上被響應,應該用中斷實現。
兩者的主要區別在於:中斷不能被相同類型的中斷打斷,而下半部依然可以被中斷打斷;中斷對於時間非常敏感,而下半部基本上都是一些可以延遲的工作。由於二者的這種區別,所以對於一個工作是放在上半部還是放在下半部去執行,有一些參考標準:
- 如果一個任務對時間非常敏感,將其放在中斷處理程序中執行。
- 如果一個任務和硬件相關,將其放在中斷處理程序中執行。
- 如果一個任務要保證不被其他中斷(特別是相同的中斷)打斷,將其放在中斷處理程序中執行。
- 其他所有任務,考慮放在下半部去執行。
4.2 軟中斷
軟中斷作爲下半部機制的代表,是隨着 SMP 的出現應運而生的,它也是tasklet實現的基礎(tasklet實際上只是在軟中斷的基礎上添加了一定的機制)。軟中斷一般是“可延遲函數”的總稱,有時候也包括了tasklet(請讀者在遇到的時候根據上下文推斷是否包含tasklet)。它的出現就是因爲要滿足上面所提出的上半部和下半部的區別,使得對時間不敏感的任務延後執行,而且可以在多個CPU上並行執行,使得總的系統效率可以更高。特性是:
- 產生後並不是馬上可以執行,必須要等待內核的調度才能執行。軟中斷不能被自己打斷,只能被硬件中斷打斷(上半部)。
- 可以併發運行在多個CPU上(即使同一類型的也可以)。所以軟中斷必須設計爲可重入的函數(允許多個CPU同時操作),因此也需要使用自旋鎖來保護其數據結構。
4.3 tasklet
由於軟中斷必須使用可重入函數,這就導致設計上的複雜度變高,作爲設備驅動程序的開發者來說,增加了負擔。而如果某種應用並不需要在多個CPU上並行執行,那麼軟中斷其實是沒有必要的。因此誕生了彌補以上兩個要求的tasklet。它具有以下特性:
- 一種特定類型的tasklet只能運行在一個CPU上,不能並行,只能串行執行。
- 多個不同類型的tasklet可以並行在多個CPU上。
- 軟中斷是靜態分配的,在內核編譯好之後,就不能改變。但tasklet就靈活許多,可以在運行時改變(比如添加模塊時)。
tasklet是在兩種軟中斷類型的基礎上實現的,因此如果不需要軟中斷的並行特性,tasklet就是最好的選擇。
一般而言,在可延遲函數上可以執行四種操作:初始化/激活/執行/屏蔽。屏蔽我們這裏不再敘述,前三個則比較重要。下面將軟中斷和tasklet的三個步驟分別進行對比介紹。
-
初始化
初始化是指在可延遲函數準備就緒之前所做的所有工作。一般包括兩個大步驟:首先是向內核聲明這個可延遲函數,以備內核在需要的時候調用;然後就是調用相應的初始化函數,用函數指針等初始化相應的描述符。
如果是軟中斷則在內核初始化時進行,其描述符定義如下:
struct softirq_action { void (*action)(struct softirq_action *); void *data; };
kernel/softirq.c 中的軟中斷描述符數組:static struct softirq_action softirq_vec[32]
前 6 個已經被內核註冊使用:
- tasklet 使用的 HI_SOFTIRQ
- tasklet 使用的 TASKLET_SOFTIRQ
- 網絡協議棧使用的 NET_TX_SOFTIRQ
- 網絡協議棧使用的 NET_RX_SOFTIRQ
- SCSI 存儲
- 系統計時器
其餘的軟中斷描述符可以由內核開發者使用。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
例如網絡子系統通過以下兩個函數初始化軟中斷(net_tx_action/net_rx_action是兩個函數):
open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action);
當內核中產生 NET_TX_SOFTIRQ 軟中斷之後,就會調用 net_tx_action 這個函數。
tasklet 則可以在運行時定義,例如加載模塊時。定義方式有兩種:
- 靜態聲明
DECLARE_TASKET(name, func, data) DECLARE_TASKLET_DISABLED(name, func, data)
- 動態聲明
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
其參數分別爲描述符,需要調用的函數和此函數的參數。初始化生成的就是一個實際的描述符,假設爲 my_tasklet。
-
激活
激活:標記一個可延遲函數爲掛起(pending)狀態,表示內核可以調用這個可延遲函數。類似處於 TASK_RUNNING 狀態的進程,處在這個狀態的進程只是準備好了被 CPU 調度,但並不一定馬上就會被調度。
軟中斷使用 raise_softirq() 函數激活,接收的參數就是上面初始化時用到的數組索引 nr。
tasklet 使用 tasklet_schedule() 激活,該函數接受 tasklet 的描述符作爲參數,例如上面生成的 my_tasklet:
tasklet_schedule(&my_tasklet)
-
執行
執行就是內核運行可延遲函數的過程,但是執行只發生在某些特定的時刻。
每個CPU上都有一個32位的掩碼__softirq_pending,表明此CPU上有哪些掛起(已被激活)的軟中斷。此掩碼可以用local_softirq_pending()宏獲得。所有的掛起的軟中斷需要用do_softirq()函數的一個循環來處理。
對於 tasklet,軟中斷初始化時設置了發生 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 軟中斷時所執行的函數:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
tasklet_action 和 tasklet_hi_action 內部實現不同,軟中斷和 tasklet 因此具有了不同的特性。
4.4 工作隊列
上面的可延遲函數運行在中斷上下文中(如上章所述,軟中斷的一個檢查點就是 do_IRQ 退出的時候),於是導致了一些問題:軟中斷不能睡眠、不能阻塞。由於中斷上下文出於內核態,沒有進程切換,所以如果軟中斷一旦睡眠或者阻塞,將無法退出這種狀態,導致內核會整個僵死。但可阻塞函數不能用在中斷上下文中實現,必須要運行在進程上下文中,例如訪問磁盤數據塊的函數。因此,可阻塞函數不能用軟中斷來實現。但是它們往往又具有可延遲的特性。
因此在 2.6 版的內核中出現了在內核態運行的工作隊列(替代了 2.4 內核中的任務隊列)。它也具有一些可延遲函數的特點(需要被激活和延後執行),但是能夠能夠在不同的進程間切換,以完成不同的工作。
參考文獻
- 陳香蘭老師《Linux內核源代碼導讀》講義
- Understanding the Linux Kernel, Third Edition
- Interrupt in Linux (硬件篇)
- Linux 中斷處理淺析
- 超強的 Linux 中斷分析
- 中斷描述符表
- 中斷的硬件環境
- 軟中斷/tasklet/工作隊列