首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >FreeRTOS任务调度

FreeRTOS任务调度

原创
作者头像
HaloMay
发布2025-06-20 17:27:21
发布2025-06-20 17:27:21
2110
举报
文章被收录于专栏:FreeRTOSFreeRTOS

前言

在前文中,我们知道所谓的任务切换,就是在滴答定时器中断函数中修改某个寄存器的位,触发PendSV中断,然后完成上下文保存和下一个任务的上下文加载,就完成了任务切换。那么在多任务的情况下,怎么合理安排任务的执行呢?也就是说当前这个任务执行的任务要占据几个tick?下一个要执行的任务是哪个呢?

任务调度

FreeRTOS中采用的调度策略是高优先级抢占---同等优先级时间片轮转的策略。

在说任务调度之前,我们先要搞清楚下面几个概念。

列表

列表这个概念可以形象的理解为一个挂衣钩,其结构如下:

挂衣钩
挂衣钩

列表就是钉在墙上的那一整个物体。我们来看看代码。

代码语言:txt
复制
typedef struct xLIST
{
 listFIRST_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */
 volatile UBaseType_t uxNumberOfItems; /* 列表中列表项的数量 */
 ListItem_t * configLIST_VOLATILE pxIndex; /* 用于遍历列表 */
 MiniListItem_t xListEnd; /* 最后一个列表项 */
 listSECOND_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */
} List_t;

确实,这个结构体重反复提到了一个概念----列表项。其他成员好像都是为了这个列表项而服务的,比如uxNumberOfItems用来表示当前挂了几件衣服(列表项数量)、pxIndex用来表示取衣服用的撑衣杆(指向列表项的指针)。

撑衣杆
撑衣杆

看样子列表更像是一个收集挂起衣服的杆子,而真正的衣服存储在列表项中。

FreeRTOS种定义了以下几个列表:

代码语言:txt
复制
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; 
/**< 用于存储就绪态(Ready)的任务列表. */
PRIVILEGED_DATA static List_t xDelayedTaskList1;                         
/**< 用于存储延迟(Delayed)的任务 */
PRIVILEGED_DATA static List_t xDelayedTaskList2;                         
/**< xDelayedTaskList2 用于存储那些延迟时间超过了当前 tick 计数范围的任务. */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;              
/**< 当前正在使用的延迟任务列表.延迟时间超过了当前 tick 计数器的范围时,这些任务会被放入  */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;      
/**< 指向当前用于存储溢出延迟任务的列表. */
PRIVILEGED_DATA static List_t xPendingReadyList;                         
/**< 用于存储那些由于某些事件(如信号量释放、队列操作等)而变为就绪态的任务 */

1. pxReadyTasksLists

  • 作用:存储所有就绪态(Ready)的任务,按优先级分组。
  • 数量configMAX_PRIORITIES 个列表,每个列表对应一个优先级。
  • 用途:调度器从中选择最高优先级的就绪任务来运行。

2. xDelayedTaskList1xDelayedTaskList2

  • 作用:存储延迟(Delayed)的任务。
  • 用途:调度器在滴答中断中检查这些列表,将延迟时间已到的任务移入就绪列表。
  • 溢出处理:两个列表交替使用,以处理滴答计数器溢出的情况。

3. pxDelayedTaskListpxOverflowDelayedTaskList

  • 作用:分别指向当前使用的延迟任务列表和溢出延迟任务列表。
  • 用途:在滴答计数器溢出时,调度器会切换这两个指针所指向的列表。

4. xPendingReadyList

  • 作用:存储那些因事件(如信号量释放、队列操作等)而变为就绪态的任务。
  • 用途:在滴答中断或上下文切换时,调度器会检查这个列表,将任务移入就绪列表。

我们可以这样理解,家里有三个挂衣钩,一个专门用来挂等待洗的脏衣服,一个用来专门用来挂刚从洗衣机洗完要晒的衣服还有一个是专门用来挂出门要穿的衣服。这三个挂衣钩就是列表。

列表项

我们上面说列表是用来服务列表项的,列表项的本质上就是一个双向循环链表:

代码语言:txt
复制
struct xLIST_ITEM
{
 listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测列表项的数据完整性 */
 configLIST_VOLATILE TickType_t xItemValue; /* 列表项的值 */
 struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* 下一个列表项 */
 struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 上一个列表项 */
 void * pvOwner; /* 列表项的拥有者 */
 struct xLIST * configLIST_VOLATILE pxContainer; /* 列表项所在列表 */
 listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测列表项的数据完整性 */
};
typedef struct xLIST_ITEM ListItem_t;/* 重定义成 ListItem_t */

如果说列表是挂衣钩,那么列表项就是衣架(还是相邻的连在一起的)铁索连环?

衣架
衣架

而我们的衣服就是挂载这个列表项的pvOwner上,实际上这个指针最后会指向每个任务的TCB。

