淺談uCOS-II的任務(上)

williamgavin發表於2018-11-19

前言

大概花了四天時間將將uCOS的任務看了一下,因為之前學過一點作業系統的知識,所以看得不是特別費勁,下面具體來談談uCOS-II的任務是什麼樣的吧

什麼是任務

在實際生活中,處理一個大問題的時候通常會將其分解為若干個小問題,當這些小問題解決了,大問題自然而然就解決了,這是典型的“分而治之”的思想。在程式設計方面,如果要做一個比較複雜龐大的工程,通常會將這個工程劃分為若干個函式,每個函式都可以執行特定的功能,通過設計這些小函式來達到完成這個工程的目的。

這裡的“小問題”、“小函式”在uCOS–II中對應的程式實體就叫做:**任務。**uCOS-II是一個作業系統,作業系統是對各種硬體進行管理的,其中CPU和記憶體的管理是作業系統的核心內容。對CPU的管理說到底就是對任務的管理。任務管理好了,CPU自然也就管理好了。

對CPU管理一個很重要的概念就是:**併發。**同樣,在uCOS-II也存在這方面的內容。併發的實質就是儘可能讓cpu“同時”執行多個任務,具體的可以參見一下我前面的部落格。在嵌入式方面,cpu通常只有一個,因此任務的併發在微觀上看還是“序列”執行的;但是在某段時間看來,這些任務是同時執行的。

從程式碼的層面看,uCOS-II的任務就是一個個c語言函式。uCOS-II要做的就是管理好這些函式的執行,那麼如何管理呢?從作業系統中的學習中知道,cpu是通過一個叫程式控制塊(即TCB)的東西來管理各個程式的,同樣uCOS-II的任務也存在這樣的一個東西,叫做任務控制塊。任務控制塊存在的意義就是讓作業系統更方便的管理任務。

任務的管理

uCOS-II中的任務分為兩種:系統任務和使用者任務;系統任務是指由系統提供併為系統管理服務的任務;我們一般可以不用管它。使用者任務是指為了解決應用問題由使用者編寫的任務。uCOS-II最多可以有64個任務(系統任務和使用者任務一起)。

前面說過任務控制塊就是為了方便使用者管理而設計的,那麼它裡面包含什麼東西呢?最基本的肯定要有一個指向任務的具體程式碼的指標,這是肯定的;除此之外還有什麼?為了提供cpu的使用效率需要cpu併發執行多個任務,也就是“同時”執行多個任務,既然要做到併發,那麼肯定有任務的切換,任務的切換就涉及到保留現場(當前程式執行的位置、各個暫存器的值等等),那麼每個任務就都要有一個私有堆疊,即任務堆疊。任務堆疊也是通過指標指向的。除此之外,任務控制塊還有一些標誌位和優先順序等等。

前面說了uCOS最多存在64個任務,任務控制塊的數量也不少,因此任務控制塊也需要進行管理;在uCOS-II的使用的辦法就是用連結串列將所有的任務控制塊連結了起來。這個連結串列也叫任務登錄檔,把一個任務控制塊加進來也叫做任務的註冊。

小結一下,為了方便任務的管理提出了任務控制塊的概念,任務控制塊裡面有:指向任務程式碼的指標、指向任務堆疊的指標、任務優先順序和一些標誌位等等。使用連結串列將所有任務控制塊連結在一起,形成任務登錄檔。

在作業系統裡面存在程式和執行緒的概念,一個很重要的區別就是有無獨立的記憶體執行空間,也就是記憶體對映是不是一樣的。在uCOS-II中,多個執行程式實體共同使用同一套MMU,因此uCOS-II裡面的任務不能叫程式,只能叫執行緒。

任務的狀態

