Linux中斷機制


原文:http://blog.csdn.net/kanghua/article/details/1843788  點擊打開鏈接

 

Linux中斷解析

摘要:本章將向讀者依次解釋中斷概念,解析Linux中的中斷實現機理以及Linux下中斷如何被使用。作為實例我們第一將向《i386體系結構》一章中打造的系統加入一個時鍾中斷;第二將為大家注解RTC中斷,希望通過這兩個實例可以幫助讀者掌握中斷相關的概念、實現和編程方法。

中斷是什么

中斷的漢語解釋是半中間發生阻隔、停頓或故障而斷開。那么,在計算機系統中,我們為什么需要“阻隔、停頓和斷開”呢?

舉個日常生活中的例子,比如說我正在廚房用煤氣燒一壺水,這樣就只能守在廚房里,苦苦等着水開——如果水溢出來澆滅了煤氣,有可能就要發生一場災難了。等啊等啊,外邊突然傳來了驚奇的叫聲“怎么不關水龍頭?”於是我慚愧的發現,剛才接水之后只顧着抱怨這份無聊的差事,居然忘了這事,於是慌慌張張的沖向水管,三下兩下關了龍頭,聲音又傳到耳邊,“怎么干什么都是這么馬虎?”。伸伸舌頭,這件小事就這么過去了,我落寞的眼神又落在了水壺上。

門外忽然又傳來了鏗鏘有力的歌聲,我最喜歡的古裝劇要開演了,真想奪門而出,然而,聽着水壺發出“咕嘟咕嘟”的聲音,我清楚:除非等到水開,否則沒有我享受人生的時候。

這個場景跟中斷有什么關系呢?

如果說我專心致志等待水開是一個過程的話,那么叫聲、電視里傳出的音樂不都讓這個過程“半中間發生阻隔、停頓或故障而斷開”了嗎?這不就是活生生的“中斷”嗎?

在這個場景中,我是唯一具有處理能力的主體,不管是燒水、關龍頭還是看電視,同一個時間點上我只能干一件事情。但是,在我專心致志干一件事情時,總有許多或緊迫或不緊迫的事情突然出現在面前,都需要去關注,有些還需要我停下手頭的工作馬上去處理。只有在處理完之后,方能回頭完成先前的任務,“把一壺水徹底燒開!”

中斷機制不僅賦予了我處理意外情況的能力,如果我能充分發揮這個機制的妙用,就可以“同時”完成多個任務了。回到燒水的例子,實際上,無論我在不在廚房,煤氣灶總是會把水燒開的,我要做的,只不過是及時關掉煤氣灶而已,為了這么一個一秒鍾就能完成的動作,卻讓我死死的守候在廚房里,在10分鍾的時間里不停的看壺嘴是不是冒蒸汽,怎么說都不划算。我決定安下心來看電視。當然,在有生之年,我都不希望讓廚房成為火海,於是我上了鬧鍾,10分鍾以后它會發出“尖叫”,提醒我爐子上的水燒開了,那時我再去關煤氣也完全來得及。我用一個中斷信號——鬧鈴——換來了10分鍾的歡樂時光,心里不禁由衷的感嘆:中斷機制真是個好東西。

正是由於中斷機制,我才能有條不紊的“同時”完成多個任務,中斷機制實質上幫助我提高了並發“處理”能力。它也能給計算機系統帶來同樣的好處:如果在鍵盤按下的時候會得到一個中斷信號,CPU就不必死守着等待鍵盤輸入了;如果硬盤讀寫完成后發送一個中斷信號,CPU就可以騰出手來集中精力“服務大眾”了——無論是人類敲打鍵盤的指尖還是來回讀寫介質的磁頭,跟CPU的處理速度相比,都太慢了。沒有中斷機制,就像我們苦守廚房一樣,計算機談不上有什么的並行處理能力。

跟人相似,CPU也一樣要面對紛繁蕪雜的局面——現實中的意外是無處不在的——有可能是用戶等得不耐煩,猛敲鍵盤;有可能是運算中碰到了0除數;還有可能網卡突然接收到了一個新的數據包。這些都需要CPU具體情況具體分析,要么馬上處理,要么暫緩響應,要么置之不理。無論如何應對,都需要CPU暫停“手頭”的工作,拿出一種對策,只有在響應之后,方能回頭完成先前的使命,“把一壺水徹底燒開!”

先讓我們感受一下中斷機制對並發處理帶來的幫助。

讓我們用程序來探討一下燒水問題,如果沒有“中斷”(注意,我們這里只是模仿中斷的場景,實際上是用異步事件——消息——處理機制來展示中斷產生的效果。畢竟,在用戶空間沒有辦法與實際中斷產生直接聯系,不過操作系統為用戶空間提供的異步事件機制,可以看作是模仿中斷的產物),設計如下:

 

void StayInKitchen()
{
    bool WaterIsBoiled = false;
    while ( WaterIsBoiled != true )
    {
        bool VaporGavenOff  = false;
        if  (VaporGavenOff )
            WaterIsBoiled  = true;
        else
            WaterIsBoiled  = false;
    }
    // 關煤氣爐
    printf(“Close gas oven./n”);
    // 一切安定下來,終於可以看電視了,10分鍾的寶貴時間啊,逝者如斯夫…
    watching_tv();
    return;
}

 

可以看出,整個流程如同我們前面描述的一樣,所有工作要順序執行,沒有辦法完成並發任務。

如果用“中斷”,在開始燒水的時候設定一個10分鍾的“鬧鈴”,然后讓CPU去看電視(有點難度,具體實現不在我們關心的范圍之內,留給讀者自行解決吧:>)。等鬧鍾響的時候再去廚房關爐子。

 

#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdio.h>
 
// 鬧鍾到時會執行此程序
void sig_alarm(int signo)
{
       //關煤氣爐
       printf(“Close gas oven./n”);
}
 
void watching_tv()
{
    while(1)
    {
        // 呵呵,優哉游哉
    }
}
 
int main()
{
    // 點火后設置定時中斷
    printf(“Start to boil water, set Alarm”);
    if (signal( SIGALRM, sig_alrm ) == SIG_ERR)
    {
        perror("signal(SIGALRM) error");
        return -1;
    }
 
    // 然后就可以欣賞電視節目了
    printf(“Watching TV!/n”);
 
    watching_tv();
    return 0;
}

 

這兩段程序都在用戶空間執行。第二段程序跟中斷也沒有太大的關系,實際上它只用了信號機制而已。但是,通過這兩個程序的對比,我們可以清楚地看到異步的事件處理機制是如何提升並發處理能力的。

