天天看點

Linux中斷--很好的一篇文章

本文整理了 Linux 核心中斷的相關知識,其他 Linux 相關文章均收錄于貝貝貓的文章目錄。

大家應該很清楚,系統在執行時可以處于兩種可能的狀态:核心态和使用者态。之前我們讨論過的系統調用,就能使程序從使用者态切換到核心态去執行某些任務,當執行成功後再回到使用者程序中。大家可能還記得這是通過軟中斷來實作的,那麼中斷到底是什麼呢? 接下來我将介紹與中斷相關的一些知識。

通常中斷可以分為如下兩個類别:

同步中斷和異常。這些由 CPU 自發地針對目前執行的程式産生的。異常可能因種種原因觸發: 由于運作時發生的程式設計錯誤(除0),或由于出現了異常的情況或條件,緻使處理區需要外部幫助才能處理。前一種情況下,核心必須通知應用程式出現了異常,比如使用信号機制,這樣應用程式才有機會輸出一些适當的錯誤資訊。但是,異常也不見得一定是程式内部導緻的,比如缺頁異常,這時候就需要核心出面解決。

異步中斷。多數由裝置産生,可能發生在任何時間。不同與同步中斷,異步中斷并不與特定的程序關聯。比如網卡通過發送中斷通知核心有新的資料到來,因為資料可能随時到來,是以目前 CPU 上執行的可能是和該網絡資料無關的程序。為了避免損害該程序的執行時間,核心必須快速的完成資料的處理工作,使得 CPU 時間能夠返還給目前程序。但是處理網絡資料并不簡單,要花費許多的時間,是以核心采取的辦法是将中斷分為兩半,前半段盡可能地快速完成(将網絡資料緩存在記憶體中,緩存好了之後就将 CPU 返還給程序),後半段在不是那麼繁忙的時候再處理。

無論是上述的哪種中斷,都會涉及到中斷處理過程中的一個關鍵流程,那就是:如果在發生中斷時,目前 CPU 沒有處于核心态,則發起從使用者态到核心态的轉換。接下來,在核心中執行一個專門的中斷處理程式。

另外,我們知道中斷是可以被禁用的,比如在一些中斷處理程式中,可能通過禁用中斷的方式來達到資料臨界區的效果,但是,禁用中斷的時間過長的話,注定會影響到系統的性能,還有可能漏掉其他重要的中斷,是以中斷處理程式被劃分為兩個部分,關鍵性的任務會在前半段(禁用中斷時)處理,而不那麼重要的工作會在後半段異步延期處理。這裡講這麼多就是為了讓大家明白中斷可能會被分階段處理,如果一個中斷對應的工作很快就能完成,那麼一般都會以同步的方式處理,而如果一個中斷的處理過程可能花費很長時間的話,可能就會分成兩段,前一段已同步的方式處理關鍵性的任務,後一段以異步的形式處理次要任務。

那麼系統是怎麼将中斷與對應的處理程式挂鈎的呢?一個簡單的想法是:每個中斷都有一個編号,比如配置設定給一個網卡的中斷号是 m,配置設定給 SCSI 控制器的中斷号是 n,那麼核心即可區分兩個裝置,并在中斷發生時對應地執行特定于裝置的操作。同樣的方案也可适應于異常,不同的異常指派了不同的編号。但遺憾的是,由于系統架構的原因,情況并不總是像描述的那樣簡單。在一些系統架構中,可用的中斷編号少得可憐,是以必須由幾個裝置共享一個編号,這個過程被稱為中斷共享。在 IA-32 處理器上,硬體中斷的最大數目通常是15,這個值可不怎麼大,此外,還要考慮到有些中斷編号已經永久性地配置設定給了标準的系統元件(鍵盤、定時器,等),這就限制了其他外設的中斷編号數。

