任务延时列表的实现
1. 任务延时列表的工作原理
1.1 为什么需要任务延时列表?
在之前的实现中,任务阻塞延时是通过在TCB中记录xTicksToDelay变量,然后在每次SysTick中断中扫描整个就绪列表,将所有任务的延时计数器减1。这种方法存在严重的效率问题:
- 时间复杂度高:每次SysTick中断都需要遍历所有优先级的就绪链表
- 资源浪费:当系统中任务数量较多时,CPU大量时间浪费在无效扫描上
- 实时性差:扫描过程会延长中断处理时间,影响系统实时响应
1.2 延时列表的核心思想
FreeRTOS引入任务延时列表来优化延时管理,核心思想是:
只管理需要延时的任务,而不是扫描所有任务
工作原理:
- 当任务需要延时(调用
vTaskDelay)时,将其从就绪列表移除,插入到延时列表 - 延时列表中的任务按照唤醒时间(当前时间 + 延时值)升序排列
- 系统维护一个全局变量
xNextTaskUnblockTime,记录下一个将要唤醒的任务的时间 - 每次SysTick中断时,只需比较当前时间与
xNextTaskUnblockTime,判断是否有任务需要唤醒 - 如果当前时间 >=
xNextTaskUnblockTime,则从延时列表头部依次取出超时任务,移入就绪列表
1.3 两个延时列表的设计
FreeRTOS设计了两个延时列表来处理系统时基计数器溢出的情况:
static List_t xDelayedTaskList1; /* 延时列表1 */
static List_t xDelayedTaskList2; /* 延时列表2 */
static List_t * volatile pxDelayedTaskList; /* 当前使用的延时列表 */
static List_t * volatile pxOverflowDelayedTaskList; /* 溢出延时列表 */
设计原理:
- 当系统时基计数器
xTickCount没有溢出时,新延时的任务插入pxDelayedTaskList - 当
xTickCount溢出时,两个列表交换角色,原本的溢出列表变成当前列表 - 这种设计避免了在中断中处理复杂的溢出判断,提高了效率
2. 实现任务延时列表
2.1 定义任务延时列表
在task.c中定义延时列表相关的全局变量:
/* 两个延时列表 */
static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;/* 指向当前使用的延时列表和溢出延时列表的指针 */
static List_t * volatile pxDelayedTaskList;
static List_t * volatile pxOverflowDelayedTaskList;/* 下一个任务的解锁时刻 */
static volatile TickType_t xNextTaskUnblockTime = (TickType_t)0U;/* 记录系统时基计数器溢出的次数 */
static volatile BaseType_t xNumOfOverflows = (BaseType_t)0;
各变量作用:
xDelayedTaskList1/xDelayedTaskList2:两个实际的延时列表pxDelayedTaskList:指向当前活跃的延时列表(未溢出时使用)pxOverflowDelayedTaskList:指向溢出延时列表(溢出时使用)xNextTaskUnblockTime:下一个需要唤醒的任务的唤醒时间xNumOfOverflows:记录溢出次数,用于调试和列表切换
2.2 任务延时列表初始化
在prvInitialiseTaskLists()函数中初始化延时列表:
void prvInitialiseTaskLists(void)
{UBaseType_t uxPriority;/* 初始化就绪列表(所有优先级) */for (uxPriority = (UBaseType_t)0U; uxPriority < (UBaseType_t)configMAX_PRIORITIES; uxPriority++){vListInitialise(&(pxReadyTasksLists[uxPriority]));}/* 初始化两个延时列表 */vListInitialise(&xDelayedTaskList1);vListInitialise(&xDelayedTaskList2);/* 设置列表指针的初始指向 */pxDelayedTaskList = &xDelayedTaskList1; /* 当前使用列表1 */pxOverflowDelayedTaskList = &xDelayedTaskList2; /* 溢出列表2 */
}
初始化后的状态:
- 两个延时列表均为空链表
pxDelayedTaskList指向列表1(当前活跃)pxOverflowDelayedTaskList指向列表2(备用)
2.3 定义xNextTaskUnblockTime
xNextTaskUnblockTime在调度器启动时初始化为最大值:
void vTaskStartScheduler(void)
{/* ... 创建空闲任务 ... *//* 初始化下一个任务的解锁时刻为最大值 */xNextTaskUnblockTime = portMAX_DELAY;xTickCount = (TickType_t)0U;/* 启动调度器 */if (xPortStartScheduler() != pdFALSE) { ... }
}
portMAX_DELAY在32位系统中为0xffffffffUL,表示“无限远”的时间。
3. 修改代码,支持任务延时列表
3.1 修改vTaskDelay()函数
原来的vTaskDelay只是设置xTicksToDelay后调用taskYIELD(),现在需要将任务插入延时列表:
void vTaskDelay(const TickType_t xTicksToDelay)
{TCB_t *pxTCB = NULL;/* 获取当前任务的TCB */pxTCB = pxCurrentTCB;/* 将任务插入到延时列表(不再使用xTicksToDelay) */prvAddCurrentTaskToDelayedList(xTicksToDelay);/* 任务切换 */taskYIELD();
}
prvAddCurrentTaskToDelayedList()函数详解
static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait)
{TickType_t xTimeToWake;/* 获取当前系统时间 */const TickType_t xConstTickCount = xTickCount;/* ===== 步骤1:将任务从就绪列表中移除 ===== */if (uxListRemove(&(pxCurrentTCB->xStateListItem)) == (UBaseType_t)0){/* 如果该优先级链表变为空,清除优先级位图中的对应位 */portRESET_READY_PRIORITY(pxCurrentTCB->uxPriority, uxTopReadyPriority);}/* ===== 步骤2:计算唤醒时间 ===== */xTimeToWake = xConstTickCount + xTicksToWait;/* ===== 步骤3:设置列表项的排序值为唤醒时间 ===== */listSET_LIST_ITEM_VALUE(&(pxCurrentTCB->xStateListItem), xTimeToWake);/* ===== 步骤4:根据是否溢出插入不同的延时列表 ===== */if (xTimeToWake < xConstTickCount) /* 溢出情况 */{vListInsert(pxOverflowDelayedTaskList, &(pxCurrentTCB->xStateListItem));}else /* 未溢出情况 */{vListInsert(pxDelayedTaskList, &(pxCurrentTCB->xStateListItem));/* 更新下一个任务的解锁时刻 */if (xTimeToWake < xNextTaskUnblockTime){xNextTaskUnblockTime = xTimeToWake;}}
}
关键点解析:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 从就绪列表移除 | 任务进入延时状态,不再参与调度 |
| 2 | 计算唤醒时间 | 唤醒时间 = 当前时间 + 延时值 |
| 3 | 设置排序值 | 列表项按唤醒时间升序排列 |
| 4 | 插入延时列表 | 根据是否溢出选择插入哪个列表 |
溢出判断:
xTimeToWake < xConstTickCount表示唤醒时间小于当前时间- 当
xTickCount接近最大值时,加上一个值会导致溢出回绕 - 溢出的任务插入
pxOverflowDelayedTaskList,正常任务插入pxDelayedTaskList
3.2 修改xTaskIncrementTick()函数
这是SysTick中断调用的核心函数,负责唤醒到期的任务:
void xTaskIncrementTick(void)
{TCB_t *pxTCB;TickType_t xItemValue;/* 更新系统时基计数器 */const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;/* ===== 处理溢出:切换延时列表 ===== */if (xConstTickCount == (TickType_t)0U){taskSWITCH_DELAYED_LISTS(); /* 溢出时切换列表 */}/* ===== 检查是否有任务需要唤醒 ===== */if (xConstTickCount >= xNextTaskUnblockTime){for (;;){/* 当前延时列表为空? */if (listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE){xNextTaskUnblockTime = portMAX_DELAY;break;}else{/* 获取延时列表头部的任务 */pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList);xItemValue = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem));/* 检查当前时间是否已到达唤醒时间 */if (xConstTickCount < xItemValue){xNextTaskUnblockTime = xItemValue;break;}/* 唤醒任务:从延时列表移除,添加到就绪列表 */(void)uxListRemove(&(pxTCB->xStateListItem));prvAddTaskToReadyList(pxTCB);}}}/* 任务切换 */portYIELD();
}
taskSWITCH_DELAYED_LISTS()函数
当系统时基计数器溢出时,需要切换两个延时列表的角色:
#define taskSWITCH_DELAYED_LISTS() \
{ \List_t *pxTemp; \pxTemp = pxDelayedTaskList; \pxDelayedTaskList = pxOverflowDelayedTaskList; \pxOverflowDelayedTaskList = pxTemp; \xNumOfOverflows++; \prvResetNextTaskUnblockTime(); \
}
切换逻辑:
- 交换
pxDelayedTaskList和pxOverflowDelayedTaskList的指向 - 原来的溢出列表成为当前列表,原来的当前列表成为溢出列表
- 溢出计数器加1
- 重新计算下一个任务的解锁时刻
prvResetNextTaskUnblockTime()函数
static void prvResetNextTaskUnblockTime(void)
{TCB_t *pxTCB;if (listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE){/* 当前延时列表为空,设置为最大值 */xNextTaskUnblockTime = portMAX_DELAY;}else{/* 获取列表头部任务的唤醒时间 */pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList);xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem));}
}
3.3 修改taskRESET_READY_PRIORITY()函数
在延时列表方案中,清除优先级位图时需要检查该优先级下是否还有其他就绪任务:
#define taskRESET_READY_PRIORITY(uxPriority) \
{ \/* 只有当该优先级的就绪链表为空时,才清除优先级位图中的对应位 */ \if (listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[(uxPriority)])) == 0) \{ \portRESET_READY_PRIORITY((uxPriority), (uxTopReadyPriority)); \} \
}
为什么需要这个判断?
之前只有一个就绪列表,任务延时意味着从就绪列表移除,该优先级肯定没有其他任务了。但引入延时列表后,情况变得复杂:
- 同一个优先级可能有多个任务
- 其中一个任务延时,从就绪列表移除
- 但该优先级下可能还有其他就绪任务(如同优先级的其他任务)
- 因此不能简单地清除优先级位图中的位,必须先检查链表是否真的为空
4. 代码修改总结
| 修改点 | 原实现 | 新实现 |
|---|---|---|
| 延时管理 | 扫描所有就绪列表 | 使用延时列表,按唤醒时间排序 |
| 任务延时 | 设置TCB中的xTicksToDelay |
插入延时列表 |
| SysTick处理 | 遍历所有任务减1 | 只处理到期的任务 |
| 优先级位图清除 | 直接清除 | 检查链表是否为空再清除 |
| 溢出处理 | 无 | 双列表切换机制 |
5. 实验现象
实验现象与上一章相同,但实现方式发生了本质变化:
波形特征:
flag1高电平:20msflag2高电平:40ms
效率提升:
- 原来每次SysTick中断需要遍历所有优先级的就绪列表(最坏情况O(N))
- 现在只需检查延时列表头部(O(1)),大幅提升了系统效率
6. 关键要点总结
-
延时列表的核心优势:将O(N)的扫描操作优化为O(1)的头部检查
-
两个延时列表的设计:优雅地处理了系统时基计数器溢出的边界情况
-
xNextTaskUnblockTime的作用:作为“闹钟”,让系统知道下一个任务何时被唤醒,避免无意义的检查 -
升序排列:延时列表按唤醒时间升序排列,确保最先到期的任务在头部
-
优先级位图清除的条件:必须确认该优先级链表确实为空,避免误清除
通过引入任务延时列表,FreeRTOS的任务延时管理从“被动扫描”变为“主动唤醒”,这是实时操作系统设计中的一个重要优化思路——用空间换时间,用结构化管理替代穷举扫描。
