【內核】——中斷和異常


中斷和異常

  • 定義:中斷通常被定義為一個事件,該事件改變了CPU的執行順序。
  • 分類:中斷常分為同步中斷和異步中斷。在intel微處理器中,把同步中斷也稱為異常,異步中斷稱為中斷
    • 同步中斷(異常):當指令執行時由CPU控制單元產生的,之所以稱為同步,是因為只有在一條指令執行終止執行后CPU才會發出中斷。異常是程序的錯誤產生的,或者是由內核必須處理的異常條件產生的。
    • 異步中斷(中斷):其他設備發出的中斷,具有"隨機性",也叫外部中斷。由間隔定時器和IO設備產生。
  • 中斷或異常處理執行的代碼不是一個進程,他是一個內核控制路徑。另外,中斷處理程序比一個進程要“輕”,因為中斷的上下文很少,建立或終止中斷處理需要的時間很少。
  • 中斷優先級分級(另一種分類情況下的分級):故障中斷>訪管中斷/程序性中斷>外中斷>IO中斷
  • 中斷處理是由內核執行的最敏感任務之一,針對中斷的響應包括這么幾部分:關中斷,保存斷點,針對中斷源(緊急就准備執行,不緊急就排隊)。
    中斷響應的時刻圖

IRQ和中斷

  • 每個能發出中斷請求的硬件設備控制器都有一條IRQ(interrupt ReQuest)的輸出線。而所有IRQ線都和一個可編程中斷控制器(PIC)相連。響應中斷的具體流程如下:
    • PIC監視IRQ
    • IRQ發出信號給PIC,PIC把這個信號轉換為一個向量,並存放在一個IO端口,從而允許CPU通過總線讀取這個向量
    • 發送給了CPU,即產生一個中斷
    • 等待直到CPU通過這個中斷信號寫進PIC的一個IO端口來確認他。
    • 繼續監視
  • 顯然上述的情況是針對單個CPU的簡單情況,現在已經是多CPU核心了,為了充分發揮SMP體系結構的並行性,能夠把中斷傳遞給系統中的每個CPU至關重要。因此,Intel引入了IO高級可編程控制器(IO APIC)(IO Advanced Programmable interrupt Controller)。使用方式如下圖:
  • 關於irq的相關代碼位於./arch/i386/kernel/iqr.c中(這里的例子是以i386(intel32位)體系架構為例,現在的PC體系架構應該是在./arch/x86_64/kernel中),這里展示部分代碼:

中斷描述表IDT

  • 中斷描述表(interrupt description table IDT)是一個系統表(其他的系統表還有進程proc表,file表,inode表等等),每一個向量在表中有相應的中斷或異常處理程序的入口地址。
  • IDT有三種類型的描述符。分別是任務門描述符,中斷門描述符和陷阱門描述符。linux利用中斷門處理中斷,利用陷阱門處理異常
  • 中斷和異常的硬件處理:從硬件的角度來看CPU如何處理中斷和異常。可以參考:https://blog.csdn.net/windeal3203/article/details/44591609

中斷和異常處理程序的嵌套執行

  • 每個中斷或異常都會引起一個內核控制路徑,相應的內核控制路徑的第一部分指令就是把那些寄存器的內容保存在內核堆棧的指令中,最后一部分指令就是恢復寄存器內容並讓CPU返回到用戶態的那些指令。而內核控制路徑可以任意嵌套,和中斷響應的時刻圖類似。對中斷進行處理的內核控制路徑,其最后一部分指令並不總能使當前進程返回到用戶態,如果嵌套深度大於1,這些指令將執行上次被打斷的內核控制路徑,此時CPU還在內核態。

  • /proc/interrupts文件會顯示IRQ(interrupt request),從左至右依次是中斷號,中斷在各CPU發生的次數,中斷設備名稱,硬件中斷號,中斷處理函數:

  • /proc/iqr目錄下面會為每個注冊的irq創建一個以irq編號為名字的子目錄,每個子目錄下分別有以下條目:

    • smp_affinity irq和cpu之間的親緣綁定關系,(default_smp_affinity=3,系統默認每個中斷可以由兩個cpu進行處理)
    • smp_affinity_hint 只讀條目,用於用戶空間做irq平衡只用;
    • spurious 可以獲得該irq被處理和未被處理的次數的統計信息;
    • handler_name 驅動程序注冊該irq時傳入的處理程序的名字;