實際上,外設并不會直接産生中斷,它們會有電路連接配接到中斷控制器,在需要發送中斷時,外設會向中斷控制器放中斷請求(IRQ),随後中斷控制器将中斷請求(IRQ)轉化為對應的中斷号,最終傳輸到 CPU 的中斷輸入中。當 CPU 得知發生中斷後,它将進一步的處理委托給一個中斷處理程式,該程式可能會修複故障、提供專門的處理或者将事件通知使用者程序等。由于每個中斷和異常都有一個唯一的編号,核心使用了一個數組來維護中斷号到對應處理程式的映射關系,就如下圖所示。

Linux中斷--很好的一篇文章

這裡大家肯定會有疑問,中斷号是怎麼共享的呢?實際上每個中斷号對應一個中斷處理程式隻是一個籠統的說法,當多個裝置共用同一個中斷号時,顯然一個中斷号會對應一組處理程式。實際上,在我們安裝裝置的驅動時,就會将該裝置對應的中斷處理程式注冊到對應的中斷号上,換句話說,核心會為每個中斷号,維護一個處理程式連結清單(下圖 action),連結清單上的每個節點都對應了一個使用該中斷号的裝置。

Linux中斷--很好的一篇文章

對于每個裝置的 irqaction 除了會記錄其對應的處理程式位址外,還會記錄一個簡要的裝置名和裝置 id,裝置名主要用于顯示給人看​<code>​/proc/irq/{Num}/{Name}​</code>​,而裝置 id 用來描述某一指定的裝置,這樣在解除安裝裝置驅動時,隻要指出該裝置的中斷号以及該裝置的 id 就能将其中斷處理程式從上述連結清單中删除。

現在我們已經知道了如何動态的注冊和删除中斷處理程式,接下來我們還要解釋一下當中斷發生時,核心如何定位應該讓哪個中斷處理程式處理。因為 CPU 在中斷發生時,隻能拿到中斷号這一個參數,是以核心的處理方式非常粗暴:

它會先檢查目前是否該中斷是否被屏蔽

逐一調用注冊在該中斷号上的所有處理函數

每個中斷處理程式需要自己确定該中斷是不是自己的裝置發出的,一般有兩個方案:

比較近代的裝置上都會有一個裝置寄存器,記錄着該裝置剛才有沒有發出中斷信号,如果寄存器為 1 則說明是自己發出的,那麼就将寄存器置為 0 然後開始處理

如果裝置沒有寄存器,則會檢查是否有裝置資料可用,有的話處理資料,沒有的話,則傳回

每個中斷處理程式如果正确的處理的 IRQ 則傳回 IRQ_HANDLED,否則如果不是有自己負責的話傳回 IRQ_NONE

核心會逐一調用每個處理函數,無論是否有處理程式已經正确的處理

那麼,當核心開始進行中斷時都需要做什麼呢?下圖就簡要的描述了整個中斷處理的過程。

Linux中斷--很好的一篇文章

進入路徑的一個關鍵任務是,從使用者态棧切換到核心态棧。但是,隻有這點還不夠。因為核心還要使用 CPU 資源執行其代碼,進入路徑必須儲存使用者應用程式目前的寄存器狀态,以使在中斷活動結束後恢複。這與排程期間用于上下文切換的機制是相同的。在進入核心态時,隻儲存部分寄存器的内容,因為核心并不會使用全部寄存器。舉例來說,核心代碼中不使用浮點操作(隻有整數計算),因而并不儲存浮點寄存器。随後核心跳轉到與中斷号對應的中斷處理程式中執行特定的任務,在退出時,核心會檢查如下事項:

排程器是否應該選擇一個新程序代替舊的程序。

是否有信号必須投遞到原程序。

從中斷傳回之後,隻有确認了這兩個問題,核心才能完成其正常任務,即還原寄存器集合、切換到使用者态棧、切換到适當的處理器狀态(如果原來是使用者态,就切換回使用者态)。

實作中斷處理程式時,也會遇到很多問題,比如在進行中斷期間,發生了其他中斷,盡管可以通過禁用中斷來防止這個問題,但是這又會引入别的問題,比如遺漏重要中斷。是以禁用中斷這個功能必須隻能短時間使用。總結一下中斷處理程式要滿足如下幾個要求:

實作(特别是要禁用其他中斷時)要盡可能簡單,以支援快速處理