uCOS-II是按照一個cpu來進行設計的,同一時刻真正只有一個任務在執行狀態,其他任務只能處於其他狀態;在uCOS-II中,一共存在五種狀態,如下:

  • 睡眠狀態:
    任務只是以程式碼的形式駐留在程式空間(ROM或RAM),還沒有交給作業系統管理時的情況叫做睡眠狀態。簡單地說,任務在沒有被配備任務控制塊或被剝奪了任務控制塊時的狀態叫做任務的睡眠狀態。

  • 就緒狀態:
    如果系統為任務配備了務控制塊且在任務就緒表中進行了就緒登記,則任務就具備執行的充分條件,這時任務的狀態叫做就緒狀態。

  • 執行狀態:
    處於就緒狀態的任務如果經排程器判斷獲得了CPU的使用權,則任務就進人執行狀態,任何時刻只能有一個任務處於執行狀態,就緒的任務只有當所有優光級高於本任務的任務都轉為等待狀態時,才能進入執行狀態。

  • 等待狀態:
    正在執行的任務,需要等待一段時間或需要等待一個事件發生再執行時,該任務就會把CPU的使用權讓給其他任務而使任務進入等待狀態。

  • 中斷服務狀態:
    一個正在執行的任務一旦響應中斷申請就會中止執行而去執行中斷服務程式,這時任務的狀態叫做中斷服務狀態。

系統任務

前面說過,系統任務是作業系統所需要的。uCOS-II預定義了兩個系統任務:空閒任務和統計任務。

空閒任務是每個應用程式必須要有的,計算機硬體是不能停下來的(除非掉電或者時間脈衝丟失),空閒任務的意義就是讓cpu沒事的時候有事幹。因為是沒事的時候做的,所以有事的時候肯定不能幹,因此它的優先順序肯定要最低。

void OSTaskIdle(void * pdata)
{
#if OS_CRITICAL_METHOD == 3
	OS_CPU_SR cpu_sr;
# endif
	pdata = pdata;		// 使用一下引數,防止編譯器報錯
	
	for (;;)
	{
		OS_ENTER_CRITICAL();		// 關中斷,進臨界區
		OSdleCtr++;
		OS_EXIT_CRITICAL();			// 開中斷,出臨界區
	}
}

臨界區指的是不能被中斷的區域。從上面的程式碼也能看到,OSTaskIdle僅僅是計數器加一,幾乎什麼事都沒幹。

統計任務不是每個應用程式所必須的。函式名是OSTaskStat()。這個任務每秒執行計算一次cpu在單位時間內被使用的時間,並把計算結果以百分比的形式存放在變數OSCPUsage中,以便其他應用程式來了解cpu的使用情況。

任務的優先權以及優先順序別

如何從就緒佇列裡面找到一個任務執行,這就是排程。在uCOS-II裡面給每個任務都設定了優先順序,每次都是拿優先順序高的任務執行就行了。uCOS-II裡面優先順序預設為64級,即0~63。數字越小,優先順序越高。

由於大多數程式的任務數都小於64,所以使用者可以通過OS_CFG.H裡面的OS_LOWEST_PRIO變數來設定最低優先順序,如果為其賦值了,那麼相應的優先順序的級別就為:0~OS_LOWEST_PRIO,任務數也不能超過OS_LOWEST_PRIO+1個。

一般情況下,最低優先順序OS_LOWEST_PRIO自動賦給空閒任務,OS_LOWEST_PRIO自動賦給統計任務。

任務的優先順序需要在定義時顯示定義。

任務堆疊

堆疊就是儲存器中按照“後進先出”的原則組織的連續儲存空間。uCOS-II每個任務控制塊都有一個指向該任務堆疊的指標。

任務堆疊的建立

在檔案OS_CPU.H中專門定義了一個資料型別OS_STK:

typedef unsigned int OS_STK;		// 16位

這樣定義堆疊的時候只要定義一個這樣的陣列就行了。如:

# define TASK_STK_SIZE 512		// 1024個位元組
OS_STK TaskStk[TASK_STK_SIZE];	// 定義一個陣列來作為任務堆疊

可以使用OSTaskCreate()來建立一個任務,

INT8U OSTaskCreate(
	void (*) task(void * pd),		// 指向任務的指標
	void * pdata,					// 傳遞給任務的引數
	OS_STK * pots,					// 指定任務堆疊棧頂指標
	INT8U prio						// 指定任務優先順序別的引數
)

這裡需要注意的是:不同的處理器可能堆疊的增長方向不一樣,有的向下增長,有的向上增長,因此使用OSTaskCreate()建立任務的時候要注意第三個引數是陣列的首地址還是陣列的最後一個元素的地址。