異常處理

  • 處理異常中設置三個門結構,陷阱門,任務門和中斷門初始化,其結構位於include/asm-x86_84\desc.h定義,(這里的asm應該是匯編的意思吧,但是卻是一個頭文件):

相應的實現則是在./arch/x84_64/kernel/traps.c中:

  • 異常處理結構有一共標准的結構,由以下三部分組成:
    • 在內核堆棧中保存大多數寄存器的內容(這部分由匯編實現),
    • 用高級的C函數處理異常
    • 通過ret_from_exception()函數從異常處理程序退出

一個典型的異常處理程序被調用的步驟

  • 為異常程序保存寄存器的值:假設一個通用的異常處理的名字為handler_name,每一個異常處理程序都以下列的匯編指令開始:
    handler_name: pushl $0 /* only for some exceptions*/ pushl $do_handler_name jmp error_code
  • 當異常發生時,如果控制單元沒有自動地把一個硬件出錯代碼插入到棧中,相應的匯編語言片段會包含一條pushl $0指令,在棧中墊一個空值,然后,把高級C函數的地址壓棧,它的名字由異常處理程序和do_前綴構成。

進入和離開異常處理程序

  • 執行異常處理程序的C函數名總是由do_前綴和處理程序組成。其中的大部分函數把硬件出錯碼和異常向量保存在當前進程的描述符中,然后向當前進程發送一個合適的信號。用代碼描述如下:
    ··
    current->thread.error_code = error_code;
    current->thread.trap_no = vector;
    force_sig(sig_number, current);
    ··
  • 異常處理程序一終止,當前進程就關注這個信號,該信號要么由用戶態進程自己處理,要么由內核態直接殺死。
  • 異常處理程序總是會檢查異常是發生在用戶態還是內核態,后一種情況下還要進行動態地址檢查,因為出現在內核態的任何其他異常都是由於內核bug引起的。在這種情況下,異常處理程序會認為內核行為失常了,那么就要防止磁盤上數據崩潰,調用die()函數處理。

中斷處理

  • 中斷處理不同於異常處理,中斷處理依賴於中斷類型,主要分為3類:
    • IO中斷處理:包括IRQ線共享,動態分配,do_IRQ函數,IRQ在多處理器系統上的分發等等

    • 處理器間中斷處理:處理器間中斷允許一個CPU想系統中的其他CPU發送中斷信號。處理器間的中斷(IPI)不是通過IRQ線傳輸的,而是作為信號直接放在連接所有CPU本地APIC的總線上。

軟中斷

一個中斷處理程序的幾個中斷服務例程之間是串行執行的,並且通常在一個中斷的處理程序結束前,不應該再次出現這個中斷。(這種情況下,系統性能瓶頸是CPU資源,需要增加更多的CPU來解決該問題)相反,可延遲中斷可以在開中斷的情況下執行。把可延遲中斷從中斷處理程序中抽出來有助於使內核保持較短的響應時間。

linux2.6有兩種非緊迫,可中斷的內核函數:

  • 延遲函數(包括軟中斷和tasklet)
  • 通過工作隊列執行的函數

軟中斷和tasklet有密切關系,tasklet是在軟中斷的基礎上實現。而出現在內核代碼中的屬於“軟中斷(softirq)”常常表示可延時函數的所有種類。而“中斷上下文”表示內核當前正在執行一個中斷處理程序或一個可延遲的函數。

軟中斷 tasklet
分配是靜態的(編譯時定義) 分配和初始化可以在運行時進行
可以並發地運行在多個CPU上 總是被串行執行,不能在兩個CPU上同時運行相同類型的tasklet
可重入的 不必是可重入的

可延遲函數可以執行四種操作:

  • 初始化(initialization):定義一個新的可延遲函數,這個操作通常在內核自身初始化或加載模塊時執行。
  • 激活(activation):標記一個可延遲函數為“掛起”(在可延遲函數的下一輪調度中執行)。激活可以在任何時候進行(即使在處理中斷)。
  • 屏蔽(masking):有選擇地屏蔽一個可延遲函數,這樣即使他被激活,內核也不會執行他。(為了同步屏蔽他)
  • 執行(execution):執行一個掛起的可延遲函數和同類型的其他所有掛起的可延遲函數,執行是在特定的時間進行的。

