繼續......
整個UCOSII嵌入式操作系統的任務調度策略便是如此,現在進行一個總結:
- 某個任務在執行中,每隔一定週期發生滴答時鐘中斷,在中斷中遍歷整個任務鏈表,更新每個任務的延時時間,修改就緒狀態。
- 任務執行完畢後,進入延時函數,在延時函數中會把當前任務掛起(清空當前任務的就緒狀態,使其進入未就緒狀態),然後根據查表發找到在就緒任務中,優先級最高的那一個任務。
- 找到新任務以後,人工強制發生一箇中斷,保存上個任務的堆棧信息,彈出下個任務的堆棧信息,同時更改PC指針,進行任務切換。
經過以上三個步驟,便可以完成任務的調度。
現在回到第一篇提出的那個問題:UCOSII到底是如何保證它的實時性的呢?
如果任務的調度都是發生在當前任務進入延時之後,似乎操作系統根本無法自身的保障實時性。
比如一個優先級最低的任務由於某些處理非常耗費時間,它一直無法進入延時,導致無法進入任務切換,那麼優先級高的任務反而是一隻都無法被執行了……
同樣在第一篇說過,UCOSII系統除了在當前任務進入延時函數會發生調度之外,還有別的時機會進行任務切換:
- 當前任務進入了延時;
- 當前任務被掛起;
- 當前任務執行時,發生了某些中斷;
第1點我們已經全部講完,第2點非常好理解,我們現在看一個函數:OSTaskSuspend()
這個函數的作用是把某個任務掛起(也就是不進行調度),現在來分析一個實例:
有一個任務調用了這個函數:
void App1_task(void *pdata)
{
while(1)
{
if (OS_ERR_NONE != OSTaskSuspend(OS_PRIO_SELF))
{
Dbg_SendStr("App1_task Suspend Error£¡\r\n");
}
delay_ms(10);
};
}
當前任務執行了紅色代碼之後,便會把自身掛起來,如果沒有再別的地方對它進行激活,這個任務便永遠也不會執行下去了。
深入分析OSTaskSuspend函數:
INT8U OSTaskSuspend (INT8U prio)
{
BOOLEAN self;
OS_TCB *ptcb;
INT8U y;
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
#if OS_ARG_CHK_EN > 0u
if (prio == OS_TASK_IDLE_PRIO) { /* Not allowed to suspend idle task */
return (OS_ERR_TASK_SUSPEND_IDLE);
}
if (prio >= OS_LOWEST_PRIO) { /* Task priority valid ? */
if (prio != OS_PRIO_SELF) {
return (OS_ERR_PRIO_INVALID);
}
}
#endif
OS_ENTER_CRITICAL();
if (prio == OS_PRIO_SELF) { /* See if suspend SELF */
prio = OSTCBCur->OSTCBPrio;
self = OS_TRUE;
} else if (prio == OSTCBCur->OSTCBPrio) { /* See if suspending self */
self = OS_TRUE;
} else {
self = OS_FALSE; /* No suspending another task */
}
ptcb = OSTCBPrioTbl[prio];
if (ptcb == (OS_TCB *)0) { /* Task to suspend must exist */
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_SUSPEND_PRIO);
}
if (ptcb == OS_TCB_RESERVED) { /* See if assigned to Mutex */
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_NOT_EXIST);
}
y = ptcb->OSTCBY;
OSRdyTbl[y] &= (OS_PRIO)~ptcb->OSTCBBitX; /* Make task not ready */
if (OSRdyTbl[y] == 0u) {
OSRdyGrp &= (OS_PRIO)~ptcb->OSTCBBitY;
}
ptcb->OSTCBStat |= OS_STAT_SUSPEND; /* Status of task is 'SUSPENDED' */
OS_EXIT_CRITICAL();
if (self == OS_TRUE) { /* Context switch only if SELF */
OS_Sched(); /* Find new highest priority task */
}
return (OS_ERR_NONE);
}
直接從紅色代碼部分開始看,他首先判斷一下我要掛起的任務是不是自己,現在我們傳的參數就是OS_PRIO_SELF,所有它應該執行第一個if判斷。
在這個if判斷中保存了一下需要掛起的任務的優先級,然後用藍色代碼判斷一下需要掛起的任務是否存在(由於我們掛起的是自身,自身肯定是存在的,但是這並不表示這個判斷多餘,因爲如果是一個優先級爲1的任務調用這個函數去掛起一個優先級爲2的任務,那判斷一下還是很必要的)。
然後接下來的幾句代碼就不用再解釋了,和任務進入延時函數把自己的就緒狀態情況是一毛一樣的處理。
直接看ptcb->OSTCBStat |= OS_STAT_SUSPEND這句代碼,變量OSTCBStat 很容易理解,它表示當前任務的狀態,整句代碼的意義就是給當前任務設定一個已經被人工掛起了的狀態,免得在任務調度的時候被調度出來(在滴答時鐘中斷中有這個變量的判斷)。
這句代碼以後:
if (self == OS_TRUE) { /* Context switch only if SELF */
OS_Sched(); /* Find new highest priority task */
}
這幾句代碼也已經很熟悉了,中間那個函數就是任務切換,先看看那個判斷,如果我要掛起的是當前任務,那麼就立即進行切換,如果掛起的是別的任務,那就不用切換,這個理解起來應該不難。
在理解的第一種切換時機的前提下,第二種任務切換的時機很好理解,但是第二種任務切換的時機仍然不能保證任務執行的實時性,如果低優先級的任務既不進入延時,也不掛起,高優先級的任務依然無法執行。
現在來看第三種,當中斷髮生時,任務切換……
在任務執行期間,發生頻繁的中斷必然就是滴答時鐘中斷,現在重新回到以前看過的那個中斷服務函數:
void SysTick_Handler(void)
{
if(delay_osrunning==1) //OS開始跑了,才執行正常的調度處理
{
OSIntEnter(); //進入中斷
OSTimeTick(); //調用ucos的時鐘服務程序
OSIntExit(); //觸發任務切換軟中斷
}
}
這一次的重點不再是第二個函數,而是第一個和第三個函數:OSIntEnter,OSIntExit。
這兩個函數是成對出現,從函數名便可看出,OSIntEnter是進入中斷時候調用,OSIntExit是離開中斷時候調用。
由於滴答時鐘是週期性調用,因此這兩個函數也是週期性被調用。
OSIntEnter的定義如下:
void OSIntEnter (void)
{
if (OSRunning == OS_TRUE) {
if (OSIntNesting < 255u) {
OSIntNesting++; /* Increment ISR nesting level */
}
}
}
入口函數的定義很簡單,就是對變量OSIntNesting執行加處理,表示我現在正在執行中斷函數,如果發生了中斷,或者有中斷嵌套,那麼這個變量肯定是大於1的,在系統的很多地方,都需要判斷這個變量,因爲很多地方都不能在中斷中執行。
出口函數的定義就有些複雜了:
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
if (OSRunning == OS_TRUE) {
OS_ENTER_CRITICAL();
if (OSIntNesting > 0u) { /* Prevent OSIntNesting from wrapping */
OSIntNesting--;
}
if (OSIntNesting == 0u) { /* Reschedule only if all ISRs complete ... */
if (OSLockNesting == 0u) { /* ... and not locked. */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Keep track of the number of ctx switches */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
}
OS_EXIT_CRITICAL();
}
}
直接從紅色部分開始看,首先判斷系統是否在運行,在系統運行的前提下,對變量OSIntNesting進行減處理。
當進入中斷以後,調用入口函數,對變量OSIntNesting加1,中斷內容處理完以後,對變量OSIntNesting減1,當變量OSIntNesting爲0的時候,表示沒有進行中斷處理,這個時候纔可以進行任務切換。
if (OSLockNesting == 0u) { /* ... and not locked. */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Keep track of the number of ctx switches */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
然後判斷一下系統是否上鎖,如果上鎖了,任然不能進行調度。
當一切條件就緒以後,調用函數OS_SchedNew,這個函數也已經熟悉了,作用就是尋找在就緒任務中,優先級最高的那一個。
把優先級最高的任務保存在OSPrioHighRdy中,如果當前任務不等於優先級最高的任務,那麼就調用系統函數OSIntCtxSw進行任務切換……
看到這裏,應該能夠回答那個問題了:如何保證系統的實時性?
void SysTick_Handler(void)
{
if(delay_osrunning==1) //OS開始跑了,才執行正常的調度處理
{
OSIntEnter(); //進入中斷
OSTimeTick(); //調用ucos的時鐘服務程序
OSIntExit(); //觸發任務切換軟中斷
}
}
在中斷服務函數中,第二個函數負責更新任務就緒表,第三個任務負責切換任務,因爲滴答中斷是週期性發生的,所以任務切換也是週期性發生的。
當有一個優先級低的任務執行時,如果有優先級更高的任務就緒了,那麼只要發生了一次滴答中斷,任務就能被立即切換過去,延時只有一個滴答時鐘的時間,如果定義的時鐘週期是1ms,那麼低優先級的任務最多也就能運行1ms,然後便會強行剝奪CPU的執行權限,轉交給高優先級的任務。
由於存在這種機制,因此便能保證UCOSII系統任務的實時性。
總結
個人認爲,對於一個嵌入式操作系統而言,最核心和最重要的,便是任務調度的機制與策略,只要實現了這個功能,那麼一個嵌入式操作系統的架構也就搭建了起來,至於其他的消息,郵箱,隊列等功能,都是在這個架構上實現增值產品。
只要深入理解了UCOSII系統的任務調度的原理,那麼自己手動實現一個簡易的操作系統內核,似乎也並不是一件觸不可及的事情。