1. 程式人生 > >深入理解 Linux 核心---中斷和異常

深入理解 Linux 核心---中斷和異常

中斷或異常會改變處理器執行的指令順序。

異常:

  • 來源:CPU 控制單元,
  • 時機:只有在一條指令終止執行後 CPU 才會發出中斷。
  • 原因:程式產生錯誤,或核心必須處理的異常條件。

中斷:

  • 來源:間隔定時器或 I/O 裝置。
  • 時機:隨機產生。
  • 原因:依照 CPU 時鐘訊號。

中斷訊號的作用

為什麼要引入中斷訊號?因為中斷信可使得處理器轉而去執行正常控制流之外的程式碼。

當中斷訊號到來時,CPU 需進行切換。在核心態堆疊儲存程式計數器的當前值(eip、cs),並把中斷型別相關的地址放入程式計數器(eip、cs)。

中斷處理與程序切換的明顯差異:中斷或異常處理程式執行的程式碼不是程序,而是一個核心控制路徑,比程序“輕”(中斷的上下文很少,建立或終止中斷處理所需的時間很少)。

中斷處理滿足如下約束:

  • 為了儘快處理完中斷,需儘量推遲更多的處理。
  • 允許中斷巢狀。
  • 在核心程式碼的某些臨界區,中斷處理程式以關中斷方式執行,但這種情況儘量少。

中斷和異常

中斷型別:

  • 可遮蔽中斷
  • 非遮蔽中斷

異常型別:

  • 處理器探測異常
  • 故障,eip 儲存引起故障的指令地址。
  • 陷阱,eip 儲存隨後要執行的指令的地址。
  • 終止,eip 不儲存值。
  • 程式設計異常,軟中斷,2種用途:執行系統呼叫;向除錯程式通報一個特定事件。

每個中斷和異常由 0~255 之間的數標識。Intel 將該 8 位無符號整數稱為向量。非遮蔽中斷和異常的向量固定,可遮蔽中斷的向量可通過對中斷控制器的程式設計改變。

IRQ 和中斷

每個能發出中斷請求的硬體裝置都有一條 IRQ(Interrupt ReQuest) 輸出線,IRQ 線與 PIC(Programmable Interrpt Controuer, 可程式設計中斷控制器)的輸入引腳相連。PIC 執行下列動作:

  • 監視 IRQ 線是否產生訊號。如有多條 IRQ 線產生訊號,選擇引腳編號較小的。
  • 如果監視到訊號:
    • 將訊號轉換為對應的向量。
    • 將向量放入 PIC 的一個 I/O 埠,CPU 便可通過資料匯流排讀該向量。
    • 將訊號送到處理器的 INTR 引腳,引發中斷。
    • 等待,直到 CPU 確認該訊號(通過將其寫入 PIC 的 I/O 埠);清除 INTR 線。
  • 返回第 1 步。

第一條 IRQ 通常為 IRQ0,與 IRQn 關聯的 Intel 的預設向量是 n+32,PIC 可修改 IRQ 和向量之間的對映。

可對 PIC 程式設計從而禁止 IRQ,禁止的中斷不會丟失,一旦被啟用,PCI 又把它們傳送到 CPU,這允許中斷處理程式逐次處理同一型別的 IRQ。

eflags 暫存器的 IF 標誌被清 0 時,PCI 釋出的可遮蔽中斷會被 CPU 暫時忽略。cli 和 sti 分別清除、設定 IF 標誌。

高階 PIC(Advanced PIC,APIC)

當系統包含多個 CPU 時,需要能把中斷傳遞給每個 CPU。因此 APIC 取代了 PIC。

每個 CPU 都有一個本地 APIC,每個本地 APIC 有 32 位的暫存器、一個內部時鐘、一個本地定時裝置、額外的兩條 IRQ 線 LINT0 和 LINT1(為本地 APIC 中斷保留的)。

本地 APIC 連線到一個外部 IO APIC,形成一個多 APIC 系統。

在這裡插入圖片描述

I/O APIC 與 IRQ 引腳不同,中斷優先順序不與引腳關聯,中斷重定向表的每一項可被單獨指明中斷向量和優先順序。

來自外部硬體的中斷請求在可用 CPU 之間的分發方式:

  • 靜態分發。中斷會傳遞給一個特定的 CPU,或一組 CPU,或所有 CPU。
  • 動態分發。中斷會傳遞給當前執行最低優先順序程序的 CPU,如果有多個 CPU 滿足,則使用仲裁技術分配。

多 APIC 系統還允許 CPU 產生處理器間中斷(InterProcessor Interrupt,IPI)。

異常

核心必須為每種異常提供一個專門的異常處理程式。對於某些異常,CPU 控制單元在執行異常處理程式前會產生一個硬體出錯碼,並壓入核心堆疊。

中斷描述符表(Interrupt Descriptor Table,IDT)

每一項對應一箇中斷或異常向量,每個向量由 8 個位元組組成。

IDT 中有中斷或異常處理程式的入口地址。

idtr CPU 暫存器指定 IDT 的線性基址及最大長度。lidt 彙編指令初始化 idtr。

IDT 包含三種類型的描述符:

  • 任務門。中斷訊號發生時,存放新程序的 TSS 選擇符。
  • 中斷門。處理中斷。處理器會清 IF 標誌,從而關閉可能會發生的可遮蔽中斷。
  • 陷阱門。處理異常。不修改 IF 標誌。

Linux 相對於 Intel 多了系統門、系統中斷門。

中斷和異常的硬體處理

假設核心已被初始化,CPU 在保護模式下執行。

