官术网_书友最值得收藏!

2.5 VxWorks中斷處理——多層次的中斷轉移

中斷是外設為得到CPU服務而向CPU發送的一個信號,CPU在接收到一個中斷信號后,暫停當前執行的任務,轉去執行中斷處理程序,完成對外設的服務。為使一個中斷能夠得到服務,除了操作系統需要進行配合外,還必須配置CPU硬件寄存器。以ARM處理器為例,ARM處理器支持兩個中斷管腳。換句話說,ARM處理器可以從外界接收兩個不同源產生的服務需求,IRQ為普通中斷服務請求,FIQ為快速中斷服務請求,其中,FIQ優先級較高,即當IRQ、FIQ同時有中斷請求時,ARM處理器優先處理FIQ中斷。VxWorks操作系統只使用了IRQ中斷,FIQ閑置未用,“未用”的含義即負責FIQ的內核中斷響應程序只是簡單地返回,而不進行任何有效的操作。

實際上,對于一個平臺而言,處理器提供的中斷管腳根本無法滿足外設的數量需求,故一般外設發出的中斷都會通過一個中斷控制器輸入到CPU。中斷控制器是可編程的,可接收多個中斷源,并對這些中斷源進行優先級排序的中斷源控制設備,可以單獨禁止和使能某個中斷。一般而言,中斷控制器具有多個中斷源輸入管腳,而只有一個中斷輸出管腳,與ARM處理器的IRQ或者FIQ直接相連。故而平臺所有外設的中斷都作為ARM處理器IRQ或者FIQ中斷。由于VxWorks操作系統只支持IRQ中斷管腳,故在VxWorks下,中斷控制器的輸出管腳必須與ARM處理器的IRQ中斷輸入管腳連接在一起,此時ARM處理器的FIQ管腳閑置不用。

由于此時所有的外設中斷源都通過中斷控制器的管理以處理器單一中斷的形式而存在,故操作系統對中斷的響應采用了多層分級查找的方式。首先,VxWorks內核提供一個IRQ中斷總入口函數,由這個函數查詢中斷控制器得到當前請求服務的最高優先級中斷號,進而由這個中斷號調用對應該中斷的具體的中斷處理函數,完成一次中斷的響應過程。為此,VxWorks內核單獨維護一張外設中斷程序表,該表實現上實際為一個結構數組,這個結構定義可以是如下形式:

        typedef struct {  /* VEC_ENTRY */
            VOIDFUNCPTR    routine;
            ULONG arg;
        } VEC_ENTRY;

根據特定平臺總的外設中斷源的數目,在VxWorks內核初始化過程中,將創建這樣一個結構數組,外設中斷源編號作為數組索引,而用戶注冊的中斷處理程序則以中斷源編號為索引,在數組對應的某項中填入中斷函數地址和參數,完成中斷程序的注冊過程。當ARM處理器接收到一個IRQ中斷后,首先由VxWorks內核提供的IRQ中斷響應函數進行處理,該總入口函數作為IRQ處理函數注冊到ARM處理器系統中斷向量表中。IRQ總入口函數并不對具體的中斷進行服務,其主要完成中斷服務查詢和轉移的工作,具體如下:

● 讀取中斷控制器相關寄存器,獲取當前中斷號。注意,中斷控制器自身具有中斷源優先級排序功能,故當某個時刻有多個中斷產生時,中斷控制器將選擇最高優先級的中斷進行服務,此時中斷控制器相關寄存器中存儲的就是這個最高優先級中斷的中斷號。

● 根據獲取的中斷號,索引VxWorks自身維護的外設中斷程序表,得到中斷號對應的表項,調用表項中之前注冊的中斷處理程序,完成中斷服務的轉移。

● 用戶注冊的中斷程序被調用,完成特定中斷的服務。

基于此二級中斷服務機制,需要注意以下幾個方面。

