VxWorks的學習與理解(三)

感謝前輩分享,附上鍊接:http://www.prtos.org/vxworks-wind-scheduler/

本篇文章分析Wind內核調度器的設計原理以及其工作流程,設計支持多任務RTOS的關鍵是設計調度器,Wind內核調度器的目標是保證優先級最高的就緒任務處於運行狀態。爲了達到這一目的,需要在Wind內核的調度點判斷就緒隊列中優先級最高的任務是否正在運行,如果不在運行,調度器就會讓這個優先級最高的任務搶佔正在運行任務的CPU。

保證就緒隊列中優先級最高的任務始終佔據CPU是Wind內核可搶佔的實質,其採用位圖查找算法來定位最高優先級的進程,該算法的時間複雜度爲O(1),且與系統當前任務總數無關,即與系統負載無關,但是與系統支持的優先級數有關。Wind內核的調度點分成兩處,一處是從內核態返回(調用windExit()),另一處是中斷返回(調用intExit())。

3.1 Wind內核調度器工作原理

Wind內核調度器的作用:

  • 使得任務狀態的改變完全依賴於事件和系統調用的發生;
  • 將一個任務從一個隊列移動到另一個隊列;
  • 如果存在的話,執行任務切換(switch)和交換(swap)鉤子函數,其中任務交換鉤子函數僅僅提供了wind內核使用,任務切換鉤子函數是提供給用戶使用;
  • 當系統空閒的時自旋尋找內部工作隊列的空閒工作(Job)來執行;
  • 具有電源管理接口。

在Wind內核中調度器reschedule()的實現有兩個版本,一個是用C實現新的可移植性版本,一個是具體平臺彙編實現(比如x86平臺彙編)的優化版本。爲了便於分析,我們選擇C實現的可移植版本

reschedule()調度目的是選擇合適的任務到CPU上運行,然後調用切換(switch)和交換(swap)鉤子函數,最後載入挑選出來的任務的上下文。比較複雜的因素在於內核工作隊列(kernel work queue)在reschedule()調用結束之前必須被檢查是否爲空。在C的可移植性版本中,檢查內核工作隊列和加載任務的上下文是在關中斷的情況下完成的,這樣做的目的是避免中斷ISR的競爭。而在reschedule()的優化版本中,將加載任務上下文的部分儘可能的放在關中斷和檢查工作隊列是否爲空之前,這將會極大的降低中斷延遲時間。

如果沒有任務就緒,Wind內核將會在idle狀態中不斷的檢查內核工作隊列中是否有工作Job要做。由於Wind內核的idle狀態不在任務的上下文當中,所以切換和交換鉤子函數在內核處於idle狀態時均是不能被調用的。一旦離開idle狀態,如果新挑選的任務不同於內核進入idle狀態時正在執行的任務(即存在新的更高優先級的任務就緒),那麼切換和交換鉤子函數都會被調用。

代碼如下(爲了方便閱讀,我刪除了不相干的代碼):

void reschedule (void)

{

    FAST WIND_TCB     *taskIdPrevious;

    FAST int                ix;

    FAST UINT16          mask;

    int                              oldLevel;

unlucky:

    taskIdPrevious = taskIdCurrent;               /* 記住舊的任務 */

    workQDoWork ();                                        /* 執行內核隊列中的所有Job ,清空內核隊列*/

 

    /*在此處標記idle狀態,直到有任務準備執行*/

    kernelIsIdle = TRUE;                /* 標記此時處於idle狀態 */

    while (((WIND_TCB *) Q_FIRST (&readyQHead)) == NULL)

     {

             workQDoWork ();/*就緒隊列爲空,空轉尋找內核隊列中的Job執行*/

     }

/*執行到此處,說明就緒隊列不空,有新的任務就緒*/

taskIdCurrent = (WIND_TCB *) Q_FIRST (&readyQHead);/*挑選優先級最高的任務*/

kernelIsIdle = FALSE;                       /* 標記此時脫離idle狀態 */

 

    /* taskIdCurrent 指向將要運行的任務,如果其和

     * taskIdPrevious保存的舊任務不同,內核將會執行切換和交換鉤子函數.

     */

    if (taskIdCurrent != taskIdPrevious)         /*如果不同則意味着是一個新的任務 */

    {

         /* 執行鉤子函數*/

         mask = taskIdCurrent->swapInMask | taskIdPrevious->swapOutMask;

         for (ix = 0; mask != 0; ix++, mask = mask << 1)

             if (mask & 0x8000)

                   (* taskSwapTable[ix]) (taskIdPrevious, taskIdCurrent);

 

         /* do switch hooks */

         for (ix = 0; (ix < VX_MAX_TASK_SWITCH_RTNS) && (taskSwitchTable[ix] != NULL);++ix)

             {

                     (* taskSwitchTable[ix]) (taskIdPrevious, taskIdCurrent);

             }

    }

 

    oldLevel = intLock ();                           /* 關中斷 */

    if (!workQIsEmpty)

    {                                                                /*內核隊列中又有新的Job*/

         intUnlock (oldLevel);                           /* 開中斷*/

         goto unlucky;                                        /* 從頂部重新開始執行 */

    }

    else

    {

         kernelState = FALSE;                           /* 退出內核態 */

         /*現在我們恢復調度器reschedule()選中的任務上下文,從代碼中我們可以看出:整個

         *恢復任務上下文和判斷內核隊列爲空的操作都是在關中斷的情況下進行的,在具體

         *平臺的彙編優化版本中,將會把恢復任務上下文的過程放入到關中斷的前面,這樣

         *僅有判斷內核隊列是否爲空的操作處於關中斷的情況下,這將極大減低中斷時延!

         */

         windLoadContext ();                           /* dispatch the selected task */

     }

}