在處理下一條指令時,控制單元會檢查在執行前一條指令時是否發生了一箇中斷或異常,如果發生,控制單元執行下列操作:

  • 確定中斷或異常關聯的向量 i(0~255)。
  • 讀 idtr 暫存器指向的 IDT 表的第 i 項(假定包含一箇中斷門或陷阱門)。
  • 從 gdtr 獲得 GDT 的基地址,在 GDT 中查詢 IDT 表第 i 項中選擇符標識的段描述符。該描述符指定中斷或異常處理程式所在段的基地址。
  • 確信中斷是由授權的中斷髮生源發出的。如果 CPL(cd 暫存器的低兩位)> 段描述符(GDT 中)的描述符特權級,則產生異常,因為說明引起中斷的處理程式的特權>中斷處理程式的特權 。對於程式設計異常,還需比較 CPL 與 IDT 中的門描述符 DPL,大於則產生異常,可避免使用者程式訪問特殊的陷阱門或中斷門。
  • 檢查是否發生特權級的變化,即 CPU 不等於當前段描述符的 DPL。如果是,控制單元必須使用與新特權級相關的棧。
    – 讀 tr 暫存器,訪問執行程序的 TSS 段。
    – 將 TSS 中新特權級相關的棧段、棧指標裝載 ss、esp 暫存器。
    – 新的棧中儲存 ss、esp 以前的值。
  • 如果產生故障,用引起異常的指令的地址裝載 cs 和 eip 暫存器。
  • 將 eflags、cs 及 eip 的內容儲存到棧中。
  • 如果異常產生了一個硬體出錯碼,儲存到棧中。
  • 用 IDT 表中第 i 項門描述符的段選擇符和偏移量欄位裝載 cs 和 eip 暫存器,為中斷或異常處理程式的第一條指令的邏輯地址。

總結:確定異常、中斷向量;許可權、特權檢查;針對不同型別的異常、中斷,儲存不同的內容;將從異常、中斷向量得到的中斷或異常處理程式地址裝入 cs、eip 暫存器。

中斷或異常處理完後,處理程式產生 iret 指令,控制權交給被中斷的程序,迫使控制單元:

  • 用儲存在棧中的值裝載 cs、eip 或 eflags 暫存器。如果一個硬體碼被壓入棧,並在 eip 上方,執行 iret 前彈出。
  • 檢查處理程式的 CPL 是否等於 cs 中低兩位,如果是,則 iret 終止;否則,轉入下一步。
  • 返回到與就特權級相關的棧,用棧中內容裝載 ss 和 esp 暫存器。
  • 檢查 ds、es、fs 及 gs 段暫存器的內容,如果其中一個包含的選擇符是段描述符,且其 DPL 小於 CPL,清相應的段暫存器,可禁止使用者態程式(CPL=3)利用以前所用的段暫存器(DPL=0)。

總結:彈出儲存在棧中的內容;根據特權級變化決定是否返回棧;清相關段暫存器,防止使用者惡意訪問核心空間。

中斷和異常處理程式的巢狀執行

在這裡插入圖片描述

核心控制路徑巢狀必須付出代價,那就是中斷處理程式執行期間不能發生程序切換。因為巢狀的核心控制路徑恢復執行時需要的所有資料都存在核心態堆疊中,而該堆疊屬於當前程序。

大多數異常在 CPU 處於使用者態時發生,發生在核心態的唯一異常是缺頁異常。缺頁異常不會進一步引起異常,所以至多兩個核心控制路徑堆疊。

與異常不同,I/O 裝置產生的中斷不引用當前程序的專有資料資料結構,因為中斷髮生時,無法預測哪個程序將會執行。

中斷處理程式可搶佔中斷處理程式、異常處理程式;而異常處理程式只能搶佔異常處理程式。中斷處理程式從不執行可導致缺頁的操作。

Linux 交錯執行核心控制路徑的原因:

  • 提供 PIC 和裝置控制器的吞吐量。
  • 實現一組沒有優先順序的中斷模型。多 CPU 系統中,核心控制路徑可併發執行。異常先在一個 CPU 上執行,然後由於程序切換移到另一個 CPU。

初始化中斷描述符表

核心啟用中斷前,必須把 IDT 表的初始地址裝到 idtr 暫存器,並初始化表中的每一項。IDT 的初始化必須小心,中斷門或陷阱門描述符的 DPL 欄位需設定成 0。

少數情況下,使用者態程序必須能發出一個程式設計異常,執行把中斷或陷阱門描述符的 DPL 欄位設定成 3。

真實模式中,IDT 被初始化並被 BIOS 使用。一旦 Linux 接管,IDT 被移到 RAM 的另一個區域,進行二次初始化,因為 Linux 不使用 BIOS。

IDT 放在 idt_table 表中,共 256 個表項。

6 位元組的 idt_descr 變數指定了 IDT 的大小和地址。

核心初始化過程中,setup_idt() 組合語言函式用同一個中斷門(ignore_int())填充所有 idt_table 表項。

setup_idt:
	lea ignore_int, %edx
	movl $(_ _ KERNEL_CS << 16), %eax   // cs
	movw %dx, %ax                       // 中斷門,dpl = 0
	lea idt_table, %edi   
	mov $256, %ecx                      // 迴圈次數
rp_sidt:                                // 迴圈 256 次
	movl %eax, (%edi)
	movl %edx, 4(%edi)
	addl $8, %edi
	dec %ecx
	jne rp_sidt
	ret

預初始化後,核心將在 IDT 中第二次初始化,用有意義的陷阱和中斷處理程式代替空處理程式。

異常處理

異常發生時,核心向引起異常的程序傳送一個訊號,向它通知一個反常條件,該程序採取必要步驟來恢復或終止執行。

Linux 利用 CPU 異常有效管理硬體資源的兩種情況:

  • 儲存和載入 FPU、MMX 和 XMM 暫存器。
  • 缺頁異常。

異常處理標準結構:

  • 將大多數暫存器的內容儲存在核心堆疊(彙編)。
  • C 函式處理異常。
  • ret_from_exception() 從異常處理程式中返回。

trap_init() 將一些處理異常的函式插入到 IDT 的非遮蔽中斷及異常表項中。