Alarm定時器:alarm相當於系統中的一個定時器,如果我們調用alarm(5),那么5秒鍾后就會“響起一個鬧鈴”(實際上靠信號機制實現的,我們這里不想深入細節,如果你對此很感興趣,請參考Richard Stevens不朽著作《Unix環境高級編程》)。在鬧鈴響起的時候會發生什么呢?系統會執行一個函數,至於到底是什么函數,系統允許程序自行決定。程序員編寫一個函數,並調用signal對該函數進行注冊,這樣一旦定時到來,系統就會調用程序員提供的函數(CallBack函數?沒錯,不過在這里如何實現並不關鍵,我們就不引入新的概念和細節了)。上面的例子里我們提供的函數是sig_alarm,所做的工作很簡單,打印“關閉煤氣灶”消息。

上面的兩個例子很簡單,但很能說明問題,首先,它證明采用異步的消息處理機制可以提高系統的並發處理能力。更重要的是,它揭示了這種處理機制的模式。用戶根據需要設計處理程序,並可以將該程序和特定的外部事件綁定起來,在外部事件發生時系統自動調用處理程序,完成相關的工作。這種模式給系統帶來了統一的管理方法,也帶來無盡的功能擴展空間。

計算機系統實現中斷機制是非常復雜的一件工作,再怎么說人都是高度智能化的生物,而計算機作為一個鐵疙瘩,沒有程序的教導就一事無成。而處理一個中斷過程,它受到的限制和需要學習的東西太多了。

首先,計算機能夠接收的外部信號形式非常有限。中斷是由外部的輸入引起的,可以說是一種刺激。在燒水的場景中,這些輸入是叫聲和電視的音樂,我們這里只以聲音為例。其實現實世界中能輸入人類CPU——大腦的信號很多,圖像、氣味一樣能被我們接受,人的信息接口很完善。而計算機則不然,接受外部信號的途徑越多,設計實現就越復雜,代價就越高。因此個人計算機(PC)給所有的外部刺激只留了一種輸入方式——特定格式的電信號,並對這種信號的格式、接入方法、響應方法、處理步驟都做了規約(具體內容本文后面部分會繼續詳解),這種信號就是中斷或中斷信號,而這一整套機制就是中斷機制。

其次,計算機不懂得如何應對信號。人類的大腦可以自行處理外部輸入,我從來不用去擔心鬧鍾響時會手足無措——走進廚房關煤氣,這簡直是天經地義的事情,還用大腦想啊,小腿肚子都知道——可惜計算機不行,沒有程序,它就紋絲不動。因此,必須有機制保證外部中斷信號到來后,有正確的程序在正確的時候被執行。

還有,計算機不懂得如何保持工作的持續性。我在看電視的時候如果去廚房關了煤氣,回來以后能繼續將電視進行到底,不受太大的影響。而計算機則不然,如果放下手頭的工作直接去處理“意外”的中斷,那么它就再也沒有辦法想起來曾經作過什么,做到什么程度了。自然也就沒有什么“重操舊業”的機會了。這樣的處理方式就不是並發執行,而是東一榔頭,西一棒槌了。

那么,通用的計算機系統是如何解決這些問題的呢?它是靠硬件和軟件配合來協同實現中斷處理的全過程的。我們將通過Intel X86架構的實現來介紹這一過程。

中斷流程處理

CPU執行完一條指令后,下一條指令的邏輯地址存放在cs和eip這對寄存器中。在執行新指令前,控制單元會檢查在執行前一條指令的過程中是否有中斷或異常發生。如果有,控制單元就會拋下指令,進入下面的流程:

  1.  確定與中斷或異常關聯的向量i (0£i£255)
  2.  尋找向量對應的處理程序
  3.  保存當前的“工作現場”,執行中斷或異常的處理程序
  4.  處理程序執行完畢后,把控制權交還給控制單元
  5.  控制單元恢復現場,返回繼續執行原程序

整個流程如下圖所示:

讓我們深入這個流程,看看都有什么問題需要面對。

1、異常是什么概念?

在處理器執行到由於編程失誤而導致的錯誤指令(例如除數是0)的時候,或者在執行期間出現特殊情況(例如缺頁),需要靠操作系統來處理的時候,處理器就會產生一個異常。對大部分處理器體系結構來說,處理異常和處理中斷的方式基本是相同的,x86架構的CPU也是如此。異常與中斷還是有些區別,異常的產生必須考慮與處理器時鍾的同步。實際上,異常往往被稱為同步中斷。

2、中斷向量是什么?

中斷向量代表的是中斷源——從某種程度上講,可以看作是中斷或異常的類型。中斷和異常的種類很多,比如說被0除是一種異常,缺頁又是一種異常,網卡會產生中斷,聲卡也會產生中斷,CPU如何區分它們呢?中斷向量的概念就是由此引出的,其實它就是一個被送通往CPU數據線的一個整數。CPU給每個IRQ分配了一個類型號,通過這個整數CPU來識別不同類型的中斷。這里可能很多朋友會尋問為什么還要弄個中斷向量這么麻煩的東西?為什么不直接用IRQ0~IRQ15就完了?比如就讓IRQ0為0,IRQ1為1……,這不是要簡單的多么?其實這里體現了模塊化設計規則,及節約規則。

首先我們先談談節約規則,所謂節約規則就是所使用的信號線數越少越好,這樣如果每個IRQ都獨立使用一根數據線,如IRQ0用0號線,IRQ1用1號線……這樣,16個IRQ就會用16根線,這顯然是一種浪費。那么也許馬上就有朋友會說:那么只用4根線不就行了嗎?(2^4=16)。

這個問題,體現了模塊設計規則。我們在前面就說過中斷有很多類,可能是外部硬件觸發,也可能是由軟件觸發,然而對於CPU來說中斷就是中斷,只有一種,CPU不用管它到底是由外部硬件觸發的還是由運行的軟件本身觸發的,應為對於CPU來說,中斷處理的過程都是一樣的:中斷現行程序,轉到中斷服務程序處執行,回到被中斷的程序繼續執行。CPU總共可以處理256種中斷,而並不知道,也不應當讓CPU知道這是硬件來的中斷還是軟件來的中斷,這樣,就可以使CPU的設計獨立於中斷控制器的設計,這樣CPU所需完成的工作就很單純了。CPU對於其它的模塊只提供了一種接口,這就是256個中斷處理向量,也稱為中斷號。由這些中斷控制器自行去使用這256個中斷號中的一個與CPU進行交互,比如,硬件中斷可以使用前128個號,軟件中斷使用后128個號,也可以軟件中斷使用前128個號,硬件中斷使用后128個號,這與CPU完全無關了,當你需要處理的時候,只需告訴CPU你用的是哪個中斷號就行,而不需告訴CPU你是來自哪兒的中斷。這樣也方便了以后的擴充,比如現在機器里又加了一片8259芯片,那么這個芯片就可以使用空閑的中斷號,看哪一個空閑就使用哪一個,而不是必須要使用第0號,或第1號中斷號了。其實這相當於一種映射機制,把IRQ信號映射到不同的中斷號上,IRQ的排列或說編號是固定的,但通過改變映射機制,就可以讓IRQ映射到不同的中斷號,也可以說調用不同的中斷服務程序。

