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() 函数实现的,现在创建一个函数用于在任务创建后自动将其添加到就绪链表中,具体函数流程如下所示

  1. 当前系统中任务数量加一
  2. 如果第一次创建任务,就初始化任务相关的链表(就绪链表数组等)
  3. 如果不是第一次创建任务,就根据任务的优先级将 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<configMAX_PRIORITIES; 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 简单内核已实现的功能有

  1. 静态方式创建任务
  2. 手动切换任务
  3. 临界段保护
  4. 任务阻塞延时
  5. 支持任务优先级

当前 RTOS 简单内核存在的缺点有

  1. 缺少阻塞链表
  2. 不支持时间片轮询

FreeRTOS简单内核实现6 优先级的更多相关文章

  1. 源码级别gdb远程调试(实现OS简单内核)

    最近在学着编写一个操作系统的简单内核,需要debug工具,我们这里使用gdb来进行调试,由于虚拟机运行和本机是两个部分,所以使用 gdb 的远程调试技术,这里对 gdb 常见调试以及远程调试方式做一个 ...

  2. 基于mykernel完成时间片轮询多道进程的简单内核

    基于mykernel完成时间片轮询多道进程的简单内核 原创作品转载请注明出处+中科大孟宁老师的linux操作系统分析:https://github.com/mengning/linuxkernel/ ...

  3. FreeRTOS和Ucos在任务优先级的区别

    而ucos的任务优先级是任务优先级的数组越小,任务优先级越高.和STM32的中断优先级保持一样的分析,和freeRTOS相反.

  4. 基于mykernel完成多进程的简单内核

    学号351 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ mykernel简介 mykernel是由孟宁老师建立的一个用于开发您自己的操 ...

  5. linux简单内核链表排序

    #include <stdio.h> #include <stdlib.h> #define container_of(ptr, type, mem)(type *)((uns ...

  6. [驱动] 一个简单内核驱动,通过qemu调试(1)

    模块 通过在HOST上修改linux kernel源代码,重新编译一个vmlinux,然后,通过qemu根据这个bzImage 启动一个vm,进行调试 #cat drivers/char/test.c ...

  7. 【freertos】011-信号量、互斥量及优先级继承机制源码分析

    目录 前言 11.1 任务同步 11.2 信号量概念 11.3 二值信号量 11.3.1 二值信号量概念 11.3.2 优先级翻转 11.3.3 二值信号量运作机制 11.4 计数信号量 11.4.1 ...

  8. freeRTOS内核学习笔记(1)-编程标准

    在开始具体的学习之前,你应该先了解freeRTOS的编程标准.这能够方便你在接下来的阅读中快速的了解一些内容 的基本信息,并方便记忆.此外,良好的编程风格也是工作效率的保障. 你可以在https:// ...

  9. FreeRTOS 使用指南(转)

    源:FreeRTOS 使用指南 繁星电子开发团队制作 作为一个轻量级的操作系统,FreeRTOS 提供的功能包括:任务管理.时间管理.信号量.消息队列.内存管理.记录功能等,可基本满足较小系统的需要. ...

  10. freeRTOSConfig.h文件对FreeRTOS进行系统配置

    FreeRTOS内核是高度可定制的,使用配置文件FreeRTOSConfig.h进行定制.每个FreeRTOS应用都必须包含这个头文件,用户根据实际应用来裁剪定制FreeRTOS内核.这个配置文件是针 ...

随机推荐

  1. [Go] assignment count mismatch 1 = 2

    Golang 中这个错误的的意思是赋值变量的数目不匹配. 举例: result := json.Marshal(List) 由于没有给返回值中的 error 正确赋值,就会报  assignment ...

  2. [Go] 注意 go build -o <output> 选项的准确含义

    -o <output> 选项强制执行把构建的可执行文件写入到目标文件或者目标目录中. 如果 output 是已存在的目录,那么所有构建好的文件都将写入到该目录中. 注意:如果目录不存在的话 ...

  3. linux服务器配置查看

    查看linux服务器配置 查硬盘信息 sblk 看sda sdb sdc之类的 以下可以看出是500G sda第一块,sdb是第二块 以下可以看出是 1T+100G 查内存 free -h 查cpu ...

  4. 【源码研读】MLIR Dialect 分层设计

    以「疑问 - 求解」的形式来组织调研,此处记录整个过程. 1. MLIR 中的 Dialect 是「分层」设计的么? 先问是不是,再谈为什么.从 LLVM 社区 可以看出,至少在做 Codegen 时 ...

  5. linux用户与用户组管理

    linux用户与用户组管理 目录 linux用户与用户组管理 1.linux用户管理 1.1 用户基础 1.2 /etc/passwd:用户信息文件 1.3 /etc/shadow:用户密码信息文件 ...

  6. 关于SQL数据库Varchar字符串类型长度设计问题(转载)

    为什么要合理的设计数据库字段数据类型的长度?个人观点:一个是降低物理上的存储空间,一个是提高数据库的处理速度,还有一个附带功能是能校验数据是否合法.   现代数据库一般都支持CHAR与VARCHAR字 ...

  7. 🔥🔥httpsok-谷歌免费SSL证书如何申请

    httpsok-谷歌免费SSL证书如何申请 使用场景: 部署CDN证书.OSS云存储证书 证书类型: 单域名 多域名 通配符域名 混合域名 证书厂商: ZeroSSL Let's Encrypt Go ...

  8. 九、.net core(.NET 6)添加通用的Redis功能

     .net core 编写通用的Redis功能 在 Package项目里面,添加包:StackExchange.Redis: 在Common工具文件夹下,新建 Wsk.Core.Redis类库项目,并 ...

  9. docker 修改运行容器环境变量,如何修改容器中的环境变量env使长期有效

    @ 目录 前言 第一步:查看Docker Root目录 第二步:查到容器的长id(container id) 第三步:停止容器 第四步:编辑修改环境变量env 第五步:重载服务的配置文件 第六步:重启 ...

  10. 预见预判_AIRIOT智慧交通管理解决方案

    随着机动车保有量的逐步增加,城市交通压力日益增大.同时,新能源车辆的快速发展虽然带来了环保效益,但也因不限号政策而进一步加剧了道路拥堵问题.此外,各类赛事和重大活动的交通管制措施也时常导致交通状况复杂 ...