“Double fault”異常表示核心有嚴重的非法操作,所以其處理是通過任務門而不是陷阱門或系統門完成的。

儲存暫存器的值

handler_name:                // 代指異常處理程式的名字
	pushl $0                 // 異常發生時,如果控制單元沒有自動地把一個硬體出錯碼壓棧,則執行該語句
	pushl $do_handler_name   // 將 C 函式的地址壓棧
	jmp error_code           // 對所有的異常處理程式都相同,除了“Device not availabler”異常

error_code:
	將 C 函式可能用到的暫存器壓棧
	cld 清 elfags 的方向標誌 DF,使得 edi 和 esi 暫存器的值自動增加
	將棧中 esp+36 處的硬體出錯碼拷貝到 edx,將棧中該位置置 -1,以將 0x80 異常與其他異常隔離開
	將棧中 esp+32 處的 do_handler_name() 的地址裝入 edi 暫存器,將 es 中內容寫入棧中 esp+32 處
	將核心棧的棧頂拷貝到 eax 暫存器。該地址中存放第 1 步儲存的最後一個暫存器的值
	把使用者資料段的選擇符拷貝到 ds 和 es 暫存器
	呼叫 edi 中的 C 函式
	addl $8 %esp
	jmp ret_from_exception  // 離開異常處理程式

異常處理程式 C 函式

異常處理程式的 C 函式名的字首為 do_,一般可描述為:

current->thread.error_code = error_code;  // 把硬體出錯碼儲存在當前程序的描述符中
current->thread.trap_no = vector;         // 把異常向量儲存在當前程序的描述符中
force_sig(sig_number, current);           // 向當前程序傳送一個訊號

異常處理程式一終止,當前程序就關注該訊號。訊號可能在使用者態由程序自己的訊號處理程式處理,也可能由核心處理(一般殺死該程序)。

異常處理程式會檢查異常發生在使用者態還是核心態。如果是核心態,檢查是否由系統的無效引數引起,如果是,呼叫 die() 函式列印所有 CPU 暫存器的內容,並呼叫 do_exit() 終止當前程序。

從異常處理程式返回

addl $8 %esp
jmp ret_from_exception

中斷處理

核心只要給引起異常的程序傳送一個訊號就能處理大多數異常,但不適用於中斷。

中斷的處理依賴於中斷型別:

  • I/O 中斷,相應的中斷處理程式必須查詢裝置以決定如何處理。
  • 時鐘中斷,大部分作為 I/O 中斷處理。
  • 處理器間中斷。

IO 中斷處理

PCI 匯流排的體系結構中,幾個裝置可共享同一個 IRQ 線,所以不能僅靠中斷向量確定中斷源。

中斷處理程式有兩種實現:

  • IRQ 共享。中斷處理程式執行多箇中斷服務例程(ISR),每個 ISR 與單獨的裝置相關。產生 IRQ 時,每個 ISR 都被執行。
  • IRQ 動態分配。一條 IRQ 線在需要的時候才與一個裝置關聯。

Linux 把緊隨中斷執行的操作分為三類:

  • 緊急的。在關中斷的情況下儘快執行。
  • 非緊急的。在開中斷的情況下儘快執行。
  • 非緊急可延遲的。由獨立的函式執行,通過“軟中斷及 tasklet”方式執行。

所有的 I/O 中斷處理程式執行 4 個相同的基本操作:

  • 將 IRQ 值和暫存器內容壓入核心態堆疊。
  • 為正給 IRQ 線服務的 PCI 傳送應答,允許 PCI 進一步發出中斷。
  • 執行共享該 IRQ 的所有 ISR。
  • 跳到 ret_from_intr() 後終止。

在這裡插入圖片描述

中斷向量

物理 IRQ 可分配給 32~238 範圍內的任何向量。128 用於系統呼叫。

IBM PC 相容的體系結構要求,一些裝置必須被靜態地連線到指定的 IRQ 線。

為 IRQ 可配置裝置選擇一條線的三種方式:

  • 設定硬體跳接器(僅適用於舊式裝置卡)。
  • 安裝裝置時執行一個程式,可讓使用者選擇 IRQ 號。
  • 系統啟動時啟動一個硬體協議。

核心必須在啟用中斷前發現 IRQ 號與 I/O 裝置之間的對應,該對應時在初始化每個裝置驅動程式時建立。

IRQ 資料結構
在這裡插入圖片描述

每個中斷向量都有自己的 irq_desc_t 描述符,存放於 irq_desc 陣列。

irq_desc_t 描述符:

  • handler,指向 PIC 物件(hw_irq_controller 描述符)。面向物件的表達方式。
  • handler_data,指向 PIC 方法使用的資料。
  • action,要呼叫的中斷服務例程。指向 IRQ 的 irqaction 描述符連結串列的第一個元素。
  • status,IRQ 線狀態標誌。系統初始化期間,init_IRQ() 設定為 IRQ_DISABLED,然後呼叫 setup_idt() 建立中斷門。
  • depth,與 status=IRQ_DISABLED 表示 IRQ 線是否被禁用。
  • irq_count,中斷的次數
  • irqs_unhandled,意外中斷的次數,超過某值時,核心禁用該條 IRQ 線。
  • lock,用於序列訪問 IRQ 描述符和 PIC 的自旋鎖。

irq_desc_t 描述符中的 handler 欄位:

 // hw_interrupt_type 也叫 hw_irq_controller 描述符。假設使用 8259A 晶片。
struct hw_interrupt_type i8259A_irq_type = {   
	.typename = "XT-PIC",           // PCI 的名字