3、什么是中斷服務程序?

在響應一個特定中斷的時候,內核會執行一個函數,該函數叫做中斷處理程序(interrupt handler)或中斷服務程序(interrupt service routine(ISR))。產生中斷的每個設備都有相應的中斷處理程序。例如,由一個函數專門處理來自系統時鍾的中斷,而另外一個函數專門處理由鍵盤產生的中斷。

一般來說,中斷服務程序要負責與硬件進行交互,告訴該設備中斷已被接收。此外,還需要完成其他相關工作。比如說網絡設備的中斷服務程序除了要對硬件應答,還要把來自硬件的網絡數據包拷貝到內存,對其進行處理后再交給合適的協議棧或應用程序。每個中斷服務程序根據其要完成的任務,復雜程度各不相同。

一般來說,一個設備的中斷服務程序是它設備驅動程序(device driver)的一部分——設備驅動程序是用於對設備進行管理的內核代碼。

4、隔離變化

不知道您有沒有意識到,中斷處理前面這部分的設計是何等的簡單優美。人是高度智能化的,能夠對遇到的各種意外情況做有針對性的處理,計算機相比就差距甚遠了,它只能根據預定的程序進行操作。對於計算機來說,硬件支持的,只能是中斷這種電信號傳播的方式和CPU對這種信號的接收方法,而具體如何處理這個中斷,必須得靠操作系統實現。操作系統支持所有事先能夠預料到的中斷信號,理論上都不存在太大的挑戰,但在操作系統安裝到計算機設備上以后,肯定會時常有新的外圍設備被加入系統,這可能會帶來安裝系統時根本無法預料的“意外”中斷。如何支持這種擴展,是整個系統必須面對的。

而硬件和軟件在這里的協作,給我們帶來了完美的答案。當新的設備引入新類型的中斷時,CPU和操作系統不用關注如何處理它。CPU只負責接收中斷信號,並引用中斷服務程序;而操作系統提供默認的中斷服務——一般來說就是不理會這個信號,返回就可以了——並負責提供接口,讓用戶通過該接口注冊根據設備具體功能而編制的中斷服務程序。如果用戶注冊了對應於一個中斷的服務程序,那么CPU就會在該中斷到來時調用用戶注冊的服務程序。這樣,在中斷來臨時系統需要如何操作硬件、如何實現硬件功能這部分工作就完全獨立於CPU架構和操作系統的設計了。

而當你需要加入新設備的時候,只需要告訴操作系統該設備占用的中斷號、按照操作系統要求的接口格式撰寫中斷服務程序,用操作系統提供的函數注冊該服務程序,設備的中斷就被系統支持了。

中斷和對中斷的處理被解除了耦合。這樣,無論是你在需要加入新的中斷時,還是在你需要改變現有中斷的服務程序時、又或是取消對某個中斷支持的時候,CPU架構和操作系統都無需作改變。

5、保存當前工作“現場”

在中斷處理完畢后,計算機一般來說還要回頭處理原先手頭正做的工作。這給中斷的概念帶來些額外的“內涵”[1]。“回頭”不是指從頭再來重新做,而是要接着剛才的進度繼續做。這就需要在處理中斷信號之前保留工作“現場”。“現場”這個詞比較晦澀,其實就是指一個信息集,它能反映某個時間點上任務的狀態,並能保證按照這些信息就能恢復任務到該狀態,繼續執行下去。再直白一點,現場不過就是一組寄存器值。而如何保護現場和恢復場景是中斷機制需要考慮的重點之一。

每個中斷處理都要經歷這個保存和恢復過程,我們可以抽象出其中的步驟:

  1. 保存現場
  2. 執行具體的中斷服務程序
  3. 從中斷服務返回
  4. 恢復現場

上面說過了,“現場”看似在不斷變化,沒有哪個瞬間相同。但實際上組成現場的要素卻不會有任何改變。也就是說,這要我們保存了相關的寄存器狀態,現場就能保存下來。而恢復“現場”就是重新載入這些寄存器。換句話說,對於任何一個中斷,保護現場和恢復現場所作的都是完全相同的操作。

既然操作相同,實現操作的過程和代碼就相同。減少代碼的冗余是模塊化設計的基本准則,實在沒有道理讓所有的中斷服務程序都重復的實現這樣的功能,應該將它作為一種基本的結構由底層的操作系統或硬件完成。而對中斷的處理過程需要迅速完成,因此,Intel CPU的控制器就承擔了這個任務,非但如此,上面的所有步驟次序都被固化下來,由控制器驅動完成。保存現場和恢復現場都由硬件自動完成,大大減輕了操作系統和設備驅動程序的負擔。

6、硬件對中斷支持的細節

下面的部分,本來應該介紹8259、中斷控制器編程、中斷描述符表等內容,可是我看到了瀟寒寫的“保護模式下的8259A芯片編程及中斷處理探究”,前人之述備矣,讀者直接讀它好了。

Linux下的中斷

在Linux中,中斷處理程序看起來就是普普通通的C函數。只不過這些函數必須按照特定的類型聲明,以便內核能夠以標准的方式傳遞處理程序的信息,在其他方面,它們與一般的函數看起來別無二致。中斷處理程序與其它內核函數的真正區別在於,中斷處理程序是被內核調用來響應中斷的,而它們運行於我們稱之為中斷上下文的特殊上下文中。關於中斷上下文,我們將在后面討論。

中斷可能隨時發生,因此中斷處理程序也就隨時可能執行。所以必須保證中斷處理程序能夠快速執行,這樣才能保證盡可能快地恢復被中斷代碼的執行。因此,盡管對硬件而言,迅速對其中斷進行服務非常重要。但對系統的其它部分而言,讓中斷處理程序在盡可能短的時間內完成執行也同樣重要。

即使最精簡版的中斷服務程序,它也要與硬件進行交互,告訴該設備中斷已被接收。但通常我們不能像這樣給中斷服務程序隨意減負,相反,我們要靠它完成大量的其它工作。作為一個例子,我們可以考慮一下網絡設備的中斷處理程序面臨的挑戰。該處理程序除了要對硬件應答,還要把來自硬件的網絡數據包拷貝到內存,對其進行處理后再交給合適的協議棧或應用程序。顯而易見,這種運動量不會太小。

現在我們來分析一下Linux操作系統為了支持中斷機制,具體都需要做些什么工作。

首先,操作系統必須保證新的中斷能夠被支持。計算機系統硬件留給外設的是一個統一的中斷信號接口。它固化了中斷信號的接入和傳遞方法,拿PC機來說,中斷機制是靠兩塊8259和CPU協作實現的。外設要做的只是把中斷信號發送到8259的某個特定引腳上,這樣8259就會為此中斷分配一個標識——也就是通常所說的中斷向量,通過中斷向量,CPU就能夠在以中斷向量為索引的表——中斷向量表——里找到中斷服務程序,由它決定具體如何處理中斷。這是硬件規定的機制,軟件只能無條件服從。

