Linux 2.6內核筆記【中斷、異常、搶佔內核】

2009.6.18更新:參考http://linux.derkeiler.com/Mailing-Lists/Kernel/2004-03/4562.html ,查證LXR,重新詮釋PREEMPT_ACTIVE標誌。

 

中斷信號分類

 

中斷信號是一個統稱,統稱那些改變CPU指令執行序列的事件。但它又分爲兩種:

 

一種是同步的,沒那麼突然,因爲它只在一個指令的執行終止之後才發生,書中依從Intel的慣例,稱爲異常(Exception)。一般是編程錯誤(一般的處理是發信號)或者內核必須處理的異常情況(內核會採取恢復異常所需的一些步驟);

 

一種是異步的,突然一些,因爲它是由間隔定時器和I/O設備產生的,只遵循CPU時鐘信號,所以可能在任何時候產生,書中也依從Intel的慣例,稱爲中斷(Interrupt)。

 

內核控制路徑

 

內核在允許中斷信號到來之前,必須先準備好對它們的處理,也就是適當地初始化中斷描述符表(Interrupt Descriptor Table, IDT)。

 

中斷信號一來,CPU控制單元就自動把當前的程序計數器(eip、cs)和eflags保存到內核stack,然後把事先與發生的中斷信號類型關聯好的處理程序的地址(保存在IDT中)放進程序計數器。這時,內核控制路徑(kernel control path)橫空出世。

 

什麼是內核控制路徑?它是不是一個進程?不是。內核進程?也不是。它雖然也需要切換上下文,需要保存那些它可能使用的寄存器的並在返回時恢復,但這是一個非常輕的上下文切換。它誕生的時候並沒有發生進程切換,處理中斷的主語仍然是中斷髮生時正在執行的那個進程。那個進程就像突然被內核抓進了一間小屋做事,或者突然潛入了水(內核)裏不見蹤影,但它仍然在使用分配給它的那段時間片。

 

有趣的是,如果一個進程還在處理一個異常的時候,分配給它的時間片到期了,會發生什麼事情呢?這取決於有沒有啓用內核搶佔(Kernel Preemption),如果沒有啓用,進程就繼續處理異常,如果啓用了,進程可能會立即被搶佔,異常的處理也就暫停了,直到schedule()再度選擇原先那個進程(注意:內核處理中斷的時候,必然會禁用內核搶佔,所以這裏才說是異常)。

 

中斷信號處理的約束

 

中斷信號處理需要滿足下面三個嚴格的約束:

 

1)中斷處理要儘可能塊地完成、返回。因此只執行關鍵而緊急的部分,儘可能把更多的後續處理過程僅僅標誌一下,放到之後再去執行。

 

2)一箇中斷還在處理的時候,另外一箇中斷可能又來了,這個時候最好能先放下手中的處理,先去處理新的中斷,然後在回頭來接着處理這個中斷,這稱之爲中斷和異常處理程序的嵌套執行(nested execution),或者說是內核控制路徑的嵌套執行。要實現這一點,有一點必須滿足,那就是中斷處理程序運行期間不能阻塞,不能發生進程切換。

 

如果對異常的種類做一番思考,就會發現,異常最多嵌套兩層,一個由系統調用產生,一個由系統調用執行過程中的缺頁產生(這時必然掛起當前進程,發生進程切換)。與之相反,在複雜的情況下,中斷產生的嵌套則可能任意多。

 

3)內核中存在一些臨界區,在這些臨界區,中斷必須被禁止。中斷處理程序要儘可能地減少進入臨界區的次數和時間,爲了內核的響應性能,中斷應該在大部分時間都是啓用的。

 

異常的種類

 

異常有很多種,其中比較有趣的有:

 

 

編號

異常

異常處理程序

信號

有趣之處

1

Debug

debug( )

SIGTRAP

用於調試

3

Breakpoint

int3( )

SIGTRAP

7

Device not available

device_not_available( )

None

用於在需要的時候才加載FPUMMXXMM
(
cr0TS 標誌被設置)

14

Page Fault

page_fault( )

SIGSEGV

