Linux 內核中斷內幕

原文網址:http://www.ibm.com/developerworks/cn/linux/l-cn-linuxkernelint/index.html

簡介: 本文對中斷系統進行了全面的分析與探討,主要包括中斷控制器、中斷分類、中斷親和力、中斷線程化與 SMP 中的中斷遷徙等。首先對中斷工作原理進行了簡要分析,接着詳細探討了中斷親和力的實現原理,最後對中斷線程化與非線程化中斷之間的實現機理進行了對比分析。

發佈日期: 2007 年 5 月 14 日
級別: 中級
訪問情況 : 15198 次瀏覽
評論: 2 (查看 | 添加評論 - 登錄)

平均分 4 星 共 39 個評分 平均分 (39個評分)
爲本文評分

什麼是中斷

Linux 內核需要對連接到計算機上的所有硬件設備進行管理,毫無疑問這是它的份內事。如果要管理這些設備,首先得和它們互相通信才行,一般有兩種方案可實現這種功能:

  1. 輪詢(polling 讓內核定期對設備的狀態進行查詢,然後做出相應的處理;
  2. 中斷(interrupt 讓硬件在需要的時候向內核發出信號(變內核主動爲硬件主動)。

第一種方案會讓內核做不少的無用功,因爲輪詢總會週期性的重複執行,大量地耗用 CPU 時間,因此效率及其低下,所以一般都是採用第二種方案 。註釋 1

從物理學的角度看,中斷是一種電信號,由硬件設備產生,並直接送入中斷控制器(如 8259A)的輸入引腳上,然後再由中斷控制器向處理器發送相應的信號。處理器一經檢測到該信號,便中斷自己當前正在處理的工作,轉而去處理中斷。此後,處理器會通知 OS 已經產生中斷。這樣,OS 就可以對這個中斷進行適當的處理。不同的設備對應的中斷不同,而每個中斷都通過一個唯一的數字標識,這些值通常被稱爲中斷請求線。

APIC vs 8259A

X86計算機的 CPU 爲中斷只提供了兩條外接引腳:NMI 和 INTR。其中 NMI 是不可屏蔽中斷,它通常用於電源掉電和物理存儲器奇偶校驗;INTR是可屏蔽中斷,可以通過設置中斷屏蔽位來進行中斷屏蔽,它主要用於接受外部硬件的中斷信號,這些信號由中斷控制器傳遞給 CPU。

常見的中斷控制器有兩種:

1. 可編程中斷控制器8259A

傳統的 PIC(Programmable Interrupt Controller)是由兩片 8259A 風格的外部芯片以“級聯”的方式連接在一起。每個芯片可處理多達 8 個不同的 IRQ。因爲從 PIC 的 INT 輸出線連接到主 PIC 的 IRQ2 引腳,所以可用 IRQ 線的個數達到 15 個,如圖 1 所示。


圖 1:8259A 級聯原理圖
8259A 級聯原理圖

2. 高級可編程中斷控制器(APIC)

8259A 只適合單 CPU 的情況,爲了充分挖掘 SMP 體系結構的並行性,能夠把中斷傳遞給系統中的每個 CPU 至關重要。基於此理由,Intel 引入了一種名爲 I/O 高級可編程控制器的新組件,來替代老式的 8259A 可編程中斷控制器。該組件包含兩大組成部分:一是“本地 APIC”,主要負責傳遞中斷信號到指定的處理器;舉例來說,一臺具有三個處理器的機器,則它必須相對的要有三個本地 APIC。另外一個重要的部分是 I/O APIC,主要是收集來自 I/O 裝置的 Interrupt 信號且在當那些裝置需要中斷時發送信號到本地 APIC,系統中最多可擁有 8 個 I/O APIC。

每個本地 APIC 都有 32 位的寄存器,一個內部時鐘,一個本地定時設備以及爲本地中斷保留的兩條額外的 IRQ 線 LINT0 和 LINT1。所有本地 APIC 都連接到 I/O APIC,形成一個多級 APIC 系統,如圖 2 所示。


圖 2:多級I/O APIC系統
多級I/O APIC系統

目前大部分單處理器系統都包含一個 I/O APIC 芯片,可以通過以下兩種方式來對這種芯片進行配置:

1) 作爲一種標準的 8259A 工作方式。本地 APIC 被禁止,外部 I/O APIC 連接到 CPU,兩條 LINT0 和 LINT1 分別連接到 INTR 和 NMI 引腳。

2) 作爲一種標準外部 I/O APIC。本地 APIC 被激活,且所有的外部中斷都通過 I/O APIC 接收。