	// 用於對 PCI 程式設計的 6 個函式指標
	.startup = startup_8259A_irq,   // 啟動晶片的 IRQ 線
	.shutdown = shutdown_8259A_irq, // 關閉晶片的 IRQ 線
	.enable = enable_8259A_irq,     // 啟用 IRQ 線
	.disable = disable_8259A_irq,   // 禁用 IRQ 線
	.ack = mask_and_ack_8259A,      // 通過把適當的位元組發往 8259A I/O 埠來應答所接收的 IRQ。
	.end = end_8259A_irq,           // 在 IRQ 的中斷處理程式終止時被呼叫。

	.set_affinity = NULL            // 特定 IRQ 所在 CPU 的親和力,即哪些 CPU 用來處理特定的 IRQ。
};

irq_desc_t 描述符中的 action 欄位,irqaction 描述符:

  • handler,指向一個 I/O 裝置的中斷服務例程。允許多個裝置共享同一 IRQ。
  • flags,IRQ 與 I/O 裝置之間的關係。取值:SA_INTERRUPT,SA_SHIRQ,SA_SAMPLE_RANDOM。
  • mask,未使用。
  • name,I/O 裝置名。
  • dev_id,標識 I/O 裝置。
  • next,指向 irqaction 描述符連結串列的下一個元素。連結串列中的元素指向共享同一 IRQ 的硬體裝置。
  • irq,IRQ 線。
  • dir,指向與 IRQn 相關的 /proc/irq/n 目錄的描述符。

irq_stat 陣列包含 NR_CPUS 個元素,每個元素對應一個 CPU,每個元素型別為 irq_cpustat_t,其包含的欄位為:

  • __softirq_pending,掛起的軟中斷。
  • idle_timestamp,CPU 變為空閒的時間。
  • __nmi_count,NMI 中斷髮生的次數。
  • apic_timer_irqs,本地 APIC 時鐘中斷髮生的次數。

IRQ 在多處理器系統上的分發

多 APIC 系統有複雜的機制在 CPU 之間動態分發 IRQ 訊號。

系統啟動過程中,引導 CPU 執行 setp_IO_APIC_irqs() 來初始化 I/O APIC 晶片。所有 CPU 執行 setup_local_APIC() 初始化本地 APIC,每個晶片的任務優先順序暫存器(TPR)初始化為固定值,即所有 CPU 有相同的優先順序。

系統啟動後,多 APIC 系統使用本地 APIC 仲裁暫存器中的值,該值每次中斷後自動改變,使得 IRQ 訊號公平地在所有 CPU 之間分發。

以上步驟不能保證 IRQ 在 CPU 間公平分發時,Linux 用 kirqd 核心執行緒進行糾正。

kirqd:

  • set_ioapic_affinity_irq(被重定向的 IRQ 向量,接收該 IRQ 的 CPU)
  • do_irq_balance() 週期性執行

多種型別的核心棧

每個程序的 thread_info 描述符與 thread_union 結構中的核心棧緊鄰,如果 thread_union 大小為 4KB,核心使用 3 種類型的核心棧:

  • 異常棧。與程序關聯。
  • 硬中斷請求棧。與 CPU 關聯,佔一個單獨的頁框。
  • 軟中斷請求棧。與 CPU 關聯,佔一個單獨的頁框。

hardirq_stack 陣列:硬中斷請求。
softirq_stack 陣列:軟中斷請求。

兩個陣列的元素都為 irq_ctx 型別。irq_ctx 跨一個單獨的頁框,thread_info 在該頁的底部,其餘空間為棧。

hardirq_ctx、softirq_ctx 陣列可使核心快速確定 CPU 的硬中斷請求棧和軟中斷請求棧,元素為 irq_ctx 指標型別。

為中斷處理器儲存暫存器的值

arch/i386.kernel/entry.S 用匯編建立 interrupt 陣列,陣列包括 NR_IRQS 個元素。陣列中索引為 n 的元素存放如下組合語言的指令地址:

pushl $n-256    // 負數表示中斷,正數表示系統呼叫
jmp common_interrupt 
// 對所有的中斷處理程式執行相同的程式碼
common_interrupt:
	SAVE_ALL   // 儲存暫存器
	movl %esp, %eax
	call do_IRQ
	jmp ret_from_intr

// 巨集,在棧中儲存中斷處理程式可能會用到的所有 CPU 暫存器
// 但 eflags、cs、eip、ss、esp 由控制單元自動儲存
// SAVE_ALL 展開:
cld
push %es
push %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
movl $_ _ USER_DS, %edx  
movl %edx, %ds                   // 把使用者資料段的選擇符裝到 ds 和 es 暫存器
movl %edx, %es

然後將棧頂儲存到 eax 暫存器,中斷處理程式呼叫 do_IRQ() 函式。

do_IRQ 函式

do_IRQ() 執行某個中斷的所有中斷服務例程,宣告:

// regparam 表示函式到 eax 暫存器找引數 regs 的值
__attribut__((regparm(3))) unsigned int do_IRQ(struct pt_regs *regs) 

do_IRQ():

  1. 執行 irq_enter() 巨集 ,遞增中斷處理程式巢狀數量的計數器,計數器儲存在當前程序 thread_info 的 preempt_count 欄位。
  2. 如果 thread_union 大小為 4KB,函式切換到硬中斷請求棧:
    – 2.1. 執行 current_thread_info() 函式,獲取核心棧關聯的 thread_info 描述符地址。
    – 2.2. 將 thread_info 描述符地址與 hardirq_ctx[smp_processor_id()] 中的地址比較。相等,則說明核心已使用硬中斷請求棧,發生中斷巢狀,跳到 3。
    – 2.3. 切換核心棧。儲存當前程序描述符指標,該指標位於本地 CPU 的 irq_ctx 中的 thread_info 的 task 欄位。
    – 2.4. 把 esp 內容存入本地 CPU 的 irq_ctx 的 thread_info 的 previous_esp 欄位。
    – 2.5. 把本地 CPU 硬中斷請求棧的棧頂(= hardirq_ctx[smp_processor_id()] + 4096) 裝入 esp 暫存器;以前的 esp 值裝入 ebx 暫存器。
  3. 呼叫 __do_IRQ() 函式,把指標 regs 和 regs->orig_eax 欄位中的中斷號傳遞給該函式。
  4. 如果在 2.5 中切換到硬中斷請求棧,函式把 ebx 暫存器中的原始棧指標拷貝到 esp 暫存器,回到原來的異常棧後軟中斷請求棧。
  5. 執行 irq_exit() 巨集,遞減中斷計數器並檢查是否有可延遲函式正等待執行。
  6. 結束:ret_from_intr() 函式。