中斷處理程式也允許被其他中斷打斷,彼此還要互不幹擾

盡管後一個問題我們可以通過精妙的設計方案來解決,但是前一個就很困難了。因為中斷處理程式的每個部分并不是同等重要,是以每個中斷處理程式都可以劃分為 3 個部分,它們具有不同的意義:

關鍵操作必須在操作發生後立即執行。否則,無法維持系統的穩定,在執行此類操作時,必須禁止其他中斷。

非關鍵操作也應該盡快執行,但是允許其他中斷搶占

可延期處理的操作不是那麼重要,不必在中斷處理程式中實作。核心可以延遲處理這些工作,在時間充裕的時候進行。比如核心提供了 tasklet 機制,用于稍後執行可延期的操作。

在實作處理程式例程時,必須要注意些要點。這些會極大地影響系統的性能和穩定性。中斷處理程式在所謂的中斷上下文(interrupt context)中執行。核心代碼有時在正常上下文運作,有時在中斷上下文運作。為區分這兩種不同情況并據此設計代碼,核心提供了 in_interrupt 函數,用于指明目前是否在進行中斷。中斷上下文與普通上下文的不同之處主要有如下 3 點。

中斷是異步執行的。換句話說,它們可以在任何時間發生。因而從使用者空間來看,處理程式并不是在一個明确定義的環境中執行。是以在這種環境下,禁止通路使用者空間,特别是與使用者空間位址之間來回複制記憶體資料的行為。例如,對網絡驅動程式來說,不能将接收的資料直接轉發到等待的應用程式。畢竟,核心無法确定等待資料的應用程式此時是否在運作(事實上,這種可能性很低)

中斷上下文中不能調用排程器。因為中斷上下文具有最高執行優先級,這是核心無法排程别的程序來執行。它隻能以主動傳回的方式結束自己的處理過程。

處理程式例程不能進入睡眠狀态。因為程序睡眠後,中斷處理程式隻能永遠等待下去。因為中斷處理程式沒有排程實體,是以不可能被再次排程。當然,隻確定處理程式的直接代碼不進入睡眠狀态是不夠的。其中調用的所有其他函數都不能進入睡眠狀态。對此進行的檢查并不簡單,必須非常謹慎。

至此,中斷的主要知識就已經串完了,我們接下來還要介紹一下軟中斷,它是從軟體層面觸發中斷的途徑。介紹完軟中斷,我們才會開始介紹中斷後半段的處理方式,比如 tasklet。

軟中斷的意義是使核心可以延期執行任務,因為它的運作方式和上述的中斷類似,但完全是從軟體實作的,是以稱為軟中斷。核心借助軟中斷來獲知異常情況的發生,而該情況将在稍後有專門的處理程式解決。

軟中斷是相對稀缺的資源,因為各個軟中斷都有一個唯一的編号,是以使用其必須謹慎,不能由各種裝置驅動程式和核心元件随意使用,預設情況下,系統上隻能使用 32 個軟中斷,但這個沒什麼,因為基于軟中斷核心還衍生出了許多其他其他延期執行機制,比如 tasklet、工作隊列和核心定時器。我們稍後會介紹它們。

隻有中樞的核心代碼才使用軟中斷,軟中斷隻用于少數場景,如下就是其中相對重要的場景。其中兩個用來實作 tasklet (HI_SOFTIRQ,TASKLET_SOFTIRQ),兩個用于網絡的發送和接受(NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,這兩個是建構軟中斷機制的最主要原因),一個用于塊層,實作異步請求完成(BLOCK_SOFTIRQ),一個用于排程器(SCHED_SOFTIRQ),以實作 SMP 系統上周期性的負載均衡。在啟用高分辨率定時器時,還需要一個軟中斷(HRTIMER_SOFTIRQ)。

軟中斷的編号形成了個優先順序,雖然這并不影響各個處理程式例程執行的頻率或它們相當于其他系統活動的優先級,但影響了多個軟中斷同時處理時執行的次序。