辨別一個系統是否正在使用 I/O APIC,可以在命令行輸入如下命令:

# cat /proc/interrupts
           CPU0       
  0:      90504    IO-APIC-edge  timer
  1:        131    IO-APIC-edge  i8042
  8:          4    IO-APIC-edge  rtc
  9:          0    IO-APIC-level  acpi
 12:        111    IO-APIC-edge  i8042
 14:       1862    IO-APIC-edge  ide0
 15:         28    IO-APIC-edge  ide1
177:          9    IO-APIC-level  eth0
185:          0    IO-APIC-level  via82cxxx
...         
	

如果輸出結果中列出了 IO-APIC,說明您的系統正在使用 APIC。如果看到 XT-PIC,意味着您的系統正在使用 8259A 芯片。

中斷分類

中斷可分爲同步(synchronous)中斷和異步(asynchronous)中斷:

1. 同步中斷是當指令執行時由 CPU 控制單元產生,之所以稱爲同步,是因爲只有在一條指令執行完畢後 CPU 纔會發出中斷,而不是發生在代碼指令執行期間,比如系統調用。

2. 異步中斷是指由其他硬件設備依照 CPU 時鐘信號隨機產生,即意味着中斷能夠在指令之間發生,例如鍵盤中斷。

根據 Intel 官方資料,同步中斷稱爲異常(exception),異步中斷被稱爲中斷(interrupt)。

中斷可分爲可屏蔽中斷(Maskable interrupt)和非屏蔽中斷(Nomaskable interrupt)。異常可分爲故障(fault)、陷阱(trap)、終止(abort)三類。

從廣義上講,中斷可分爲四類:中斷故障陷阱終止。這些類別之間的異同點請參看 表 1。

表 1:中斷類別及其行爲
類別 原因 異步/同步 返回行爲
中斷 來自I/O設備的信號 異步 總是返回到下一條指令
陷阱 有意的異常 同步 總是返回到下一條指令
故障 潛在可恢復的錯誤 同步 返回到當前指令
終止 不可恢復的錯誤 同步 不會返回

X86 體系結構的每個中斷都被賦予一個唯一的編號或者向量(8 位無符號整數)。非屏蔽中斷和異常向量是固定的,而可屏蔽中斷向量可以通過對中斷控制器的編程來改變。

Linux 2.6 中斷處理原理簡介

中斷描述符表(Interrupt Descriptor Table,IDT)是一個系統表,它與每一箇中斷或異常向量相聯繫,每一個向量在表中存放的是相應的中斷或異常處理程序的入口地址。內核在允許中斷髮生前,也就是在系統初始化時,必須把 IDT 表的初始化地址裝載到 idtr 寄存器中,初始化表中的每一項。

當處於實模式下時,IDT 被初始化並由 BIOS 程序所使用。然而,一旦 Linux 開始接管,IDT 就被移到 ARM 的另一個區域,並進行第二次初始化,因爲 Linux 不使用任何 BIOS 程序,而使用自己專門的中斷服務程序(例程)(interrupt service routine,ISR)。中斷和異常處理程序很像常規的 C 函數

有三個主要的數據結構包含了與 IRQ 相關的所有信息:hw_interrupt_typeirq_desc_tirqaction,圖3 解釋了它們之間是如何關聯的。


圖 3:IRQ 結構之間的關係
IRQ結構之間的關係

在 X86 系統中,對於 8259A 和 I/O APIC 這兩種不同類型的中斷控制器,hw_interrupt_type 結構體被賦予不同的值,具體區別參見表 2。

表 2:8259A 和 I/O APIC PIC 的區別
8259A I/O APIC
static struct hw_interrupt_type i8259A_irq_type = {"XT-PIC",startup_8259A_irq,shutdown_8259A_irq,enable_8259A_irq,disable_8259A_irq,mask_and_ack_8259A,end_8259A_irq,NULL}; static struct hw_interrupt_type ioapic_edge_type = {.typename = "IO-APIC-edge",.startup = startup_edge_ioapic,.shutdown = shutdown_edge_ioapic,.enable = enable_edge_ioapic,.disable = disable_edge_ioapic,.ack = ack_edge_ioapic,.end = end_edge_ioapic,.set_affinity = set_ioapic_affinity,};static struct hw_interrupt_type ioapic_level_type = {.typename = "IO-APIC-level",.startup = startup_level_ioapic,.shutdown = shutdown_level_ioapic,.enable = enable_level_ioapic,.disable = disable_level_ioapic,.ack = mask_and_ack_level_ioapic,.end = end_level_ioapic,.set_affinity = set_ioapic_affinity,};

