天天看點

linux核心分析筆記----中斷和中斷處理程式

中斷還是中斷,我講了很多次的中斷了,今天還是要講中斷,為啥呢?因為在作業系統中,中斷是必須要講的..

       那麼什麼叫中斷呢,

中斷還是打斷,這樣一說你就不明白了。唉,中斷還真是有點像打斷。我們知道linux管理所有的硬體裝置,要做的第一件事先是通信。然後,我們天天在說一

句話:處理器的速度跟外圍硬體裝置的速度往往不在一個數量級上,甚至幾個數量級的差别,這時咋辦,你總不能讓處理器在那裡傻等着你硬體做好了告訴我一聲

吧。這很容易就和日常生活聯系起來了,這樣效率太低,不如我處理器做别的事情,你硬體裝置準備好了,告訴我一聲就得了。這個告訴,咱們說的輕松,做起來還

是挺費勁啊!怎麼着,簡單一點,輪訓(polling)可能就是一種解決方法,缺點是作業系統要做太多的無用功,在那裡傻傻的做着不重要而要重複的工作,

這裡有更好的辦法---中斷,這個中斷不要緊,關鍵在于從硬體裝置的角度上看,已經實作了從被動為主動的曆史性突破。

中斷的例子我就不說了,這個很顯然啊。分析中斷,本質上是一種特殊的電信号,由硬體裝置發向處理器,處理器接收到中斷後,會馬上向作業系統反應此信号的帶

來,然後就由OS負責處理這些新到來的資料,中斷可以随時發生,才不用操心與處理器的時間同步問題。不同的裝置對應的中斷不同,他們之間的不同從作業系統

級來看,差别就在于一個數字辨別-----中斷号。專業一點就叫中斷請求(IRQ)線,通常IRQ都是一些數值量。有些體系結構上,中斷好是固定的,有的

是動态配置設定的,這不是問題所在,問題在于特定的中斷總是與特定的裝置相關聯,并且核心要知道這些資訊,這才是最關鍵的,不是麼?哈哈.

用書上一句話說:讨論中斷就不得不提及異常,異常和中斷不一樣,它在産生時必須要考慮與處理器的時鐘同步,實際上,異常也常常稱為同步中斷,在處理器執行

到由于程式設計失誤而導緻的錯誤指令的時候,或者是在執行期間出現特殊情況,必須要靠核心來處理的時候,處理器就會産生一個異常。因為許多處理器體系結構處理

異常以及進行中斷的方式類似,是以,核心對它們的處理也很類似。這裡的讨論,大部分都是适合異常,這時可以看成是處理器本身産生的中斷。

中斷産生告訴中斷控制器,繼續告訴作業系統核心,核心總是要處理的,是不?這裡核心會執行一個叫做中斷處理程式或中斷處理例程的函數。這裡特别要說明,中

斷處理程式是和特定中斷相關聯的,而不是和裝置相關聯,如果一個裝置可以産生很多中斷,這時該裝置的驅動程式也就需要準備多個這樣的函數。一個中斷處理程

序是裝置驅動程式的一部分,這個我們在linux裝置驅動中已經說過,就不說了,後面我也會提到一些。前邊說過一個問題:中斷是可能随時發生的,是以必須

要保證中斷處理程式也能随時執行,中斷處理程式也要盡可能的快速執行,隻有這樣才能保證盡可能快地恢複中斷代碼的執行。

但是,不想說但是,大學第一節逃課的情形現在仍記憶猶新:又想馬兒跑,又想馬兒不吃草,怎麼可能!但現實問題或者不像想象那樣悲觀,我們的中斷說不定還真

有奇迹發生。這個奇迹就是将中斷處理切為兩個部分或兩半。中斷處理程式上半部(top

half)---接收到一個中斷,它就立即開始開始執行,但隻做嚴格時限的工作,這些工作都是在所有中斷被禁止的情況下完成的。同時,能夠被允許稍後完成

的工作推遲到下半部(bottom

half)去,此後,下半部會被執行,通常情況下,下半部都會在中斷處理程式傳回時立即執行。我會在後面談論linux所提供的是實作下半部的各種機制。

       說了那麼多,現在開始第一個問題:如何注冊一個中斷處理程式。我們在linux驅動程式理論裡講過,通過一下函數可注冊一個中斷處理程式:

<a href="http://www.cnblogs.com/hanyan225/archive/2011/07/17/2108609.html#" target="_blank">?</a>

1

int request_irq(unsigned int irq,irqreturn_t (*handler)(int, void *,struct pt_regs *),unsigned long irqflags,const char * devname,void *dev_id)

       有關這個中斷的一些參數說明,我就不說了,一旦注冊了一個中斷處理程式,就肯定會有釋放中斷處理,這是調用下列函數:

void free_irq(unsigned int irq, void *dev_id)

       這裡需要說明的就是要必須要從程序上下文調用free_irq().好了,現在給出一個例子來說明這個過程,首先聲明一個中斷處理程式:

static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs)

       注

意:這裡的類型和前邊說到的request_irq()所要求的參數類型是比對的,參數不說了。對于傳回值,中斷處理程式的傳回值是一個特殊類

型,irqrequest_t,可能傳回兩個特殊的值:IRQ_NONE和IRQ_HANDLED.當中斷處理程式檢測到一個中斷時,但該中斷對應的裝置

并不是在注冊處理函數期間指定的産生源時,傳回IRQ_NONE;當中斷處理程式被正确調用,且确實是它所對應的裝置産生了中斷時,傳回

IRQ_HANDLED.C此外,也可以使用宏IRQ_RETVAL(x),如果x非0值,那麼該宏傳回IRQ_HANDLED,否則,傳回

IRQ_NONE.利用這個特殊的值,核心可以知道裝置發出的是否是一種虛假的(未請求)中斷。如果給定中斷線上所有中斷處理程式傳回的都是

IRQ_NONE,那麼,核心就可以檢測到出了問題。最後,需要說明的就是那個static了,中斷處理程式通常會标記為static,因為它從來不會被

别的檔案中的代碼直接調用。另外,中斷處理程式是無需重入的,當一個給定的中斷處理程式正在執行時,相應的中斷線在所有處理器上都會被屏蔽掉,以防止在同

一個中斷上接收另外一個新的中斷。通常情況下,所有其他的中斷都是打開的,是以這些不同中斷線上的其他中斷都能被處理,但目前中斷總是被禁止的。由此可

見,同一個中斷處理程式絕對不會被同時調用以處理嵌套的中斷。      

       下面要說到的一個問題是和共享的中斷處理程式相關的。共享和非共享在注冊和運作方式上比較相似的。差異主要有以下幾點:

1.request_irq()的參數flags必須設定為SA_SHIRQ标志。

2.對每個注冊的中斷處理來說,dev_id參數必須唯一。指向任一裝置結構的指針就可以滿足這一要求。通常會選擇裝置結構,因為它是唯一的,而且中

   斷處理程式可能會用到它,不能給共享的處理程式傳遞NULL值。

3.中斷處理程式必須能夠區分它的裝置是否真的産生了中斷。這既需要硬體的支援,也需要處理程式有相關的處理邏輯。如果硬體不支援這一功能,那中

   斷處理程式肯定會束手無策,它根本沒法知道到底是否與它對應的裝置發生了中斷,還是共享這條中斷線的其他裝置發出了中斷。

在指定SA_SHIRQ标志以調用request_irq()時,隻有在以下兩種情況下才能成功:中斷目前未被注冊或者在該線上的所有已注冊處理程式都指

定了SA_SHIRQ.A。注意,在這一點上2.6與以前的核心是不同的,共享的處理程式可以混用SA_INTERRUPT. 

一旦核心接收到一個中斷後,它将依次調用在該中斷線上注冊的每一個處理程式。是以一個處理程式必須知道它是否應該為這個中斷負責。如果與它相關的裝置并沒

有産生中斷,那麼中斷處理程式應該立即退出,這需要硬體裝置提供狀态寄存器(或類似機制),以便中斷處理程式進行檢查。毫無疑問,大多數裝置都提這種功

能。

當執行一個中斷處理程式或下半部時,核心處于中斷上下文(interrupt

context)中。對比程序上下文,程序上下文是一種核心所處的操作模式,此時核心代表程序執行,可以通過current宏關聯目前程序。此外,因為進

程是程序上下文的形式連接配接到核心中,是以,在程序上下文可以随時休眠,也可以排程程式。但中斷上下文卻完全不是這樣,它可以休眠,因為我們不能從中斷上下

