天天看點

中斷、異常、搶占核心(抄錄)

====================

中斷信号分類

-------------------

中斷信号是一個統稱,統稱那些改變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)核心中存在一些臨界區,在這些臨界區,中斷必須被禁止。中斷處理程式要盡可能地減少進入臨界區的次數和時間,為了核心的響應性能,中斷應該在大部分時間都是啟用的。

異常的種類

---------------

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

中斷、異常、搶占核心(抄錄)

中斷描述符

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異常

異常處理的标準結構

-----------------------------

用彙編把大多數寄存器的值儲存到kernel stack;

用C函數處理異常

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

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

---------------------------------

将IRQ值和寄存器值儲存到kernel stack;

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

執行和所有共享此IRQ的裝置相關聯的ISR;

通過跳轉到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

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标志。

注:中斷或者中斷嵌套不會導緻程序的切換,搶占則必須發生切換。

繼續閱讀