在中斷初始化階段,調用 hw_interrupt_type 類型的變量初始化 irq_desc_t 結構中的 handle 成員。在早期的系統中使用級聯的8259A,所以將用 i8259A_irq_type 來進行初始化,而對於SMP系統來說,要麼以 ioapic_edge_type,或以 ioapic_level_type 來初始化 handle 變量。

對於每一個外設,要麼以靜態(聲明爲 static 類型的全局變量)或動態(調用 request_irq 函數)的方式向 Linux 內核註冊中斷處理程序。不管以何種方式註冊,都會聲明或分配一塊 irqaction 結構(其中 handler 指向中斷服務程序),然後調用 setup_irq() 函數,將 irq_desc_tirqaction 聯繫起來。

當中斷髮生時,通過中斷描述符表 IDT 獲取中斷服務程序入口地址,對於 32≤ i ≤255(i≠128) 之間的中斷向量,將會執行 push $i-256,jmp common_interrupt 指令。隨之將調用 do_IRQ() 函數,以中斷向量爲 irq_desc[] 結構的下標,獲取 action 的指針,然後調用 handler 所指向的中斷服務程序。

從以上描述,我們不難看出整個中斷的流程,如圖 4 所示:


圖 4:X86中斷流
X86中斷流

本文作者之一曾經對2.6.10的中斷系統進行過情景分析,有興趣的讀者可以和作者取得聯繫,獲取相關資料。

中斷綁定——中斷親和力(IRQ Affinity)

在 SMP 體系結構中,我們可以通過調用系統調用和一組相關的宏來設置 CPU 親和力(CPU affinity),將一個或多個進程綁定到一個或多個處理器上運行。中斷在這方面也毫不示弱,也具有相同的特性。中斷親和力是指將一個或多箇中斷源綁定到特定的 CPU 上運行。中斷親和力最初由 Ingo Molnar 設計並實現。

/proc/irq 目錄中,對於已經註冊中斷處理程序的硬件設備,都會在該目錄下存在一個以該中斷號命名的目錄 IRQ#IRQ# 目錄下有一個 smp_affinity 文件(SMP 體系結構纔有該文件),它是一個 CPU 的位掩碼,可以用來設置該中斷的親和力, 默認值爲 0xffffffff,表明把中斷髮送到所有的 CPU 上去處理。如果中斷控制器不支持 IRQ affinity,不能改變此默認值,同時也不能關閉所有的 CPU 位掩碼,即不能設置成 0x0

我們以網卡(eth1,中斷號 44 )爲例,在具有 8 個 CPU 的服務器上來設置網卡中斷的親和力(以下數據出自內核源碼 Documentation\IRQ-affinity.txt):

[root@moon 44]# cat smp_affinity
ffffffff
[root@moon 44]# echo 0f > smp_affinity
[root@moon 44]# cat smp_affinity
0000000f
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
...
--- hell ping statistics ---
6029 packets transmitted, 6027 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.1/0.4 ms
[root@moon 44]# cat /proc/interrupts | grep 44:
 44:   0   1785   1785   1783   1783   1   1   0   IO-APIC-level   eth1
[root@moon 44]# echo f0 > smp_affinity
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
..
--- hell ping statistics ---
2779 packets transmitted, 2777 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.5/585.4 ms
[root@moon 44]# cat /proc/interrupts | grep 44:
 44:  1068  1785  1785  1784   1784   1069   1070   1069   IO-APIC-level  eth1
[root@moon 44]#	
	

在上例中,我們首先只允許在 CPU0~3 上處理網卡中斷,接着運行 ping 程序,不難發現在 CPU4~7 上並沒有對網卡中斷進行處理。然後只在 CPU4~7 上對網卡中斷進行處理, CPU0~3 不對網卡中斷進行任何處理,運行 ping 程序之後,再次查看 /proc/interrupts 文件時,不難發現 CPU4~7 上的中斷次數明顯增加,而 CPU0~3 上的中斷次數沒有太大的變化。

在探討中斷親和力的實現原理之前,我們首先來了解 I/O APIC 中的組成。

