【讀書筆記】Linux內核設計與實現--中斷和中斷處理

衆所周知,處理器的速度跟外圍硬件設備的速度往往不在一個數量級上,因此,如果內核採取讓處理器向硬件發出一個請求,然後專門等待迴應的辦法,顯然差強人意。
既然硬件的響應這麼慢,那麼內核就應該在此期間處理其他事務,等待硬件真正完成了請求的操作之後,再回過頭來對它進行處理。

Q:如何做?
A:輪詢(polling)可能會是一種解決辦法。不過這種方法很可能會讓內核做不少無用功(輪詢總會週期性的重複執行)。
因此,需要提供一種機制,讓硬件在需要的時候再向內核發出信號(變內核主動爲硬件主動)–中斷機制。

1.中斷

中斷使得硬件得以發出通知給處理器,引起內核的關注。
中斷本質上是一種特殊的電信號,由硬件設備發向處理器。
中斷隨時可以產生,因此中斷處理程序也就隨時可能執行。
不同的設備對於的中斷不同,而每個中斷都通過一個唯一的數字標誌。
這些唯一的數字標誌稱爲中斷值(中斷請求IRQ線)

異常(也稱同步中斷):
在操作系統中,討論中斷就不能不提及異常。
異常與中斷不同,它在產生時必須考慮與處理器時鐘同步。它們的工作方式類似,其差異只在於中斷是由硬件而不是軟件引起的。

ps:系統調用就是一種異常(系統調用處理程序異常),通過軟中斷實現系統調用。

2.中斷處理程序

在響應一個特定中斷的時候,內核會執行一個函數–中斷處理程序(interrupt handler) 或 中斷服務例程(interrupt service routine,ISR)。

一個設備的中斷處理程序是它設備驅動程序的一部分–設備驅動程序是用於對設備進行管理的內核代碼。

Q:中斷處理程序和其他內核函數的區別?
A:中斷處理程序就是普通的C函數,只不過必須按照特定的類型聲明,以便內核能夠以標準的方式傳遞處理程序的信息。其真正的區別在於,中斷處理程序是被內核調用來響應中斷的,而它們運行於稱之爲中斷上下文(偶爾也稱爲原子上下文–上下文的執行代碼不可阻塞)的特殊上下文中。

ps:中斷處理程序通常不是和特定設備關聯,而是和特定中斷關聯的,也就是說,如果一個設備可以產生多種不同的中斷,那麼該設備就可以對應多箇中斷處理程序。相應的,該設備的驅動程序也就需要準備多個這樣的函數。

3.上半部與下半部的對比

又想中斷處理程序運行得快,又想中斷處理程序完成的工作量多,這兩個目的顯然有所抵觸。鑑於兩個目的之間存在此消彼長的矛盾關係,所以一般把中斷處理切爲兩個部分或兩半

中斷處理程序是上半部(top half) – 接收到一箇中斷,它就立即開始執行,在所有中斷被禁止的情況下完成(嚴格時限)。
能被允許稍後完成的工作會推遲到下半部(bottom half),然後在何時的時機,下半部會被開中斷執行(Linux提供了實現下半部的各種機制)。

4.註冊中斷處理程序–request_irq

中斷處理程序是管理硬件的驅動程序的組成部分。每一個設備都有相關的驅動程序,如果設備使用中斷,那麼相應的驅動程序就註冊一箇中斷處理程序。

驅動程序可以通過request_irq()函數註冊一箇中斷處理程序(被聲明在<linux/interrupt.h>)中,並且激活給定的中斷線,以處理中斷:

/* request_irq:分配一條給定的中斷線 */
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev);

irq:要分配的中斷號。(對某些設備該值通常預先確定的,如PC設備上的系統時鐘或鍵盤,對於其他設備,通常可以通過探測獲取,或者編程動態確定)
handler:是一個指針,指向處理這個中斷的實際中斷處理程序(回調)。只要操作系統一接收到中斷,該函數就被調用。

typedef irqreturn_t (*irq_handler_t)(int, void *);

flags:可以爲0,也可以是一個或者多個標誌的位掩碼,定義在<linux/interrupt.h>中,較重要的有IRQF_DISABLED、IRQF_SAMPLE_RANDOM、IRQF_TIMER、IRQF_SHARED等。
name:是與中斷相關的設備的ASCII文本表示。這些名字會被/proc/irq和/proc/interrupts文件使用。
dev:用於共享中斷線。當一箇中斷處理程序需要釋放時,dev將提供唯一的標誌信息(cookie),以便從共享中斷線的諸多中斷處理程序中刪除指定的那一個。如果沒有這個參數,那麼內核不可能知道在給定的中斷線上到底要刪除哪一個處理程序。如果無需共享中斷線,那麼將該參數賦值爲空值(NULL)就可以了,但是,如果中斷線是被共享的,那麼就必須傳遞唯一的信息。另外,內核每次調用中斷處理程序時,都會把這個指針傳遞給它。
ps:中斷處理程序都是預先在內核進行註冊的回調函數,而不同的函數位於不同的驅動程序中,所以在這些函數共享同一個中斷線時,內核必須準確的爲它們創造執行環境,此時就可有通過這個指針將有用的環境信息傳遞給它們。

request_irq()成功執行會返回0,如果返回非0值,就表示有錯誤發生,可根據錯誤碼判斷錯誤原因。

ps:

  1. request_irq()函數可能會睡眠,因此,不能在中斷上下文或其他不允許阻塞的代碼中調用該函數。
  2. 初始化硬件和註冊中斷處理程序的順序必須正確,以防止中斷處理程序在設備初始化完成之前就開始執行。

Q:request_irq()函數爲何可能會睡眠?
A:在註冊過程中,內核需要在/proc/irq文件中創建一個與中斷對應的項。函數proc_mkdir()就是用來創建這個新的procfs項的。proc_mkdir()通過調用函數proc_create()對這個新的profs項進行設置,而proc_create()會調用函數kmalloc()來請求分配內存–函數kmalloc()是可以睡眠的。

Q:如何釋放中斷處理程序?
A:
卸載驅動程序時,需要註銷相應的中斷處理程序,並釋放中斷線。可調用如下方法實現:

void free_irq(unsigned int irq, void *dev);

ps:如果指定的中斷線不是共享的,那麼該函數刪除處理程序的同時將禁用這條中斷線。
如果中斷線是共享的,則僅刪除dev所對應的處理程序,則這條中斷線本身只有在刪除了最後一個處理程序纔會被禁用。

因此,對於共享的中斷線,唯一的dev是很重要的。

對於共享的中斷線,需要一個唯一的信息來區分其上面的多個處理程序,並讓free_irq()僅僅刪除指定的處理程序。

不管在中斷線共享不共享,如果dev非空,它都必須與需要刪除的處理程序相匹配,必須從進程上下文中調用free_irq()。
中斷註冊方法表如下:

函數 描述
request_irq() 在給定的中斷線上註冊一給定的中斷處理程序
free_irq() 如果在給定的中斷線上沒有中斷處理程序,則註銷相應的處理程序,並禁用其中斷線

5.編寫中斷處理程序

中斷處理程序的聲明如下:

static irqreturn_t intr_handler(int irq, void *dev);

該函數的類型與request_irq()參數中的handler所要求的參數類型相匹配。
第一個參數irq就使這個處理程序要響應的中斷的中斷號。
ps:這個irq參數在沒有dev這個參數的時候比較重要,在2.0以後的版本,irq這個參數沒有太大的用處,一般打印日誌信息時會打印irq號。
第二個參數dev是一個通用指針,與中斷處理程序註冊時傳遞給request_irq()的參數dev必須一致。 如果該值由唯一確定性(爲了能支持共享),那麼它就相當於一個cookie,可以用來區分共享同一中斷處理程序的多個設備。

ps:對於每個設備而言,設備結構都是唯一的,可能在中斷處理程序中用到。

返回值是一個特殊類型:irqreturn_t。
ps:中斷處理程序通常會標記爲static,它從來不會被別的文件中的代碼直接調用。
中斷處理程序可能返回兩個特殊的值:IRQ_NONE和IRQ_HANDLED。

當中斷處理程序檢測到一箇中斷,但該中斷對應的設備並不是在註冊處理函數期間指定的產生源時返I回IRQ_NONE;
當中斷處理程序被正確調用,且確實是它鎖對應的設備產生了中斷時,返回IRQ_HANDLED。

ps:
可以使用宏IRQ_RETVLA(val)返回返回值。
即,如果val未非0值返回IRQ_HANDLED;否則,返回IRQ_NONE。

重入和中斷處理程序:
Linux中的中斷處理程序是無須重入的。
當一個給定的中斷處理程序正在執行時,相應的中斷線在所有處理器上都會被屏蔽掉,以防止在同一中斷上接收另一個新的中斷。
通常情況下,所有其他的中斷都是打開的,所以這些不同中斷線上的其他中斷都能被處理,噹噹前中斷線總是被禁止的。

5.1 共享的中斷處理程序

中斷處理程序的共享與否在註冊和運行方式上比較相似,但差異有如下幾點,所有共享中斷線的驅動程序也都必須滿足以下要求:

  1. request_irq()的參數flags必須設置IRQF_SHARED標誌
  2. 對於每個註冊的中斷處理程序來說,dev參數必須唯一。指向任意設備結構的指針就可以滿足這一要求:通常會選擇設備結構,因爲它是唯一的,而且中斷處理程序可能會用到。不能給共享的處理程序傳遞NULL值;
  3. 中斷處理程序必須能夠區分它的設備是否真的產生了中斷,也就要求中斷處理程序必須知道是與它對於的設備發出了中斷,還是共享這條中斷線的其他設備發出了這個中斷,這既需要硬件的支持,也需要處理程序中有相關的邏輯處理。

指定IRQF_SHARED標誌以調用request_irq()時,只有在以下兩種情況纔可能成功:中斷線當前未被註冊,或者在該線上的所有已註冊處理程序都指定了IRQF_SHARED。

ps:
內核接收一箇中斷後,它將依次調用在該中斷線上註冊的每一個處理程序。因此,一個處理程序必須知道它是否應該未這個中斷負責。如果與它相關的設備並沒有產生中斷,那麼處理程序應該立即退出。這需要硬件設備提供狀態寄存器(或類似機制),以便中斷處理程序進行檢測。

5.2 中斷處理程序實例-rtc驅動程序

看完該實例–實例在rtc驅動代碼的中斷函數裏,或者該書的7.5.2章節,有如下疑問:
共享中斷線就不共享中斷處理程序,共享中斷處理程序就不共享中斷線。 可以這樣理解?
希望大佬評論區留言。

6.中斷上下文

當執行一箇中斷處理程序時,內核處於中斷上下文(interrupt context)中。
進程上下文是一種內核所處的操作模式,此時內核代表進程執行。在進程上下文中,可以通過current宏關聯當前進程(java 的Context類)。
因爲進程是以進程上下文的形式連接到上下文的形式連接到內核中的,因此,進程上下文可以睡眠,也可以調用調度程序(中斷上下文不能睡眠,可參考這篇博文爲什麼中斷不能休眠)。

因爲中斷處理程序打斷了其他的代碼(甚至可能是打斷了在其他中斷線上的另一中斷處理程序),所以所有的中斷處理程序必須儘可能的迅速、簡介。儘量把工作從中斷處理程序中分離出來,放在下半部來執行,因爲下半部可以在更合適的時間運行。

在2.6以後的內核版本,內核棧和中斷棧分別獨立。
Q:何爲中斷棧?
A:爲了應對棧大小的減少,中斷處理程序擁有了自己的棧(以前共享的內核棧),每個處理器一個,大小爲一頁。這個棧就稱爲中斷棧

7.中斷處理機制的實現

中斷處理系統在Linux中的實現非常依賴於體系結構,因爲實現依賴於處理器、所使用的中斷控制器的類型、體系結構的設計及機器本身。

下圖展示了中斷從硬件到內核的路由:
在這裏插入圖片描述
設備產生中斷,通過總線把電信號發送給中斷控制器。如果中斷線是激活的(它們是允許被屏蔽的),那麼中斷控制器就會把中斷髮往處理器。
在大多數體系結構中,這個工作是通過電信號給處理器的特定管腳發送一個信號。除非在處理器上禁止該中斷,否則,處理器就會立即停止它正在做的事,關閉中斷系統,然後跳到內存中預定義的位置開始執行那裏的代碼。這個預定義的位置是由內核設置的,是中斷處理程序的入口點。

Q:硬件中斷始於硬件產生中斷到中斷線到處理器,那麼處理器(內核)的中斷以及處理是如何?
A:在內核中,中斷的旅程開始於預定義入口點,類似於系統調用通過預定義的異常句柄進入內核。對於每條中斷線,處理器都會跳到對應的一個唯一的位置。這樣,內核就可以知道所接收中斷的IRQ號了。
初始入口點只是在棧中保存這個號,並存放當前寄存器的值(這些值屬於被中斷的任務);然後,齧合開始調用函數do_IRQ()。
從這裏開始,大部分中斷處理代碼是用C編寫的–但他們依然與體系結構相關。
do_IRQ()聲明如下:

unsigned int do_IRQ(struct pt_regs regs);

C的調用慣例是要把函數參數放在棧的頂部,因此,pt_regs結構包含原始寄存器的值,這些值是以前在彙編入口例程中保存在棧中的。中斷的值也會得以保存,而do_IRQ()可以將它提取出來。

得到中斷號後,do_IRQ()對所接收的中斷進行應答,禁止這條線上的中斷傳遞。然後,do_IRQ()需要確保在這條中斷線上有一個有效的處理程序(中斷處理程序),而且這個程序已經啓動,但目前並沒有執行。如果是這樣,do_IRQ()就調用handle_IRQ_event()來運行爲這條中斷線所安裝的中斷處理程序。handle_IRQ_event()方法定義在文件kernel/irq/handler.c中。
從handle_IRQ_event()回到do_IRQ(),該函數做清理工作並返回到初始入口點,然後再從這個入口點跳到函數ret_from_intr()。
ret_from_intr()方法類似於初始入口代碼,以彙編語言編寫。該方法檢查重新調度是否正在掛起(做一些從中斷上下文出來後關於進程恢復或者調度的事情)

ps:handle_IRQ_event該方法的具體實現以及處理,可參考書中7.7章節。

8./proc/interrupts

procfs是一個虛擬文件系統,它只存在與內核內存,一般安裝於/proc目錄。
在procfs中讀寫文件都要調用內核函數,這些函數模擬從真是文件中讀寫。

/proc/interrupts文件存放的是系統中與中斷相關的統計信息。下圖是多處理器(SMP)上輸出信息:
在這裏插入圖片描述
第一列是中斷線(中斷號)
第二列是CPU0(處理器1)一個接收中斷數目的計數器。
倒數第二列是處理這個中斷的中斷控制器。
最後一列是與這個中斷相關的設備名字,這個名字是通過參數devname提供給函數request_irq()的。
如果中斷是共享的,則這條中斷線上註冊的所有設備都會列出來。

ps:
procfs代碼位於fs/proc中。提供/proc/interrupts的函數是與體系結構相關的,叫做show_interrupts()。

9.中斷控制

Linux內核提供了一組接口用於操作機器上的中斷狀態。這些接口提供了能夠禁止當前處理器的中斷系統,或屏蔽掉整個機器的一條中斷線的能力,這些例程都是與體系結構相關的,可以在<asm/system.h>和<asm/irq.h>中找到。

一般來說,控制中斷系統的原因歸根結底是需要提供同步。通過禁止中斷,可以確保某個中斷處理程序不會搶佔當前的代碼。此外,禁止中斷還以禁止內核搶佔。

ps:
禁止中斷提供保護機制,防止來自其他中斷處理程序的併發訪問,而沒有防止來自其他處理器的併發訪問。因此需要獲取某種鎖,鎖提供保護機制,保護多處理器的併發。

9.1 禁止和激活中斷

用於禁止當前處理器上的本地中斷,隨後又激活的語句爲:

local_irq_disable();
local_irq_enable();

這兩個函數通常以單個彙編指令來實現(依賴體系結構)。

ps:
在禁止中斷之前保存中斷系統的狀態會更加安全。相反,在準備激活中斷時,只需把中斷恢復到它們原來的狀態。
eg:

unsigned long flags;

local irq_save(flags);	/* 禁止中斷 */
/* ...... */
local_irq_restore(flags);	/* 中斷被恢復到它們原來的狀態 */

local_irq_save()和local_irq_restore()的調用必須在用一個函數中進行。
why?
A:這些方法至少部分要以宏的形式實現,因此表面上flags參數(這些參數必須定義爲unsigned long類型)是以值傳遞的。該參數包含具體體系結構的數據,也就是包含中斷系統的狀態。至少有一種體系結構把棧信息與值相結合(SPARC),因此flags不能傳遞給另一個函數(特別是它必須駐留在同一棧幀中)

9.2 禁止指定中斷線

Linux提供了四個接口用來屏蔽掉一條中斷線

void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);

前兩個函數禁止中斷控制器上指定的中斷線,即禁止給定中斷向系統中所有處理器的傳遞。
函數只有在當前正在執行的所有處理程序完成後,disable_irq()才能返回。因此,調用者不僅要確保不在指定線上傳遞新的中斷,同時還要確保所有已經開始執行的處理程序已全部退出。
函數disable_irq_nosync()不會等待當前中斷處理程序執行完畢。

函數synchronize_riq()等待一個特定的中斷處理程序的退出。如果該處理程序正在執行,那麼該函數必須退出後才能返回。

ps:
1.上述函數可以嵌套調用;
2.disable_irq和disable_irq_nosync的調用必須和enable成對配套使用才能保證真正的激活中端線。
3.禁止多箇中斷處理程序共享的中斷線是不合適的。禁止中斷線就禁止了這條線上的所有設備的中斷傳遞。

9.3 中斷系統的狀態

Q:如何瞭解中斷系統的狀態,或者是否正處於中斷上下文的執行狀態中?
A:宏irqs_disable()定義在<asm/system.h>中,如果本地處理器上中斷系統被禁止,則返回非0,否則返回0。
在<linux/hardirq.h>中定義的兩個宏提供一個用來檢查內核的當前上下文的接口,如下:

in_interrupt()
in_irq()

in_interrupt宏:如果內核處於任何類型的中斷處理中,返回0。 說明內核此刻正在執行中斷處理程序,或者正在執行下半部處理程序。
in_irq宏只有在內核確實在正在執行中斷處理程序時才返回非0。

中斷控制方法列表如下:

函數 說明
local_irq_disable() 禁止本地中斷傳遞
local_irq_enable() 激活本地中斷傳遞
local_irq_save() 保存本地中斷傳遞的當前狀態,然後禁止本地中斷傳遞
locar_irq_restore() 恢復本地中斷傳遞到給定的狀態
disable_irq() 禁止給定中斷線,並確保該函數返回之前在該中斷線上沒有處理程序在運行
disable_irq_nosync() 禁止給定中斷線
enable_irq() 激活給定中斷線
irqs_disabled() 如果本地中斷傳遞被禁止,則返回非0;否則返回0
in_interrupt() 如果在中斷上下文中,則返回0;如果在進程上下文中,則返回0
in_irq() 如果當前正在執行中斷處理程序,則返回非0,否則返回0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章