我們可以通過 raise_softirq(int nr) 發起一個軟中斷(類似普通中斷),軟中斷的編号通過參數指定。每個 CPU 都有一個位圖 irg_stat,其中每一位代表了一個中斷号,raise_softirq 會函數設定各 CPU 變量 irg_stat 對應的比特位。該函數會将對應的軟中斷标記為 1,但是該中斷的處理程式并不會立即運作。通過使用特定于處理器的位圖,核心才能確定幾個軟中斷(甚至是相同的)可以同時在不同的 CPU 上執行。

那麼軟中斷在什麼時候執行呢?

目前面的硬體中斷處理程式執行結束後,會檢查目前 CPU 是否有待決的軟中斷,有的話則會按照次序處理所有的待決軟中斷,每處理一個軟中斷之前,就會将其對應的比特位置零,處理完所有軟中斷的過程,我們稱之為一輪循環

一輪循環處理結束後,核心還會再檢查是否有新的軟中斷到來(通過位圖),如果有的話,一并處理了,這就會出現第二輪循環,第三輪循環

但是軟中斷不會無休止的重複下去,當處理的輪數超過 MAX_SOFTIRQ_RESTART(通常是 10) 時,就會喚醒軟中斷守護線程(每個 CPU 都有一個),然後退出

軟中斷守護線程負責在軟中斷過多時,以一個排程實體的形式(和其他程序一樣可以被排程),幫着處理軟中斷請求,在這個守護線程中會重複的檢測是否有待決的軟中斷請求

如果沒有軟中斷請求了,則會進入睡眠狀态,等待下次被喚醒

如果有請求,則會調用對應的軟中斷處理程式

這裡大家可能有一個疑問,我們在前面介紹系統調用時也說了它是通過中斷實作的,那麼在前面的軟中斷清單中怎麼沒有對應的軟中斷呢?實際上,系統調用使用到的中斷屬于 "軟體觸發的硬中斷" 而不是這裡所說的軟中斷,因為系統調用過程是要同步處理的,不能使用異步的軟中斷方式實作。在我的 linux 中執行 ​<code>​cat /proc/interrupts​</code>​ 會列印所有注冊的硬中斷,仔細觀察之後,你會發現其中包含一個名為 'CAL' 的中斷,它就是系統調用所對應的中斷号。這是通過執行機器指令觸發的,是以我才說它是軟體觸發的硬中斷。

雖然軟中斷是将操作推遲到未來時刻執行的最有效方法,但軟中斷的中斷号有限,而且該延期機制處理起來非常複雜。因為多個處理器可以同時且獨立地處理軟中斷,是以同一個軟中斷的處理程式例程可以在幾個 CPU 上同時運作,這就要求軟中斷處理程式的設計必須是可重入并且線程安全的,臨界區必須用自旋鎖保護。此外,在軟中斷還不能進入睡眠,因為軟中斷的其中一部分是在硬中斷處理結束之後進行的,這時候軟中斷執行函數沒有排程實體,是以不能進入睡眠。

既然軟中斷這麼多限制,那開發裝置驅動程式(以及其他一般的核心代碼)的同學豈不是很痛苦,實際上核心基于軟中斷建立了很多上層異步處理機制。

tasklet 是一種延期執行工作的機制,其實作基于軟中斷,但它們更易于使用,因而更适合于裝置驅動程式(以及其他一般性的核心代碼)。

在核心中每個 tasklet 都有與之對應的一個對象表示,核心以連結清單的形式管理所有的 tasklet(next 字段),而且每個 tasklet 都有兩個狀态,這兩個狀态通過 state 字段的不同位表示,其中一個代表該 tasklet 是否注冊到核心,成為一個排程實體(TASKLET_STATE_SCHED),另一個代表該 tasklet 是否正在運作(TASKLET_STATE_RUN)。通過 TASKLET_STATE_RUN 我們可以使一個 tasklet 隻在一個 CPU 上執行。此外 count 字段大于 0 表示該 tasklet 被忽略。