I/O APIC 由一組 24 條 IRQ 線,一張 24 項的中斷重定向表(Interrupt Redirection Table),可編程寄存器,以及通過 APIC 總線發送和接收 APIC 信息的一個信息單元組成。其中與中斷親和力息息相關的是中斷重定向表,中斷重定向表表中的每一項都可以被單獨編程以指明中斷向量和優先級、目標處理器及選擇處理器的方式

通過表 2,不難發現 8259A 和 APIC 中斷控制器最大不同點在於 hw_interrupt_type 類型變量的最後一項。對於 8259A 類型,set_affinity 被置爲 NULL,而對於 SMP 的 APIC 類型,set_affinity 被賦值爲 set_ioapic_affinity

在系統初始化期間,對於 SMP 體系結構,將會調用 setup_IO_APIC_irqs() 函數來初始化 I/O APIC 芯片,芯片中的中斷重定向表的 24 項被填充。在系統啓動期間,所有的 CPU 都執行 setup_local_APIC() 函數,完成本地的 APIC 初始化。當有中斷被觸發時,將相應的中斷重定向表中的值轉換成一條消息,然後,通過 APIC 總線把消息發送給一個或多個本地 APIC 單元,這樣,中斷就能立即被傳遞給一個特定的 CPU,或一組 CPU,或所有的 CPU,從而來實現中斷親和力。

當我們通過 cat 命令將 CPU 掩碼寫進 smp_affinity 文件時,此時的調用路線圖爲:write() ->sys_write() ->vfs_write() ->proc_file_write() ->irq_affinity_write_proc() ->set_affinity() ->set_ioapic_affinity() ->set_ioapic_affinity_irq() ->io_apic_write();其中在調用 set_ioapic_affinity_irq() 函數時,以中斷號和 CPU 掩碼作爲參數,接着繼續調用 io_apic_write(),修改相應的中斷重定向中的值,來完成中斷親和力的設置。當執行 ping 命令時,網卡中斷被觸發,產生了一箇中斷信號,多 APIC 系統根據中斷重定向表中的值,依照仲裁機制,選擇 CPU0~3 中的某一個 CPU,並將該信號傳遞給相應的本地 APIC,本地 APIC 又中斷它的 CPU,整個事件不通報給其他所有的 CPU。

新特性展望——中斷線程化(Interrupt Threads)

在嵌入式領域,業界對 Linux 實時性的呼聲越來越高,對中斷進行改造勢在必行。在 Linux 中,中斷具有最高的優先級。不論在任何時刻,只要產生中斷事件,內核將立即執行相應的中斷處理程序,等到所有掛起的中斷和軟中斷處理完畢後才能執行正常的任務,因此有可能造成實時任務得不到及時的處理。中斷線程化之後,中斷將作爲內核線程運行而且被賦予不同的實時優先級,實時任務可以有比中斷線程更高的優先級。這樣,具有最高優先級的實時任務就能得到優先處理,即使在嚴重負載下仍有實時性保證。

目前較新的 Linux 2.6.17 還不支持中斷線程化。但由 Ingo Molnar 設計並實現的實時補丁,實現了中斷線程化。最新的下載地址爲:

http://people.redhat.com/~mingo/realtime-preempt/patch-2.6.17-rt9

下面將對中斷線程化進行簡要分析。

在初始化階段,中斷線程化的中斷初始化與常規中斷初始化大體上相同,在 start_kernel() 函數中都調用了 trap_init()init_IRQ() 兩個函數來初始化 irq_desc_t 結構體,不同點主要體現在內核初始化創建 init 線程時,中斷線程化的中斷在 init() 函數中還將調用 init_hardirqs(kernel/irq/manage.c(已經打過上文提到的補丁)),來爲每一個 IRQ 創建一個內核線程,最高實時優先級爲 50,依次類推直到 25,因此任何 IRQ 線程的最低實時優先級爲 25。

void __init init_hardirqs(void)
{
……
	for (i = 0; i < NR_IRQS; i++) {
		irq_desc_t *desc = irq_desc + i;
		if (desc->action && !(desc->status & IRQ_NODELAY))
			desc->thread = kthread_create(do_irqd, desc, "IRQ %d", irq);
    ……
	}
}
static int do_irqd(void * __desc)
{
    ……
	/*
	 * Scale irq thread priorities from prio 50 to prio 25
	 */
	param.sched_priority = curr_irq_prio;
	if (param.sched_priority > 25)
		curr_irq_prio = param.sched_priority - 1;
   ……
}

