0、思考與回答 0.1、思考一 如何處理進入阻塞狀態的任務? 為了讓 RTOS 支持多優先順序,我們創建了多個就緒鏈表(數組形式),用每一個就緒鏈表表示一個優先順序,對於阻塞狀態的任務顯然要從就緒鏈表中移除,但是阻塞狀態的任務並不是永久阻塞了,等待一段時間後應該從阻塞狀態恢復,所以我們需要創建一個阻塞鏈 ...
0、思考與回答
0.1、思考一
如何處理進入阻塞狀態的任務?
為了讓 RTOS 支持多優先順序,我們創建了多個就緒鏈表(數組形式),用每一個就緒鏈表表示一個優先順序,對於阻塞狀態的任務顯然要從就緒鏈表中移除,但是阻塞狀態的任務並不是永久阻塞了,等待一段時間後應該從阻塞狀態恢復,所以我們需要創建一個阻塞鏈表用來存放進入阻塞狀態的任務
0.2、思考二
還有一個問題,xTicksToDelay
是一個 32 位的變數,如何處理其潛在的溢出問題?
假設使用一個 32 位的 xNextTaskUnblockTime
變數表示任務下次解除阻塞的時間,其一般應該由如下所示的程式代碼計算
// 任務下次解除阻塞的時間 = 當前滴答定時器計數值 + 要延時的滴答次數
xNextTaskUnblockTime = xConstTickCount + xTicksToWait;
可以看出 xNextTaskUnblockTime
變數隨著運行時間流逝存在溢出風險,因此我們需要再定義一個溢出阻塞鏈表用來存放所有下次解除阻塞的時間溢出的任務,這樣我們就擁有兩個阻塞鏈表,在滴答定時器中斷服務函數中如果一旦發現滴答定時器計數值全局變數溢出,就通過鏈表指針將這兩個鏈表交換,保證永遠處理的是正確的阻塞鏈表
1、阻塞鏈表
1.1、定義
/* task.c */
// 阻塞鏈表和其指針
static List_t xDelayed_Task_List1;
static List_t volatile *pxDelayed_Task_List;
// 溢出阻塞鏈表和其指針
static List_t xDelayed_Task_List2;
static List_t volatile *pxOverflow_Delayed_Task_List;
1.2、prvInitialiseTaskLists( )
由於新增加了阻塞鏈表和溢出阻塞鏈表,因此在鏈表初始化函數中除了需要初始化就緒鏈表數組外,還需要增加對阻塞鏈表和溢出阻塞鏈表的初始化操作,如下所示
/* task.c */
// 就緒列表初始化函數
void prvInitialiseTaskLists(void)
{
// 省略未修改部分
......
// 初始化延時阻塞鏈表
vListInitialise(&xDelayed_Task_List1);
vListInitialise(&xDelayed_Task_List2);
// 初始化指向延時阻塞鏈表的指針
pxDelayed_Task_List = &xDelayed_Task_List1;
pxOverflow_Delayed_Task_List = &xDelayed_Task_List2;
}
1.3、taskSWITCH_DELAYED_LISTS( )
為什麼需要阻塞鏈表和溢出阻塞鏈表需要交換?
閱讀 ” 0.2、思考二“ 小節內容
阻塞鏈表和溢出阻塞鏈表是如何實現交換的?
利用兩個指針進行交換
/* task.c */
// 記錄溢出次數
static volatile BaseType_t xNumOfOverflows = (BaseType_t)0;
// 延時阻塞鏈表和溢出延時阻塞鏈表交換
#define taskSWITCH_DELAYED_LISTS()\
{\
List_t volatile *pxTemp;\
pxTemp = pxDelayed_Task_List;\
pxDelayed_Task_List = pxOverflow_Delayed_Task_List;\
pxOverflow_Delayed_Task_List = pxTemp;\
xNumOfOverflows++;\
prvResetNextTaskUnblockTime();\
}
1.4、prvResetNextTaskUnblockTime( )
由於將任務插入溢出阻塞鏈表時不會更新 xNextTaskUnblockTime
變數,只有在將任務插入阻塞鏈表中時才會更新xNextTaskUnblockTime
變數,所以對於溢出阻塞鏈表中存在的任務沒有對應的喚醒時間,因此當心跳溢出切換阻塞鏈表時候,需要重設 xNextTaskUnblockTime
變數的值
/* task.c */
// 記錄下個任務解除阻塞時間
static volatile TickType_t xNextTaskUnblockTime = (TickType_t)0U;
// 函數聲明
static void prvResetNextTaskUnblockTime(void);
// 重設 xNextTaskUnblockTime 變數值
static void prvResetNextTaskUnblockTime(void)
{
TCB_t *pxTCB;
// 切換阻塞鏈表後,阻塞鏈表為空
if(listLIST_IS_EMPTY(pxDelayed_Task_List) != pdFALSE)
{
// 下次解除延時的時間為可能的最大值
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
// 如果阻塞鏈表不為空,下次解除延時的時間為鏈表頭任務的阻塞時間
(pxTCB) = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(pxDelayed_Task_List);
xNextTaskUnblockTime=listGET_LIST_ITEM_VALUE(&((pxTCB)->xStateListItem));
}
}
1.5、prvAddCurrentTaskToDelayedList( )
將當前任務加入到阻塞鏈表中,具體流程可以參看程式註釋,對於延時到期時間未溢出的任務會插入到阻塞鏈表中,而對於延時到期時間溢出的任務會插入溢出阻塞鏈表中
/* task.c */
// 將當前任務添加到阻塞鏈表中
static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait)
{
TickType_t xTimeToWake;
// 當前滴答定時器中斷次數
const TickType_t xConstTickCount = xTickCount;
// 成功從就緒鏈表中移除該阻塞任務
if(uxListRemove((ListItem_t *)&(pxCurrentTCB->xStateListItem)) == 0)
{
// 將當前任務的優先順序從優先順序點陣圖中刪除
portRESET_READY_PRIORITY(pxCurrentTCB->uxPriority, uxTopReadyPriority);
}
// 計算延時到期時間
xTimeToWake = xConstTickCount + xTicksToWait;
// 將延時到期值設置為阻塞鏈表中節點的排序值
listSET_LIST_ITEM_VALUE(&(pxCurrentTCB->xStateListItem), xTimeToWake);
// 如果延時到期時間會溢出
if(xTimeToWake < xConstTickCount)
{
// 將其插入溢出阻塞鏈表中
vListInsert((List_t *)pxOverflow_Delayed_Task_List,
(ListItem_t *)&(pxCurrentTCB->xStateListItem));
}
// 沒有溢出
else
{
// 插入到阻塞鏈表中
vListInsert((List_t *)pxDelayed_Task_List,
(ListItem_t *) &( pxCurrentTCB->xStateListItem));
// 更新下一個任務解鎖時刻變數 xNextTaskUnblockTime 的值
if(xTimeToWake < xNextTaskUnblockTime)
{
xNextTaskUnblockTime = xTimeToWake;
}
}
}
2、修改內核程式
2.1、vTaskStartScheduler( )
/* task.c */
void vTaskStartScheduler(void)
{
// 省略創建空閑任務函數
......
// 初始化滴答定時器計數值,感覺有點兒多餘?全局變數定義時候已被初始化為 0
xTickCount = (TickType_t)0U;
if(xPortStartScheduler() != pdFALSE){}
}
2.2、vTaskDelay( )
阻塞延時函數,當任務調用阻塞延時函數時會將任務從就緒鏈表中刪除,然後加入到阻塞鏈表中
/* task.c */
// 阻塞延時函數
void vTaskDelay(const TickType_t xTicksToDelay)
{
// 將當前任務加入到阻塞鏈表
prvAddCurrentTaskToDelayedList(xTicksToDelay);
// 任務切換
taskYIELD();
}
2.3、xTaskIncrementTick( )
利用 RTOS 的心跳(滴答定時器中斷服務函數)對阻塞任務進行處理,具體流程如下所示
/* task.c */
// 任務阻塞延時處理
BaseType_t xTaskIncrementTick(void)
{
TCB_t *pxTCB = NULL;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
// 更新系統時基計數器 xTickCount
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
// 如果 xConstTickCount 溢出,則切換延時列表
if(xConstTickCount == (TickType_t)0U)
{
taskSWITCH_DELAYED_LISTS();
}
// 最近的延時任務延時到期
if(xConstTickCount >= xNextTaskUnblockTime)
{
for(;;)
{
// 延時阻塞鏈表為空則跳出 for 迴圈
if(listLIST_IS_EMPTY(pxDelayed_Task_List) != pdFALSE)
{
// 設置下個任務解除阻塞時間為最大值,也即永不解除阻塞
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
// 依次獲取延時阻塞鏈表頭節點
pxTCB=(TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(pxDelayed_Task_List);
// 依次獲取延時阻塞鏈表中所有節點解除阻塞的時間
xItemValue = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem));
// 當阻塞鏈表中所有延時到期的任務都被移除則跳出 for 迴圈
if(xConstTickCount < xItemValue)
{
xNextTaskUnblockTime = xItemValue;
break;
}
// 將任務從延時列表移除,消除等待狀態
(void)uxListRemove(&(pxTCB->xStateListItem));
// 將解除等待的任務添加到就緒列表
prvAddTaskToReadyList(pxTCB);
#if(configUSE_PREEMPTION == 1)
// 如果解除阻塞狀態的任務優先順序比當前任務優先順序高,則需要進行任務調度
if(pxTCB->uxPriority >= pxCurrentTCB->uxPriority)
{
xSwitchRequired = pdTRUE;
}
#endif
}
}
}
return xSwitchRequired;
}
/* task.h */
// 修改函數聲明
BaseType_t xTaskIncrementTick(void);
/* FreeRTOSConfig.h */
// 支持搶占優先順序
#define configUSE_PREEMPTION 1
2.4、xPortSysTickHandler( )
無其他變化,只是將任務切換從函數體內修改到函數體外
/* port.c */
// SysTick 中斷
void xPortSysTickHandler(void)
{
// 關中斷
vPortRaiseBASEPRI();
// 更新系統時基
if(xTaskIncrementTick() != pdFALSE)
{
taskYIELD();
}
// 開中斷
vPortSetBASEPRI(0);
}
3、實驗
3.1、測試
與 FreeRTOS簡單內核實現6 優先順序 文章中 "3.1、測試" 小節內容一致
如果使用的開發環境為 Keil 且程式工作不正常,可以勾選 Use MicroLIB
試試,如下圖所示
3.2、待改進
當前 RTOS 簡單內核已實現的功能有
- 靜態方式創建任務
- 手動切換任務
- 臨界段保護
- 任務阻塞延時
- 支持任務優先順序
- 阻塞鏈表
當前 RTOS 簡單內核存在的缺點有
- 不支持時間片輪詢