首先,VxWorks內核提供了IRQ中斷的總入口函數,所有外設中斷的服務首先進入該總入口函數,之后總入口函數通過調用外設中斷程序表中對應的中斷函數,完成中斷服務。ARM處理器為了完成中斷服務,其要求在絕對地址0處必須建立中斷向量表,稱為系統中斷向量表,以區別于VxWorks操作系統自身維護的用戶中斷向量表。

其次,為了使能第一層次中斷響應,必須配置ARM處理器CPCR寄存器,使能IRQ中斷,即將CPCR寄存器中I位清零。這一點很重要,在基于ARM處理器的平臺初始化代碼中,我們一般都會將CPCR寄存器中I、F位置1,禁止系統一切中斷,并同時設置模式為系統特權模式,從而使具有足夠的優先級進行ARM處理器其他寄存器的設置。

在VxWorks下,在響應外設中斷之前,必須重新將I位清零,事實上,在此后的內核初始化代碼中,并沒有具體的代碼完成I位清零的工作,這個工作是在任務創建時附帶完成的。usrInit函數最后通過調用kernelInit函數創建一個內核任務完成余下的內核初始化工作,新創建的任務默認將I位清零,即usrRoot函數默認運行在系統中斷使能的情況下,故此時為了完成外設中斷的服務,只需使能二級中斷即可,即配置中斷控制器相關寄存器去使能某個中斷。注意:在VxWorks內核初始化最初階段,中斷控制器被配置為禁止了所有的外設中斷。以后將根據具體需要單個使能各個中斷源。為了使能某個外設中斷,BSP開發人員必須提供intEnable和intDisable的底層實現函數,這兩個函數分別完成使能和禁止某個外設中斷的目的。具體工作的完成是通過配置中斷控制器的相關寄存器位實現的。intConnect函數完成外設中斷函數的注冊,即注冊到VxWorks內核維護的外設中斷程序表中。為了完成二級中斷轉移,BSP開發人員必須提供如下函數的底層實現。

● intEnable底層實現函數,對應sysIntLvlEnableRtn函數指針。

● intDisable底層實現函數,對應sysIntLvlDisableRtn函數指針。

● 中斷號獲取函數,對應sysIntLvlVecChkRtn函數指針。

● 中斷應答函數,對應sysIntLvlVecAckRtn函數指針。

上述四個函數都將被VxWorks內核調用,而具體實現則由BSP開發人員提供,故內核提供函數指針的形式,由BSP開發人員進行初始化,而后由VxWorks通過函數指針的方式調用這些底層實現函數。其中中斷號獲取函數和中斷應答函數將被IRQ中斷總入口函數調用,完成中斷轉移和應答。這四個函數的實現都必須有具體的操作中斷控制器,而中斷控制器是與平臺相關的,故必須作為BSP的一部分來實現。

2.5.1 VxWorks下中斷轉移過程詳解(基于ARM平臺)

VxWorks內核提供intEnt(intALib.s)作為IRQ中斷總入口函數,該函數完成相關設置后,調用__func_armIrqHandler函數(excArchLib.c)指針指向的函數完成具體的中斷服務。__func_armIrqHandler函數指針默認初始化為指向excIntHandle函數(excArchLib.c),excIntHandle函數只是調用excIntInfoShow(excArchShow.c)打印出相關的中斷信息,并不服務中斷,在VxWorks內核中斷服務初始化過程中(sysHwInit2函數中通過調用intLibInit,sysHwInit2在sysClkConnect中被調用),__func_armIrqHandler函數指針被重新初始化為指向intIntRtnNonPreempt或者intIntRtnPreempt。其中intIntRtnPreempt函數支持中斷嵌套,即允許高優先級的中斷打斷當前低優先級服務,轉而服務高優先級中斷;而intIntRtnNonPreempt則不支持中斷嵌套,即當系統在服務低優先級中斷時,不允許高優先級中斷發生,此時ARM處理器中I位被置1。如果工作在允許中斷嵌套的情況下,系統對于嵌套的層次有所限制。

