FreeRTOS简单内核实现6 优先级
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<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 简单内核已实现的功能有
- 静态方式创建任务
- 手动切换任务
- 临界段保护
- 任务阻塞延时
- 支持任务优先级
当前 RTOS 简单内核存在的缺点有
- 缺少阻塞链表
- 不支持时间片轮询
FreeRTOS简单内核实现6 优先级的更多相关文章
- 源码级别gdb远程调试(实现OS简单内核)
最近在学着编写一个操作系统的简单内核,需要debug工具,我们这里使用gdb来进行调试,由于虚拟机运行和本机是两个部分,所以使用 gdb 的远程调试技术,这里对 gdb 常见调试以及远程调试方式做一个 ...
- 基于mykernel完成时间片轮询多道进程的简单内核
基于mykernel完成时间片轮询多道进程的简单内核 原创作品转载请注明出处+中科大孟宁老师的linux操作系统分析:https://github.com/mengning/linuxkernel/ ...
- FreeRTOS和Ucos在任务优先级的区别
而ucos的任务优先级是任务优先级的数组越小,任务优先级越高.和STM32的中断优先级保持一样的分析,和freeRTOS相反.
- 基于mykernel完成多进程的简单内核
学号351 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ mykernel简介 mykernel是由孟宁老师建立的一个用于开发您自己的操 ...
- linux简单内核链表排序
#include <stdio.h> #include <stdlib.h> #define container_of(ptr, type, mem)(type *)((uns ...
- [驱动] 一个简单内核驱动,通过qemu调试(1)
模块 通过在HOST上修改linux kernel源代码,重新编译一个vmlinux,然后,通过qemu根据这个bzImage 启动一个vm,进行调试 #cat drivers/char/test.c ...
- 【freertos】011-信号量、互斥量及优先级继承机制源码分析
目录 前言 11.1 任务同步 11.2 信号量概念 11.3 二值信号量 11.3.1 二值信号量概念 11.3.2 优先级翻转 11.3.3 二值信号量运作机制 11.4 计数信号量 11.4.1 ...
- freeRTOS内核学习笔记(1)-编程标准
在开始具体的学习之前,你应该先了解freeRTOS的编程标准.这能够方便你在接下来的阅读中快速的了解一些内容 的基本信息,并方便记忆.此外,良好的编程风格也是工作效率的保障. 你可以在https:// ...
- FreeRTOS 使用指南(转)
源:FreeRTOS 使用指南 繁星电子开发团队制作 作为一个轻量级的操作系统,FreeRTOS 提供的功能包括:任务管理.时间管理.信号量.消息队列.内存管理.记录功能等,可基本满足较小系统的需要. ...
- freeRTOSConfig.h文件对FreeRTOS进行系统配置
FreeRTOS内核是高度可定制的,使用配置文件FreeRTOSConfig.h进行定制.每个FreeRTOS应用都必须包含这个头文件,用户根据实际应用来裁剪定制FreeRTOS内核.这个配置文件是针 ...
随机推荐
- Quick BI的可视分析之路
简介: Quick BI是专为云上用户量身打造的智能数据分析和可视化BI产品,帮助企业快速完成从传统的数据分析到数据云化+分析云化的转变,将企业的业务数据产出后以最快的速度被推送到各组织侧消费使用.本 ...
- 网易云音乐音视频算法的 Serverless 探索之路
简介: 基于音视频算法服务化的经验,网易云音乐曲库团队与音视频算法团队一起协作,一起共建了网易云音乐音视频算法处理平台,为整个云音乐提供统一的音视频算法处理平台.本文将分享我们如何通过 Server ...
- dotnet 6 已知问题 ManualResetEventSlim 的 Set 方法抛出空异常
本文记录一个 dotnet 6 已知问题,此问题预计是在 .NET Framework 4.5 时就引入的,我没有考古在 .NET Framework 4.5 之前是否还存在此问题.当前这个问题在 . ...
- Competition Set - AtCoder II
这里记录的是这个账号的比赛情况. ABC310 2023-7-15 Solved:6/8 1973->2053 七场ABC,两场打得蛮烂的,都因为AT炸掉Unrated了:另外五场全部满Perf ...
- 【GUI软件】抖音搜索结果批量采集,支持多个关键词、排序方式、发布时间筛选等!
目录 一.背景介绍 1.1 爬取目标 1.2 演示视频 1.3 软件说明 二.代码讲解 2.1 爬虫采集模块 2.2 软件界面模块 2.3 日志模块 三.获取源码及软件 一.背景介绍 1.1 爬取目标 ...
- 01. go-admin的下载与启动
目录 一.介绍 二.新建空文件夹 三.获取后台源码并启动 1.下载编译go代码 2.配置命令到goland IDE ,debug启动 四.获取前端ui源码并启动 1.下载编译go代码 2.启动项目 * ...
- 利用pearcmd实现裸文件包含
利用pearcmd实现裸文件包含 在 ctf 中,常常有这样一类题: 题目很简单,一般围绕一个 include 函数展开. 例: ctfshow 元旦水友赛 easy_include 这类题目没有提供 ...
- C语言:贮油点建设问题(详解题目意思)
!!!!先看解析,后面附有代码!!!!!!! ,希望大家不懂的能认真看看,这些都是我在写的过程中不能理解,遇到的困难,然后弄懂之后总结出来给大家的,想学的一定要认真看完. 规律是: 贮油点之间相差50 ...
- IPv6 — 子网划分
目录 文章目录 目录 前文列表 IPv6 的子网划分 前文列表 <IPv6 - 网际协议第 6 版> <IPv6 - 地址格式与寻址模式> <IPv6 - 协议头> ...
- java学习之旅(day.15)
IO框架 I:input O:output 流:内存与存储设备间传输数据的通道 数据借助流进行传输 流的分类 按流向分: 输入流:将存储设备中的内容读入到内存中(程序运行) 输出流:将内存中的内容写入 ...