文中調用函數。如果一個函數睡眠,就不能在中斷處理程式中使用它,這也是對什麼樣的函數能在中斷處理程式中使用的限制。還需要說明一點的是,中斷處理程式

沒有自己的棧,相反,它共享被中斷程序的核心棧,如果沒有正在運作的程序,它就使用idle程序的棧。因為中斷程式共享别人的堆棧,是以它們在棧中擷取空

間時必須非常節省。核心棧在32位體系結構上是8KB,在64位體系結構上是16KB.執行的程序上下文和産生的所有中斷都共享核心棧。

       下面給出中斷從硬體到核心的路由過程(截圖選自liuux核心分析與設計p61),然後做出總結:

linux核心分析筆記----中斷和中斷處理程式

                                          圖一       中斷從硬體到核心的路由

上面的圖内部說明已經很明确了,我這裡就不在詳談。在核心中,中斷的旅程開始于預定義入口點,這類似于系統調用。對于每條中斷線,處理器都會跳到對應的一

個唯一的位置。這樣,核心就可以知道所接收中斷的IRQ号了。初始入口點隻是在棧中儲存這個号,并存放目前寄存器的值(這些值屬于被中斷的任務);然後,

核心調用函數do_IRQ().從這裡開始,大多數中斷處理代碼是用C寫的。do_IRQ()的聲明如下:

unsigned int do_IRQ(struct pt_regs regs)

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

int irq = regs.orig_eax &amp; 0xff

計算出中斷号後,do_IRQ()對所接收的中斷進行應答,禁止這條線上的中斷傳遞。在普通的PC機器上,這些操作是由

mask_and_ack_8259A()來完成的,該函數由do_IRQ()調用。接下來,do_IRQ()需要確定在這條中斷線上有一個有效的處理程

序,而且這個程式已經啟動但是目前沒有執行。如果這樣的話,

do_IRQ()就調用handle_IRQ_event()來運作為這條中斷線所安裝的中斷處理程式,有關處理例子,可以參考linux核心設計分析一

書,我這裡就不細講了。在handle_IRQ_event()中,首先是打開處理器中斷,因為前面已經說過處理器上所有中斷這時是禁止中斷(因為我們說

過指定SA_INTERRUPT)。接下來,每個潛在的處理程式在循環中依次執行。如果這條線不是共享的,第一次執行後就退出循環,否則,所有的處理程式

都要被執行。之後,如果在注冊期間指定了SA_SAMPLE_RANDOM标志,則還要調用函數add_interrupt_randomness(),

這個函數使用中斷間隔時間為随機數産生熵。最後,再将中斷禁止(do_IRQ()期望中斷一直是禁止的),函數傳回。該函數做清理工作并傳回到初始入口

點,然後再從這個入口點跳到函數ret_from_intr().該函數類似初始入口代碼,以彙編編寫,它會檢查重新排程是否正在挂起,如果重新排程正在

挂起,而且核心正在傳回使用者空間(也就是說,中斷了使用者程序),那麼schedule()被調用。如果核心正在傳回核心空間(也就是中斷了核心本身),隻

有在preempt_count為0時,schedule()才會被調用(否則,搶占核心是不安全的)。在schedule()傳回之前,或者如果沒有挂

起的工作,那麼,原來的寄存器被恢複,核心恢複到曾經中斷的點。在x86上,初始化的彙編例程位于arch/i386/kernel/entry.S,C

方法位于arch/i386/kernel/irq.c其它支援的結構類似。

       下邊給出PC機上位于/proc/interrupts檔案的輸出結果,這個檔案存放的是系統中與中斷相關的統計資訊,這裡就解釋一下這個表:

linux核心分析筆記----中斷和中斷處理程式

上面是這個檔案的輸入,第一列是中斷線(中斷号),第二列是一個接收中斷數目的計數器,第三列是處理這個中斷的中斷控制器,最後一列是與這個中斷有關的設

備名字,這個名字是通過參數devname提供給函數request_irq()的。最後,如果中斷是共享的,則這條中斷線上注冊的所有裝置都會列出來,

如4号中斷。

        Linux

核心給我們提供了一組接口能夠讓我們控制機器上的中斷狀态,這些接口可以在和中找到。一般來說,控制中斷系統的原因在于需要提供同步,通過禁止中斷,可以確定某個中斷處理程式不會搶占目前的代碼。此外,禁止中