如果是正常缺頁,內核會掛起當前進程,然後將該頁讀入RAM ;如果是頁錯誤,就發出信號。

4

Overflow

overflow( )

SIGSEGV

調試時非常常見的一個信號SIGSEQVSegment Violation ,呵呵,關注一下都是什麼異常導致的。

5

Bounds check

bounds( )

SIGSEGV

10

Invalid TSS

invalid_TSS( )

SIGSEGV

13

General protection

general_protection( )

SIGSEGV

 

中斷描述符

 

Intel 80x86 CPU認得三種中斷描述符,Linux爲了檢驗權限,將其細分爲:

 

Interrupt Gate, DPL = 0的中斷門,set_intr_gate(n,addr),所有中斷

 

System Interrupt Gate,DPL = 3的中斷門,set_system_intr_gate(n,addr),int3異常

 

System Gate,DPL = 3的陷阱門,set_system_gate(n,addr),into、bound、int $0x80異常

 

Trap Gate, DPL = 0的陷阱門,set_trap_gate(n,addr),大部分異常

 

Task Gate, DPL = 0的任務門,set_task_gate(n,gdt),double fault異常

 

異常處理的標準結構

  1. 用匯編把大多數寄存器的值保存到kernel stack;

  2. 用C函數處理異常

  3. 通過ret_from_exception( ) 函數退出處理程序.

I/O中斷處理的標準結構

  1. 將IRQ值和寄存器值保存到kernel stack;

  2. 給服務這條IRQ線的PIC發送應答,從而允許它繼續發出中斷;

  3. 執行和所有共享此IRQ的設備相關聯的ISR;

  4. 通過跳轉到ret_from_intr( ) 的地址結束中斷處理。

IRQ(Interrupt ReQuest)線(IRQ向量)的分配

 

IRQ共享:幾個設備共享一個IRQ,中斷來時,每個設備的中斷服務例程(Interrupt Service Routine,ISR)都執行,檢查一下是否與己有關;

IRQ動態分配:IRQ可以在使用一個設備的時候才與一個設備關聯,這樣同一個IRQ就可以被不同的設備在不同時間使用。

 

中斷向量中,0-19用於異常和非屏蔽中斷,20-31被Intel保留了,32-238這個範圍內都可以分配給物理IRQ,但128(0x80)被分配給用於系統調用的可編程異常。

 

延後的工作誰來做?

 

首先是兩種非緊迫的、可中斷的內核函數——可延遲函數(deferrable functions ),然後是通過工作隊列(work queues )來執行的函數。

 

軟中斷(softirq)是可重入函數而且必須明確地使用自旋鎖保護其數據結構;tasklet在軟中斷基礎上實現,但由於內核保證不會在兩個CPU上同時運行相同類型的tasklet,所以它不必是可重入的。

 

六種軟中斷

 

 

 

Softirq

Index (priority)

Description

HI_SOFTIRQ

0

Handles high priority tasklets

TIMER_SOFTIRQ

1

Tasklets related to timer interrupts

NET_TX_SOFTIRQ

2

Transmits packets to network cards

NET_RX_SOFTIRQ

3

Receives packets from network cards

SCSI_SOFTIRQ

4

Post-interrupt processing of SCSI commands

TASKLET_SOFTIRQ

5

Handles regular tasklets

 

 

內核會在一些檢查點(適宜的時候,其中有時鐘中斷)檢查掛起的軟中斷,用__do_softirq()執行它們。__do_softirq()會循環若干次,以保證處理掉一些在處理過程中新出現的軟中斷,但如果還有更多新掛起的軟中斷,__do_softirq()就不管了,而是調用wakeup_softirq()喚醒每CPU內核進程ksoftirqd/n(這樣就可以被調度,而不會一直佔着CPU),來處理剩下的軟中斷。

 

這種做法是爲了解決一個矛盾:與網絡相關的軟中斷是高流量的,也是對實時性有一定要求的。但是如果do_softirq()爲了實時性一直處理它們,就會一直不返回,結果用戶程序就僵在那裏了;如果do_softirq()處理完一些軟中斷就返回,不論這中間機器有無空閒,直到下一個時鐘中斷才又處理其餘的,網絡處理需要的許多實時性就得不到保證。現在的做法,喚醒內核進程,讓它在後臺調度,由於內核進程優先級很低,用戶程序就有機會運行,不會僵死;但如果機器空閒下來,掛起的軟中斷很快就能被執行。

 