因此,操作系統對新中斷的支持,說簡單點,就是維護中斷向量表。新的外圍設備加入系統,首先得明確自己的中斷向量號是多少,還得提供自身中斷的服務程序,然后利用Linux的內核調用界面,把〈中斷向量號、中斷服務程序〉這對信息填寫到中斷向量表中去。這樣CPU在接收到中斷信號時就會自動調用中斷服務程序了。這種注冊操作一般是由設備驅動程序完成的。

其次,操作系統必須提供給程序員簡單可靠的編程界面來支持中斷。中斷的基本流程前面已經講了,它會打斷當前正在進行的工作去執行中斷服務程序,然后再回到先前的任務繼續執行。這中間有大量需要解決問題:如何保護現場、嵌套中斷如何處理等等,操作系統要一一化解。程序員,即使是驅動程序的開發人員,在寫中斷服務程序的時候也很少需要對被打斷的進程心存憐憫。(當然,出於提高系統效率的考慮,編寫驅動程序要比編寫用戶級程序多一些條條框框,誰讓我們頂着系統程序員的光環呢?)

操作系統為我們屏蔽了這些與中斷相關硬件機制打交道的細節,提供了一套精簡的接口,讓我們用極為簡單的方式實現對實際中斷的支持,Linux是怎么完美的做到這一點的呢?

CPU對中斷處理的流程

我們首先必須了解CPU在接收到中斷信號時會做什么。沒辦法,操作系統必須了解硬件的機制,不配合硬件就寸步難行。現在我們假定內核已被初始化,CPU在保護模式下運行。

CPU執行完一條指令后,下一條指令的邏輯地址存放在cs和eip這對寄存器中。在執行新指令前,控制單元會檢查在執行前一條指令的過程中是否有中斷或異常發生。如果有,控制單元就會拋下指令,進入下面的流程:

1.確定與中斷或異常關聯的向量i (0£i£255)。

2.籍由idtr寄存器從IDT表中讀取第i項(在下面的描述中,我們假定該IDT表項中包含的是一個中斷門或一個陷阱門)。

3.從gdtr寄存器獲得GDT的基地址,並在GDT表中查找,以讀取IDT表項中的選擇符所標識的段描述符。這個描述符指定中斷或異常處理程序所在段的基地址。

4.確信中斷是由授權的(中斷)發生源發出的。首先將當前特權級CPL(存放在cs寄存器的低兩位)與段描述符(存放在GDT中)的描述符特權級DPL比較,如果CPL小於DPL,就產生一個“通用保護”異常,因為中斷處理程序的特權不能低於引起中斷的程序的特權。對於編程異常,則做進一步的安全檢查:比較CPL與處於IDT中的門描述符的DPL,如果DPL小於CPL,就產生一個“通用保護”異常。這最后一個檢查可以避免用戶應用程序訪問特殊的陷阱門或中斷門。

5.檢查是否發生了特權級的變化,也就是說, CPL是否不同於所選擇的段描述符的DPL。如果是,控制單元必須開始使用與新的特權級相關的棧。通過執行以下步驟來做到這點:

    a.讀tr寄存器,以訪問運行進程的TSS段。

    b.用與新特權級相關的棧段和棧指針的正確值裝載ss和esp寄存器。這些值可以在TSS中找到(參見第三章的“任務狀態段”一節)。

    c.在新的棧中保存ss和esp以前的值,這些值定義了與舊特權級相關的棧的邏輯地址。

6.如果故障已發生,用引起異常的指令地址裝載cs和eip寄存器,從而使得這條指令能再次被執行。

7.在棧中保存eflag、cs及eip的內容。

8.如果異常產生了一個硬錯誤碼,則將它保存在棧中。

9.裝載cs和eip寄存器,其值分別是IDT表中第i項門描述符的段選擇符和偏移量域。這些值給出了中斷或者異常處理程序的第一條指令的邏輯地址。

控制單元所執行的最后一步就是跳轉到中斷或者異常處理程序。換句話說,處理完中斷信號后, 控制單元所執行的指令就是被選中處理程序的第一條指令。

中斷或異常被處理完后,相應的處理程序必須0x20/0x21/0xa0/0xa1

產生一條iret指令,把控制權轉交給被中斷的進程,這將迫使控制單元:

1.用保存在棧中的值裝載cs、eip、或eflag寄存器。如果一個硬錯誤碼曾被壓入棧中,並且在eip內容的上面,那么,執行iret指令前必須先彈出這個硬錯誤碼。

2.檢查處理程序的CPL是否等於cs中最低兩位的值(這意味着被中斷的進程與處理程序運行在同一特權級)。如果是,iret終止執行;否則,轉入下一步。

3. 從棧中裝載ss和esp寄存器,因此,返回到與舊特權級相關的棧。

4. 檢查ds、es、fs及gs段寄存器的內容,如果其中一個寄存器包含的選擇符是一個段描述符,並且其DPL值小於CPL,那么,清相應的段寄存器。控制單元這么做是為了禁止用戶態的程序(CPL=3)利用內核以前所用的段寄存器(DPL=0)。如果不清這些寄存器,懷有惡意的用戶程序就可能利用它們來訪問內核地址空間。

再次,操作系統必須保證中斷信息能夠高效可靠的傳遞

實例一——為自己的操作系統中加入中斷

中斷機制的實現

在這個部分,我將為大家詳細介紹SagaLinux_irq中是如何處理中斷的。為了更好的演示軟硬件交互實現中斷機制的過程,我將在前期實現的SagaLinux上加入對一個新中斷 ——定時中斷——的支持。

首先,讓我介紹一下SagaLinux_irq中涉及中斷的各部分代碼。這些代碼主要包含在kernel目錄下,包括idt.c,irq.c,i8259.s,boot目錄下的setup.s也和中斷相關,下面將對他們進行討論。

1、boot/setup.s

setup.s中相關於中斷的部分主要集中在pic_init小結,該部分完成了對中斷控制器的初始化。對8259A的編程是通過向其相應的端口發送一系列的ICW(初始化命令字)完成的。總共需要發送四個ICW,它們都分別有自己獨特的格式,而且必須按次序發送,並且必須發送到相應的端口,具體細節請查閱相關資料。

pic_init:
    cli
    mov al, 0x11            ; initialize PICs
; 給中斷寄存器編程
; 發送ICW1:使用ICW4,級聯工作
    out 0x20, al            ; 8259_MASTER
    out 0xA0, al            ; 8259_SLAVE
    ; 發送 ICW2,中斷起始號從 0x20 開始(第一片)及 0x28開始(第二片)
    mov al,   0x20          ; interrupt start 32
    out 0x21, al
    mov al,   0x28          ; interrupt start 40
    out 0xA1, al