现在明了一些了,当家里脏衣服多了,都挂在脏衣钩上时,就先将一批扔进洗衣机,然后洗完一部分后,将洗干净的衣服挂载第二个钩子上去晾晒,然后下一批脏衣服进入洗衣机,明天要出门了,选择了一件喜欢的晾好的衣服把它挂载第三个钩子上,名早出门穿。这就是调度。有一天所有的脏衣服都在排队进入洗衣机,我给对象说“明天我要开个正式的会,让我先把这个西服先洗了吧”,于是,我挂在第一个钩子上的最后一件西服,拿到了第一个,也就是下一轮洗衣机空出来就给我洗西服了。这就是抢占。有了这个生活中的例子,我相信大家对列表和列表项有一个深刻的理解。

任务调度现在也明白了,其是就是任务在不同的列表中来回切换,如果这个任务可以运行了就进入就绪列表,如果阻塞了就挂载到其他列表,总之同一个时刻只能有一个任务占据CPU。

调度器

其是不管是什么操作系统,其最底层的思想都是一样的,任务调度是任何操作系统的基本模块,在Linux上是进程管理,调度的是近程执行,在FreeRTOS中调度的是任务。完成这些功能的我们抽象一个概念叫调度器,在上层看来,我们只要按照格式创建了任务,并指定优先级,底层就会帮我们合理安排调度,这一切都是调度器完成的。它负责决定在多个可运行任务中哪一个将获得CPU时间得以执行,实际上调度器是由很多模块组成。

基本功能:

任务的选择 任务调度器会根据预设的算法从所有可运行的任务中选择一个要执行的任务。

任务的优先级 任务通常会被分配一个优先级,优先级越高的任务在抢占式调度中会优先执行。

任务状态管理 任务可能处于就绪状态(可立即运行)、阻塞状态(等待某些条件满足)或者挂起状态(暂停执行)。

上下文切换 任务调度器负责在不同任务之间进行上下文切换。这意味着将当前任务的状态保存起来,然后加载另一个任务的状态,以便它可以继续执行。

定时器管理 任务调度器通常会关注系统中的定时器,以便能够在特定事件发生时唤醒相应的任务。

中断处理 任务调度器需要与系统的中断处理程序协同工作,以确保在中断上下文中也能够正常进行任务调度。

今天我们主要去理解FreeRTOS内部不同的任务调度算法。

调度策略

时间片轮转

这个策略最容易理解,在大多数多任务系统中,时间片轮转都是最容易想到的,因为简单粗暴,不管什么优先级,只要是任务大家一律平等,每个任务执行一样的时间,到点就切换下一个。enmm。。这样的策略确实不错,所有的任务都有一样的概率执行,并且大家占据cpu资源的时间都是一样的,确实不错。但仔细一想,任务少了还行,比如3个任务,每个任务执行100ms,肉眼感觉还可以,系统响应行还行,但现在任务多起来了,变成了30个,这下坏了,某一个任务执行完毕后,要等待3秒之后才能再次执行,这太夸张了,我们敲击一下键盘,如果碰巧监听键盘的任务在执行,显示器显示字符,如果恰好这个任务刚执行完,我们敲击键盘三秒都没有反应,实在是不行。

时间片轮转
时间片轮转

每个任务按顺序执行,若没有延时或等待信号量等事物,时间片耗尽就接着挂载到就绪列表的最后一个,等待下次执行。

抢占式调度

这也是FreeRTOS之所以能够称为实时操作系统的原因,就是因为高优先级的任务能够打断当前低优先级的任务,提升了系统的实时性。比如上面的例子,如果我们把键盘任务优先级提升到较高级别,当我们按下键盘的一瞬间,这个任务立马打断当前的任务,来响应我们的操作。了解这个思想即可。

问题

下面说两个面试中常问的问题:

(1)在抢占式调度中,怎么避免优先级低的任务被饿死?

思考:我们来想一下,抢占式调度确实提高了系统的响应速度,但是抢占调度有个致命的缺陷,就是任务“饿死”。如果一个高优先级的任务抢占了CPU,却不释放,那么低优先级的任务将没有再次执行的可能性,这就是“饿死”。有个疑问----什么情况下任务会让出CPU呢?答案是使用非阻塞延时函数、等待信号量、标志位等,任务执行到这,就会被挂载到延时列表中,这样低优先级任务就能够运行了。所以,如果任务优先级相同,则开时间片轮转。如果任务优先级不同,就得从任务的设计上出发了,必须让任务让出cpu,不能一直占据。

答:避免优先级低的任务被饿死,就必须让任务交出cpu的使用权,不能一直占据cpu。所以在任务设计的时候,如果在没有信号量等条件的情况下,必须在任务后面添加非阻塞延时函数,比如vTaskDelay()和vTaskDelayUntil(),让任务短暂的交出cpu,让低优先级任务得以执行。这里需要注意的是不能使用HAL库带的HAL_Delay(),因为这个函数是个阻塞式的函数,相当于没有让出CPU。

(2)如何避免优先级反转?

Todo

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 任务调度
    • 列表
      • 1. pxReadyTasksLists
      • 2. xDelayedTaskList1 和 xDelayedTaskList2
      • 3. pxDelayedTaskList 和 pxOverflowDelayedTaskList
      • 4. xPendingReadyList
    • 列表项
    • 调度器
  • 调度策略
    • 时间片轮转
    • 抢占式调度
  • 问题
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档