1. LiteOS內核的任務管理
Huawei LiteOS 內核提供任務的創建、刪除、延遲、掛起、恢復等功能,以及鎖定和解鎖任務調度,支持任務按優先級高低的搶占調度及同優先級時間片輪轉調度。
1.1. 任務
在 LiteOS 中,一個任務就是一個線程,多個任務按照優先級進行搶占式調度,達到多個任務“同時”運行的目的。
1.2. 任務的狀態
Huawei LiteOS 系統中的每個任務都有多種運行狀態。當系統初始化完成並啟動調度器后,系統中所有創建的任務就由內核進行調度,在不同運行狀態之間切換,同時在系統中競爭一定的資源。
任務的狀態有以下四種:
- 就緒(Ready):該任務在就緒列表中,只等待 CPU;
- 運行(Running):該任務正在執行;
- 阻塞(Blocked):該任務不在就緒列表中。包含任務被掛起、任務被延時、任務正在等待信號量、讀寫隊列或者等待讀寫事件等;
- 退出態(Dead):該任務運行結束,等待系統回收資源。
1.3. 任務ID
任務 ID 在任務創建時通過參數返回給用戶,作為任務的一個非常重要的標識。
用戶可以通過任務ID對指定任務進行任務掛起、任務恢復、查詢任務名等操作。
1.4. 任務優先級
優先級表示任務執行的優先順序。任務的優先級決定了在發生任務切換時即將要執行的任務,在就緒列表中的最高優先級的任務將得到執行。
Huawei LiteOS 的任務一共有 32 個優先級 (0-31),最高優先級為 0,最低優先級為 31。
因為是LiteOS的內核是搶占式調度內核,所以:
高優先級的任務可打斷低優先級任務,低優先級任務必須在高優先級任務阻塞或結束后才能得到調度。
1.5. 任務入口函數
任務入口函數是每個新任務得到調度后將執行的函數,該函數由用戶實現,在任務創建時,通過任務創建結構體指定。
1.6. 多任務運作背后的機制
在多任務操作系統的內核中,為了方便對每個任務進行管理,每一個任務都有一個任務控制塊(TCB),其中包含了任務上下文棧指針(stack pointer)、任務狀態、任務優先級、任務ID、任務名、任務棧大小等信息,TCB 相當於每個任務在內核中的身份證,可以反映出每個任務運行情況。
那么,操作系統中這么多的任務,它們依靠TCB被系統統一管理,那么又是如何被系統執行的呢?
其實,每個任務相當於一個裸機程序,每個任務之間相互獨立,這個“獨立”指的是每個任務都有自己的運行環境 —— 棧空間,稱為任務棧,棧空間里保存的信息包含局部變量、寄存器、函數參數、函數返回地址等。
可是,系統中只有一個CPU,即使每個任務的任務棧是獨立的,可是多個任務都需要被同一個CPU所執行,CPU的資源是共用的吧。
對的,CPU的資源是多個任務共用的,這些CPU的寄存器只有在任務執行的時候被使用,稱為任務上下文,因此,內核在任務切換時會將切出任務的上下文信息保存在自身的任務棧空間里面,以便任務恢復時還原現場,從而在任務恢復后在切出點繼續開始執行。
用戶創建任務時,系統會先申請任務控制塊需要的內存空間,申請成功后,系統會將任務棧進行初始化,預置上下文。此外,系統還會將“任務入口函數”地址放在相應位置。這樣在任務第一次啟動進入運行態時,將會執行“任務入口函數”。
2. 任務管理API
Huawei LiteOS 任務管理模塊提供任務創建、任務刪除、任務延時、任務掛起和任務恢復、更改任務優先級、鎖任務調度和解鎖任務調度、根據任務控制塊查詢任務 ID、根據 ID 查詢任務控制塊信息功能。
Huawei LiteOS 任務管理提供的 API 都是以 LOS 開頭,但是這些 API 使用起來比較復雜,所以本文中我們使用 Huawei IoT Link SDK 提供的統一API接口進行實驗,這些接口底層已經使用 LiteOS 提供的API實現,對用戶而言更為簡潔,API列表如下:
osal的api接口聲明在<osal.h>中,使用相關的接口需要包含該頭文件,關於函數的詳細參數請參考該頭文件的聲明。
任務相關的接口定義在osal.c中,基於LiteOS的接口實現在 liteos_imp.c文件中:
| 接口名 | 功能描述 |
|---|---|
| osal_task_create | 創建任務 |
| osal_task_kill | 刪除任務(非自身) |
| osal_task_exit | 任務退出 |
| osal_task_sleep | 任務休眠 |
2.1. osal_task_create
osal_task_create的接口用於創建一個任務,其接口原型如下:
void* osal_task_create(const char *name,int (*task_entry)(void *args),\
void *args,int stack_size,void *stack,int prior)
{
void *ret = NULL;
if((NULL != s_os_cb) &&(NULL != s_os_cb->ops) &&(NULL != s_os_cb->ops->task_create))
{
ret = s_os_cb->ops->task_create(name, task_entry,args,stack_size,stack,prior);
}
return ret;
}
該接口的參數說明如下表:
| 參數名稱 | 參數說明 |
|---|---|
| name | 任務名稱 |
| tsak_entry | 任務入口函數的函數指針 |
| args | 任務入口函數的參數列表 |
| stack_size | 任務棧大小 |
| stack | 任務棧地址 |
| prior | 任務優先級 |
| 返回值 | 任務ID |
2.2. osal_task_kill
osal_task_kill用於刪除某個其他任務(非自身),其接口原型如下:
int osal_task_kill(void *task)
{
int ret = -1;
if((NULL != s_os_cb) &&(NULL != s_os_cb->ops) &&(NULL != s_os_cb->ops->task_kill))
{
ret = s_os_cb->ops->task_kill(task);
}
return ret;
}
該接口的參數說明如下表:
| 參數名稱 | 參數說明 |
|---|---|
| task | 任務ID |
| 返回值 | 0-刪除成功 |
| 返回值 | -1-刪除失敗 |
2.3. osal_task_exit
osal_task_exit接口用於任務退出(自身),目前暫未支持,可以直接return退出。
2.4. osal_task_sleep
osal_task_sleep接口用於任務主動休眠,單位是ms,其接口原型如下:
void osal_task_sleep(int ms)
{
if((NULL != s_os_cb) &&(NULL != s_os_cb->ops) &&(NULL != s_os_cb->ops->task_sleep))
{
s_os_cb->ops->task_sleep(ms);
}
return ;
}
3. 動手實驗 —— 體驗任務的創建與切換
實驗內容
本實驗中將創建兩個任務,一個低優先級任務task1,一個高優先級任務task2,兩個任務都會每隔2s在串口打印自己的任務id號,在串口終端中觀察兩個任務的運行情況。
實驗代碼
首先打開之前創建的 HelloWorld 工程,基於此工程進行實驗。
在Demo文件夾右擊,選擇新建文件夾:

新建osal_kernel_demo文件夾,用於存放內核的實驗文件:

接下來在此osal_kernel_demo文件夾中新建第一個實驗文件osal_task_demo.c文件,開始編寫代碼:

/* 使用osal接口需要包含該頭文件 */
#include <osal.h>
/* 任務優先級宏定義(shell任務的優先級為10) */
#define USER_TASK1_PRI 12 //低優先級
#define USER_TASK2_PRI 11 //高優先級
/* 任務ID */
uint32_t user_task1_id = 0;
uint32_t user_task2_id = 0;
/* 任務task1入口函數 */
static int user_task1_entry()
{
int n = 0;
/* 每隔2s在串口打印一次,打印5次后主動結束 */
for(n = 0; n < 5; n++)
{
printf("task1: my task id is %ld, n = %d!\r\n", user_task1_id, n);
/* 任務主動掛起2s */
osal_task_sleep(2*1000);
}
printf("user task 1 exit!\r\n");
/* 任務結束 */
return 0;
}
/* 任務task2入口函數 */
static int user_task2_entry()
{
/* 每隔2s在串口打印一次,不結束 */
while (1)
{
printf("task 2: my task id is %ld!\r\n", user_task2_id);
/* 任務主動掛起2s */
osal_task_sleep(2*1000);
}
}
/* 標准demo啟動函數,函數名不要修改,否則會影響下一步實驗 */
int standard_app_demo_main()
{
/* 創建任務task1 */
user_task1_id = osal_task_create("user_task1",user_task1_entry,NULL,0x400,NULL,USER_TASK1_PRI);
/* 創建任務task2 */
user_task2_id = osal_task_create("user_task2",user_task2_entry,NULL,0x400,NULL,USER_TASK2_PRI);
return 0;
}
編寫完成之后,要將我們編寫的osal_task_demo.c文件添加到makefile中,加入整個工程的編譯:
這里有個較為簡單的方法,直接修改Demo文件夾下的user_demo.mk配置文件,添加如下代碼:
#example for osal_task_demo
ifeq ($(CONFIG_USER_DEMO), "osal_task_demo")
user_demo_src = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/osal_kernel_demo/osal_task_demo.c}
user_demo_defs = -D CONFIG_OSAL_TASK_DEMO_ENABLE=1
endif
添加位置如圖:

這段代碼的意思是:
如果 CONFIG_USER_DEMO 宏定義的值是osal_task_demo,則將osal_task_demo.c文件加入到makefile中進行編譯。
那么,如何配置 CONFIG_USER_DEMO 宏定義呢?在工程根目錄下的.sdkconfig文件中的末尾即可配置:


因為我們修改了mk配置文件,所以點擊重新編譯按鈕
進行編譯,編譯完成后點擊下載按鈕燒錄程序。
實驗現象
程序燒錄之后,即可看到程序已經開始運行,在串口終端中可看到實驗的輸出內容:
linkmain:V1.2.1 AT 11:30:59 ON Nov 28 2019
WELCOME TO IOT_LINK SHELL
LiteOS:/>task 2: my task id is 5!
task1: my task id is 4, n = 0!
task 2: my task id is 5!
task1: my task id is 4, n = 1!
task 2: my task id is 5!
task1: my task id is 4, n = 2!
task 2: my task id is 5!
task1: my task id is 4, n = 3!
task 2: my task id is 5!
task1: my task id is 4, n = 4!
task 2: my task id is 5!
user task 1 exit!
task 2: my task id is 5!
……
可以看到,系統啟動后,首先打印版本號,串口shell的優先級為10,最先打印shell信息,接下來task1先創建,但是優先級較低,所以后創建的task2搶占執行,task2打印后主動掛起2s,這時task1開始執行,依次執行5次后task1結束,task2一直保持運行。