__do_IRQ() 函式

__do_IRQ() 引數為 IRQ 號(eax)和指向 pt_regs 結構的指標(edx)

__do_IRQ():

spin_lock(&irq_desc[irq].lock));  // 在訪問主 IRQ 描述符前,核心獲得相應的自旋鎖,避免 CPU 併發訪問
irq_desc[irq].handler->ack(irq)   // 遮蔽 IRQ 線,確保該中斷處理程式結束前,CPU 不進一步接受該種中斷,但可能被別的 CPU 接受
irq_desc[irq].status &= ~(IRQ_REPLAY | IRQ_WAITING);  // 清除 IRQ_REPLAY 和 IRQ_WAITING 標誌
irq_desc[irq].status |= IRQ_PENDING;  // 表示中斷已被應答,但還沒處理

// IRQ_DISABLED:即使 IRQ 線被禁止,CPU 也可能執行 __do_IRQ() 函式,挽救丟失的中斷
// IRQ_INPROGRESS:另一個 CPU 可能處理同一個中斷的前一次出現,將本次中斷推遲到那個 CPU
// irq_desc.action 為 NULL:中斷沒有相關的中斷服務例程
if (!(irq_desc[irq].status & ( IRQ_DISABLED| IRQ_INPROGESS)) && irq_desc[irq].action)  
{
	irq_desc[irq].status |= IRQ_INPROGRESS;  
	do
	{
		irq_desc[irq].status &= ~IRQ_PENDING;  // 清 IRQ_PENDING 標誌,表示開始正式處理中斷
		spin_unlock(&irq_desc[irq].lock));     // 釋放中斷自旋鎖
		handle_IRQ_event(irq, regs, irq_desc[irq].action);  // 執行中斷服務例程
		spin_lock(&irq_desc[irq].lock);        // 再次獲得自旋鎖
	}while(irq_desc[irq].status] & IRQ_PENDING);  // 檢查 IRQ_PENDING 標誌

	irq_desc[irq].handler->end(irq);     // 準備終止
	spin_unlock(&(irq_desc[irq].lock));  // 釋放自旋鎖
}

挽救丟失的中斷

多 APIC 系統,CPU 在應答中斷前,該 IRQ 線被另一個 CPU 遮蔽,導致中斷丟失,可通過 enable_irq() 函式挽救。

enable_irq():

spin_lock_irqsave(&(irq_desc[irq].lock), flags);

if(--irq_desc[irq].depth == 0)
{
	irq_desc[irq].status &= ~IRQ_DISABLED;

	// 通過檢查 IRQ_PENDING 標誌檢測到一箇中斷丟失
	// 離開中斷處理程式時,該標誌總置為0
	// 因此,如果 IRQ 線被禁止,且該標誌被設定,則中斷出現但未被處理
	if(irq_desc[irq].status & (IRQ_PENDING | IRQ_REPLAY)) == IRQ_PENDING)  
	{
		irq_desc[irq].status |= IRQ_REPLAY;         // 確保只產生一個自我中斷
		hw_resend_irq(irq_desc[irq].handler, irq);  // 強制本地 APIC 產生一個自我中斷
	}

	irq_desc[irq].handler->enable(irq);
}
spin_lock_irqrestore(&(irq_desc[irq].lcok), flags);

中斷服務例程 ISR

ISR 呼叫 handle_IRQ_event() 函式

  1. 如果 SA_INTERRUPT 標誌清 0,用 sti 指令啟用本地中斷。
  2. 執行每個中斷的 ISR。
retval = 0
do {
	// action 指向 irqaction 連結串列的開始,irqaction 表示接受中斷後要採取的操作
	// irq:IRQ 號,允許一個單獨的 ISR 處理幾條 IRQ 線
	// dev_id:裝置識別符號,允許一個單獨的 ISR 照顧幾個同類型的裝置
	// regs:指向核心(異常)棧的 pt_regs 的指標,棧種包含中斷後隨機儲存的暫存器
	// regs 允許 ISR 訪問被中斷的核心控制路徑的執行上下文。
	retval |= action->handler(irq, action->dev_id, regs); 

	action = action->next;
}while(action);
  1. cli 指令禁止本地中斷。
  2. 返回 retval。即如果沒有 ISR,返回 0,否則返回 1。

IRQ 線的動態分配

通過將一些裝置的活動序列化,使得同一條 IRQ 線可讓幾個硬體裝置使用,以便一次只能有一個裝置擁有該 IRQ 線。

大體流程:

  • request_irq():由使用 IRQ 線的裝置的驅動程式呼叫。建立一個新的 irqaction 描述符,並用引數初始化。
  • setup_irq():把描述符插入到合適的 IRQ 連結串列。如果返回一個出錯碼,裝置驅動程式終止操作,因為 IRQ 線已被另一個裝置使用。
  • free_irq():裝置操作結束時,從 IRQ 連結串列中刪除該描述符,並釋放相應的記憶體區。

