1,調度器的主要工作
在多任務系統中,令CPU中止當前正在運行的任務轉而去運行另一個任務的工作叫做任務切換,而按某種規則進行任務切換的工作叫做任務調度。 這番話有火車站的味道,列車的調度
在ucos中,任務調度由任務調度器來完成。任務調度器的主要工作有兩項:1,從任務就緒表中查找具有最高優先級別的就緒任務2,實現任務的切換。ucos中有兩種調度器:一種是任務級的調度器,另一種是中斷級的調度器。任務級的調度器主要有OSSched()來實現。而中斷級的調度器由OSIntExt()來實現。
2獲得待運行就緒任務控制塊的指針
由於操作系統是通過任務的控制塊TCB來管理任務的,因此調度器真正實施任務切換之前的主要工作就是要獲得待運行任務的任務控制塊指針和當前任務的任務控制塊指針。
由於被中止任務的任務控制塊指針就存放在全局變量OSTCBCur中,所以調度器這部分的主要工作是要獲得待運行任務的任務控制塊指針
void OSSched(void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cup_sr;
#endif
INT8U y;
OS_ENTER_CRITICAL();
if((OSLockNesting | OSIntNesting) == 0)
{
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); //獲得最高優先級任務
if(OSPrioHighRdy != OSPrioCur)
{
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; //統計任務切換次數的計數器加1
OSCtxSwCtr++;
OS_TASK_SW();
}
}
OS_EXIT_CRITICAL();
}
ucos允許應用程序通過調用函數OSSchedLock()和OSSchedUnlock()給調度器上鎖和解鎖。爲了記錄調度器被鎖和解鎖的情況,ucos定義了一個變量OSLockNesting;調度器每被上鎖一次,變量OSLockNesting就加1;反之,調度器每被解鎖一次,變量OSLockNesting就減1.因此可以通過訪問變量OSLockNesting來了解調度器上鎖的嵌套次數。
調度器OSSched()再確認未被上鎖並且不是中斷服務程序調用調度器的情況下,首先從任務就緒表中查得的最高優先級別就緒任務的優先級別OSPrioHighRdy;然後在確認了這個就緒任務不是當前正在運行的任務(OSPrioCur是存放正在運行任務的優先級別的變量)的條件下,用OSPrioHighRdy作爲下標去訪問數組OSTCBPrioTbl[],把數組元素OSTCBPrioTbl[OSPrioHighRdy]的值(及待運行就緒任務的任務控制塊指針)賦給指針變量OSTCBHighRdy。於是下面就可以依據OSTCBHighRdy和OSTCBCur這兩個分別指向待運行任務控制塊和當前任務控制塊的指針在宏OS_TASK_SW()中實施切換
衆所周知,CPU是按CPU中的一個特殊功能寄存器--程序指針PC的指向來運行程序的。或者說,只有使PC寄存器獲得新任務的地址,纔會使CPU運行新的任務。既然如此,對於被中止任務,應該把任務的斷點指針(在PC寄存器中)壓入堆棧;而對於待運行任務而言,應該把任務堆棧中上次任務被中止時存放在堆棧中的中斷指針推入PC寄存器。但遺憾的是,目前的處理器一般沒有對程序指針寄存器PC的入棧和出棧指令。 所以要想辦法引發一次中斷(或者一次調用),並讓中斷向量指向OSCtxSw()(其實這個函數就是中斷服務程序),利用系統在跳轉到中斷服務程序時會自動把斷點壓入堆棧的功能,把斷點指針送入堆棧,而利用中斷返回指令,能把斷點指針推入CPU的PC寄存器的功能,恢復待運行任務的斷點,這樣就可以實現斷點的保存和恢復了
由於任務切換時需要對CPU寄存器進行操作,因此在一般情況下,中斷服務程序OSCtxSw()要用匯編來編寫
由什麼來引發中斷呢?
這就是宏OS_TASK_SW()的作用了。如果使用微處理器具有軟中斷指令的話,可以在宏中封裝一個軟中斷指令即可;如果使用的微處理器沒有提供軟中斷指令,那麼就可以嘗試宏OS_TASK_SW()封裝其他可以使PC等相關寄存器入棧的指令.
看了書,對uC/OS-II的任務調度又重新認識了,好書啊。
uC/OS-II有兩種任務調度器:任務級的調度器OSSched(),中斷級的調度器OSIntExt()。
OSSched()的任務調度部分
調度首先要做的就是找到當前最高優先級的任務並運行它,在uC/OS-II中,我們在任務就緒表中找到最高優先級任務標識(即它的優先級),進而獲得該任務的依據——任務控制塊。
因爲找到最高優先級別並不難,所以調度器OSSched()的算法也簡單。如下:
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y<<3) + OSUnMapTbl[OSRdyTbl[y]]);
通過上面兩行代碼將當前最高優先級的任務的優先級存放在OSPrioHighRdy變量中。然後通過此變量從存放任務控制塊指針的數組OSTCBPrioTbl[]中獲得該任務的任務控制塊指針,並存放在指針變量OSTCBHighRdy中。代碼如下:
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
只要獲得了最高就緒任務的任務控制塊指針,再加上存放在指針變量OSTCBCur中的當前運行任務的任務控制塊,就可以進行任務切換的工作了。
OSSched()代碼如下:
- void OS_Sched (void)
- {
- #if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
- OS_CPU_SR cpu_sr;
- #endif
- INT8U y;
- OS_ENTER_CRITICAL();
- if ((OSIntNesting == 0) && (OSLockNesting == 0)) { /* Sched. only if all ISRs done & not locked */
- y = OSUnMapTbl[OSRdyGrp]; /* Get pointer to HPT ready to run */
- OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
- if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
- OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
- OSCtxSwCtr++; /* Increment context switch counter */
- OS_TASK_SW(); /* Perform a context switch */
- }
- }
- OS_EXIT_CRITICAL();
- }
在上面的代碼中,調度禁區是用代碼
- if ((OSIntNesting == 0) && (OSLockNesting == 0))
在調度器禁區這個方面,uC/OS-II是明顯優於一般操作系統的。因爲一般操作系統是禁止在系統調用中進行調度的,而uC/OS-II沒有這個限制。所以uC/OS-II的調度禁區與其他操作系統相比就顯得更小,可剝奪型也就顯得更爲強硬。所以,uC/OS-II是真正的可剝奪型內核。
OSSched()的任務切換部分
調度器獲得了最高級就緒任務的任務控制塊指針後,任務切換的工作是由宏OSCtxSw()來執行的。
所謂任務切換,就是中止正在運行的任務,轉而去運行另外一個任務的工作。
任務斷點的保存
這裏很關鍵。作者寫的也很清楚明瞭。
如果把任務被中止運行的位置叫做斷點,而把當時處理器的PC、PSW等各寄存器中數據的集合叫做斷點數據,那麼當任務再次運行時,必須在斷點處以斷點數據作爲初始數據接着運行才能實現“無縫”的繼續運行。要實現這個目標,就必須在任務被中止時,把該任務斷點數據保存起來,重新運行時再恢復這些斷點數據。
斷點數據保存到何處呢?當然,誰的東西誰保存這是最好的。就是哪個任務的斷點數據則由哪個任務的堆棧來保存。這就是爲什麼每個任務都有一個私立的堆棧。
通常情況下,任務的斷點數據叫做任務的上下文。
需要注意的是,在保存斷點數據之後,還有把任務堆棧當前的指針(SP)保存在任務控制塊的成員變量OSTCBStkPtr中。
任務切換
任務的切換實質是斷點數據的切換,斷點數據的切換也就是處理器堆棧指針的切換,被中止運行任務的任務堆棧指針要保護到該任務的任務控制塊中,待運行任務的任務堆棧指針要由該任務控制塊轉存到處理器的SP中。保證完成上述任務的前提是要獲得被中止任務和待運行任務的任務控制塊,在此又一次看到了任務控制塊的重要性。
爲了完成任務切換,uC/OS-II定義了一個函數OSCtxSw(),它要完成下面7項工作:
- 把被中止任務的斷點指針保存到任務堆棧中;
- 把處理器通用寄存器的內容保存到任務堆棧中;
- 把被中止任務的任務堆棧指針當前值保存到該任務的任務控制塊的OSTCBStkPtr中;
- 獲得待運行任務的任務控制塊
- 使處理器通過任務控制塊獲得待運行任務的任務堆棧指針;
- 把待運行任務堆棧中通用寄存器的內容恢復到處理器的通用寄存器中;
- 使處理器獲得待運行任務的斷點指針(該指針是待運行任務在上一次被調度器中止運行時保留在任務堆棧中的)
由於uC/OS-II總是把當前正在運行任務的任務控制塊的指針存放在指針變量OSTCBCur中,並且在調度器的調度過程中已經得到了待運行任務的任務控制塊指針OSTCBHighRdy,所以完成第2~6項工作非常容易。示意性代碼如下:
- 用壓棧指令把處理器通用寄存器R1,/R2...壓入堆棧:
- OSTCBCur->OSTCBStkPtr = SP; //把SP保存在中止任務控制塊中
- OSTCBCur = OSTCBHighRdy; //使系統獲得待運行任務控制塊
- SP = OSTCBHighRdy->OSTCBStkPtr //把待運行任務堆棧指針賦予SP
- 用出棧指令把R1、R2...彈入處理器的通用寄存器;
處理第一項和第七項有些麻煩。因爲處理器是按一種特殊功能處理器——程序指針PC(也叫做程序計數器)的指向來運行程序的。或者說,只有使PC寄存器獲得新任務的地址,纔會使處理器運行新的任務。既然如此,對於被中止任務,應把任務的斷點指針(在PC寄存器中)壓入任務堆棧;而對於待運行任務,應把任務堆棧裏上次任務被中止時存放在堆棧中的中斷指針推入PC寄存器。但是目前處理器一般沒有對程序指針寄存器PC的出棧和入棧指令。所以不得不想其他辦法用其他可以改變PC的指令來變通一下。也就是想辦法引發一次中斷(或者一次調用),並讓中斷向量指向OSCtxSw()(這個函數就是中斷服務程序),利用系統在跳轉到中斷服務程序時會自動把斷點指針壓入堆棧的功能,把斷點指針存入堆棧,而利用中斷返回指令能把斷點指針推入處理器的PC寄存器的功能,恢復待運行任務的斷點,這樣就可以實現斷點的保存和恢復了。
由於任務切換時需要對處理器的寄存器進行操作,因此在一般情況下,中斷服務程序OSCtxSw()都要用彙編語言來編寫。適宜性代碼如下:
- void OSCtxSw(void)
- {
- 用壓棧指令把處理器通用寄存器R1,/R2...壓入堆棧:
- OSTCBCur->OSTCBStkPtr = SP; //把SP保存在中止任務控制塊中
- OSTCBCur = OSTCBHighRdy; //使系統獲得待運行任務控制塊
- OSPrioCur = OSPrioHighRdy;
- SP = OSTCBHighRdy->OSTCBStkPtr //把待運行任務堆棧指針賦予SP
- 用出棧指令把R1、R2...彈入處理器的通用寄存器;
- IRET;
- }
用什麼引發中斷呢?宏OS_TASK_SW()的作用就體現在這裏了。如果使用的微處理器具有軟中斷指令,則可以在這個宏中封裝一個軟中斷指令即可;如果使用的微處理器沒有提供軟中斷指令,則可以試試在宏OS_TASK_SW()中封裝其他可使PC等相關寄存器壓棧的指令。
調度的時機
對於uC/OS-II,只有就緒任務表的內容發生變化時才需要調度。在uC/OS-II中,就緒任務表發生變化的情況有以下幾種:
- 有新任務被創建,並在就緒任務表中進行了登記;
- 有任務被刪除;
- 有處於等待狀態的任務被喚醒;
- 由於異步事件的發生,在中斷服務程序中激活了一個或幾個任務;
- 正在運行的任務需要等待某個事件而進入等待狀態;
- 正在運行的任務調用延時函數而自願進入等待狀態。
綜上所述,uC/OS-II應在所有系統調用函數的末尾及中斷服務程序結束之前調用調度器OSSched()。