通常為了提高程式的可移植性,會將兩種形式都寫出來,然後根據處理器的型別來選擇

void main(void)
{
# if OS_STK_GROWTH == 1
	OSTaskCreate(App1Task, (void *)0, &App1Task_Stk[App1Task_StkSize-1], 20);
# else
	OSTaskCreate(App1Task, (void *)0, 
	&App1Task_Stk[0], 20);
# endif
	// 其他程式碼
}

任務堆疊的初始化

應用程式建立一個新任務時,需要將這個任務需要的資源都存放在堆疊裡面,比如PC指標,暫存器的值等等,以便在這個任務得到cpu的使用權之後可以正確地執行。把任務初始化資料放到任務堆疊裡面地工作過程就叫做任務堆疊的初始化。為了完成這個任務,uCOS-II提供了任務堆疊初始化函式OSTaskStkInit()。

OS_STK *OSTaskStkInit 
(
	void (*task)(void *pd), 
	void *pdata, 
	OS_STK *ptos, 
	INT16U opt
)

通常使用者不會直接接觸到這個函式,而是通過OSTaskCreate()來呼叫。另外這個函式在uCOS移植的時候需要重新寫,因為處理器不一樣。

任務控制塊及其連結串列

前面提到過為了方便對任務進行管理,提出了任務控制塊。uCOS-II是使用兩條連結串列把系統所有的任務控制塊連結起來,並通過它們管理各個任務。

任務控制塊的定義如下:

typedef struct os_tcb {
    OS_STK          *OSTCBStkPtr;           /* Pointer to current top of stack                         */

#if OS_TASK_CREATE_EXT_EN > 0u
    void            *OSTCBExtPtr;           /* Pointer to user definable data for TCB extension        */
    OS_STK          *OSTCBStkBottom;        /* Pointer to bottom of stack                              */
    INT32U           OSTCBStkSize;          /* Size of task stack (in number of stack elements)        */
    INT16U           OSTCBOpt;              /* Task options as passed by OSTaskCreateExt()             */
    INT16U           OSTCBId;               /* Task ID (0..65535)                                      */
#endif

    struct os_tcb   *OSTCBNext;             /* Pointer to next     TCB in the TCB list                 */
    struct os_tcb   *OSTCBPrev;             /* Pointer to previous TCB in the TCB list                 */

#if (OS_EVENT_EN)
    OS_EVENT        *OSTCBEventPtr;         /* Pointer to          event control block                 */
#endif

#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
    OS_EVENT       **OSTCBEventMultiPtr;    /* Pointer to multiple event control blocks                */
#endif

#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
    void            *OSTCBMsg;              /* Message received from OSMboxPost() or OSQPost()         */
#endif

#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
#if OS_TASK_DEL_EN > 0u
    OS_FLAG_NODE    *OSTCBFlagNode;         /* Pointer to event flag node                              */
#endif
    OS_FLAGS         OSTCBFlagsRdy;         /* Event flags that made task ready to run                 */
#endif

    INT32U           OSTCBDly;              /* Nbr ticks to delay task or, timeout waiting for event   */
    INT8U            OSTCBStat;             /* Task      status                                        */
    INT8U            OSTCBStatPend;         /* Task PEND status                                        */
    INT8U            OSTCBPrio;             /* Task priority (0 == highest)                            */

    INT8U            OSTCBX;                /* Bit position in group  corresponding to task priority   */
    INT8U            OSTCBY;                /* Index into ready table corresponding to task priority   */
    OS_PRIO          OSTCBBitX;             /* Bit mask to access bit position in ready table          */
    OS_PRIO          OSTCBBitY;             /* Bit mask to access bit position in ready group          */

#if OS_TASK_DEL_EN > 0u
    INT8U            OSTCBDelReq;           /* Indicates whether a task needs to delete itself         */
#endif

#if OS_TASK_PROFILE_EN > 0u
    INT32U           OSTCBCtxSwCtr;         /* Number of time the task was switched in                 */
    INT32U           OSTCBCyclesTot;        /* Total number of clock cycles the task has been running  */
    INT32U           OSTCBCyclesStart;      /* Snapshot of cycle counter at start of task resumption   */
    OS_STK          *OSTCBStkBase;          /* Pointer to the beginning of the task stack              */
    INT32U           OSTCBStkUsed;          /* Number of bytes used from the stack                     */
#endif

#if OS_TASK_NAME_EN > 0u
    INT8U           *OSTCBTaskName;
#endif

#if OS_TASK_REG_TBL_SIZE > 0u
    INT32U           OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];