當嵌套層次超過系統最大允許值時,將不再允許中斷嵌套的發生。由于中斷發生時,ARM處理器I位被置1(intEnt函數中完成),故實際上intIntRtnNonPreempt和intIntRtnPreempt的區別主要在于intIntRtnPreempt函數中重新將ARM處理器I位清零,從而允許其他高優先級中斷在服務當前中斷的過程中產生,即產生中斷嵌套。通過以上分析,基于ARM處理器VxWorks的中斷服務流程如下:中斷產生→intEnt→intIntRtnNonPreempt或者intIntRtnPreempt→用戶注冊的具體的中斷函數。

注意

以上中斷服務流程中,具體完成中斷服務的是最后被調用的用戶注冊的中斷函數,intEnt以及intIntRtnNonPreempt或者intIntRtnPreempt函數都是做輔助性的工作,當然這些輔助性的工作對于內核而言是至關重要的。

由于ARM處理器只有兩個中斷管腳,而VxWorks操作系統只使用了IRQ普通中斷管腳,故平臺下所有的外設中斷都是通過一個中斷控制器外部設備進行管理,包括系統時鐘中斷也是通過中斷控制器向ARM處理器請求中斷服務。系統時鐘中斷是整個VxWorks操作系統的脈搏,也是任務調度的固定觸發點,同時還是系統各種定時器的源,對于整個系統而言,具有至關重要的作用。下面我們以系統時鐘中斷的初始化和響應過程為例介紹VxWorks下中斷注冊和服務過程。

VxWorks系統時鐘中斷用戶層注冊函數為sysClkInt。系統時鐘中斷的注冊在sysClkConnect中完成。sysClkConnect被usrRoot調用專門負責系統時鐘的初始化。該函數被調用情景的代碼如下。

        sysClkConnect ((FUNCPTR) usrClock, 0);  /* connect clock interrupt routine */
        sysClkRateSet (SYS_CLK_RATE);             /* set system clock rate */
        sysClkEnable ();                       //配置外設時鐘,使其正常工作,按固定時間間隔產生時鐘中斷

注意

sysClkConnect的第一個輸入參數為系統時鐘中斷的服務例程,然而這并非直接中斷服務例程。換句話說,從上文分析的中斷服務流程來看,其還不屬于用戶注冊的中斷函數,而是更低一級,由用戶注冊的中斷函數調用。事實上,對于系統時鐘中斷而言,用戶注冊的中斷函數是sysClkInt,這是在sysHwInit2中注冊完成的。sysHwInit2由sysClkConnect函數調用,如下為sysClkConnect函數(xxx_timer.c中“xxx”表示相關平臺)實現的代碼。

        STATUS sysClkConnect
        (
              FUNCPTR routine,  /* routine to be called at each clock interrupt */
              int arg              /* argument with which to call routine */
        )
        {
              static BOOL beenHere = FALSE;
              if (!beenHere)
              {
                  beenHere = TRUE;
                  sysHwInit2 ();
              }
              sysClkRoutine = NULL;
              sysClkArg = arg;
              sysClkRoutine = routine;
              return (OK);
        }

作為參數傳入的usrClock函數地址被存儲在sysClkRoutine函數指針中,而不是作為intConnect的函數注冊到外設中斷程序表中。其中調用的sysHwInit2完成系統時鐘中斷程序的注冊,代碼如下。

        /*sysLib.c*/
        void sysHwInit2 (void)
        {
              static BOOL initialised = FALSE;
              if (initialised)
              return;
              /* initialise the interrupt library and interrupt driver */
              intLibInit (NUM_OF_INTERRUPT,NUM_OF_INTERRUPT,
                          INT_NON_PREEMPT_MODEL );
              xxxIntDevInit ();
              /* connect sys clock interrupt */
              (void)intConnect ( INUM_TO_IVEC(INT_TINT0), sysClkInt, 0);
              intEnable ( INT_TINT0 );
            …
              initialised = TRUE;
        }