; 發送 ICW3
    mov al,   0x04          ; IRQ 2 of 8259_MASTER
    out 0x21, al       
; 發送 ICW4
    mov al,   0x02          ; to 8259_SLAVE
    out 0xA1, al
; 工作在80x86架構下
    mov al,   0x01          ; 8086 Mode
    out 0x21, al
    out 0xA1, al
; 設置中斷屏蔽位 OCW1 ,屏蔽所有中斷請求
    mov al,   0xFF          ; mask all
    out 0x21, al
    out 0xA1, al
    sti

 

2、kernel/irq.c

    irq.c提供了三個函數enable_irq、disable_irq和request_irq,函數原型如下:

    void enable_irq(int irq)
    void disable_irq(int irq)
    void request_irq(int irq, void (*handler)())

    enable_irq和disable_irq用來開啟和關閉右參數irq指定的中斷,這兩個函數直接對8259的寄存器進行操作,因此irq對應的是實實在在的中斷號,比如說X86下時鍾中斷一般為0號中斷,那么啟動時鍾中斷就需要調用enable_irq(1),而鍵盤一般占用2號中斷,那么關閉鍵盤中斷就需要調用disable_irq(2)。irq對應的不是中斷向量。

    request_irq用來將中斷號和中斷服務程序綁定起來,綁定完成后,命令8259開始接受中斷請求。下面是request_irq的實現代碼:

  

void request_irq(int irq, void (*handler)())
{
  irq_handler[irq] = handler;
  enable_irq(irq);
}

 

    其中irq_handler是一個擁有16個元素的數組,數組項是指向函數的指針,每個指針可以指向一個中斷服務程序。irq_handler[irq] = handler 就是一個給數組項賦值的過程,其中隱藏了中斷號向中斷向量映射的過程,在初始化IDT表的部分,我會介紹相關內容。

3、kernel/i8259.s[2]

i8259.c負責對外部中斷的支持。我們已經討論過了,8259芯片負責接收外部設備——如定時器、鍵盤、聲卡等——的中斷,兩塊8259共支持16個中斷。

我們也曾討論過,在編寫操作系統的時候,我們不可能知道每個中斷到底對應的是哪個中斷服務程序。實際上,通常在這個時候,中斷服務程序壓根還沒有被編寫出來。可是,X86體系規定,在初始化中斷向量表的時候,必須提供每個向量對應的服務程序的偏移地址,以便CPU在接收到中斷時調用相應的服務程序,這該如何是好呢?

巧婦難為無米之炊,此時此刻,我們只有創造所有中斷對應的服務程序,才能完成初始化IDT的工作,於是我們制造出16個函數——__irq0到__irq15,在注冊中斷服務程序的時候,我們就把它們填寫到IDT的描述符中去。(在SagaLinux中當前的實現里,我並沒有填寫完整的IDT表,為了讓讀者看得較為清楚,我只加入了定時器和鍵盤對應的__irq和__irq1。但這樣一來就帶來一個惡果,讀者會發現在加入新的中斷支持時,需要改動idt.c中的trap_init函數,用set_int_gate對新中斷進行支持。完全背離了我們強調的分隔變化的原則。實際上,只要我們在這里填寫完整,並提供一個缺省的中斷服務函數就可以解決這個問題。我再強調一遍,這不是設計問題,只是為了便於讀者觀察而做的簡化。)

可是,這16個函數怎么能對未知的中斷進行有針對性的個性化服務呢?當然不能,這16個函數只是一個接口,我們可以在其中留下后門,當新的中斷需要被系統支持時,它實際的中斷服務程序就能被這些函數調用。具體調用關系請參考圖2

 

 如圖2所示,__irq0到__irq15會被填充到IDT從32到47(之所以映射到這個區間是為了模仿Linux的做法,其實這部分的整個實現都是在模仿Linux)這16個條目的中斷描述符中去,這樣中斷到來的時候就會調用相應的__irq函數。所有irq函數所作的工作基本相同,把中斷號壓入棧中,再調用do_irq函數;它們之間唯一區別的地方就在於不同的irq函數壓入的中斷號不同。

 do_irq首先會從棧中取出中斷號,然后根據中斷號計算該中斷對應的中斷服務程序在irq_handler數組中的位置,並跳到該位置上去執行相應的服務程序。

 還記得irq.c中介紹的request_irq函數嗎,該函數綁定中斷號和中斷服務程序的實現,其實就是把指向中斷服務程序的指針填寫到中斷號對應的irq_handler數組中去。現在,你應該明白我們是怎樣把一個中斷服務程序加入到SagaLinux中的了吧——通過一個中間層,我們可以做任何事情。

在上圖的實現中,IDT表格中墨綠色的部分——外部中斷對應的部分——可以浮動,也就是說,我們可以任意選擇映射的起始位置,比如說,我們讓__irq0映射到IDT的第128項,只要后續的映射保持連續就可以了。

4、kernel/idt.c

idt.c當然是用來初始化IDT表的了。

在i8259.s中我們介紹了操作系統是如何支持中斷服務程序的添加的,但是,有兩個部分的內容沒有涉及:一是如何把__irq函數填寫到IDT表中,另外一個就是中斷支持了,那異常怎么支持呢?idt.c負責解決這兩方面的問題。

idt.c提供了trap_init函數來填充IDT表。

 

void trap_init()
{
  int i;
  idtr_t idtr;
 
  // 填入系統默認的異常,共17個
  set_trap_gate(0, (unsigned int)÷_error);
  set_trap_gate(1, (unsigned int)&debug);
  set_trap_gate(2, (unsigned int)&nmi);
  set_trap_gate(3, (unsigned int)&int3);
  set_trap_gate(4, (unsigned int)&overflow);
  set_trap_gate(5, (unsigned int)&bounds);
  set_trap_gate(6, (unsigned int)&invalid_op);
  set_trap_gate(7, (unsigned int)&device_not_available);
  set_trap_gate(8, (unsigned int)&double_fault);
  set_trap_gate(9, (unsigned int)&coprocessor_segment_overrun);
  set_trap_gate(10,(unsigned int) &invalid_TSS);
  set_trap_gate(11, (unsigned int)&segment_not_present);
  set_trap_gate(12, (unsigned int)&stack_segment);
  set_trap_gate(13, (unsigned int)&general_protection);
  set_trap_gate(14, (unsigned int)&page_fault);
  set_trap_gate(15, (unsigned int)&coprocessor_error);
  set_trap_gate(16, (unsigned int)&alignment_check);
 
  // 17到31這15個異常是intel保留的,最好不要占用
  for (i = 17;i<32;i++)
     set_trap_gate(i, (unsigned int)&reserved);
 
  // 我們只在IDT中填入定時器和鍵盤要用到的兩個中斷
  set_int_gate(32, (unsigned int)&__irq0); 
  set_int_gate(33, (unsigned int)&__irq1);
 
  // 一共有34個中斷和異常需要支持
  idtr.limit = 34*8;
  idtr.lowerbase = 0x0000;
  idtr.higherbase = 0x0000;
  cli();
 
  // 載入IDT表,新的中斷可以用了
  __asm__ __volatile__ ("lidt (%0)"
            ::"p" (&idtr));
  sti();
}
void set_trap_gate(int vector, unsigned int handler_offset)
{
  trapgd_t* trapgd = (trapgd_t*) IDT_BASE + vector;
  trapgd->loffset = handler_offset & 0x0000FFFF;
  trapgd->segment_s = CODESEGMENT;
  trapgd->reserved = 0x00;
  trapgd->options =  0x0F | PRESENT | KERNEL_LEVEL;
  trapgd->hoffset = ((handler_offset & 0xFFFF0000) >> 16);
}
 