斷還可以禁止核心搶占。然而,不管是禁止中斷還是禁止核心搶占,都沒有提供任何保護機制來防止來自其他處理器的并發通路。Linux支援多處理器,是以,

核心代碼一般都需要擷取某種鎖,防止來自其他處理器對共享資料的并發通路,擷取這些鎖的同時也伴随着禁止本地中斷。鎖提供保護機制,防止來自其他處理器的

并發通路,而禁止中斷提供保護機制,則是防止來自其他中斷處理程式的并發通路。

       在linux裝置驅動理論帖裡詳細介紹過linux的中斷操作接口,這裡就大緻過一下,禁止/使能本地中斷(僅僅是目前處理器)用:

2

local_irq_disable();

local_irq_enable();

如果在調用local_irq_disable()之前已經禁止了中斷,那麼該函數往往會帶來潛在的危險,同樣的local_irq_enable()也

存在潛在的危險,因為它将無條件的激活中斷,盡管中斷可能在開始時就是關閉的。是以我們需要一種機制把中斷恢複到以前的狀态而不是簡單地禁止或激活,核心

普遍關心這點,是因為核心中一個給定的代碼路徑可以在中斷激活餓情況下達到,也可以在中斷禁止的情況下達到,這取決于具體的調用鍊。面對這種情況,在禁止

中斷之前儲存中斷系統的狀态會更加安全一些。相反,在準備激活中斷時,隻需把中斷恢複到它們原來的狀态:

3

unsigned long flags;

local_irq_save(flags);

local_irq_restore(flags);

參數包含具體體系結構的資料,也就是包含中斷系統的狀态。至少有一種體系結構把棧資訊與值相結合(SPARC),是以flags不能傳遞給另一個函數(換

句話說,它必須駐留在同一個棧幀中),基于這個原因,對local_irq_save()的調用和local_irq_restore()的調用必須在同

一個函數中進行。前面的所有的函數既可以在中斷中調用,也可以在程序上下文使用。

       前面我提到過禁止整個CPU上所有中斷的函數。但有時候,好奇的我就想,我幹麼沒要禁止掉所有的中斷,有時,我隻需要禁止系統中一條特定的中斷就可以了(屏蔽掉一條中斷線),這就有了我下面給出的接口:

4

void disable_irq(unsigned int irq);

void disable_irq_nosync(unsigned int irq);

void enable_irq(unsigned int irq);

void synchronise_irq(unsigned int irq);

對有關函數的說明和注意,我前邊已經說的很清楚了,這裡飄過。另外,禁止多個中斷處理程式共享的中斷線是不合适的。禁止中斷線也就禁止了這條線上所有裝置

的中斷傳遞,是以,用于新裝置的驅動程式應該傾向于不使用這些接口。另外,我們也可以通過宏定義在中的宏irqs_disable()

來擷取中斷的狀态,如果中斷系統被禁止,則它傳回非0,否則,傳回0;用定義在中的兩個宏

in_interrupt()和in_irq()來檢查核心的目前上下文的接口。由于代碼有時要做一些像睡眠這樣隻能從程序上下文做的事,這時這兩個函數

的價值就展現出來了。

       最後,作為對這篇部落格的總結,這裡給出我前邊提到的用于控制中斷的方法清單:

linux核心分析筆記----中斷和中斷處理程式

From:http://www.cnblogs.com/hanyan225/archive/2011/07/17/2108609.html

補充:

《Linux 核心設計與實作》一書 60 頁 5.5 中斷處理機制的實作

...... 在核心中,中斷的旅程開始于預定義入口點,這類似于系統調用。對于每條中斷線,處理器都會跳到對應的一個唯一的位置。這樣,核心就可知道所接收的中斷的 IRQ 号了。初始入口點隻是在棧中儲存這個号,并存放目前寄存器的值;然後,核心調用函數 do_IRQ()。...... do_IRQ() 的聲明如下: 因為 C 的調用慣例是要把函數參數放在棧的頂部,是以 pt_regs 結構包含原始寄存器的值,這些值是以前在彙編入口例程中儲存在棧中的。中斷的值也會得以儲存,是以 do_IRQ() 可以将它提取出來。 X86 中的代碼為: int irq=regs.orig_eax &amp; 0xff;

