FreeRTOS 就绪列表详解:任务调度的核心枢纽

一、前言

在FreeRTOS中,任务调度是整个操作系统的核心功能。而就绪列表(Ready List) ,正是支撑任务调度的最关键数据结构。简单来说,就绪列表就是用来存放所有"准备好了、随时可以运行"的任务的地方。

你可以把就绪列表想象成候车大厅------所有准备好上车的乘客(任务)都在这里排队等候,而调度器就像检票员,每次从候车大厅中挑选优先级最高的乘客上车(占用CPU)。

二、什么是就绪列表?

2.1 本质是一个数组

FreeRTOS的就绪列表本质上是一个静态数组,数组的每个元素都是一个链表:

c 复制代码
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

这里的关键点在于:

  • configMAX_PRIORITIES 是一个宏,定义了系统支持的最大优先级数量
  • 数组的下标 代表任务的优先级
  • 数组的每个元素都是一个链表List_t类型),用来挂载该优先级下的所有就绪任务

2.2 优先级与下标的关系

在FreeRTOS中,数字越大,优先级越高。也就是说:

  • pxReadyTasksLists[0] ------ 优先级0(最低)的就绪任务链表
  • pxReadyTasksLists[1] ------ 优先级1的就绪任务链表
  • ...
  • pxReadyTasksLists[configMAX_PRIORITIES - 1] ------ 最高优先级的就绪任务链表

空闲任务(Idle Task)的优先级最低,默认是0,所以它永远挂在pxReadyTasksLists[0]上。

2.3 为什么是"数组 + 链表"?

这个设计非常巧妙,兼具两种数据结构的优点:

数据结构 作用
数组 按优先级快速索引,O(1)时间定位到某个优先级
链表 同一优先级的多个任务可以无限扩展,动态增删效率高

如果用纯数组来实现,每个优先级能容纳的任务数量是固定的,扩展性差且浪费内存。而"数组+链表"的组合,既保证了按优先级查找的高效性,又保证了同一优先级下任务数量的灵活性。

三、就绪列表的初始化

系统启动时,需要先初始化就绪列表。这个工作由 prvInitialiseTaskLists() 函数完成:

c 复制代码
static void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;
    
    for( uxPriority = 0U; uxPriority < configMAX_PRIORITIES; uxPriority++ )
    {
        vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
    }
}

简单来说,就是遍历数组的每一个元素,把每个链表都初始化成空链表。初始化后的链表,头尾指针都指向自己,表示"目前还没有任何任务就绪"。

四、任务如何进入就绪列表?

当一个任务被创建时,或者从阻塞/挂起状态被唤醒时,它就会进入就绪列表。

核心操作由 prvAddNewTaskToReadyList() 函数完成:

c 复制代码
static void prvAddNewTaskToReadyList( TCB_t * pxNewTCB )
{
    /* 将任务添加到对应优先级的就绪链表尾部 */
    vListInsertEnd( &( pxReadyTasksLists[ pxNewTCB->uxPriority ] ),
                    &( pxNewTCB->xStateListItem ) );
}

整个过程是这样的:

  1. 根据任务的优先级(pxNewTCB->uxPriority)找到对应的链表
  2. 将任务的状态列表项xStateListItem)插入到该链表的尾部
  3. 如果是相同优先级的多个任务,它们会按先来后到的顺序排队

注意 :就绪列表中存储的不是TCB本身,而是TCB里面的 xStateListItem 节点。TCB通过这个节点"挂"到链表上。

五、调度器如何利用就绪列表?

5.1 跟踪最高优先级

为了快速找到最高优先级的就绪任务,FreeRTOS维护了一个全局变量 uxTopReadyPriority

c 复制代码
static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
  • 初始值为0(空闲任务的优先级)
  • 每当把一个任务加入就绪列表时,如果该任务的优先级高于 uxTopReadyPriority,就更新它

这样,调度器永远知道当前就绪的最高优先级是多少,不用每次都从头扫描整个数组。

5.2 选择下一个运行的任务

在任务切换时,调度器会调用 taskSELECT_HIGHEST_PRIORITY_TASK() 来选择下一个要运行的任务:

c 复制代码
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{
    UBaseType_t uxTopPriority = uxTopReadyPriority;
    
    /* 从最高优先级开始,向下找第一个非空的链表 */
    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )
    {
        --uxTopPriority;
    }
    
    /* 从该链表中取出第一个任务来运行 */
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,
                                 &( pxReadyTasksLists[ uxTopPriority ] ) );
}

逻辑非常清晰:

  1. uxTopReadyPriority(当前最高就绪优先级)开始
  2. 如果该优先级的链表为空,就往下找(优先级减1)
  3. 找到第一个非空链表后,取出链表头部的任务来运行

5.3 同等优先级:时间片轮转

如果有多个任务处于同一个最高优先级,它们会按时间片轮转的方式执行:

  • 当前任务运行完一个时间片后,被移到链表尾部
  • 链表头部的下一个任务获得CPU
  • 如此循环,实现公平调度

六、一张图看懂就绪列表

复制代码
pxReadyTasksLists[]
        │
        ├── [0] ──→ 空闲任务  ←──┐
        │                       │
        ├── [1] ──→ Task_A ←──┐ │
        │                     │ │
        ├── [2] ──→ (空)      │ │
        │                     │ │
        ├── [3] ──→ Task_B ←─┘  │
        │            ↑          │
        │            └──────────┘
        │
        └── [MAX-1] ──→ (空)

解读

  • 数组下标 = 优先级,下标越大优先级越高
  • 每个链表挂载该优先级下的所有就绪任务
  • 调度器总是从最高非空下标的链表中取任务运行

七、总结

就绪列表是FreeRTOS任务调度的核心枢纽,它的设计精髓可以概括为三点:

设计要点 说明
数组按优先级索引 下标即优先级,查找O(1)
链表存储同优先级任务 数量不限,动态增删高效
uxTopReadyPriority加速 实时跟踪最高优先级,避免全局扫描

理解了就绪列表,也就理解了FreeRTOS任务调度的半壁江山。下次当你创建任务、设置优先级、观察任务切换时,不妨想想------这些任务此刻正安静地躺在就绪列表的某个链表中,等待调度器的召唤。

相关推荐
hsjiasb2 天前
FreeRTOS学习(二十六)——动态内存管理heap_1到heap_5
stm32·单片机·学习·学习笔记·freertos
hsjiasb3 天前
FreeRTOS学习(二十三)——消息缓冲区(Message Buffer)
stm32·单片机·嵌入式硬件·学习·freertos
圆奋奋6 天前
FreeRTOS学习(三)- 任务调度模块
学习·开源·freertos
危桥带雨1 个月前
Freertos——任务通知
stm32·单片机·嵌入式硬件·freertos
Funing71 个月前
FreeRTOS 学习复盘(四):事件组、软件定时器与任务通知机制
操作系统·freertos·嵌入式软件
dozenyaoyida1 个月前
多 Claude Code 实例任务协调:文件系统队列设计与 Bash 实现
任务调度·ai编程助手·claude code·多实例协作·文件系统队列
离凌寒1 个月前
十一、在zephyr上使用rtc,rng,counter,crc,wdg等外设的演示
freertos
离凌寒1 个月前
八、在zephyr配置旋转编码器+timer配置演示
freertos
离凌寒1 个月前
六、在zephyr上配置sdmmc和fatfs演示
freertos