整個UCOSII嵌入式操作系統的任務調度策略便是如此,現在進行一個總結:
①某個任務在執行中,每隔一定周期發生滴答時鍾中斷,在中斷中遍歷整個任務鏈表,更新每個任務的延時時間,修改就緒狀態。
②任務執行完畢后,進入延時函數,在延時函數中會把當前任務掛起(清空當前任務的就緒狀態,使其進入未就緒狀態),然后根據查表發找到在就緒任務中,優先級最高的那一個任務。
③找到新任務以后,人工強制發生一個中斷,保存上個任務的堆棧信息,彈出下個任務的堆棧信息,同時更改PC指針,進行任務切換。
經過以上三個步驟,便可以完成任務的調度。
現在回到第一篇提出的那個問題:UCOSII到底是如何保證它的實時性的呢?
如果任務的調度都是發生在當前任務進入延時之后,似乎操作系統根本無法自身的保障實時性。
比如一個優先級最低的任務由於某些處理非常耗費時間,它一直無法進入延時,導致無法進入任務切換,那么優先級高的任務反而是一只都無法被執行了……
同樣在第一篇說過,UCOSII系統除了在當前任務進入延時函數會發生調度之外,還有別的時機會進行任務切換:
1.當前任務進入了延時。
2.當前任務被掛起。
3.當前任務執行時,發生了某些中斷。
第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系統的任務調度的原理,那么自己手動實現一個簡易的操作系統內核,似乎也並不是一件觸不可及的事情。
如果有興趣,可以去這里看看(https://www.zhihu.com/question/25628124)里面有很多大神都實現了自己的操作系統。