#endif
} OS_TCB;

其中OSTCBStat用來存放當前任務狀態,可取的值為:

OS_STAT_RDY : 就緒

OS_STAT_SEM : 等待訊號量狀態

OS_STAT_MBOX : 等待訊息郵箱狀態

OS_STAT_Q : 等待訊息佇列狀態

OS_STAT_SUSPEND : 被掛起狀態

OS_STAT_MUTEX : 等待互斥型訊號量狀態

先不理解沒關係的,大概知道有這麼回事就行。

任務控制塊連結串列

uCOS-II中一共存在兩個關於任務控制塊的連結串列。一個連結串列上全是空閒的任務塊(以下簡稱空任務控制塊連結串列),即沒有賦值的;另一條是當前程式已經使用了的任務控制塊形成的(以下簡稱任務控制塊連結串列)。當有新任務時,從空任務控制塊連結串列中取下來一個,根據當前任務資訊初始化,然後加到任務控制塊連結串列上。由於加快任務控制塊的訪問速度,專門用一個陣列OSPrioTbl[]來儲存任務控制塊連結串列每個元素的地址,並且陣列元素是根據任務的優先順序來存放指向對應任務的任務控制塊指標的,這樣就能快速找到某個任務的任務控制塊。

下面稍微具體說一下與此相關的東西。

首先:系統在呼叫OSInit()對uCOS-II系統進行初始化時,就先在RAM中建立一個OS_TCB結構型別的陣列OSTCBTbl[],然後連結成一個連結串列,這就是空任務塊連結串列。這個空任務塊連結串列一共是:OS_MAX_TASK+OS_N_SYS_TASKS個,其中OS_MAX_TASK是定義在OS_CFG.H中的常數,指明瞭使用者任務最大數目。而OS_N_SYS_TASKS是定義在UCOS_II.H中的常數,指明瞭系統任務數目。之後每當系統呼叫OSTaskCreate()或者OSTaskCreateExt()(另外一個建立使用者任務的函式)時,系統就會將空任務控制塊連結串列頭指標OSTCBFreeList指向的任務控制塊分配給該任務。在給任務控制塊中的各成員賦值之後,系統就按任務控制塊連結串列的頭指標OSTCBList將其加入到任務控制塊連結串列中。

任務控制塊連結串列為雙向連結串列。為了方便,把處於執行狀態的任務所屬的任務控制塊叫做當前任務控制塊,由OSTCBCur指向。

uCOS-II允許使用者使用OSTaskDel()刪除一個任務。刪除任務實際上就是把該任務的任務控制塊從任務控制塊連結串列中拿走,並歸還給空任務控制塊連結串列。就相當於吊銷了身份;任務控制塊對於任務的重要性就像身份證對於人一樣。

關於使用OSTaskDel()刪除任務後面還會說。

任務控制塊的初始化

當使用者程式呼叫OSTaskCreate()建立任務時,會呼叫系統函式OSTCBInit()為任務控制塊進行初始化。

INT8U OSTCBInit()
{
	INT8U prio,					// 任務優先順序,儲存在OSTCBPrio中
	OS_STK * ptos,				// 任務堆疊棧頂指標,儲存在OSTCBStkPtr中
	OS_STK * pbots,				// 任務堆疊棧底指標,儲存在OSTCBStkButtom中
	INT16U id,					// 任務識別符號,儲存在OSTCBId中
	INT16U stk_size,			// 任務堆疊長度,儲存在OSTCBStkSize中
	void * pext,				// 任務控制塊擴充套件指標,儲存在OSTCB中
	INT16U opt					// 任務控制塊選擇項,儲存在OSTCBOpt中
}

參考資料

嵌入式實時作業系統uC/OS-II原理及應用
uCOS-II系統中的任務
uCOS-II OSTaskCreate函式分析

相關文章