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. 实时数仓入门训练营:实时计算 Flink 版 SQL 实践

    ​简介: <实时数仓入门训练营>由阿里云研究员王峰.阿里云资深技术专家金晓军.阿里云高级产品专家刘一鸣等实时计算 Flink 版和 Hologres 的多名技术/产品一线专家齐上阵,合力搭 ...

  2. [ML] 通过llama.cpp与羊驼聊天的网页界面- 详解 Serge 的启动使用

      Serge 虽然能够让我们在笔记本上跑起来 7B模型, 但实际运行非常消耗CPU,对话生成响应非常非常慢. 1. 官方指导是使用如下命令直接运行: $ docker run -d -v weigh ...

  3. [PHP] 如何让 php-fpm 的循环 echo 实时输出到浏览器

      PHP 里开启实时输出方法是 ob_implicit_flush() , 但它大部分情况下都不管用, 因为 php.ini 配置里 output_buffering 输出缓冲大部分是 On 开启的 ...

  4. [Unreal] 虚拟现实理论与最佳实践

    优秀的虚拟现实场景所需要具备的特点: 真实感.沉浸感.舒适性.流畅性. VR的这些特点上有其它媒体所不具备的优势,主要通过硬件来实现. VR 的沉浸感让用户置身于周围世界,也引发了一些独特的症状: 眼 ...

  5. [Docker] 使 Volume 独立于容器运行时的方式 - 让容器引擎去处理

    在单纯使用 run 命令运行一个容器时,通常会使用 -v 挂载的方式来实现宿主机数据卷映射到容器内. 使用命令: $ docker run --name mysql-con -v /my/custom ...

  6. [ERROR] listen tcp :80: bind: permission denied

    出现这类提示的时候,表明当前用户没有权限进行 bind 操作. 在某些 Linux 云服务器提供商的运行环境中会出现. 解决方式:使用 sudo 切换为 root,然后在执行原操作. Refer:li ...

  7. WPF 字体 FontStyle 的 Italic 和 Oblique 的区别

    本文介绍在 WPF 里面的字体属性 FontStyle 的 Italic 和 Oblique 的斜体差别 本文的图片和知识来自: #265 – Specifying Values for FontSt ...

  8. Mysql带条件取多条随机记录

    有个文章段落表part,有两种类型的段落,即part_type取1或2,要从表中随机取多条任意类型的段落,比如3条. 方法一 ORDER BY后接RAND() select * from part w ...

  9. 004—Orcad创建简单分裂元件

    004-Orcad创建简单分裂元件 以TPS545为例,先查看datasheet,管脚图,PCB封装.新建库,设置名称和part的数量,然后添加管脚,设定管脚属性.电源管脚要勾选Pin Visble. ...

  10. kali linux 渗透测试 01 kali介绍

    安全问题的根源 分层思想------盲人摸象 只追求功能实现----比较片面 最大的安全威胁是人---- 安全目标 先于攻击者发现和防止漏洞出现 攻击型安全 防护性安全 渗透测试 尝试击破安全防御机制 ...