sysHwInit2首先調用intLibInit完成內核中斷服務的相關初始化,其中最重要的是重新初始化__func_armIrqHandler函數指針,intLibInit函數的第三個參數指定是否允許中斷嵌套,此處調用中不允許中斷嵌套,故__func_armIrqHandler函數指針被初始化為指向intIntRtnNonPreempt函數;繼而調用xxxIntDevInit函數完成前文所述的四個中斷底層實現函數的注冊,即初始化sysIntLvlEnableRtn、sysIntLvlDisableRtn、sysIntLvlVecChkRtn、sysIntLvlVecAckRtn四個函數指針(這四個函數指針的含義見上文分析);而后調用intConnect進行外設中斷程序的注冊,即將sysClkInt函數作為系統時鐘中斷的服務程序注冊到內核維護的外設中斷程序表中,最后調用intEnable使能該中斷,intEnbale實現上調用sysIntLvlEnableRtn指向的函數(BSP開發人員實現)完成中斷控制器的配置,從而使能系統時鐘中斷。

注意

可以將此處的intEnable函數放入usrRoot中調用的sysClkEnable函數(xxx_timer.c)中,如果此處進行了intEnable的調用,則無須在sysClkEnable中再次進行調用,此時sysClkEnable只需進行定時器外設的硬件配置即可。所以,單從系統時鐘中斷的角度來看,sysHwInit2將sysClkInt函數注冊到內核中作為系統時鐘中斷的服務程序。sysClkInt定義在xxx_timer.c中,其代碼結構如下。

        void sysClkInt (void)
        {
              if( ( sysClkRunning == TRUE ) && ( sysClkRoutine != NULL ) )
              {
                      /* call system clock service routine */
                      (* sysClkRoutine) (sysClkArg);
              }
        }

在sysClkConnect函數中,sysClkRoutine被設置為第一個參數指向的函數,即usrClock。由此,我們可以將系統時鐘中斷產生時整個系統的服務流程表示如下:

系統時鐘中斷產生→intEnt→intIntRtnNonPreempt→sysClkInt→usrClock→tickAnnounce

usrClock調用的tickAnnounce函數由VxWorks操作系統提供,tickAnnounce函數主要完成如下工作:

① 對vxTick變量做加1運算。vxTick表示系統自啟動之時到現在的tick數,所以用vxTick乘以系統時鐘間隔就是開機時間。

② 對處于等待狀態的任務進行檢查,將超時任務(那些調用taskDelay延遲的任務)重新設置為ready狀態,并轉移到調度隊列中。

③ 遍歷內核工作隊列,對延遲的內核工作進行執行。

④ 進程調度。選擇最高優先級任務作為當前任務運行。

系統時鐘中斷的服務過程相對其他中斷較為“曲直”,其在用戶層又進行了分層。對于其他硬件中斷,通過intConnect函數注冊到系統外設中斷程序表中的中斷函數一般作為服務具體完成的場合,而不再有進一步中斷轉移的過程。系統時鐘中斷的特殊之處在于其具體中斷服務函數(tickAnnounce)是由內核提供的,但又必須在用戶層對時鐘進行管理,故必須經過中間一步轉換的過程。

2.5.2 中斷上下文中為何不可調用可引起睡眠的函數

無論是在閱讀相關操作系統理論書籍還是閱讀實際代碼,基本都說到了在中斷處理程序中,不可以調用可以引起睡眠或者阻塞的函數。經典的解釋如下:

Code running in interrupt context is unable to sleep, or block, because interrupt context does not has a backing process with which to reschedule. Therefore, because interrupt handler is not associated with a process, there is nothing for the scheduler to put to sleep and, more importantly, nothing for the scheduler to wake up …