詳細流程,以訪問軟盤為例:

  1. request_irq(6, floppy_interrupt, SA_INTERRUPT|SA_SAMPLE_RANDOM, “floppy”, NULL);
    floppy_interrupt:中斷服務例程。
    SA_INTERRUPT:關中斷。
    SA_SHIRQ:不共享 IRQ。
    SA_SAMPLE_RANDOM:對軟盤的訪問是核心用於產生隨機數的一個較好的隨機事件源。

  2. setup_irq(),引數為 irq_nr(IRQ 號)和 new(剛分配的 irqaction 描述符的地址)。

  • 檢查 irq_nr 是否被使用,如果是,檢查兩個裝置的 irqaction 描述符中的 SA_SHIRQ 標誌是否都指定 IRQ 線能被共享,如果不能,返回出錯碼。
  • 把 *new 加到由 irq_desc[irq_nr]->action 指向的連結串列末尾。
  • 如果沒有其他裝置共享 irq_nr,清 *new 的 flags 的相關標誌,並呼叫 irq_desc[irq_nr]->handler PIC 物件的 startup 方法啟用 IRQ 訊號。
  1. free_irq(6, NULL);
    驅動程式釋放 IRQ6。

處理器間中斷處理

處理器間中斷(IPI)不通過 IRQ 線傳輸,而是作將訊號直接放在連線所有 CPU 本地 APIC 的總線上。

Linux 定義了 3 種處理器間中斷:

  • CALL_FUNCTION_VECTOR(向量 0xfb)。傳送其他所有 CPU,強制它們執行傳遞過來的函式。
  • RESCHEDULE_VECTOR(向量 0xfc)。一個 CPU 接收該型別的中斷後,處理程式只能自己應答中斷。從中斷返回後,所有排程自動進行。
  • INVALIDATE_TLB_VECTOR(向量 0xfd)。發往所有其他 CPU,強制它們的 快表(TLB)無效。處理程式重新整理某些 TLB 表項。

BUILD_INTERRUPT:組合語言,處理 IPI。儲存暫存器,從棧頂壓入向量號減256的值,然後呼叫 C 函式,其名字為低階處理程式的名字加字首 smp_。C 函式應答本地 APIC 上的 IPI,然後指向由中斷觸發的特定操作。

軟中斷及 tasklet

把可延遲中斷從中斷處理程式中抽出來有助於核心保持較短的響應時間。

非緊迫、可中斷的核心函式的應對方式:

  • 可延遲函式(軟中斷與 tasklets)
  • 通過工作佇列執行的函式

tasklet 在軟中斷之上實現。

軟中斷是靜態的,而 tasklet 的分配和初始化可在執行時進行。

軟中斷可併發地執行在多個 CPU 上,因此必須是可重入函式且使用自旋鎖。而 tasklet 函式不必是可重入的。

一般,可延遲函式執行 4 種操作:

  • 初始化。定義新一個的可延遲函式,一般在核心初始化或載入模組時進行。
  • 啟用。將一個可延遲函式標記為“掛起”,以便在核心對可延遲函式的下一輪排程中執行。
  • 遮蔽。有選擇地遮蔽一個可延遲函式,即便啟用,核心也不執行。
  • 執行。執行一個掛起的可延遲函式和同型別的其他所有掛起的可延遲函式。

軟中斷

軟中斷所使用的資料結構

softirq_vec 陣列表示軟中斷,包含型別為 softirq_action 的 32 個元素,優先順序為陣列下標,只有前 6 個元素被使用。

softirq_action 欄位:

  • action,指向軟中斷函式的指標。
  • data,指向軟中斷需要的通用資料結構的指標。

preempt_count 欄位跟蹤核心搶佔和核心控制路徑的巢狀,存放在 thread_info 欄位中。

preempt_count 欄位:

  • 位 0~7,搶佔計數器,顯式禁用本地 CPU 核心搶佔的次數,0 表示允許核心搶佔。
  • 位 8~15,軟中斷計數器,可延遲函式被禁用的程度,0 表示可延遲函式處於啟用態。
  • 位 16~27,硬中斷計數器,在本地 CPU 上中斷處理程式的巢狀數。

每個 CPU 都有 32 位掩碼(描述掛起的軟中斷),存放於 irq_cpustat_t 資料結構中。

處理軟中斷

open_softirq(軟中斷下標,指向要執行的軟中斷函式的指標,可能由軟中斷函式使用的資料結構的指標) 處理軟中斷的初始化

raise_softirq(軟中斷下標 nr) 啟用軟中斷:

  1. local_irq_save 巨集儲存 eflags 暫存器的 IF 標誌,禁用本地 CPU 上的中斷。
  2. 把軟中斷標記為掛起狀態,通過設定本地 CPU 的軟中斷掩碼中與 nr 相關的位實現。
  3. in_interrupt() 產生的值為 1,跳到 5,說明:在中斷上下文呼叫了 raise_softirq(),或當前禁用了軟中斷;否則,跳到 4。
  4. 需要時呼叫 wakeup_softirqd() 喚醒本地 CPU 的 ksoftirqd 核心執行緒。
  5. local_irq_restore 巨集恢復第 1 步保持的 IF 標誌。

在幾個特殊的檢查點上執行 raise_softirq() 函式。

  • 啟用本地 CPU 的軟中斷時。
  • 完成中斷處理時,包括 I/O 中斷、本地定時器中斷、處理器間中斷。
  • 一個特殊的 ksoftirqd/n 核心執行緒被喚醒時。

do_softirq() 函式

檢測到掛起的軟中斷時,呼叫 do_softirq() 。

  1. in_interrupt() 產生值 1,返回。說明:在中斷上下文中呼叫了 do_softirq() 函式,或當前禁用軟中斷。
  2. local_irq_save 儲存 IF 標誌,禁用本地 CPU 上的中斷。
  3. 如果 thread_union 的大小為 4KB,需要時切換到軟中斷請求棧。
  4. 呼叫__do_softirq() 函式。
  5. 如果第 3 步已切換到軟中斷請求棧,把最初的棧指標恢復到 esp 暫存器,切換回以前的異常棧。
  6. local_irq_restore 恢復儲存的 IF 標誌,返回。

__do_softirq() 函式

讀取本地 CPU 的軟中斷掩碼,執行與每個設定位相關的可延遲函式。