備註:這裏需要指出的是,爲什麼在reschedule()的末尾再次判斷內核隊列是否爲空,如果非空重新執行調度器呢?

這是因爲,在執行內核切換和交換鉤子函數期間,很有可能會有更高優先級的任務需要被喚醒,由於此時Wind內核正在被reschedule()調度器互斥訪問(即Wind內核處於內核態,kernelState=TRUE),將這些任務轉爲就緒態的工作都會被賽道內核隊列延遲運行。如果在reschedule()的末尾檢查內核隊列非空,則證實了這種判斷,reschedule()需要重新執行內核隊列中的Job,使的更高優先級的任務可以進入就緒態,因而可以被提供一次調度運行的機會,這樣確保了只有優先級最高的任務佔據CPU。

另外,從Wind內核調度器的實現過程可以看出正在CPU上運行的任務仍然在就緒隊列中,Wind內核這樣處理有一個優點,如果正在運行的任務是優先級最高的任務,其會佔據優先級隊列的隊首;如果其已經不是優先級最高的任務,將不會在優先級隊列的首部。這樣Wind內核只需要將優先級隊列的隊首元素和正在運行的任務比較,如果兩者不同則說明當前任務已經不是優先級最高的任務,隊首任務此時是優先級最高的任務,需要進行任務切換!

Wind內核調度器工作流程如圖3.1。

VxWorks內核解讀-3

圖3.1 Wind內核調度器工作流程

3.2 調度器調度時機

調度器reschedule()在Wind內核中僅被兩處例程調用:退出內核的函數例程windExit()和退出中斷的函數例程intExit()。這裏需要指出的是Wind內核用一個全局變量kernelState來標誌Wind內核是否進入內核態。

進入內核態,只需要簡單地置kernelState爲TRUE;

退出內核,顧名思義只需要將kernelState置爲FALSE即可,但是退出內核態涉及到內核隊列的清空操作,從而引發新的任務就緒,wind內核爲了從形式上統一這個過程,將其封裝在windExit()函數例程中,由於reschedule()調度器也是被windExit()調用,因此我們可以從更高的角度來說,退出內核的例程統一爲windExit()例程

爲了清楚地描述Wind內核的調度器reschedule()在wind內核的地位,我以時鐘中斷爲例,來考察Wind內核的運行機制。我們首先描述一下時鐘中斷的流程,當Wind內核運行的某一個時刻,時鐘中斷髮生時(暫不考慮時鐘中斷嵌套的情況),執行流程如下:

  • 步驟1: Wind內核首先調用intEnt()例程保存當前任務的上下文到中斷棧中,這一步是在關中斷的情況下執行;
  • 步驟2:Wind內核執行時鐘中斷處理函數tickAnnounce()(這裏我去除了一系列的封裝,VxWorks時鐘中斷實際的調用過程是sysClkInt()->usrClock()->tickAnnounce()):

void tickAnnounce (void)