NIC中斷處理過程

當網卡控制器的FIFO收到的來自以太網的數據的時候(例如半滿的時候,可以軟件設定),可以將該事件通過irq signal送達Interrupt Controller。Interrupt Controller可以把中斷分發給系統中的Processor A or B。

NIC的中斷處理過程大概包括:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>handle Data in the ram----------->unmask interrupt controller

我們先假設Processor A處理了這個網卡中斷事件,於是NIC的中斷handler在Processor A上歡快的執行,這時候,Processor A的本地中斷是disable的。NIC的中斷handler在執行的過程中,網絡數據仍然源源不斷的到來,但是,如果NIC的中斷handler不操作NIC的寄存器來ack這個中斷的話,NIC是不會觸發下一次中斷的。還好,我們的NIC interrupt handler總是在最開始就會ack,因此,這不會導致性能問題。ack之后,NIC已經具體再次trigger中斷的能力。當Processor A上的handler 在處理接收來自網絡的數據的時候,NIC的FIFO很可能又收到新的數據,並trigger了中斷,這時候,Interrupt controller還沒有unmask,因此,即便還有Processor B(也就是說有處理器資源),中斷控制器也無法把這個中斷送達處理器系統。因此,只能眼睜睜的看着NIC FIFO填滿數據,數據溢出,或者向對端發出擁塞信號,無論如何,整體的系統性能是受到嚴重的影響。

注意:對於新的interrupt controller,可能沒有mask和unmask操作,但是原理是一樣的,只不過NIC的handler執行完畢要發生EOI而已。

要解決上面的問題,最重要的是盡快的執行完中斷handler,打開中斷,unmask IRQ(或者發送EOI),方法就是把耗時的handle Data in the ram這個步驟踢出handler,讓其在bottom half中執行。

為什么會有softirq

  • linux內核把中斷分為top half(handler 硬中斷)和bottom half。bottom half提供了softirq、taskleth和workqueue機制

  • workqueue和softirq、tasklet有本質的區別:workqueue運行在process context,而softirq和tasklet運行在interrupt context。

  • 對於中斷而言,是硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間,這個過程中硬件的一些變量和參數也要傳遞給內核,這些參數和被中斷的進程的上下文就是中斷上下文。對於用戶進程而言,則是通過內核調用,進入內核空間,切換進程上下文的。

  • 因此,出現workqueue是不奇怪的,在有sleep需求的場景中,defering task必須延遲到kernel thread中執行,也就是說必須使用workqueue機制。softirq和tasklet是怎么回事呢?從本質上將,bottom half機制的設計有兩方面的需求,一個是性能,一個是易用性。設計一個通用的bottom half機制來滿足這兩個需求非常的困難,因此,內核提供了softirq和tasklet兩種機制。softirq更傾向於性能,而tasklet更傾向於易用性。

  • 為了性能,同一類型的softirq有可能在不同的CPU上並發執行,這給使用者帶來了極大的痛苦,因為驅動工程師在撰寫softirq的回調函數的時候要考慮重入,考慮並發,要引入同步機制。但是,為了性能,我們必須如此。

  • 而tasklet是串行執行的(注:不同的tasklet還是會並發的),不需要考慮重入的問題

