手把手,嘴對嘴,講解UCOSII嵌入式操作系統的任務調度策略(五)

繼續......

整個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系統的任務調度的原理,那麼自己手動實現一個簡易的操作系統內核,似乎也並不是一件觸不可及的事情。

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