{

    if (kernelState)               /* defer work if in kernel */

                  workQAdd0 ((FUNCPTR)windTickAnnounce);

    else

         {

                  kernelState = TRUE;      /* KERNEL_ENT */

                  windTickAnnounce ();     /* advance tick queue */

                  windExit ();                          /* KERNEL_EXIT */

         }

}

這一步是在開中斷的條件下執行的;

  • 步驟3:執行退出中斷處理函數intExit()

 

第一種情況:

假設發生時鐘中斷的時刻,Wind內核當前不處於內核態(kernelStare=FALSE)時,則時鐘中斷處理函數通過將kernelState置爲TRUE進入內核,直接執行wind內核態時鐘中斷處理例程windTickAnnounce(),執行完畢後調用windExit()退出內核態。windExit的執行流程如圖3.2。

VxWorks內核解讀-3

圖3.2 windExit()執行流程圖

由於此時windExit()還在時鐘中斷上下文中,執行圖中左邊的流程:

  1. 如果內核隊列爲空,則開中斷、退出內核態,跳轉到3;
  2. 如果內核隊列非空,先處理內核隊列中的Job,處理完畢後跳轉到2;
  3. 返回;

接着執行intExit()退出中斷處理函數,intExit()處理流程如圖3.3。

VxWorks內核解讀-3

圖3.3 intExit()退出中斷處理流程

 

此時如果被中斷任務仍然是優先級最高的任務,則恢復被中斷任務的上下文,繼續執行;

如果存在更高優先級的任務就緒,但被中斷的任務禁止搶佔,且處於就緒態,則恢復被中斷任務的上下文,繼續執行;如果被中斷的任務支持搶佔或者主動放棄CPU,則恢復最高優先級的任務。

如果時鐘中斷髮生嵌套時,發生的階段只能在正在執行核心中斷處理例程期間(tickAnnounce()執行期間),因爲這一階段是開中斷,並且此階段正處於Wind內核的內核態中。根據tickAnnounce()的處理流程,其會將Wind內核中斷服務例程作爲一個Job直接放置到內核工作隊列中返回,接着執行intExit()函數。由於此時正處於中斷嵌套階段,根據intExit()處理流程,其會進一步返回到上一層中斷中繼續執行。

那麼本次中斷的處理程序windTickAnnounce() 什麼時候進行處理呢?

我們知道最外層中斷處理函數佔據着wind的內核態,當其調用windExit()從內核態退出時,根據windExit()的處理流程,其會判斷內核隊列是否爲空,在內核隊列不空的情況下,執行內核隊列中的Job,清空內核隊列,此時嵌套的中斷得到處理。

備註:從這裏的處理流程我們可以看出wind內核中斷嵌套處理程序均是採用延後處理的機制,處理的時機在於最外層的中斷退出內核態之前(windExit()執行流程)。

 

第二種情況:

中斷髮生的時刻,Wind內核正處於內核態。正在內核態訪問的代碼可以是一段中斷ISR,我們在上面已經分析過;也可以是一個任務正在調用內核態例程,比如正在執行idle空轉。這種情況只能發生在當前任務從就緒態轉換爲阻塞態,並且此時就緒隊列爲空的情況下。

 

第三種情況:

中斷髮生的時刻,Wind內核正處於非內核態,此時處於最高優先級的任務正在執行。此時時鐘中斷的執行流程是:

  • 第一步:執行intEnt()例程,該函數例程關中斷,保存當前任務的上下文到中斷棧中;
  • 第二步:執行中斷處理函數:

VxWorks時鐘中斷實際的調用過程是sysClkInt()->usrClock()->tickAnnounce()):

void tickAnnounce (void)

{

    if (kernelState)                            /* defer work if in kernel */

                  workQAdd0 ((FUNCPTR)windTickAnnounce);

    else

         {

                  kernelState = TRUE;                  /* KERNEL_ENT */

                  windTickAnnounce ();                /* advance tick queue */

                  windExit ();                          /* KERNEL_EXIT */

         }

}

這一步是在開中斷的條件下執行的,由於此時內核態沒有被佔用(kernelState=FALSE),不需要放在內核延遲隊列中,直接執行windTickAnnounce()

  • 第三步:中斷返回intExit(),如圖3.3所示。

由於此時中斷沒有嵌套,intExit()將執行右邊的流程。