理解softirq

  • preempt_count:這個成員被用來判斷當前進程是否可以被搶占。當內核代碼明確不允許發生搶占的或內核正在中斷上下文運行時,必須禁止內核的搶占功能。如果preempt_count不等於0(可能是代碼調用preempt_disable顯式的禁止了搶占,也可能是處於中斷上下文等),說明當前不能進行搶占,如果preempt_count等於0,說明已經具備了搶占的條件(當然具體是否要搶占當前進程還是要看看thread info中的flag成員是否設定了_TIF_NEED_RESCHED這個標記,可能是當前的進程的時間片用完了,也可能是由於中斷喚醒了優先級更高的進程)。 具體preempt_count的數據格式可以參考下圖。


  • 對softirq count進行操作有兩個場景:

    (1)也是在進入soft irq handler之前給 softirq count加一,退出soft irq handler之后給 softirq count減去一。由於soft irq handler在一個CPU上是不會並發的,總是串行執行,因此,這個場景下只需要一個bit就夠了,也就是上圖中的bit 8。通過該bit可以知道當前task是否在sofirq context。

    (2)由於內核同步的需求,進程上下文需要禁止softirq。這時候,kernel提供了local_bh_enable和local_bh_disable這樣的接口函數。這部分的概念是和preempt disable/enable類似的,占用了bit9~15,最大可以支持127次嵌套。

    (3)本質上,關本地中斷(irqs_disabled)是一種比關本地bottom half更強勁的鎖,關本地中斷實際上是禁止了top half和bottom half搶占當前進程上下文的運行。也許你會說:這也沒有什么,就是有些浪費,至少代碼邏輯沒有問題。但事情沒有這么簡單,在local_bh_enable--->do_softirq--->__do_softirq中,有一條無條件打開當前中斷的操作,也就是說,原本想通過local_irq_disable/local_irq_enable保護的臨界區被破壞了,其他的中斷handler可以插入執行,從而無法保證local_irq_disable/local_irq_enable保護的臨界區的原子性,從而破壞了代碼邏輯。(bh:bottom half雖然已經沒有了,但是還是有這么一個特殊類型的可延遲函數)

    (4)barrier是優化屏障(Optimization barrier).

  • 一個task的各種上下文,簡而言之就是IRQ context + softirq context+NMI (Non Maskable Interrupt不可屏蔽上下文)context。

    • IRQ context這里其實是硬中斷上下文。也就是說明當前正在執行中斷handler(top half),只要preempt_count中的hardirq count大於0,那么就是IRQ context。
    • softirq context並沒有那么的直接,一般人會認為當sofirq handler正在執行的時候就是softirq context。

softirq機制

  • softirq number:和IRQ number一樣,對於軟中斷,linux kernel也是用一個softirq number唯一標識一個softirq。主要定義了6中軟中斷

  • softirq的主要數據結構是softirq_vec數組,包含類型為softirq_action的32個元素。另外一個重要的數據結構就是上面的preempt_count字段(int類型32位)

處理軟中斷

  • 相關函數大部分位於kernel\softirq.c中,包括軟中斷的相關函數和tasklet的相關函數。

  • open_softirq()函數處理軟中斷的初始化。

  • raise_softirq()函數用於激活軟中斷,觸發softirq的接口函數有兩個版本,一個是raise_softirq,有關中斷的保護,另外一個是raise_softirq_irqoff,調用者已經關閉了中斷,不需要關中斷來保護“soft irq status register”。具體過程如下:

    • 執行local_irq_save宏以保存eflags寄存器的IF標志的狀態值並禁用本地CPU上的中斷。
  • do_softirq()函數:如果在這樣的一個檢查點(local_softirq_pending不為0)檢測到掛起的軟中斷,內核就調用do_softirq來處理他們。具體就是調用__do_softirq()函數,讀取本地CPU的軟中斷掩碼並執行與每個設置位相關的可延遲函數。

    • 為了防止__do_softirq運行很長時間,而大大延遲用戶態進程的執行,因此他每次制作固定次數的循環,然后就返回,如果還有其余掛起的軟中斷,內核線程ksoftirqd將會在預期的時間內處理他們。
    • ksoftirqd內核線程:當內核線程被喚醒時,就檢查local_softirq_pending()中的軟中斷位掩碼並在必要時調用do_softirq()。如果沒有掛起的軟中斷,函數吧當前進程狀態設置為TASK_INTERRUPTABLE,隨后,如果當前進程需要就調用cond_resched()函數來實現進程切換。ksoftirqd/n內核線程為重要而難以平衡的問題提供了解決方案,軟中斷的連續高流量可能會產生問題,該問題就是由引入的內核線程來解決。

    • 如果開發人員自己解決,就有兩種方案,一個是忽略在do_softirq運行時新出現的軟中斷。那么可能存在機器空閑,也只有在下一次時間中斷到來時,中斷才被執行。另一個是不斷地檢查掛起的軟中斷。do_softirq()函數只有在沒有掛起的軟中斷才終止。這會使得用戶態進程無法執行。(零拷貝技術)

tasklet

