当前位置: 首页 > news >正文

FreeRTOS学习笔记(7):任务延时列表的实现

任务延时列表的实现

1. 任务延时列表的工作原理

1.1 为什么需要任务延时列表?

在之前的实现中,任务阻塞延时是通过在TCB中记录xTicksToDelay变量,然后在每次SysTick中断中扫描整个就绪列表,将所有任务的延时计数器减1。这种方法存在严重的效率问题:

  • 时间复杂度高:每次SysTick中断都需要遍历所有优先级的就绪链表
  • 资源浪费:当系统中任务数量较多时,CPU大量时间浪费在无效扫描上
  • 实时性差:扫描过程会延长中断处理时间,影响系统实时响应

1.2 延时列表的核心思想

FreeRTOS引入任务延时列表来优化延时管理,核心思想是:

只管理需要延时的任务,而不是扫描所有任务

工作原理

  1. 当任务需要延时(调用vTaskDelay)时,将其从就绪列表移除,插入到延时列表
  2. 延时列表中的任务按照唤醒时间(当前时间 + 延时值)升序排列
  3. 系统维护一个全局变量xNextTaskUnblockTime,记录下一个将要唤醒的任务的时间
  4. 每次SysTick中断时,只需比较当前时间与xNextTaskUnblockTime,判断是否有任务需要唤醒
  5. 如果当前时间 >= 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();                     \
}

切换逻辑

  • 交换pxDelayedTaskListpxOverflowDelayedTaskList的指向
  • 原来的溢出列表成为当前列表,原来的当前列表成为溢出列表
  • 溢出计数器加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高电平:20ms
  • flag2高电平:40ms

效率提升

  • 原来每次SysTick中断需要遍历所有优先级的就绪列表(最坏情况O(N))
  • 现在只需检查延时列表头部(O(1)),大幅提升了系统效率

6. 关键要点总结

  1. 延时列表的核心优势:将O(N)的扫描操作优化为O(1)的头部检查

  2. 两个延时列表的设计:优雅地处理了系统时基计数器溢出的边界情况

  3. xNextTaskUnblockTime的作用:作为“闹钟”,让系统知道下一个任务何时被唤醒,避免无意义的检查

  4. 升序排列:延时列表按唤醒时间升序排列,确保最先到期的任务在头部

  5. 优先级位图清除的条件:必须确认该优先级链表确实为空,避免误清除


通过引入任务延时列表,FreeRTOS的任务延时管理从“被动扫描”变为“主动唤醒”,这是实时操作系统设计中的一个重要优化思路——用空间换时间,用结构化管理替代穷举扫描

http://www.jsqmd.com/news/540112/

相关文章:

  • Qwen2.5-72B-Instruct-GPTQ-Int4部署教程:镜像内预置benchmark脚本使用
  • Chatbot Arena排行榜单实战指南:从数据采集到模型优化
  • 2026年包装机械行业铝塑泡罩包装机推荐指南 - 资讯焦点
  • 2026PCB生产环节过滤材料优质供应商推荐 - 资讯焦点
  • 智能客服方案库物流JSON格式优化:从数据冗余到高效解析
  • 基于数据库的制造过程查询智能客服:从零搭建与性能优化实战
  • 如何高效优化多语言模型:专业部署的完整策略
  • Harepacker-resurrected:开源WZ文件编辑工具提升MapleStory资源定制效率指南
  • 红外遥控技术原理与实现方案详解
  • 2026眼霜抗皱淡纹指南:分龄适配不踩雷,BFBY淡纹眼霜解锁全肤质守护 - 资讯焦点
  • 2026包装设备厂家推荐 高效适配多行业需求 - 资讯焦点
  • 为什么你的MacBook打不出€和™?Option键的26种高阶用法详解
  • 效率直接起飞!盘点2026年全网顶尖的AI论文工具
  • 螺旋压榨机产业链(2026更新):从产业集群分布到高性价比供应商推荐 - 资讯焦点
  • 基于AI多因子与流动性模型的黄金再定价分析:4500关口修复后的“黄金坑”是否成立?
  • 2026余干县口碑肠胃科优质机构推荐指南 - 资讯焦点
  • Burpsuite加解密插件Galaxy实战入门:从安装到首条解密请求
  • 检测+跟踪一体化!4.39M参数、8.3W功耗,轻量化模型让无人机在露天矿实时巡检
  • 西北旅游团选哪家靠谱?从4个维度筛选,避免踩坑! - 资讯焦点
  • OpenMemories-Tweak完整指南:如何安全解锁索尼相机的隐藏功能
  • vue新手技巧之区分组件
  • GPM降水数据在ArcGIS和Matlab中的实战应用:以南海区域为例
  • Windows 11 安装 RabbitMQ 消息队列(完整规范版)
  • 企业AI应用开发:从智能体概念到生产落地的完整指南
  • 南京装修公司真实口碑榜:2026业主用真金白银选出的10家靠谱企业 - 资讯焦点
  • 智能客服系统架构设计与实现:从NLP到高并发的技术选型
  • 从模拟到现实:用LLM生成的任务数据如何提升机器人泛化能力?XArm-7实测案例
  • WebSocket vs HTTP性能对比:HarmonyOS下如何选择实时通信方案?
  • 2026余干县优质医院推荐指南 - 资讯焦点
  • 跨越ROS版本鸿沟:构建稳定的一主多从分布式机器人系统