0、思考与回答
0.1、思考一
如何实现 RTOS 内核支持多优先级?
因为不支持优先级,所以所有的任务都插入了一个名为 pxReadyTasksLists 的就绪链表中,相当于所有任务的优先级都是一致的,那如果我们创建一个就绪链表数组,数组下标代表优先级,优先级为 x 的任务就插入到 pxReadyTasksLists[x] 中,这样通过一个就绪链表数组就实现了将不同优先级的任务放在不同的就绪链表中,方便在进行任务调度时支持任务优先级
1、就绪链表
1.1、创建
将原来的就绪链表修改为就绪链表数组
/* task.c */ // 就绪链表数组 List_t pxReadyTasksLists[configMAX_PRIORITIES];
configMAX_PRIORITIES 是一个表示 RTOS 内核支持的最大优先级的宏定义,值得提醒的是目前 RTOS 支持的最大优先级数量为 32 个(这与后面使用到的记录优先级的位图有关,具体内容会在后面遇到优先级位图时做介绍)
/* FreeRTOSConfig.h */ // 设置 RTOS 支持的最大优先级 #define configMAX_PRIORITIES 5
1.2、初始化
修改就绪链表初始化函数,即遍历整个就绪链表数组然后依次对每个就绪链表进行初始化,具体如下所示
/* task.c */ // 就绪链表始化函数 void prvInitialiseTaskLists(void) { UBaseType_t uxPriority; // 初始化就绪任务链表 for(uxPriority = (UBaseType_t)0U; uxPriority < (UBaseType_t)configMAX_PRIORITIES; uxPriority++) { vListInitialise(&(pxReadyTasksLists[uxPriority])); } }
1.3、添加任务
1.3.1、prvAddNewTaskToReadyList( )
已完成的内核中添加任务到就绪链表是对每个任务手动调用 vListInsertEnd() 函数实现的,现在创建一个函数用于在任务创建后自动将其添加到就绪链表中,具体函数流程如下所示
- 当前系统中任务数量加一
- 如果第一次创建任务,就初始化任务相关的链表(就绪链表数组等)
- 如果不是第一次创建任务,就根据任务的优先级将 pxCurrentTCB 指向最高优先级任务的 TCB
注意:if(pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority) 判断 pxCurrentTCB 指向最高优先级任务的 TCB 时取了 = 号,也就意味着,如果系统中创建了两个相同优先级的任务,那启动调度器后第一个执行的任务将是最后创建的那个任务
任务控制块的 uxPriority 参数将在 "2.1、TCB" 小节中添加
/* task.c */ // 全局任务计数器 static volatile UBaseType_t uxCurrentNumberOfTasks = (UBaseType_t)0U; // 添加任务到就绪链表中 static void prvAddNewTaskToReadyList(TCB_t* pxNewTCB) { // 进入临界段 taskENTER_CRITICAL(); { // 全局任务计数器加一操作 uxCurrentNumberOfTasks++; // 如果 pxCurrentTCB 为空,则将 pxCurrentTCB 指向新创建的任务 if(pxCurrentTCB == NULL) { pxCurrentTCB = pxNewTCB; // 如果是第一次创建任务,则需要初始化任务相关的列表 if(uxCurrentNumberOfTasks == (UBaseType_t)1) { // 初始化任务相关的列表 prvInitialiseTaskLists(); } } else // 如果pxCurrentTCB不为空 // 则根据任务的优先级将 pxCurrentTCB 指向最高优先级任务的 TCB { if(pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority) { pxCurrentTCB = pxNewTCB; } } // 将任务添加到就绪列表 prvAddTaskToReadyList(pxNewTCB); } // 退出临界段 taskEXIT_CRITICAL(); }
1.3.2、prvAddTaskToReadyList( )
是怎么把任务添加到就绪链表的?
首先将要添加任务的优先级记录在优先级位图中,然后通过 vListInsertEnd() 函数将任务插入到对应优先级的就绪链表中,具体如下所示
/* task.c */ // 32位的优先级位图,默认全 0 ,记录了所有存在的优先级 static volatile UBaseType_t uxTopReadyPriority = 0; // 根据任务优先级置位优先级位图 #define taskRECORD_READY_PRIORITY(uxPriority) portRECORD_READY_PRIORITY(uxPriority, uxTopReadyPriority) // 根据任务优先级添加任务到对应的就绪链表 #define prvAddTaskToReadyList(pxTCB) \ taskRECORD_READY_PRIORITY((pxTCB)->uxPriority); \ vListInsertEnd(&(pxReadyTasksLists[(pxTCB)->uxPriority]), \ &((pxTCB)->xStateListItem)); \
什么是优先级位图?
优先级位图本质是一个 32 位的数,如果有对应的优先级任务,就将优先级位图这个变量的对应位标记为 1 (比如当前任务的优先级为 2 ,则将优先级位图的从右向左第二位置一)
为什么要使用优先级位图记录任务优先级?
方便快速找到当前系统中存在的最高优先级,通过计算优先级位图的前导零个数,然后让 31 减去前导零个数就可以很快找到最高优先级
/* protMacro.h */ #define portRECORD_READY_PRIORITY(uxPriority, uxReadyPriorities) (uxReadyPriorities) |= (1UL << (uxPriority))
1.4、寻找最高优先级任务
RTOS 支持任务优先级后,任务的调度策略就可以修改为始终让系统中处于就绪态的最高优先级的任务得到执行,因此我们需要寻找最高优先级任务,寻找到之后将 pxCurrentTCB 指向该任务,然后任务调度切换任务时就会切换到该最高优先级的任务
/* task.c */ // 找到就绪列表最高优先级的任务并更新到 pxCurrentTCB #define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority; \ /* 寻找最高优先级 */ \ portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority); \ /* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */ \ listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, \ &(pxReadyTasksLists[uxTopPriority])); \ }
获取系统中存在的最高优先级任务的原理正如 "1.3.2、prvAddTaskToReadyList( )" 小节中 ”为什么要使用优先级位图记录任务优先级?“ 问题所述内容
/* protMacro.h */ #define portGET_HIGHEST_PRIORITY(uxTopPriority, uxReadyPriorities) uxTopPriority = (31UL - (uint32_t) __clz((uxReadyPriorities)))
2、修改内核程序
2.1、TCB
在任务控制块中增加任务优先级参数
/* task.h */ typedef struct tskTaskControlBlock { // 省略之前的结构体成员定义 UBaseType_t uxPriority; // 优先级 }tskTCB;
2.2、xTaskCreateStatic( )
修改静态创建任务函数,在参数列表中增加任务优先级参数,然后将创建好的任务直接自动添加到就绪链表中,不再需要额外手动将任务插入
/* task.c */ // 静态创建任务函数 #if (configSUPPORT_STATIC_ALLOCATION == 1) TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, const char* const pcName, const uint32_t ulStackDepth, void* const pvParameters, UBaseType_t uxPriority, // 优先级 StackType_t* const puxTaskBuffer, TCB_t* const pxTaskBuffer) { // 省略未改变的代码 ...... // 真正的创建任务函数 prvInitialiseNewTask(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, // 优先级 &xReturn, pxNewTCB); // 创建完任务自动将任务添加到就绪链表 prvAddNewTaskToReadyList(pxNewTCB); // 省略未改变的代码 ...... } #endif /* task.h */ // 函数声明 TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode, const char* const pcName, const uint32_t ulStackDepth, void* const pvParameters, UBaseType_t uxPriority, // 优先级 StackType_t* const puxTaskBuffer, TCB_t* const pxTaskBuffer);
2.3、prvInitialiseNewTask( )
由于增加了优先级参数,因此需要在真正的创建任务函数中增加对任务优先级初始化的部分,具体如下所示
/* task.c */ // 真正的创建任务函数 static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, const char* const pcName, const uint32_t ulStackDepth, void* const pvParameters, UBaseType_t uxPriority, TaskHandle_t* const pxCreatedTask, TCB_t* pxNewTCB) { // 省略未改变的代码 ...... // 初始化优先级 if(uxPriority >= (UBaseType_t)configMAX_PRIORITIES) { uxPriority = (UBaseType_t)configMAX_PRIORITIES - (UBaseType_t)1U; } pxNewTCB->uxPriority = uxPriority; if((void*)pxCreatedTask != NULL) { *pxCreatedTask = (TaskHandle_t)pxNewTCB; } }
2.4、vTaskStartScheduler( )
由于在启动任务调度器函数中创建了空闲任务,因此还需要在创建空闲任务的参数列表中增加优先级参数,为了不抢占任何其他任务的运行,空闲任务的优先级应该保持为最低优先级,使用 taskIDLE_PRIORITY 宏定义表示,具体如下所示
/* task.c */ // 启动任务调度器 void vTaskStartScheduler(void) { // 创建空闲任务 TaskHandle_t xIdleTaskHandle = xTaskCreateStatic((TaskFunction_t)prvIdleTask, (char *)"IDLE", (uint32_t)confgiMINIMAL_STACK_SIZE, (void *)NULL, (UBaseType_t)taskIDLE_PRIORITY, (StackType_t *)IdleTasKStack, (TCB_t *)&IdleTaskTCB); if(xPortStartScheduler() != pdFALSE){} }
/* task.h */ // 空闲任务优先级最低 #define taskIDLE_PRIORITY ((UBaseType_t) 0U)
2.5、vTaskDelay( )
当一个任务调用阻塞延时函数时,可以将其优先级从优先级位图上清除掉,这样在寻找最高优先级任务时就不会找到阻塞状态的该任务
值得提醒的是,这种做法虽然可以简单的达到让进入阻塞状态的任务暂时脱离调度的效果,但是由于其仍然存在就绪链表中,并不是真正的从就绪链表中移除,因此在遍历就绪链表对就绪态的任务操作时会产生额外的操作
/* task.c */ // 阻塞延时函数 void vTaskDelay(const TickType_t xTicksToDelay) { // 省略未修改程序 ...... // 将任务从优先级位图上清除,这样调度时就不会找到该任务 taskRESET_READY_PRIORITY(pxTCB->uxPriority); // 主动产生任务调度,让出 MCU taskYIELD(); }
/* task.c */ // 根据任务优先级清除优先级位图 #define taskRESET_READY_PRIORITY(uxPriority) \ { \ portRESET_READY_PRIORITY((uxPriority), (uxTopReadyPriority)); \ }
与置位优先级位图原理刚好相反,清除优先级位图则是根据任务的优先级将优先级位图上对应的位清零,具体如下所示
/* protMacro.h */ #define portRESET_READY_PRIORITY(uxPriority, uxReadyPriorities) (uxReadyPriorities) &= ~(1UL << (uxPriority))
2.6、vTaskSwitchContext( )
任务调度函数中始终选择系统中就绪状态的最高优先级任务
/* task.c */ // 任务调度函数 void vTaskSwitchContext(void) { taskSELECT_HIGHEST_PRIORITY_TASK(); }
2.7、xTaskIncrementTick( )
更新任务阻塞延时参数,如果任务延时时间到期,将其任务优先级在优先级位图上恢复,然后产生任务调度切换任务
/* task.c */ // 更新任务延时参数 void xTaskIncrementTick(void) { TCB_t *pxTCB = NULL; uint8_t i =0; uint8_t xSwitchRequired = pdFALSE; // 更新 xTickCount 系统时基计数器 const TickType_t xConstTickCount = xTickCount + 1; xTickCount = xConstTickCount; // 扫描就绪列表中所有任务,如果延时时间不为 0 则减 1 for(i=0; i { pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY((&pxReadyTasksLists[i])); if(pxTCB->xTicksToDelay > 0) { pxTCB->xTicksToDelay--; } // 延时时间到,将任务就绪 else { taskRECORD_READY_PRIORITY(pxTCB->uxPriority); xSwitchRequired = pdTRUE; } } // 如果就绪链表中有任务从阻塞状态恢复就产生任务调度 if(xSwitchRequired == pdTRUE){ // 产生任务调度 portYIELD(); } }
3、实验
3.1、测试
测试程序与 FreeRTOS简单内核实现5 阻塞延时 几乎一致,但是已经不需要我们手动将任务插入就绪链表中了,不过创建任务时需要额外指定任务的优先级参数,另外我们添加一个模拟任务一直运行的延时函数,具体如下所示
/* main.c */ /* USER CODE BEGIN Includes */ #include "FreeRTOS.h" /* USER CODE END Includes */ /* USER CODE BEGIN PV */ // 软件延时 void delay(uint32_t count) { for(;count!=0;count--); } TaskHandle_t Task1_Handle; #define TASK1_STACK_SIZE 128 StackType_t Task1Stack[TASK1_STACK_SIZE]; TCB_t Task1TCB; UBaseType_t Task1Priority = 1; TaskHandle_t Task2_Handle; #define TASK2_STACK_SIZE 128 StackType_t Task2Stack[TASK2_STACK_SIZE]; TCB_t Task2TCB; UBaseType_t Task2Priority = 2; // 任务 1 入口函数 void Task1_Entry(void *parg) { for(;;) { HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin); vTaskDelay(100); } } // 任务 2 入口函数 void Task2_Entry(void *parg) { for(;;) { HAL_GPIO_TogglePin(ORANGE_LED_GPIO_Port, ORANGE_LED_Pin); // 模拟高优先级任务一直运行 delay(10000000); } } /* USER CODE END PV */ /* USER CODE BEGIN 2 */ // 创建任务 1 和 2 Task1_Handle = xTaskCreateStatic((TaskFunction_t)Task1_Entry, (char *)"Task1", (uint32_t)TASK1_STACK_SIZE, (void *)NULL, (UBaseType_t)Task1Priority, (StackType_t *)Task1Stack, (TCB_t *)&Task1TCB); Task2_Handle = xTaskCreateStatic((TaskFunction_t)Task2_Entry, (char *)"Task2", (uint32_t)TASK2_STACK_SIZE, (void *) NULL, (UBaseType_t)Task2Priority, (StackType_t *)Task2Stack, (TCB_t *)&Task2TCB ); // 启动任务调度器,永不返回 vTaskStartScheduler(); /* USER CODE END 2 */
使用逻辑分析仪捕获 GREEN_LED 和 ORANGE_LED 两个引脚的电平变化,具体如下图所示

可以发现因为我们使用软件延时模拟高优先级 Task2 任务一直运行,导致低优先级的 Task1 任务完全没时间运行,也即 Task1 被饿死了
将 Task1 的优先级修改为 3 ,重新编译烧录程序再次观察两个引脚的电平变化,具体如下图所示

可以发现 Task1 不再被饿死,通过上述测试可以证明目前的 RTOS 已经支持多任务优先级
3.2、待改进
当前 RTOS 简单内核已实现的功能有
- 静态方式创建任务
- 手动切换任务
- 临界段保护
- 任务阻塞延时
- 支持任务优先级
当前 RTOS 简单内核存在的缺点有
- 缺少阻塞链表
- 不支持时间片轮询