為避免執行時間過長,只做固定次數的迴圈,其餘掛起的中斷在 ksoftirqd 核心執行緒中處理。

  1. 迴圈計數器 = 10。
  2. pending = 本地 CPU 軟中斷位掩碼。
  3. local_bh_disable() 使軟中斷計數器值加 1,禁用可延遲函式。
  4. 清本地 CPU 軟中斷位掩碼,以便可啟用新的軟中斷。
  5. local_irq_enable() 啟用本地中斷。
  6. 根據 pending 每一位的設定,執行對應的軟中斷處理函式。軟中斷函式地址:softirq_vec[nr]->action。
  7. local_irq_disable() 禁用本地中斷。
  8. pending = 本地 CPU 軟中斷位掩碼,遞減迴圈計數器。
  9. if pending != 0,說明至少一個軟中斷被啟用,當迴圈計數器仍然是正數時,返回 4。
  10. 如果還有更多掛起的軟中斷,呼叫 wakeup_softirqd() 喚醒 ksoftirqd 核心執行緒處理。
  11. 軟中斷計數器減 1,可重新啟用可延遲函式。

ksoftirqd 核心執行緒

ksoftirqd/n 核心執行緒解決了難以平衡的問題:核心執行緒有較低的優先順序,因此使用者程式有機會執行;但機器空閒時,掛起的軟中斷很快被執行

for(;;) {
	// 如果沒有掛起的軟中斷,將當前程序狀態設定為 TASK_INTERRUPTIBLE
	set_current_state(TASK_INTERRUPTIBLE);  

	schedule();

	// 檢查 local_softirq_pend() 中的軟中斷位掩碼,必要時呼叫 do_softirq()
	while(local_softirq_pending()) {
		preempt_disable();

		// 確定哪些軟中斷是掛起的,然後執行這些軟中斷函式。
		// 如果已經執行的軟中斷又被啟用,do_softirq() 喚醒核心執行緒並終止。
		do_softirq();  

		preempt_enable();

		// thread_info 的 TIF_NEED_RESCHED 標誌被設定,呼叫 cond_resched() 切換程序
		cond_resched();
	}
}

tasklet

tasklet 是 I/O 驅動程式中實現可延遲函式的首選方式。

tasklet 建立在兩個叫 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 的軟中斷之上。

資料結構

tasklet_vec:tasklet 陣列。
tasklet_hi_vec:高優先順序的 tasklet 陣列。

兩個陣列中元素個數為 NR_CPUS,元素的型別為 tasklet_struct *tasklet_head。

tasklet_struct,tasklet 描述符,欄位:

  • next,指向連結串列中下一個描述符。
  • state,taslet 的狀態,取值為 TASKLET_STATE_SCHED,TASKLET_STATE_RUN。
  • count,鎖計數器。
  • func,指向 tasklet 函式的指標。
  • data,無符號長整數,由 tasklet 函式使用。

初始化

tasklet_init (tasklet 描述符的地址,tasklet 函式的地址,tasklet 函式的可選整型引數) 初始化 tasklet_struct 資料結構。

遮蔽

tasklet_disable_nosync() 或 tasklet_disable() 選擇性地禁止 tasklet。增加 tasklet 描述符的 count 欄位,但後一個函式在 tasklet 函式結束後再返回。

啟用

tasklet_enable() 重新啟用 tasklet。

tasklet_schedule() 或 tasklet_hi_schedule() 根據不同的優先順序啟用 tasklet:

  1. 檢查 TASKLET_STATE_SCHED 標誌,如果設定說明 tasklet 已被排程,返回。
  2. local_irq_save 儲存 IF 標誌,禁用本地中斷。
  3. 在 tasklet_vec[n] 或 tasklet_hi_vec[n] 指向的連結串列的起始處增加 tasklet 描述符,n 為本地 CPU 邏輯號。
  4. raise_softirq_irqoff() 啟用 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 型別的軟中斷。
  5. local_irq_restore() 恢復 IF 標誌。

執行

啟用軟中斷後,do_softirq() 執行。與 HI_SOFTIRQ 軟中斷相關的軟中斷函式叫 tasklet_hi_action(),與 TASKLET_SOFTRIQ 相關的函式叫 tasklet_action(),這兩個函式執行下列操作:

  1. 禁用本地中斷。
  2. 獲得本地 CPU 的邏輯號 n。
  3. 把 tasklet_vec[n] 或 tasklet_hi_vec[n] 指向的連結串列地址存入區域性變數 list。
  4. tasklet_vec[n] = NULL 或 tasklet_hi_vec[n] = NULL。
  5. 開啟本地中斷。
  6. 對於 list 指向的連結串列中的每個 tasklet 描述符:
    6.1 在多 CPU 系統中,檢查 tasklet 的 TASKLET_STATE_RUN 標誌。
    – 6.1.1 如果被設定,說明同一型別的 tasklet 在另一個 CPU 上執行,將任務描述符重新插入 tasklet_vec[n] 或 tasklet_hi_vec[n] 指向的連結串列,再次啟用 TASKLET_SOFTIRQ 或 HI_SIFTIRQ 軟中斷,使得當同類型的其他 tasklet 在其他 CPU 上執行時,該 tasklet 被延遲。
    – 6.1.2 如果該標誌未被設定,設定該標誌,使得 tasklet 不能在其他 CPU 上執行。
    6.2 檢視 tasklet 描述符的 count 欄位,檢查 tasklet 是否被禁止。如果是,清 TASKLET_STATE_RUN 標誌,將任務描述符重新插入 tasklet_vec[n] 或 tasklet_hi_vec[n] 指向的連結串列,再次啟用 TASKLET_SOFTIRQ 或 HI_SIFTIRQ 軟中斷。
    6.3 如果 tasklet 被啟用,清 TASKLET_STATE_SCHED 標誌,執行 tasklet 函式。