void set_int_gate(int vector,  unsigned int handler_offset)
{
  intgd_t* intgd = (intgd_t*) IDT_BASE + vector;
  intgd->loffset =  handler_offset & 0x0000FFFF;
  intgd->segment_s = CODESEGMENT;
  intgd->reserved = 0x0;
  intgd->options =  0x0E | PRESENT | KERNEL_LEVEL;
  intgd->hoffset = ((handler_offset & 0xFFFF0000) >> 16); 
}

我們可以發現,它們所作的工作就是根據中斷向量號計算出應該把指向中斷或異常服務程序的指針放在什么IDT表中的什么位置,然后把該指針和中斷描述符設置好就行了。同樣,中斷描述符的格式請查閱有關資料。

現在,來關注一下set_trap_gate的參數,又是指向函數的指針。在這里,我們看到每個這樣的指針指向一個異常處理函數,如divide_error、debug等:

void divide_error(void)
{
  sleep("divide error");
}
void debug(void)
{
  sleep("debug");
}

每個函數都調用了sleep,那么sleep是有何作用?是不是像——do_irq一樣調用具體異常的中斷服務函數呢?

// Nooooo ... just sleep :)
void sleep(char* message)
{
  printk("%s",message);
  while(1);
}

    看樣子不是,這個函數就是休眠而已!實際上,我們這里進行了簡化,對於Intel定義好的前17個內部異常,目前SagaLinux還不能做有針對性的處理,因此我們直接讓系統無限制地進入休眠——跟死機區別不大。因此,當然也不用擔心恢復“現場”的問題了,不用考慮棧的影響,所以直接用C函數實現。

 

此外,由於這17個異常如何處理在這個時候我們已經確定下來了——sleep,既然沒有什么變化,我們也就不用耗盡心思的考慮去如何支持變化了,直接把函數硬編碼就可以了。

Intel規定中斷描述符表的第17-31項保留,為硬件將來可能的擴展用,因此我們這里將它閑置起來。

void reserved(void)
{
  sleep("reserved");
}

 

下面的部分是對外部中斷的初始化,放在trap_init中是否有些名不正言不順呢?確實如此,這個版本暫時把它放在這里,以后重構的時候再調整吧。注意,這個部分解釋了我們是如何把中斷服務程序放置到IDT中的。此外,可以看出,我們使用手工方式對中斷向量號進行了映射,__irq0對應32號中斷,而__irq1對應33號中斷。能不能映射成別的向量呢?當然可以,可是別忘了修改setup.s中的pic_init部分,要知道,我們初始化8259的時候定義好了外部中斷對應的向量,如果你希望從8259發來的中斷信號能正確的觸發相應的中斷服務程序,當然要把所有的接收——處理鏈條上的每個映射關系都改過來。

 

我們只填充了34個表項,每個表項8字節長,因此我們把IDT表的長度上限設為34x8,把IDT表放置在邏輯地址起始的地方(如果我們沒有啟用分頁機制,那么就是在線性空間起始的地方,也就是物理地址的0位置處)。

最后,調用ldtr指令啟用新的中斷處理機制,SagaLinux的初步中斷支持機制就完成了。

擴展新的中斷

下面,我們以定時器(timer)設備為例,展示如何通過SagaLinux目前提供的中斷服務程序接口來支持設備的中斷。

IBM PC兼容機包含了一種時間測量設備,叫做可編程間隔定時器(PIT)。PIT的作用類似於鬧鍾,在設定的時間點到來的時候發出中斷信號。這種中斷叫做定時中斷(timer interrupt)。在Linux操作系統中,就是它來通知內核又一個時間片斷過去了。與鬧鍾不同,PIT以某一固定的頻率(編程控制)不停地發出中斷。每個IBM PC兼容機至少都會包含一個PIT,一般來說,它就是一個使用0x40~0x43 I/O端口的8254CMOS芯片。

    SagaLinux目前的版本還不支持進程調度,因此定時中斷的作用還不明顯,不過,作為一個做常見的中斷源,我們可以讓它每隔一定時間發送一個中斷信號,而我們在定時中斷的中斷服務程序中計算流逝過去的時間數,然后打印出結果,充分體現中斷的效果。

我們在kernel目錄下編寫了timer.c文件,也在include目錄下加入了相應的timer.h,下面就是具體的實現。

// 流逝的時間
static volatile ulong_t counter;
 
// 中斷服務程序
void timer_handler()
{
  // 中斷每10毫秒一次
  counter += 10;
}
 
// 初始化硬件和技術器,啟用中斷
void timer_init()
{
  ushort_t pit_counter = CLOCK_RATE * INTERVAL / SECOND;
  counter = 0;
 
  outb (SEL_CNTR0|RW_LSB_MSB|MODE2|BINARY_STYLE, CONTROL_REG);
  outb (pit_counter & 0xFF, COUNTER0_REG);
  outb (pit_counter >> 8, COUNTER0_REG);
  // 申請0號中斷,TIMER定義為0
  request_irq(TIMER, timer_handler);
}
 
// 返回流逝過去的時間
ulong_t uptime()
{
  return counter;
}

timer_init函數是核心函數,負責硬件的初始化和中斷的申請,對8254的初始化就不多做糾纏了,請查閱有關資料。我們可以看到,申請中斷確實跟預想中的一樣容易,調用request_irq,一行語句就完成了中斷的注冊。

而中斷服務程序非常簡單,由於把8254設置為每10毫秒發送一次中斷,因此每次中斷到來時都在服務程序中對counter加10,所以counter表示的就是流逝的時間。

在kernel.c中,我們調用timer_init進行初始化,此時定時中斷就被激活了,如果我們的中斷機制運轉順利,那么流逝時間會不斷增加。為了顯示出這樣的結果,我們編寫一個循環不斷的調uptime函數,並把返回的結果打印在屏幕上。如果打印出的數值越來越大,那就說明我們的中斷機制確確實實發揮了作用,定時中斷被驅動起來了。