這是我們聽到的最經典的解釋:因為中斷是運行在中斷上下文中的,所以不可以調用可引起阻塞或者睡眠的函數。再進一步:因為調度必須是以進程上下文為條件的,而中斷并沒有這樣一個對應的進程存在,所以中斷不可調用可引起進程切換的函數(即引起阻塞或睡眠的函數)。

基于Linux內核代碼的開源性,故在以下的討論中,我們以Linux操作系統為例。我們知道,當前執行進程控制塊由current指針指向,當進程調度時,current指針被賦值指向下一個即將運行的進程。當某個進程運行時,發生一個中斷,此時CPU按照一定的流程從用戶或者內核進程切換到中斷處理程序運行。注意,此時current指針仍然指向被中斷的進程。對于在用戶態被中斷的情況來看,其與系統調用的情景差不多,最大的差別在于中斷時硬件自動完成全局中斷的禁止(注意:這一點在下文的討論中是至關重要的)。此時將從用戶態切換到內核態,以下寄存器將被壓入進程的內核堆棧:用戶態SS、用戶態ESP、EFLAGS、用戶態CS、用戶態EIP、出錯碼(若存在的話。但對于外部中斷,都沒有出錯碼)。中斷處理程序將使用進程的內核堆棧。

對于在內核態被中斷的情景,此時不發生堆棧的切換,其他相同。以從用戶態被中斷的情景為依據進行分析,我們可以分析得出,其與用戶進程進行系統調用時發生的情況幾乎完全相同。一個最本質的區別是中斷時硬件自動關閉了系統中斷使能位,故直到中斷處理程序退出或者其主動開啟使能位之前,所有其他的系統中斷都將被屏蔽,包括timer interrupt(系統時鐘終端),這個進程調度的脈搏和激勵源。由此不同之處,我們可以分析出為何一般的系統調用可以進行睡眠,而由中斷引起的進程卻不可以進行睡眠。

首先需要提及的一點是,系統調用一般是為進行系統調用的進程服務的,或者說,就是我們通常所說的服務于用戶進程,即此時的內核執行代碼與當前current指向的進程是相互綁定的。如果內核執行過程中某個條件不滿足,需要進行等待,那么其綁定的進程進入睡眠狀態是合乎情理的。對于中斷的情況,則完全不同,中斷的發生完全是異步的,這就是說,當中斷發生時,我們無法預知當前執行的是哪一個進程。換句話說,單從上下文來看,中斷發生時,當前執行進程被這個中斷強行“綁架”了。或者說,中斷處理程序的執行并不是以當前進程的“利益”為其首要準則,它是一個顯然的侵犯者,它為著自身的利益強行中斷了當前進程的執行。此時除了全局中斷被硬件處理流程關閉外,系統所有的上下文環境與普通的系統調用沒有任何差異。

對于中斷處理程序(或者說中斷上下文中)不可以調用可引起睡眠或者阻塞的函數的傳統經典解釋中,僅僅表達了“因為是中斷上下文,不是進程上下文,所以不可調用。”從以上的說明可以看出,中斷就是執行在進程上下文中,current指針依然指向一個有效的位置,就是當前被中斷的進程(無論這個進程與這個中斷相關還是不相關)。所以,如果此時中斷處理程序中調用一個可引起睡眠(即引起進程調度)的函數,那么switch_to仍然可以毫無困難地執行,至多只是將一個被綁架的進程調度了出去。當這個被綁架的進程被調度回來后(暫不考慮是如何被調度回來的,下文討論這一點),那么它將仍然繼續之前的處理:先執行完尚未執行完的中斷處理代碼,而后退出,從被綁架進程內核堆棧中彈出CS、EIP等,恢復這個被綁架的可憐的進程的執行,有何不可?

以上討論中跳過了一點,即被綁架進程被調度出去后,是如何被調度回來的。由于作為反方進行論證,所以只需給出一種可能的情況即可,只要能被調度回來就可以。例如,中斷程序調用kmalloc函數引起睡眠,那么此時被綁架進程將被掛入到一個內核指定的隊列中,當系統又有可用內存時,內核相關代碼會喚醒由于kmalloc調用而睡眠的進程,當然包括這個可憐的被中斷綁架的進程。在某個時刻這個被綁架進程重新被調度回來進行執行,有何不可?