此時如果被中斷任務仍然是優先級最高的任務,則恢復被中斷任務的上下文繼續執行;如果期間由於中斷ISR的執行,導致了新的更高優先級的任務就緒的話,intExit()將會根據情況走兩個分支:

  • 第一個:如果被中斷任務禁止搶佔,並處於就緒態,則恢復被中斷任務繼續執行;
  • 第二個:如果被中斷任務允許搶佔,或者被中斷任務已經處於非就緒態,則執行調度器挑選優先級最高的任務執行。

這裏大家可以存在一個疑問,由於這個場景中中斷ISR直接進入內核態執行中斷處理函數windTickAnnounce (),執行完畢後就可能存在更高優先級的任務就緒,緊接着中斷ISR執行退出內核態例程windExit()。由於windExit()存在一個調度點。更高優先級的任務會不在這裏進行調度,而不需要等到中斷返回intExit()的時刻?我們可以分析一下在這種情景下的windExit()執行流程,爲了方便描述,再次畫一下WindExit()的執行流程,如圖3.4。

VxWorks內核解讀-3

圖3.4 WindExit()的執行流程

從圖中我們可以看出,由於此時仍處在中斷上下文中,windExit()走左邊的分支,這裏只可能會做的就是執行內核Job來清空內核隊列,調用完畢後直接返回。此時的windExit()充當一個函數例程的角色,不涉及到搶佔點的引入。因而只有可能在intExit()中才會給新就緒的任務一個執行的機會。

 

第四種情況:

任務在執行的過程中,需要調用內核態例程的服務,比如延遲自己,創建一個新任務等等,具體的內核例程如下:

1.       void windSpawn(FAST WIND_TCB *pTcb):創建一個任務,並將該任務置爲阻塞態(suspend state),放置到活動隊列(active queue)中。

2.       STATUS windDelete(FAST WIND_TCB *pTcb):刪除任務,並重新排列該任務所在的任何隊列,該例程假設被刪除的任務不具有任務遺傳性的信號量。

3.       void windSuspend(FAST WIND_TCB *pTcb):阻塞(suspend)一個任務,阻塞態(suspension)是一個附加的狀態。當一個任務處於就緒隊列中時,其將會從中移除;該阻塞的任務不是處於就緒態時,其將會在已有的狀態上再加上阻塞態(suspension)。

4.       void windResume(FAST WIND_TCB *pTcb):喚醒一個指定的任務,如果必要的話,將其放置到就緒隊列中。

 

5.       oid windPriNormalSet(WIND_TCB *pTcb, UINT priNormal):設置任務的正常優先級數,如果不發生優先級繼承,任務按照其正常優先級數執行。

6.       void windPrioritySet(FAST WIND_TCB *pTcb, FAST UINT priority):設置任務的實際優先級數,並且考慮到實際的優先級翻轉安全性。如果任務擁有任何的優先級翻轉安全信號量,那麼優先級將不允許降低。

7.       void windSemDelete(FAST SEM_ID semId):刪除一個信號量,並將在該信號量上等待的所有任務解除阻塞。

8.       void windTickAnnounce(void):處理延遲隊列,使得延遲時間到期的任務就緒。如果配置了時間片輪轉策略,則執行時間片輪轉調度策略;通知執行任務到期的看門狗程序。

9.       STATUS windDelay(FAST int timeout):將任務根據延遲的長度timeout插入到延時隊列的合適位置。

10.   STATUS windUndelay(WIND_TCB *pTcb):將睡眠的任務喚醒

11.   STATUS windWdStart(WDOG *wdId, int timeout):啓動或者重啓一個開門夠。如果看門狗已經在定時隊列(tickQHead) 中,它將根據timeout的值重啓排序。如果在內核隊列中存在多個windWdStart() Job,正如變量deferStartCn所計數的那樣,windWdStart()函數將什麼也不做,直接返回。

12.   void windWdCancel(WDOG *wdId):取消看門狗,如果需要的話,將其從定時器隊列中移除。

13.   void windPendQGet(Q_HEAD *pQHead):從pQHead指定的等待隊列中取出隊頭任務,清除其等待狀態,如果其處於延時狀態,將其從延時狀態清除,並剔除出延時隊列。如果其原來處於就緒態,則將其放入就緒隊列。

14.   void windReadyQPut(WIND_TCB *pTcb):將一個先前在某個共享信號量上阻塞的任務放到就緒隊列中,其具體做法是將這個任務的TCB控制塊從共享信號量的等待隊列中移除(由釋放該共享信號量的CPU負責處理),如果其處於延時狀態,將其從延時狀態清除,並剔除出延時隊列。如果其原來處於就緒態,則將其放入就緒隊列。