為什么要tasklet?

將中斷處理分成top half(cpu和外設之間的交互,獲取狀態,ack狀態,收發數據等)和bottom half(后段的數據處理)已經深入人心,對於任何的OS都一樣,將不那么緊急的事情推遲到bottom half中執行是OK的,具體如何推遲執行分成兩種類型:有具體時間要求的(對應linux kernel中的低精度timer和高精度timer)和沒有具體時間要求的。對於沒有具體時間要求的又可以分成兩種:

  1. 越快越好型,這種實際上是有性能要求的,除了中斷top half可以搶占其執行,其他的進程上下文(無論該進程的優先級多么的高)是不會影響其執行的,一言以蔽之,在不影響中斷延遲的情況下,OS會盡快處理。
  2. 隨遇而安型。這種屬於那種沒有性能需求的,其調度執行依賴系統的調度器。

本質上講,越快越好型的bottom half不應該太多,而且tasklet的callback函數不能執行時間過長,否則會產生進程調度延遲過大的現象,甚至是非常長而且不確定的延遲,對real time的系統會產生很壞的影響。

在linux kernel中,“越快越好型”有兩種,softirq和tasklet,“隨遇而安型”也有兩種,workqueue和threaded irq handler。“越快越好型”能否只留下一個softirq呢?對於崇尚簡單就是美的程序員當然希望如此。為了回答這個問題,我們先看看tasklet對於softirq而言有哪些好處:

  1. tasklet可以動態分配,也可以靜態分配,數量不限。
  2. 同一種tasklet在多個cpu上也不會並行執行,這使得程序員在撰寫tasklet function的時候比較方便,減少了對並發的考慮(當然損失了性能)。
  3. tasklet和softirq都不能sleep

tasklet的基本原理

tasklet建立在兩個叫做HI_SOFTIRQ和TASKLET_SOFTIRQ的軟中斷之上。幾個tasklet可以與同一個軟中斷相關聯,每個tasklet都有自己的函數。tasklet和高優先級的tasklet分別存放在tasklet_vec和tasklet_hi_vec數組中。兩者都包括類型為tasklet_head的多個元素,每個元素都由一個指向tasklet描述符的指針組成。而tasklet描述符具體結構體如下所示(位於interrupt.h):

struct tasklet_struct
{
	struct tasklet_struct *next;  // 指向CPU維持的鏈表中的下一個tasklet
	unsigned long state;  // tasklet的狀態,包括TASKLET_STATE_SCHED和TASKLET_STATE_RUN
	atomic_t count;  // 鎖計數器
    // func和data成員描述了該tasklet的callback函數
	void (*func)(unsigned long);  // func是調用函數
	unsigned long data;  // data是傳遞給func的參數
};

tasklet屬於一種特殊的軟中斷(從他的相關函數都放在softirq.c中就可以看出來),tasklet的執行和softirq是由不同的driver函數執行,一個是do_softirq函數(背后是softirq_action結構體),一個直接是tasklet_action函數(參數是softirq_action結構體)。兩者的操作基本相似:

  1. 禁止本地中斷
  2. 獲得本地CPU的邏輯號n
  3. 把tasklet_vec[n]或tasklet_hi_vec[n](存放softirq_action結構體具體成員的地方)指向的鏈表的地址存入局部變量list中
  4. 上面的數組賦值為0,因此,已經調度了tasklet描述符的鏈表被清空。
  5. 打開本地中斷

注意:除非tasklet函數重新激活自己,否則,tasklet的每次激活至多觸發tasklet函數的一次執行。

推薦閱讀:http://www.wowotech.net/irq_subsystem/tasklet.html

工作隊列

​ linux內核2.6版本的工作隊列(workqueue)對應於之前2.4版本的任務隊列。這個版本中要引入了CMWQ(concurrency managed workerqueue)。可延遲函數和工作隊列非常相似,但是可延遲函數運行在中斷上下文中,而工作隊列運行在進程上下文中。執行可阻塞函數(比如訪問磁盤數據塊的函數)的唯一方式是在進程上下文中。因為,中斷上下文不能發生進程切換。而工作隊列和可延遲函數都不能訪問用戶態地址空間。