在kernel.c中:

// 初始化
int i = 0;
timer_init();
i = uptime();
while(1)
{
  int temp = uptime();
  // 發生變化才打印,否則看不清楚
  if (temp != i)
  {
     printk(" %d ", temp);
     i = temp;
  }
}

當SagaLinux_irq引導后,你會發現屏幕上開始不停的打印逐漸增大的數字,系統對定時中斷的支持,確實成功了。

為了驗證中斷支持的一般性,我們又加入了對鍵盤的支持。這樣還可以充分體現中斷對並發執行任務帶來的幫助,在你按下鍵盤的時候,定時中斷依然不斷觸發,屏幕上會打印出時間,當然,也會打印出你按下的字符。不過,這里就不對此做進一步描述了。

實例二——從RTC設備學習中斷

系統實時鍾

每台PC機都有一個實時鍾(Real Time Clock)設備。在你關閉計算機電源的時候,由它維持系統的日期和時間信息。

此外,它還可以用來產生周期信號,頻率變化范圍從2Hz到8192Hz——當然,頻率必須是2的倍數。這樣該設備就能被當作一個定時器使用,比如我們把頻率設定為4Hz,那么設備啟動后,系統實時鍾每秒就會向CPU發送4次定時信號——通過8號中斷提交給系統(標准PC機的IRQ 8是如此設定的)。由於系統實時鍾是可編程控制的,你也可以把它設成一個警報器,在某個特定的時刻拉響警報——向系統發送IRQ 8中斷信號。由此看來,IRQ 8與生活中的鬧鈴差不多:中斷信號代表着報警器或定時器的發作。

在Linux操作系統的實現里,上述中斷信號可以通過/dev/rtc(主設備號10,從設備號135,只讀字符設備)設備獲得。對該設備執行讀(read)操作,會得到unsigned long型的返回值,最低的一個字節表明中斷的類型(更新完畢update-done,定時到達alarm-rang,周期信號periodic);其余字節包含上次讀操作以來中斷到來的次數。如果系統支持/proc文件系統,/proc/driver/rtc中也能反映相同的狀態信息。

該設備只能由每個進程獨占,也就是說,在一個進程打開(open)設備后,在它沒有釋放前,不允許其它進程再打開它。這樣,用戶的程序就可以通過對/dev/rtc執行read()或select()系統調用來監控這個中斷——用戶進程會被阻塞,直到系統接收到下一個中斷信號。對於一些高速數據采集程序來說,這個功能非常有用,程序無需死守着反復查詢,耗盡所有的CPU資源;只要做好設定,以一定頻率進行查詢就可以了。

#include <stdio.h>
#include <linux/rtc.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
 
int main(void)
{
  int i, fd, retval, irqcount = 0;
  unsigned long tmp, data;
  struct rtc_time rtc_tm;
 
  // 打開RTC設備
  fd = open ("/dev/rtc", O_RDONLY);
 
  if (fd ==  -1) {
    perror("/dev/rtc");
    exit(errno);
  }
 
  fprintf(stderr, "/n/t/t/tEnjoy TV while boiling water./n/n");
 
  // 首先是一個報警器的例子,設定10分鍾后"響鈴"   
  // 獲取RTC中保存的當前日期時間信息
  /* Read the RTC time/date */
  retval = ioctl(fd, RTC_RD_TIME, &rtc_tm);
  if (retval == -1) {
    perror("ioctl");
    exit(errno);
  }
 
  fprintf(stderr, "/n/nCurrent RTC date/time is %d-%d-%d,%02d:%02d:%02d./n",   
      rtc_tm.tm_mday, rtc_tm.tm_mon + 1, rtc_tm.tm_year + 1900,
      rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
 
  // 設定時間的時候要避免溢出
  rtc_tm.tm_min += 10;
  if (rtc_tm.tm_sec >= 60) {
    rtc_tm.tm_sec %= 60;
    rtc_tm.tm_min++;
  }
  if  (rtc_tm.tm_min == 60) {
    rtc_tm.tm_min = 0;
    rtc_tm.tm_hour++;
  }
  if  (rtc_tm.tm_hour == 24)
    rtc_tm.tm_hour = 0;
 
  // 實際的設定工作
  retval = ioctl(fd, RTC_ALM_SET, &rtc_tm);
  if (retval == -1) {
    perror("ioctl");
    exit(errno);
  }
 
  // 檢查一下,看看是否設定成功
  /* Read the current alarm settings */
  retval = ioctl(fd, RTC_ALM_READ, &rtc_tm);
  if (retval == -1) {
    perror("ioctl");
    exit(errno);
  }
 
  fprintf(stderr, "Alarm time now set to %02d:%02d:%02d./n",
      rtc_tm.tm_hour, rtc_tm.tm_min, rtc_tm.tm_sec);
 
  // 光設定還不成,還要啟用alarm類型的中斷才行
  /* Enable alarm interrupts */
  retval = ioctl(fd, RTC_AIE_ON, 0);
  if (retval == -1) {
    perror("ioctl");
    exit(errno);
  }
 
  // 現在程序可以耐心的休眠了,10分鍾后中斷到來的時候它就會被喚醒
  /* This blocks until the alarm ring causes an interrupt */
  retval = read(fd, &data, sizeof(unsigned long));
  if (retval == -1) {
    perror("read");
    exit(errno);
  }
  irqcount++;
  fprintf(stderr, " okay. Alarm rang./n");
}

這個例子稍微顯得有點復雜,用到了open、ioctl、read等諸多系統調用,初看起來讓人眼花繚亂。其實如果簡化一下的話,過程還是“燒開水”:設定定時器、等待定時器超時、執行相應的操作(“關煤氣灶”)。

 

讀者可能不理解的是:這個例子完全沒有表現出中斷帶來的好處啊,在等待10分鍾的超時過程中,程序依然什么都不能做,只能休眠啊?

讀者需要注意自己的視角,我們所說的中斷能夠提升並發處理能力,提升的是CPU的並發處理能力。在這里,上面的程序可以被看作是燒開水,在燒開水前,鬧鈴已經被上好,10分鍾后CPU會被中斷(鬧鈴聲)驚動,過來執行后續的關煤氣工作。也就是說,CPU才是這里唯一具有處理能力的主體,我們在程序中主動利用中斷機制來節省CPU的耗費,提高CPU的並發處理能力。這有什么好處呢?試想如果我們還需要CPU烤面包,CPU就有能力完成相應的工作,其它的工作也一樣。這其實是在多任務操作系統環境下程序生存的道德基礎——“我為人人,人人為我”。

好了,這段程序其實是我們進入Linux中斷機制的引子,現在我們就進入Linux中斷世界。

更詳細的內容和其它一些注意事項請參考內核源代碼包中Documentations/rtc.txt

      RTC中斷服務程序

RTC中斷服務程序包含在內核源代碼樹根目錄下的driver/char/rtc.c文件中,該文件正是RTC設備的驅動程序——我們曾經提到過,中斷服務程序一般由設備驅動程序提供,實現設備中斷特有的操作。

SagaLinux中注冊中斷的步驟在Linux中同樣不能少,實際上,兩者的原理區別不大,只是Linux由於要解決大量的實際問題(比如SMP的支持、中斷的共享等)而采用了更復雜的實現方法。

RTC驅動程序裝載時,rtc_init()函數會被調用,對這個驅動程序進行初始化。該函數的一個重要職責就是注冊中斷處理程序:

if (request_irq(RTC_IRQ,rtc_interrupt,SA_INTERRUPT,”rtc”,NULL)){
  printk(KERN_ERR “rtc:cannot register IRQ %d/n”,rtc_irq);
  return –EIO;
}

 

這個request_irq函數顯然要比SagaLinux中同名函數復雜很多,光看看參數的個數就知道了。不過頭兩個參數兩者卻沒有區別,依稀可以推斷出:它們的主要功能都是完成中斷號與中斷服務程序的綁定。

關於Linux提供給系統程序員的、與中斷相關的函數,很多書籍都給出了詳細描述,如“Linux Kernel Development”。我這里就不做重復勞動了,現在集中注意力在中斷服務程序本身上。

 

static irqreturn_t rtc_interrupt(int irq, void *dev_id,
struct pt_regs *regs)
{
    /*
     *     Can be an alarm interrupt, update complete interrupt,
     *     or a periodic interrupt. We store the status in the
     *     low byte and the number of interrupts received since
     *     the last read in the remainder of rtc_irq_data.
     */
    spin_lock (&rtc_lock);
    rtc_irq_data += 0x100;
    rtc_irq_data &= ~0xff;
    rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);
 
    if (rtc_status & RTC_TIMER_ON)
       mod_timer(&rtc_irq_timer,jiffies + HZ/rtc_freq + 2*HZ/100);
    spin_unlock (&rtc_lock);
    /* Now do the rest of the actions */
    spin_lock(&rtc_task_lock);
    if (rtc_callback)
       rtc_callback->func(rtc_callback->private_data);
    spin_unlock(&rtc_task_lock);
    wake_up_interruptible(&rtc_wait);      
    kill_fasync (&rtc_async_queue, SIGIO, POLL_IN);
    return IRQ_HANDLED;
}

 

