一、前言
在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 ) );
}
整个过程是这样的:
- 根据任务的优先级(
pxNewTCB->uxPriority)找到对应的链表 - 将任务的状态列表项 (
xStateListItem)插入到该链表的尾部 - 如果是相同优先级的多个任务,它们会按先来后到的顺序排队
注意 :就绪列表中存储的不是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 ] ) );
}
逻辑非常清晰:
- 从
uxTopReadyPriority(当前最高就绪优先级)开始 - 如果该优先级的链表为空,就往下找(优先级减1)
- 找到第一个非空链表后,取出链表头部的任务来运行
5.3 同等优先级:时间片轮转
如果有多个任务处于同一个最高优先级,它们会按时间片轮转的方式执行:
- 当前任务运行完一个时间片后,被移到链表尾部
- 链表头部的下一个任务获得CPU
- 如此循环,实现公平调度
六、一张图看懂就绪列表
pxReadyTasksLists[]
│
├── [0] ──→ 空闲任务 ←──┐
│ │
├── [1] ──→ Task_A ←──┐ │
│ │ │
├── [2] ──→ (空) │ │
│ │ │
├── [3] ──→ Task_B ←─┘ │
│ ↑ │
│ └──────────┘
│
└── [MAX-1] ──→ (空)
解读:
- 数组下标 = 优先级,下标越大优先级越高
- 每个链表挂载该优先级下的所有就绪任务
- 调度器总是从最高非空下标的链表中取任务运行
七、总结
就绪列表是FreeRTOS任务调度的核心枢纽,它的设计精髓可以概括为三点:
| 设计要点 | 说明 |
|---|---|
| 数组按优先级索引 | 下标即优先级,查找O(1) |
| 链表存储同优先级任务 | 数量不限,动态增删高效 |
| uxTopReadyPriority加速 | 实时跟踪最高优先级,避免全局扫描 |
理解了就绪列表,也就理解了FreeRTOS任务调度的半壁江山。下次当你创建任务、设置优先级、观察任务切换时,不妨想想------这些任务此刻正安静地躺在就绪列表的某个链表中,等待调度器的召唤。