從最前面一段看,入口點在棧中儲存了兩樣東西:IRQ 号和目前寄存器值。但從最後一段看,do_IRQ 把儲存的 eax 值當成 IRQ 号。 這樣似乎就不一緻了。

而且中斷說來就來,如果把中斷号存放在 eax 中,似乎會影響被中斷的程序?

2.6.11中do_IRQ的原型:

        fastcall unsigned int do_IRQ(struct pt_regs *regs)

fastcall =&gt; (以i386為例)

        #define FASTCALL(x)     x __attribute__((regparm(3)))

           #define fastcall        __attribute__((regparm(3)))

這樣,do_IRQ實際上是(參見ULK3 4.6.1.6. The do_IRQ() function):

    __attribute__((regparm(3))) unsigned int do_IRQ(struct pt_regs *regs)

        ULK3中的描述:

        The regparm keyword instructs the function to go to the eax

register                 to find the value of the regs argument; eax

points to the stack         location containing the last register value

pushed on by SAVE_ALL.

下面看一下是如何走到do_IRQ的:

SAVE_ALL:

    cld

    push %es

    push %ds

    pushl %eax     

    pushl %ebp

    pushl %edi

    pushl %esi

    pushl %edx

    pushl %ecx

    pushl %ebx

    movl $ _ _USER_DS,%edx

    movl %edx,%ds

    movl %edx,%es

    # eflags、cs、eip、ss和esp的值由控制單元自動儲存

而SAVE_ALL是作為一個先前例程被common_interrupt所調用的

common_interrupt:

        SAVE_ALL

        movl %esp,%eax

        call do_IRQ

        jmp ret_from_intr

    儲存寄存器值後,目前棧頂位置就存放到了%eax中,之後再調用do_IRQ

    這就對應了ULK3中描述的“eax points to the stack location containing the last register value pushed on by SAVE_ALL”

下面看一下struct pt_regs的定義,順便對SAVE_ALL所儲存的内容做一個對應,注意目前%eax存放的是棧頂的值

struct pt_regs {

          long ebx;                # pushl %ebx       

          long ecx;                # pushl %ecx

          long edx;                。。。

          long esi;

          long edi;

          long ebp;

          long eax;                # pushl %eax

          int  xds;                # push %ds

          int  xes;                # push %es

          long orig_eax;

          long eip;

          int  xcs;

          long eflags;

          long esp;

          int  xss;

  };

這裡可以看到SAVE_ALL儲存的内容中并不涉及orig_eax的值,那這個值是從哪裡來的呢?

根據ULK3中4.6.1.5. Saving the registers for the interrupt handler的描述:

        pushl $n-256

        jmp common_interrupt

這裡$n是中斷向量表(核心自己定義,不同與8086下的IDT;可以了解為中斷例程表)的索引

(具體可以參考4.6.1.5. Saving the registers for the interrupt handler,描述的很清楚)

因為核心使用正數來表示系統調用,是以這裡使用一個負數值來描述中斷

這裡可以看到orig_eax存放的實際上是與中斷向量号相關的一個值:$n-256

在do_IRQ中的第一條語句:

        int irq = regs-&gt;orig_eax &amp; 0xff;

可以這樣了解:

        $n-256 = $n + (-256) # 256使用4個位元組時16進制值為0x0100

                             # 那麼-256的16進制值為0xff00

               = $n + 0xff00

        ($n-256) &amp; 0xff

               = $n &amp; 0xff + (-256) &amp; 0xff

               = $n &amp; 0xff

而中斷向量号采用8位值,最大為256,這樣就取得了$n的原始值

至于LZ關心的程序的%eax受影響的問題,這裡可以看到了,名字為orig_eax,但實際上使用的并不是

%eax,而且%eax的值是事先儲存的,中斷處理的第一步就是保護現場,最後是恢複現場,這個LZ就不需要擔心了

至于為什麼用這個名字,可能與系統調用的實作有關:系統調用時是通過寄存器來傳遞參數的,%eax包含系統調用号,這樣切換到核心态之後就可以擷取系統調用号了;當然,系統調用之前,庫例程也會提前儲存現場。可能說的不太準确,僅供參考

另外,LZ可以看看《情景分析》一書,忘了具體是第幾章了,可能是第3章,裡面對于系統調用以及中斷時的堆棧布局用圖的方式描述了一下,應該對LZ有幫助

繼續閱讀