這里先提醒讀者注意一個細節:中斷服務程序是static類型的,也就是說,該函數是本地函數,只能在rtc.c文件中調用。這怎么可能呢?根據我們從SagaLinux中得出的經驗,中斷到來的時候,操作系統的中斷核心代碼一定會調用此函數的,否則該函數還有什么意義?實際上,request_irq函數會把指向該函數的指針注冊到相應的查找表格中(還記得SagaLinux中的irq_handler[]嗎?)。static只能保證rtc.c文件以外的代碼不能通過函數名字顯式的調用函數,而對於指針,它就無法畫地為牢了。

 

程序用到了spin_lock函數,它是Linux提供的自旋鎖相關函數,關於自旋鎖的詳細情況,我們會在以后的文章中詳細介紹。你先記住,自旋鎖是用來防止SMP結構中的其他CPU並發訪問數據的,在這里被保護的數據就是rtc_irq_data。rtc_irq_data存放有關RTC的信息,每次中斷時都會更新以反映中斷的狀態。

接下來,如果設置了RTC周期性定時器,就要通過函數mod_timer()對其更新。定時器是Linux操作系統中非常重要的概念,我們會在以后的文章中詳加解釋。

代碼的最后一部分要通過設置自旋鎖進行保護,它會執行一個可能被預先設置好的回調函數。RTC驅動程序允許注冊一個回調函數,並在每個RTC中斷到來時執行。

wake_up_interruptible是個非常重要的調用,在它執行后,系統會喚醒睡眠的進程,它們等待的RTC中斷到來了。這部分內容涉及等待隊列,我們也會在以后的文章中詳加解釋。

感受RTC——最簡單的改動

我們來更進一步感受中斷,非常簡單,我們要在RTC的中斷服務程序中加入一條printk語句,打印什么呢?“I’m coming, interrupt!”。

下面,我們把它加進去:

 

spin_unlock(&rtc_task_lock);

printk(“I’m coming , interrupt!/n”);

wake_up_interruptible(&rtc_wait);    

 

沒錯,就先做這些,請你找到代碼樹的drivers/char/rtc.c文件,在其中irqreturn_t rtc_interrupt函數中加入這條printk語句。然后重新編譯內核模塊(當然,你要在配置內核編譯選項時包含RTC,並且以模塊形式)現在,當我們插入編譯好的rtc.o模塊,執行前面實時鍾部分介紹的用戶空間程序,你就會看到屏幕上打印的“I’m coming , interrupt!”信息了。

這是一次實實在在的中斷服務過程,如果我們通過ioctl改變RTC設備的運行方式,設置周期性到來的中斷的話,假設我們將頻率定位8HZ,你就會發現屏幕上每秒打印8次該信息。

動手修改RTC實際上是對中斷理解最直觀的一種辦法,我建議你不但注意中斷服務程序,還可以看一下RTC驅動中ioctl的實現,這樣你會更加了解外部設備和驅動程序、中斷服務程序之間實際的互動情況。

不僅如此,通過修改RTC驅動程序,我完成了不少稀奇古怪的工作,比如說,在高速數據采集過程中,我就是利用高頻率的RTC中斷檢查高速AD采樣板硬件緩沖區使用情況,配合DMA共同完成數據采集工作的。當然,在有非常嚴格時限要求的情況下,這樣不一定適用。但是,在兩塊12位20兆采樣率的AD卡交替工作,對每秒1KHz的雷達視頻數據連續采樣的情況下,我的RTC跑得相當好。

當然,這可能不是一種美觀和標准的做法,但是,我只是一名程序員而不是藝術家,只是了解了這么一點點中斷知識,我就完成了工作,我想或許您也希望從系統底層的秘密中獲得收益吧,讓我們在以后的文章中再見。



[1]那么PowerOff(關機)算不算中斷呢?如果從字面上講,肯定符合漢語對中斷的定義,但是從信號格式、處理方法等方面來看,就很難符合我們的理解了。Intel怎么說的呢?該中斷沒有采用通用的中斷處理機制。那么到底是不時中斷呢?我也說不上來:(

[2]之所以這里使用匯編而不是C來實現這些函數,是因為C編譯器會在函數的實現中推入額外的棧信息。而CPU在中斷來臨時保存和恢復現場都按照嚴格的格式進行,一個字節的變化都不能有。


免責聲明!

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



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