如果某個中斷號狀態位中的 IRQ_NODELAY 被置位,那麼該中斷不能被線程化。

在中斷處理階段,兩者之間的異同點主要體現在:兩者相同的部分是當發生中斷時,CPU 將調用 do_IRQ() 函數來處理相應的中斷,do_IRQ() 在做了必要的相關處理之後調用 __do_IRQ()。兩者最大的不同點體現在 __do_IRQ() 函數中,在該函數中,將判斷該中斷是否已經被線程化(如果中斷描述符的狀態字段不包含 IRQ_NODELAY 標誌,則說明該中斷被線程化了),對於沒有線程化的中斷,將直接調用 handle_IRQ_event() 函數來處理。

fastcall notrace unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
……
	if (redirect_hardirq(desc))
		goto out_no_end;
……
action_ret = handle_IRQ_event(irq, regs, action);
……
}
int redirect_hardirq(struct irq_desc *desc)
{
……
	if (!hardirq_preemption || (desc->status & IRQ_NODELAY) || !desc->thread)
		return 0;
……
	if (desc->thread && desc->thread->state != TASK_RUNNING)
		wake_up_process(desc->thread);
……
}

對於已經線程化的情況,調用 wake_up_process() 函數喚醒中斷處理線程,並開始運行,內核線程將調用 do_hardirq() 來處理相應的中斷,該函數將判斷是否有中斷需要被處理,如果有就調用 handle_IRQ_event() 來處理。handle_IRQ_event() 將直接調用相應的中斷處理函數來完成中斷處理。

不難看出,不管是線程化還是非線程化的中斷,最終都會執行 handle_IRQ_event() 函數來調用相應的中斷處理函數,只是線程化的中斷處理函數是在內核線程中執行的。

並不是所有的中斷都可以被線程化,比如時鐘中斷,主要用來維護系統時間以及定時器等,其中定時器是操作系統的脈搏,一旦被線程化,就有可能被掛起,這樣後果將不堪設想,所以不應當被線程化。如果某個中斷需要被實時處理,它可以像時鐘中斷那樣,用 SA_NODELAY 標誌來聲明自己非線程化,例如:

static struct irqaction irq0 = {
	timer_interrupt, SA_INTERRUPT | SA_NODELAY, CPU_MASK_NONE, "timer", NULL, NULL
};

其中,SA_NODELAYIRQ_NODELAY 之間的轉換,是在 setup_irq() 函數中完成的。

中斷負載均衡—SMP體系結構下的中斷

中斷負載均衡的實現主要封裝在 arch\ arch\i386\kernel\io-apic.c 文件中。如果在編譯內核時配置了 CONFIG_IRQBALANCE 選項,那麼 SMP 體系結構中的中斷負載均衡將以模塊的形式存在於內核中。

late_initcall(balanced_irq_init);
#define late_initcall(fn)		module_init(fn)  //include\linux\init.h

balanced_irq_init() 函數中,將創建一個內核線程來負責中斷負載均衡:

static int __init balanced_irq_init(void)
{   ……
	printk(KERN_INFO "Starting balanced_irq\n");
	if (kernel_thread(balanced_irq, NULL, CLONE_KERNEL) >= 0) 
		return 0;
	else 
		printk(KERN_ERR "balanced_irq_init: failed to spawn balanced_irq");
    ……
}

balanced_irq() 函數中,每隔 5HZ=5s 的時間,將調用一次 do_irq_balance() 函數,進行中斷的遷徙。將重負載 CPU 上的中斷遷移到較空閒的CPU上進行處理。

總結

隨着中斷親和力和中斷線程化的相繼實現,Linux 內核在 SMP 和實時性能方面的表現越來越讓人滿意,完全有理由相信,在不久的將來,中斷線程化將被合併到基線版本中。本文對中斷線程化的分析只是起一個拋磚引玉的作用,當新特性發布時,不至於讓人感到迷茫。

  • 註釋 1:輪詢也不是毫無用處,比如NAPI,就是輪詢與中斷相結合的經典案例。

參考資料

  1. Rebert Love,《Linux Kernel Development,2rd Edition》,機械工業出版社,2006。
  2. Daniel P. Bovet,Marco Cesati,《Understanding the Linux Kernel,3rd Edition》,東南大學出版社,2006。
  3. Jonatban Corbet 等,魏永明等譯,《Linux設備驅動程序》,中國電力出版社,2006。
  4. Gordon Fischer 等,《The Linux Kernel Prime》,機械工業出版社,2006。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章