工作佇列

工作佇列和可延遲函式的主要區別:可延遲函式執行在中斷上下文中,工作佇列中的函式執行在程序上下文中。執行可阻塞函式的唯一方式是在程序上下文中執行。

工作佇列的資料結構

workqueue_struct 描述符,包括一個有 NR_CPUS 元素的陣列,其中每個元素是 cpu_workqueue_struct 型別的描述符。

cpu_workqueue_struct 欄位:

  • lock
  • remove_sequence
  • insert_sequence
  • worklist,掛起連結串列的頭節點,連結串列元素為 work_struct
  • more_work,等待佇列,其中的工作執行緒處於睡眠狀態。
  • work_done
  • wq,指向 workqueue_struct 結構的指標。
  • thread
  • run_depth

work_struct 表示每一個掛起的函式,欄位:

  • pending
  • entry
  • func
  • data
  • wq_data,通常是指向 cpu_workqueue_struct 描述符的父節點指標。
  • timer

工作佇列函式

create_workqueue(“foo”) 返回新建立工作佇列的 workqueue_struct 描述符的地址。還會建立 n 個工作者執行緒。

create_singlethread_workqueue() 不管有多少個 CPU,只建立一個工作者執行緒。

destroy_workqueue() 撤銷工作佇列。引數為指向 workqueue_struct 陣列的指標。

queue_work(workqueue_struct * wq, work_struct * work) (封裝於 work_struct 描述符中)把函式插入工作佇列。

  1. 如果 work->pending == 1,說明函式已經在工作佇列中,結束。
  2. 將 work 加到工作佇列連結串列,work->pending = 1。
  3. 如果工作執行緒在本地 CPU 的 cpu_workqueue_struct 描述符的 more_work 等待佇列上睡眠,該函式喚醒這個執行緒。

queue_delayed_work() 接收一個以系統滴答數表示時間延遲的引數。

工作執行緒被喚醒後,呼叫 run_workqueue() 函式,從工作者執行緒的工作佇列連結串列中刪除所有 work_struct 描述符並執行相應的掛起函式。

flush_workqueue(workqueue_struct 描述符的地址) ,在工作佇列中的所有掛起函式結束之前使呼叫程序一直處於阻塞狀態。

預定義工作佇列

events:預定義工作佇列,避免為執行一個函式而建立整個工作者執行緒的開銷,所有核心開發者都可以隨意使用。

不應該使預定義工作佇列執行的函式長時間處於阻塞狀態,因為其中掛起的函式是在每個 CPU 上序列方式執行的。

從中斷和異常返回

在這裡插入圖片描述
入口點

ret_from_exception:
	cli    // 只有從異常返回時才使用 cli,禁用本地中斷
ret_from_intr:
	movl $-8192, %ebp  // 將當前 thread_info 描述符的地址裝載到 ebp 暫存器
	andl %esp, %ebp
	movl 0x30(%esp), %eax
	movb 0x2c(%esp), %al

	// 根據發生中斷或異常壓入棧中的 cs 和 eflags 暫存器的值,
	// 確定中斷的程式在中斷時是否執行在使用者態
	testl $0x0002003, %eax  
	jnz resume_userspace
	jpm resume_kernel

恢復核心控制路徑

rusume_kernel:
	cli
	cmpl $0, 0x14(%ebp)  // 如果 thread_info 描述符的 preempt_count 欄位為0(執行核心搶佔)
	jz need_resched      // 跳到 need_resched
restore_all:       // 否則,被中斷的程式重新開始執行
	popl %ebx
	popl %ecx
	popl %edx
	popl %esi
	popl %edi
	popl %ebp
	popl %eax
	popl %ds
	popl %es
	addl $4, %esp
	iret   // 結束控制

檢查核心搶佔

need_resched:
	movl 0x8(%ebp), %ecx
	testb $(1<<TIF_NEED_RESCHED), %cl  // 如果 current->thread_info 的 flags 欄位中的 TIF_NEED_RESCHED == 0,沒有需要切換的程序
	jz restore_all                     // 因此跳到 restore_all
	testl $0x00000200, 0x30(%ebp)      // 如果正在被恢復的核心控制路徑是在禁用本地 CPU 的情況下執行
	jz restore_all                     // 也跳到 restore_all,否則程序切換可能回破壞核心資料結構
	call preempt_schedule_irq          // 程序切換,設定 preempt_count 欄位的 PREEMPT_ACTIVE 標誌,大核心鎖計數器暫時設定為 -1,呼叫 schedule()
	jmp need_resched 

恢復使用者態程式

resume_userspace:
	cli  // 禁用本地中斷
	movl 0x8(%ebp), %ecx

	// 檢測 current->thread_info 的 flags 欄位,
	// 如果只設置了 TIF_SYSCALL_TRACE,TIF_SYSCALL_AUDIT 或 TIF_SINGLESTEP 標誌,
	// 跳到 restore_all
	andl $0x0000ff6e, %ecx
	je restore_all

	jmp work_pending

檢測重排程標誌

work_pending:
	testb $(1<<TIF_NEED_RESCHED), %cl
	jz work_notifysig
work_resched:
	call schedule  // 如果程序切換請求被掛起,選擇另外 一個程序執行
	cli
	jmp resume_userspace  // 當前面的程序要恢復時

處理掛起訊號、虛擬 8086 模式和單步執行

work_notifysig:
	movl %esp, %eax
	testl $0x00020000, 0x30(%esp)
	je 1f

// 如果使用者態程式 eflags 暫存器的 VM 控制標誌被設定
work_notifysig_v86:
	pushl %ecx
	call save_v86_state    // 在使用者態地址空間建立虛擬8086模式的資料結構
	popl %ecx
	movl %eax, %esp
1:
	xorl %edx, %edx
	call do_notify_resume  // 處理掛起訊號和單步執行
	jmp restore_all        // 恢復被中斷的程式