====================
中斷信号分類
-------------------
中斷信号是一個統稱,統稱那些改變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标志。
注:中斷或者中斷嵌套不會導緻程序的切換,搶占則必須發生切換。