中斷上下文和進程上下文

  • 中斷上下文包括:
    • 執行該中斷的處理函數(interrupt handler 或者top half),也就是hard interrupt context。
    • 執行軟中斷處理函數,執行tasklet函數,執行timer_callback函數。統稱為bottom half,也就是software interrupt context。
    • 中斷上下文沒有自己的棧。當中斷發生的時候,遇到哪一個進程就借用哪一個進程的資源,內核態的棧。
  • 進程上下文,在UNIX System V中,進程上下文由用戶級上下文,寄存器上下文以及系統級上下文組成。所有進程(包括內核進程和普通進程)都有一個內核棧,在x86的32位機器上內核棧大小可以為4KB或8KB,這個可以在編譯內核的時候配置。
    • 用戶級上下文:DS(data segment)數據段寄存器,SS(stack segment)棧段寄存器,IP(instruction pointer)指令指針寄存器,CS(code segment)代碼段寄存器(正文段)
      • 在 8086PC 機中,任意時刻,CPU 將 CS:IP 指向的內容當做指令執行。
      • 指令和數據在內存中沒有區別,它們都是一些二進制信息,把這些二進制信息看作指令還是數據,是讓編程人員通過控制 CPU 中的寄存器來完成的
      • 進程陷入內核態后,先把用戶態堆棧的地址保存在內核棧之中,然后設置堆棧指針寄存器的內容為內核棧的地址,這樣就完成了用戶棧向內核棧的轉換;當進程從內核態恢復到用戶態之行時,在內核態之行的最后將保存在內核棧里面的用戶棧的地址恢復到堆棧指針寄存器即可。這樣就實現了內核棧和用戶棧的互轉。
    • 寄存器上下文:也稱為硬件上下文,就是共享CPU寄存器的值。
      • 硬件上下文存放在TTS中,任務狀態段。
    • 系統級上下文:進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。

如何判斷當前的上下文

  • in_irq()是用來判斷是否在hard interrupt context的

為什么中斷上下文不能sleep

本來呢interrupt context身輕如燕,沒有依賴的task,調度器其實是不知道如何調度interrupt context的(它處理的都是task),在interrupt context借了一個外殼后,從理論上將,調度器是完全可以block該interrupt context執行,並將其他的task調入進入running狀態。然而,block該interrupt context執行也就block其外殼task的執行,多么的不公平,多么的不確定,中斷命中你,你就活該被schedule out,擁有正常思維的linux應該不會這么做的。因此,在中斷上下文中(包括hard interrupt context和software interrupt context)不能睡眠。

為何需要workqueue

workqueue和其他的bottom half最大的不同是它是運行在進程上下文中的,它可以睡眠,這和其他bottom half機制有本質的不同,大大方便了驅動工程師撰寫中斷處理代碼。當然,驅動模塊也可以自己創建一個kernel thread來解決defering work,但是,如果每個driver都創建自己的kernel thread,那么內核線程數量過多,這會影響整體的性能。因此,最好的方法就是把這些需求匯集起來,提供一個統一的機制,也就是傳說中的work queue了。

struct cpu_workqueue_struct {  // 每個CPU的workqueue結構

	spinlock_t lock;  // 保護該數據結構的自旋鎖

	long remove_sequence;	/* Least-recently added (next to run) */
	long insert_sequence;	/* Next to add  */

	struct list_head worklist; 
	wait_queue_head_t more_work; // 等待隊列頭
	wait_queue_head_t work_done; // 當前正在處理的work

	struct workqueue_struct *wq;  // 指向work queue struct,其中包含該描述符
	task_t *thread;  // worker thread task(工作線程任務描述符指針)
	struct completion exit; 

} ____cacheline_aligned;

worker thread要處理work,這些work被掛入work queue中的鏈表結構。由於每個processor都需要處理自己的work,因此這個work list是per cpu的。worklist成員就是這個per cpu的鏈表頭,當worker thread被調度到的時候,就從這個隊列中一個個的摘下work來處理。

然后看看work_struct(其實就是deferrable task抽象而來的數據結構):

struct work_struct {
	unsigned long pending;  //如果函數位於工作隊列,置位1否則為0
	struct list_head entry; //掛起函數鏈表前一個或后一個元素的指針
	void (*func)(void *); // 掛起函數的地址
	void *data; // 函數的參數
	void *wq_data;  //指向cpu_workqueue_struct描述符的父節點的指針
	struct timer_list timer;  //延遲掛起函數執行的軟定時器
};