一些讀者可能已經意識到了,以上的討論中省略了最關鍵的一點,即調度器要在工作著。換句話說,系統的脈搏要在搏動。但是注意到當中斷發生時,硬件處理流程關閉了系統中斷,包括系統脈搏中斷timer interrupt。

首先我們討論一下沒有timer interrupt進程調度的可能性。沒有了timer interrupt,系統能否工作?回答是:不能。如此情況下,進程的調度將完全依賴于一個執行的進程主動調用schedule函數。對于某些進程,這一點是可以滿足的,但是對于Linux內核中絕大多數進程,這一點是不滿足的,只要當前被調用執行的進程不曾主動調用schedule函數,那么我們只能等待它執行到死。對于用戶而言,此時將表現為系統死機。因為用戶判斷系統是死是活,通常是通過界面是否響應用戶的命令,對于一個一直在執行某特定單一進程的操作系統而言,這一點是無法完成的,那就是我們無論干什么,系統都沒有響應,即系統死了。(此處的討論不很深入,不過到此已經達到了說明的目的了。)

以上是最平常的情況,即中斷處理程序一般的工作方式(禁止嵌套中斷)。現在我們討論另一種可能性:中斷處理程序在一開始就將系統關閉著的中斷開啟,即允許中斷嵌套。此時timer interrupt獲得了執行的資格,系統重新擁有了脈搏。那么被中斷綁架的進程將有希望被重新調度。問題是,如果這個中斷的中斷源自系統開機到關機之間只發出一個中斷,那么將不會造成任何問題。這個中斷綁架的進程最多會無故多延遲等待一會執行而已,不至于造成整個系統死掉。

問題的關鍵有兩點:其一,中斷會發生多次;其二,每發生一次,系統就多一個進程被強行綁架。第一點引起的問題是,幾乎每次中斷都會對同一個問題進行加重,如對同一個資源的重新申請;第二點引起的問題是,系統到最后可能所有的進程都被綁架,到最后系統無進程可以調度,因為所有的進程都在被綁架著睡大覺,甚至包括idle進程。這個陷阱的出路是:每次下一個中斷來臨前,前一次中斷退出。但考慮到如果一旦允許中斷處理程序中可以調用可引起阻塞或者睡眠的函數,那么引起該類問題的中斷源就不只一個了,某個小中斷源可以,串口可以否,硬盤可以否,網口可以否,繼而timer interrupt可以否?顯然不可以。既然有不可以的,那么大家就都不可以。這一點不是可以商量的,那是必需的。所以就有中斷處理程序中(或者說中斷上下文中)不可以調用可引起阻塞或者睡眠的函數。這是一條必須嚴格遵守的游戲規則。但是如果強行不遵守它,是不是一定出問題?不一定,如果中斷頻率不高,而調用的可引起阻塞的函數不是VIP高級會員,如kmalloc(在系統內存足夠時),那么調用一下不會引起什么問題,但是這是一個錯誤,是一個潛在的危險源,如果有一天系統死機了,那么這可能就是罪魁禍首。

主站蜘蛛池模板: 兰西县| 江永县| 东丽区| 孟津县| 青冈县| SHOW| 马边| 拉萨市| 伊金霍洛旗| 河源市| 余江县| 磐安县| 北川| 济南市| 贺兰县| 榕江县| 桦川县| 兖州市| 海淀区| 沾化县| 和硕县| 通化市| 琼海市| 克东县| 临潭县| 永嘉县| 浪卡子县| 翁源县| 土默特左旗| 晋州市| 渝中区| 元江| 通榆县| 宝坻区| 临夏市| 北宁市| 屏边| 吉林市| 武鸣县| 望谟县| 临漳县|