當我們注冊 tasklet 時,如果發現 TASKLET_STATE_SCHED 已經被置為 1,則說明該 tasklet 已經注冊了,就不會重複注冊。那麼 tasklet 在什麼時候執行呢? tasklet 的執行被關聯到 TASKLET_SOFTIRQ 軟中斷。因而,在調用 raise_softirq(TASKLET_SOFTIRQ) 時,tasklet 就會在合适的時機執行。執行過程是這樣的:

檢查 tasklet 的 TASKLET_STATE_RUN 是否被置為 1,是的話則說明其他 CPU 正在執行它,那麼目前 CPU 就跳過它

檢查其是否被禁用(count 是否大于零)

将 TASKLET_STATE_RUN 置為 1

調用 tasklet 的 func

因為 tasklet 本質上也是在軟中斷的處理程式中進行的,是以它并不能睡眠或者阻塞,但是它能保證同一時刻某個 tasklet 隻會在一個 CPU 上執行,這就有天生的線程安全保障。

除了普通的 tasklet 之外,核心還提供了另一種 tasklet,它具有更高的優先級,除此之外,它們兩個完全相同。高優先級的 tasklet 通過 HI_SOFTIRQ 軟中斷觸發而不是 TASKLET_SOFTIRQ,這兩種 tasklet 在不同的連結清單中維護。這裡的高優先級是指軟中斷的處理程式 HI_SOFTIRQ 比其他軟中斷處理程式更先執行,因為它排在軟中斷号的第一位。很多聲霸卡驅動以及高速網卡都是依賴高優先級 tasklet 實作的。

我們已經知道 tasklet 不能解決睡眠和阻塞的問題,那麼當裝置驅動要等待某一特定事件發生的時候,有什麼辦法嗎?我們可以通過等待隊列來完成這個需求。既然要睡眠和阻塞,勢必須要一個排程實體,換句話說,等待隊列中的項不再是一個簡單的處理函數,而是一個類似于背景程序一樣的存在。

等待隊列的使用分為如下兩部分。

為使目前程序在一個等待隊列中睡眠,需要調用 wait_event 函數。程序進入睡眠,将控制權釋放給排程器。核心通常會在向塊裝置發出傳輸資料的請求後,調用該函數。因為傳輸不會立即發生,而在此期間又沒有其他事情可做,是以程序可以睡眠,将 CPU 時間讓給系統中的其他程序。

就上面的例子而言,塊裝置的資料到達後,必須調用 wake_up 函數來喚醒等待隊列中的睡眠程序。在使用 wait_event 使程序睡眠之後,必須確定在核心中另一處有一個對應的 wake_up 調用。

wait_event 是一個宏,它接收兩個參數,第一個是等待隊列對象 wait_queue_t,第二個是判斷事件是否到來的 bool 表達式。這個宏的實作也很簡單,就是先将目前程序加入到等待隊列的 task_struct 連結清單中,然後循環地通過第二個參數确認是否事件已經到來,如果來了則跳出循環,否則繼續睡眠。

wake_up 函數也很簡單,第一個是等待隊列連結清單的第一個對象 wait_queue_head_t,第二個參數 mode 指定程序的狀态(TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE),第三個參數 nr_exclusive控制喚醒該隊列上的幾個程序,如果是 1 則表明是獨占的事件,隻喚醒其中一個,如果是 0 則會喚醒該隊列中的所有程序。

工作隊列是将操作延時執行的另一個手段。它和等待隊列一樣是通過守護程序實作,在使用者上下文執行,是以可以睡眠任意長的時間。它非常像一個"線程池",在建立的時候我們需要指定線程名,同時也可以指定是單個線程,還是每個 CPU 上建立一個對應的線程。

建立好工作隊列後,我們可以向其中注冊任務,每個工作任務的結構如下。注冊後的任務會維護在一個連結清單中,按照順序依次執行。

而且,在注冊工作内容時,我們還可以指定延時任務,它會在一個指定延遲後開始執行。當建立延時任務時,核心會建立一個定時器,它将在 delay jiffies 之後逾時,随後相關的處理程式會将 delayed_work 内部的 work_struct 對象加入到工作隊列的連結清單中,剩下的工作就和普通任務完全一樣了。