所謂work就是異步執行的函數。如果該函數的代碼中有些需要sleep的場景的時候,那么在中斷上下文中直接調用將產生嚴重的問題。這時候,就需要到進程上下文中異步執行。下面我們仔細看看各個成員:func就是這個異步執行的函數,當work被調度執行的時候其實就是調用func這個callback函數。work對應的callback函數需要傳遞該work的struct作為callback函數的參數。work是被組織成隊列的,entry成員就是掛入隊列的那個節點,data包含了該work的狀態flag和掛入workqueue的信息。

導致worker thread進入sleep狀態有三個條件:(a)電源管理模塊沒有請求凍結該worker thread。(b)該thread沒有被其他模塊請求停掉。(c)work list為空,也就是說沒有work要處理。

從中斷和異常中返回

內容實在太多,這里mark一下,以后有機會再細讀。

中斷的個人理解

在我看關於中斷部分的相關資料的時候(《深入理解linux內核原理》和wowo大神關於中斷部分的講解),深刻感受到中斷必然和驅動相關。而這是上面很少提到的一個點。確實中斷大部分是關於外中斷的,而外中斷大部分是外設硬件發出的信號,那么什么人經常去深入了解中斷,我們應該從什么角度去理解中斷?很顯然還是需要站在驅動工程師這個角度來看。而對驅動工程師寫driver的時候需要考慮一下幾個點:

  1. 正確使用內核提供的API。
  2. 正確使用同步機制保護驅動代碼中的臨界區
  3. 正確使用kernel提供的softirq、tasklet、workqueue等機制來完成具體的中斷處理。

應該了解的最基本的點:

  • CPU、PIC(APIC)和外設提供的request line之間的整體關系

    • CPU和PIC的映射關系
    • PIC把IRQ送給哪個CPU?
  • 外設信號HM Interrupt如何轉換為IRQ數字的(偏硬件)

中斷子系統相關的軟件框架:

由上面的block圖,我們可知linux kernel的中斷子系統分成4個部分:

  1. 硬件無關的代碼,我們稱之Linux kernel通用中斷處理模塊。
  2. CPU architecture相關的中斷處理, 和系統使用的具體的CPU architecture相關。
  3. Interrupt controller驅動代碼 。
  4. 普通外設的驅動。

名詞解釋:

  1. TTS(task state segment):任務狀態段,用來存放硬件上下文。
  2. GDT(global description table) 全局描述表:又叫段描述符表。在整個系統中,全局描述符表GDT只有一張(一個處理器對應一個GDT),GDT可以被放在內存的任何位置,但CPU必須知道GDT的入口,也就是基地址放在哪里,Intel的設計者門提供了一個寄存器GDTR用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之后,可以通過LGDT指令將GDT的入口地址裝入此寄存器,從此以后,CPU就根據此寄存器中的內容作為GDT的入口來訪問GDT了。GDTR中存放的是GDT在內存中的基地址和其表長界限。
  3. /proc是一個偽文件系統,它只存在內存當中,而不占用外存空間。它以文件系統的方式為訪問系統內核數據的操作提供接口。用戶和應用程序可以通過 proc得到系統的信息,並可以改變內核的某些參數。由於系統的信息,如進程,是動態改變的,所以用戶或應用程序讀取proc文件時,proc文件系統是 動態從系統內核讀出所需信息並提交的。
  4. 軟中斷通常包括:可延遲函數,編程異常。上面是為了區別,才換了個名字。
  5. 軟中斷和硬中斷和開中斷關中斷的區別:一個是硬件框圖,一個是流程。

推薦閱讀:

GDT:https://www.cnblogs.com/bajdcc/p/8972946.html

關於並發

tasklet 通過 TASKLET_STATE_SCHED 和 TASKLET_STATE_RUN 來防止並發,threaded irq hander 也通過IRQS_ONESHOT防止並發,workqueue上的流程1、掛入隊列。2、執行work。掛入到multi thread或者說per cpu workqueue上的指定的work,其callback是不會在一個cpu上並發執行(也就是說在多個cpu上可以並發執行)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM