天天看點

【讀書筆記】Linux核心設計與實作--中斷和中斷處理1.中斷2.中斷處理程式3.上半部與下半部的對比4.注冊中斷處理程式–request_irq5.編寫中斷處理程式6.中斷上下文7.中斷處理機制的實作8./proc/interrupts9.中斷控制

文章目錄

  • 1.中斷
  • 2.中斷處理程式
  • 3.上半部與下半部的對比
  • 4.注冊中斷處理程式--request_irq
  • 5.編寫中斷處理程式
    • 5.1 共享的中斷處理程式
    • 5.2 中斷處理程式執行個體-rtc驅動程式
  • 6.中斷上下文
  • 7.中斷處理機制的實作
  • 8./proc/interrupts
  • 9.中斷控制
    • 9.1 禁止和激活中斷
    • 9.2 禁止指定中斷線
    • 9.3 中斷系統的狀态

衆所周知,處理器的速度跟外圍硬體裝置的速度往往不在一個數量級上,是以,如果核心采取讓處理器向硬體發出一個請求,然後專門等待回應的辦法,顯然差強人意。

既然硬體的響應這麼慢,那麼核心就應該在此期間處理其他事務,等待硬體真正完成了請求的操作之後,再回過頭來對它進行處理。

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:是一個指針,指向處理這個中斷的實際中斷處理程式(回調)。隻要作業系統一接收到中斷,該函數就被調用。

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:

解除安裝驅動程式時,需要登出相應的中斷處理程式,并釋放中斷線。可調用如下方法實作:

ps:如果指定的中斷線不是共享的,那麼該函數删除處理程式的同時将禁用這條中斷線。

如果中斷線是共享的,則僅删除dev所對應的處理程式,則這條中斷線本身隻有在删除了最後一個處理程式才會被禁用。

是以,對于共享的中斷線,唯一的dev是很重要的。

對于共享的中斷線,需要一個唯一的資訊來區分其上面的多個處理程式,并讓free_irq()僅僅删除指定的處理程式。

不管在中斷線共享不共享,如果dev非空,它都必須與需要删除的處理程式相比對,必須從程序上下文中調用free_irq()。

中斷注冊方法表如下:

函數 描述
request_irq() 在給定的中斷線上注冊一給定的中斷處理程式
free_irq() 如果在給定的中斷線上沒有中斷處理程式,則登出相應的處理程式,并禁用其中斷線

5.編寫中斷處理程式

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

該函數的類型與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中的實作非常依賴于體系結構,因為實作依賴于處理器、所使用的中斷控制器的類型、體系結構的設計及機器本身。

下圖展示了中斷從硬體到核心的路由:

【讀書筆記】Linux核心設計與實作--中斷和中斷處理1.中斷2.中斷處理程式3.上半部與下半部的對比4.注冊中斷處理程式–request_irq5.編寫中斷處理程式6.中斷上下文7.中斷處理機制的實作8./proc/interrupts9.中斷控制

裝置産生中斷,通過總線把電信号發送給中斷控制器。如果中斷線是激活的(它們是允許被屏蔽的),那麼中斷控制器就會把中斷發往處理器。

在大多數體系結構中,這個工作是通過電信号給處理器的特定管腳發送一個信号。除非在處理器上禁止該中斷,否則,處理器就會立即停止它正在做的事,關閉中斷系統,然後跳到記憶體中預定義的位置開始執行那裡的代碼。這個預定義的位置是由核心設定的,是中斷處理程式的入口點。

Q:硬體中斷始于硬體産生中斷到中斷線到處理器,那麼處理器(核心)的中斷以及處理是如何?

A:在核心中,中斷的旅程開始于預定義入口點,類似于系統調用通過預定義的異常句柄進入核心。對于每條中斷線,處理器都會跳到對應的一個唯一的位置。這樣,核心就可以知道所接收中斷的IRQ号了。

初始入口點隻是在棧中儲存這個号,并存放目前寄存器的值(這些值屬于被中斷的任務);然後,齧合開始調用函數do_IRQ()。

從這裡開始,大部分中斷處理代碼是用C編寫的–但他們依然與體系結構相關。

do_IRQ()聲明如下:

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)上輸出資訊:

【讀書筆記】Linux核心設計與實作--中斷和中斷處理1.中斷2.中斷處理程式3.上半部與下半部的對比4.注冊中斷處理程式–request_irq5.編寫中斷處理程式6.中斷上下文7.中斷處理機制的實作8./proc/interrupts9.中斷控制

第一列是中斷線(中斷号)

第二列是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

繼續閱讀