15.   void windReadyQRemove(Q_HEAD *pQHead, int timeout):將當前任務從就緒隊列中移除,並將其置爲等待態(WIND_PEND)放到pQHead指向的等待隊列中去;如果該任務被定時器,則也將其放入到定時隊列的相應位置。

16.   void windPendQFlush(Q_HEAD *pQHead):將在pQHead指向的等待隊列中的所有任務結束等待狀態,如果其本來就處於就緒態,則將其放置到就緒隊列當中。

17.   STATUS windPendQPut(FAST Q_HEAD *pQHead, FAST int timeout):將當前任務放置到pQHead指向的等待隊列當中,如果當前任務被定時,則同時放置到延時隊列當中。如果timeout的值是NO_WAIT則返回ERROR。

18.   STATUS windPendQRemove(WIND_TCB *pTcb):將pTcb指定的任務從等待隊列中移除。

19.   void windPendQTerminate(Q_HEAD *pQHead):將pQHead指定的等待隊列中的所有任務移除等待隊列。這些任務清除其等待位(WIND_PEND),如果是被延時,則將其從延時隊列中移除,如果其原來處於就緒態,則將其重新放置到就緒隊列。

備註:從STATUS windPendQPut(FAST Q_HEAD *pQHead, FAST int timeout)的實現中,我們可以看出,如果當前任務因爲等待某一個信號量而被放置到該信號量的等待隊列中去時,只是設置該任務的WIND_PEND位,如果其再被要求等待一段時間timeout的話,也只設置該任務的WIND_DELAY位,並放置到延時隊列中去。特別需要指出的是其WIND_READY位並沒有被清除,這意味着,當將一個任務從等待隊列中移除時,只需要查看該任務的WIND_READY位,就可以確定該任務在放入到等待隊列之前是否處於就緒狀態,如果處於就緒狀態就放置到就緒隊列中。windLib庫中操作等待隊列的例程,均利用到了這一技巧,windPendQPut()我這裏就不粘貼了,節省篇幅。

3.3 wind內核態分析

從3.2節我們可以看出Wind內核的內核態例程一共包含了19個例程,這些例程構成了Wind內核最基本的服務,由全局變量kernelState包含。只有在kernelState變量爲TRUE是,外圍例程才能調用這些服務例程。衝這個角度來說,雖然Wind內核運行在處理器的特權態。當時kernelState利用模擬了一個更高的特權態。該特權態外圍軟件只能互斥訪問,並且禁止搶佔。

大家可能有一個疑問,這些內核態例程禁止搶佔,這會不會影響到這個VxWorks系統的響應時間呢?答案是肯定會的,影響的程度在於這些內核例程的設計,這19個內核例程的設計非常的精簡,僅僅完成最核心的功能。並且當Wind內核處於內核態時,儘管禁止強佔,但是中斷是開着的,仍然可以響應中斷。

Wind內核的創新之處在於設計了有限長度的環形內核隊列(Wind2.6版本設置爲爲64)。當Wind內核在內核態時發作中斷時,中斷的上下文的保存和恢復仍然正常處理,但是把真正的中斷處理函數作爲一個Job放置到內核隊列中,等到Wind內核退出內核態時在做處理,設置內核隊列的目的也就在此,目的是延後處理中斷ISR。

這從我們對時鐘中斷的分析可以窺見全貌,大家或許還有一個疑問那就是?這種延時處理中斷ISR的機制會不會影響到VxWorks系統的中斷響應時間呢?

答案是肯定的,畢竟中斷ISR只有等到其退出內核態纔得到處理嘛,但是問題的關鍵在於Wind內核處於內核態的這19個例程非常精簡,其在內核態的時間非常短。這種處理機制和Linux內核中將中斷分成上下半段,下半段(稱之爲軟中斷)延後處理的機制道理是相同的。

這同時也告訴我們,在設置VxWorks的具體外設的中斷處理框架時,必須分清楚哪些是必須處理的(比如最基本的是寫EOI命令),哪些是可以放在內核隊列中延後執行。這些是提高VxWorks實時性能的關鍵。

Wind內核進入內核態只需要將kernelState置爲TRUE即可,退出內核態時則需要調用windExit()例程,這兩個例程需要配套使用。

此次,關於Wind內核的調度模塊就分析到這來了,大家如果有疑問或者不清楚的地方,可以給我留言,O(∩_∩)O~

 

待續。。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章