tasklet則多用於在I/O驅動程序的開發中實現可延遲函數。

 

但是,可延遲函數有一個限制,它是運行在中斷上下文的,它執行時不可能有任何正在運行的進程,它也不能調用任何可阻塞(從而會休眠)的函數。這就是工作隊列的意義所在。工作隊列把需要執行的內核函數交給一些內核進程來執行。

 

處於效率的考慮,內核預定義了叫做events的工作隊列,內核開發者可以用schedule_work族函數隨意呼喚它們。

 

內核搶佔(Kernel Preemption)

 

本章在很多地方都涉及到了內核搶佔,我覺得還是將內核搶佔在本章的筆記記完,不必像原書那樣等到內核同步一章了。

 

在非搶佔內核的情形,一個執行在內核態的進程是不可能被另外的進程取代的(進程切換);而在搶佔內核的情形,是有可能的:但只有當內核正在執行異常處理程序(尤其是系統調用),而且內核搶佔沒有被顯式禁用的時候,纔可能搶佔內核。

 

一個例子:當A在處理異常的時候,一箇中斷的處理程序喚醒了優先級更高的B,在搶佔內核的情形,就會發生強制性進程切換。這樣做的目的是減少dispatch latency,即從進程(結束阻塞)變爲可執行狀態到它實際開始運行的時間間隔,降低了它被另外一個運行在內核態的進程延遲的風險。

 

進程描述符中的thread_info字段中有一個32位的preempt_counter字段,0-7位爲搶佔計數器,用於記錄顯式禁用內核搶佔的次數;8-15位爲軟中斷計數器,記錄可延遲函數被禁用的次數;16-27爲硬中斷計數器,表示中斷處理程序的嵌套數(irq_enter()遞增它,irq_exit()遞減它);28位爲PREEMPT_ACTIVE標誌。只要內核檢測到preempt_counter整體不爲0,就不會進行內核搶佔,這個簡單的探測一下子保證了對衆多不能搶佔的情況的檢測。

 

說明:

 

1)爲了避免在可延遲函數訪問的數據結構上發生的競爭條件,最簡單直接的方法是禁用中斷,但禁用中斷有時太誇張了,所以有了禁用可延遲函數這回事。

 

2) PREEMPT_ACTIVE標誌的本意是說明正在搶佔,設置了之後preempt_counter就不再爲0,從而執行搶佔相關工作的代碼不會被搶佔。

 

它可被非常tricky地這樣使用:

 

preempt_schedule()是內核搶佔時進程調度的入口,其中調用了schedule()。它在調用schedule()前設置PREEMPT_ACTIVE標誌,調用後清除這個標誌。而schedule()會檢查這個標誌,對於不是TASK_RUNNING(state != 0)的進程,如果設置了PREEMPT_ACTIVE標誌,就不會調用deactivate_task(),而deactivate_task()的工作是把進程從runqueue移除。

 

你可能會疑惑,爲什麼要預防已經不在RUNNING狀態的進程從runqueue中移除?設想一下,一個進程剛把自己標誌爲TASK_INTERRUPTIBL,就被preempt了,它還沒來得及把自己放進wait_queue中...這個時候當然要讓它回頭接着運行,直到把自己放進wait_queue然後自願進程切換,那時纔可以把它從runqueue中移除。

 

在面對內核的時候,思維不能僵化在操作系統提供給用戶的進程切換的抽象中,而要想象一個永不停歇運行着的、雖然有意識地跳來跳去的指令流的。所以,沒有標誌爲RUNNING不意味就不會還剩下一些(比如處理狀態轉換的)代碼需要執行哦。

 

通過這個標誌,保證了被搶佔的進程將可以被正確地重新調度和運行。

 

在中斷、異常、系統調用返回過程中也會設置PREEMPT_ACTIVE標誌。

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