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

04-优先级与延时链表

优先级与延时链表

1. 优先级

前几篇文章比较详细的解释了任务的创建过程,任务的创建就是将任务的TCB挂载到任务的就绪链表中。在优先级没有添加之前,任务在创建的过程中会指定要挂载到哪个就绪链表,例如任务1挂载到pxReadyTasksLists[1]。在任务的切换过程中,也是采用轮流切换的方式,如果任务1和任务2都处于延时状态,则执行IDLE任务。

添加优先级后的执行过程在任务创建和任务切换过程中与之前的流程会有所不同,下面具体说一下有哪些不同。

1.1 带优先级的任务创建

在这里插入图片描述

任务在创建过程中,要指定该任务的优先级,这里的优先级数值对应着任务就绪链表数组的下标,0是最低优先级;

执行xTaskCreateStatic函数,创建对应的任务,下面是具体的代码执行过程。

  • 调用prvInitialiseNewTask函数,在该函数内,将传入的优先级赋值于TCB;
  • 执行prvAddNewTaskToReadyList函数,将该任务控制块添加到就绪链表中;
    • 这段代码属于临界区代码,在执行过程中,不能被其他中断打断;
    • 判断pxCurrentTCB数值,如果为空,则指向新申请的任务控制块,如果是第一次创建任务,调用prvInitialiseTaskLists函数初始化任务的就绪链表;
    • 如果pxCurrentTCB不为空,则判断当前任务的优先级是否小于新创建任务的优先级,如果满足,则改变pxCurrentTCB的指向;
    • 最后将新创建的任务控制块添加到对应的就绪链表中;
      在这里插入图片描述
1.2 带优先级的任务切换

任务的切换是在PendSV的中断函数xPortPendSVHandler中完成的。该中断函数在切换中会调用vTaskSwitchContext函数,该函数是切换的关键,添加优先级后的函数内容如下:

void vTaskSwitchContext( void )
{    taskSELECT_HIGHEST_PRIORITY_TASK();
}

taskSELECT_HIGHEST_PRIORITY_TASK函数找到最高优先级的任务:

#define taskSELECT_HIGHEST_PRIORITY_TASK()														    \
{																								    \UBaseType_t uxTopPriority;																		\/* 寻找最高优先级 */								                            					\portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								    \/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */                                       		\listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		    \} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
  • uxTopReadyPriority变量存放着不同任务的优先级,通过portGET_HIGHEST_PRIORITY函数可以将当前状态下最高优先级赋值到uxTopPriority变量;

    在这里插入图片描述

  • portGET_HIGHEST_PRIORITY函数定义如下:

    #define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
    
    • clz指令: 计算变量的前导零,也就是最高位的1前面有几个零。
  • 通过这种方式,实现uxReadyPriorities变量上的位次代表优先级的大小,是否置1代表该优先级是否准备好。从而完成了最高优先级的查找。

采用嘀嗒定时器中断的形式实现了阻塞延时,针对阻塞延时,也有一些需要修改的地方,首先是需要在延时函数中将对应的任务挂起,这里挂起操作是在uxTopReadyPriority变量中,将任务对应优先级的位清零;然后是在嘀嗒中断中,如果如果延时时间到,则将对应任务优先级的那一位置1,从而在后续任务切换的过程中,可以找到该任务。

//更新系统时基
void xTaskIncrementTick( void ) 
{ TCB_t *pxTCB = NULL; BaseType_t i = 0; /* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */const TickType_t xConstTickCount = xTickCount + 1; xTickCount = xConstTickCount; /* 扫描就绪列表中所有任务的xTicksToDelay,如果不为0,则减1 */for (i=0; i<configMAX_PRIORITIES; i++) { pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) ); if (pxTCB->xTicksToDelay > 0) { pxTCB->xTicksToDelay --; 
+						if ( pxTCB->xTicksToDelay ==0 ) //延时时间到,任务就绪
+						{ 
+								taskRECORD_READY_PRIORITY( pxTCB->uxPriority ); 
+						}} } /* 任务切换 */portYIELD(); 
} 

2. 任务延时链表

在实现任务优先级的基础之上,实现延时链表。在延时链表实现之前,考虑下面几个问题:

  • 为何要实现任务延时链表?任务延时链表的作用是什么?
  • 如何实现延时链表,在代码中需要关注的点是什么?
  • 实现延时链表之后,任务之间的切换过程是如何进行的?
2.1 为何实现?

探讨为何实现延时链表,这部分思考的不是特别的清晰,问了以下DeepSeek,得到了如下的答复:

  • CPU效率:避免忙等待,让CPU在任务延时期间执行其他有用工作;
  • 精确计时:按唤醒时间排序,确保任务在正确的时间恢复执行;
  • 可扩展性:支持大量任务的延时管理而不显著增加开销;
  • 资源优化:只在需要时管理延时任务,不占用额外资源;
  • 系统稳定性:正确处理时钟计数器溢出的边界情况;

在这里不对这个问题做深入的分析了,要知道一点是在RTOS中,不可能仅有就绪链表,对于一些延时阻塞任务,也会有对应的链表。当涉及到多个链表时,任务之间的切换是关注的重点。

2.2 如何实现?

要实现延时链表,首先肯定是要创建延时链表,然后当任务中有阻塞延时代码时,需要将该任务从就绪链表中删除,在延时链表中插入。由于阻塞延时是在嘀嗒定时器中断中判断时间的,因此对于嘀嗒中断也需要关注。

2.2.1 创建延时链表
/* 初始化任务相关的列表 */
void prvInitialiseTaskLists( void )
{UBaseType_t uxPriority;for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ){vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );}vListInitialise( &xDelayedTaskList1 );   //创建延时链表1vListInitialise( &xDelayedTaskList2 );   //创建延时链表2pxDelayedTaskList = &xDelayedTaskList1; pxOverflowDelayedTaskList = &xDelayedTaskList2;
}

通过上述代码可以创建延时链表,创建后的链表如下所示。在代码中,创建了两个延时链表,第二个延时链表的作用是当延时溢出时,将任务挂载到溢出延时链表中。在这里忽略具体的定义,认为是一个即可。
在这里插入图片描述

prvInitialiseTaskLists函数在第一次创建任务时会执行。

2.2.2 添加任务至延时链表

当在任务的入口函数中执行vTaskDelay函数会将任务的挂载点切换,由就绪链表切换至延时链表,同时进行任务调度。

static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait )
{TickType_t xTimeToWake;    /* 获取系统时基计数器xTickCount的值 */const TickType_t xConstTickCount = xTickCount;/* 将任务从就绪列表中移除 */if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ){/* 将任务在优先级位图中对应的位清除 */portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );}/* 计算延时到期时,系统时基计数器xTickCount的值是多少 */xTimeToWake = xConstTickCount + xTicksToWait;/* 将延时到期的值设置为节点的排序值 */listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );/* 溢出 */if( xTimeToWake < xConstTickCount ){vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );}else /* 没有溢出 */{vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );/* 更新下一个任务解锁时刻变量xNextTaskUnblockTime的值 */if( xTimeToWake < xNextTaskUnblockTime ){xNextTaskUnblockTime = xTimeToWake;}}	
}

首先是获取系统嘀嗒中断的计数值xTickCount,该数值为全局变量,每次进入嘀嗒中断后会加一。然后将该任务从就绪链表中删除,删除任务优先级对应的位;之后计算要延时的时间,这个时间等于xTickCount和要延时时间的和,最后根据这个和的大小,将任务插入到延时链表中。那么这样就实现了任务延时链表中,按照升序来排序的,即延时时间短的在链表的头部。除此之外,当延时时间小于xNextTaskUnblockTime数值时,还需更新xNextTaskUnblockTime,该数值代表延时链表所有结点中,最小的延时时间是多少。
在这里插入图片描述

2.2.3 嘀嗒中断

在中断中对于任务的处理逻辑与vTaskDelay相反,中断中要判断任务的延时是否到期,如果到期,则将该任务从延时链表删除,然后添加至就绪链表,最后执行任务调度,具体的代码逻辑如下:

void xTaskIncrementTick( void )
{TCB_t * pxTCB;TickType_t xItemValue;const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;/* 如果xConstTickCount溢出,则切换延时列表 */if( xConstTickCount == ( TickType_t ) 0U ){taskSWITCH_DELAYED_LISTS();}/* 最近的延时任务延时到期 */if( xConstTickCount >= xNextTaskUnblockTime ){for( ;; ){if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE ){/* 延时列表为空,设置xNextTaskUnblockTime为可能的最大值 */xNextTaskUnblockTime = portMAX_DELAY;break;}else /* 延时列表不为空 */{pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );/* 直到将延时列表中所有延时到期的任务移除才跳出for循环 */if( xConstTickCount < xItemValue ){xNextTaskUnblockTime = xItemValue;break;}/* 将任务从延时列表移除,消除等待状态 */( void ) uxListRemove( &( pxTCB->xStateListItem ) );/* 将解除等待的任务添加到就绪列表 */prvAddTaskToReadyList( pxTCB );}}}/* xConstTickCount >= xNextTaskUnblockTime *//* 任务切换 */portYIELD();
}

首先在函数内对全局变量xTickCount进行加一,然后判断当前时刻是否有任务延时结束,这里直接比较xNextTaskUnblockTime数值,因为该数值记录着延时链表中最小的延时时间。如果有到期的,则从延时链表中删除,在就绪链表中插入。
在这里插入图片描述

这里有几个特殊的情况:

  • 如果延时链表中所有任务延时时间都一致,那么代码会将所有的任务都从延时链表删除,加入到就绪链表中,这时跳出for循环则是通过*if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )*实现。
  • 如果当前延时时间达到时对于的任务只有2项,其他任务还未到期。代码会将到期的2项任务从延时链表中删除,加入就绪链表。这时通过*if( xConstTickCount < xItemValue )*判断跳出循环,因为下一个任务的延时时间肯定比当前时刻要大了。
    在这里插入图片描述
2.3 实现后任务如何切换?

在这里插入图片描述

整个切换的过程如上图所示,假设当前系统中有三个任务,分别为任务1、任务2和IDLE任务,对应的优先级分别是2、1、0。任务2的优先级最高,在任务创建之后,第一个要执行的任务就为任务2。在任务2和1的入口函数中,加入了阻塞延时函数的调用,延时时间为2秒,在IDLE任务的入口函数中,执行空循环。

按照图示的表达,整个切换过程由以下几步组成:

  1. 当前任务TCB指向优先级2链表的Task2TCB结点;
  2. 执行pxCurrentTCB所指向的任务,将任务加载到CPU中;
  3. 在任务2的入口函数中调用了vTaskDelay函数,执行prvAddCurrentTaskToDelayedList操作;
  4. 将任务2从就绪链表中删除;
  5. 任务2插入到延时链表,此时延时链表为空,所以任务2是第一个结点;
  6. 执行任务调度,修改pxCurrentTCB指向,使其指向任务优先级为1的任务1;
  7. 将任务1加载至CPU;
  8. 执行任务1中的入口函数,调用vTaskDelay函数,进行阻塞延时;
  9. 在就绪链表中删除任务1;
  10. 将任务1插入到就绪链表中,因为任务1和2的延时时间相同,所以任务1插入到了任务2的后面,如果任务1延时时间小,则插入到任务2的前面;
  11. 执行任务调度,修改pxCurrentTCB指向,使其指向IDLE任务;
  12. 加载IDLE任务至CPU,此时IDLE任务会执行;
  13. 当系统嘀嗒定时达到20ms时,会将任务2从延时链表中删除,同时设置任务优先级的位图;
  14. 将任务2加入到优先级为2的就绪链表;
  15. 因为任务1的延时与任务2相同,则也将任务1从延时链表删除;
  16. 将任务1加入到优先级为1的就绪链表,执行调度,此时pxCurrentTCB又指向了任务2。
2.4 程序仿真异常排查
2.4.1 问题描述

在移植任务延时链表相关的代码后,执行仿真,发现结果与代码不符合,flag1和flag2状态不变;
在这里插入图片描述

当仿真强制结束时,PC指向了硬件错误例外,那说明在程序的运行过程中,出现了硬件错误。

2.4.2 排查过程
  • 排查新增函数,例如xTaskIncrementTick、prvAddCurrentTaskToDelayedList等代码逻辑;

  • 排查是否是由于链表的操作函数引起,拷贝参考工程与链表有关的代码,结果仍异常;

  • 只能仿真一步一步排查,了解执行到什么阶段会触发硬件错误例外

    • 整个程序流程为先执行任务2,当任务2遇到延时时,执行任务1,然后任务1遇到延时,执行IDLE任务;

    • 通过仿真发现问题出现的时机为任务1执行遇到延时时去执行任务IDLE的过程中,在任务切换后,pxCurrentTCB变量正确应该指向IDLE任务的TCB,异常时,pxCurrentTCB指向为0。
      在这里插入图片描述

    • 由于pxCurrentTCB是非法的地址指向,那么在后续切换任务中,会找这个非法指向的入口函数,最终异常的入口函数导致了HardFault_Handler。

  • 有了上述基本的分析,接下来通过任务栈分析为何pxCurrentTCB指向为0?

    在代码中任务栈1和2的定义如下,在两者中间夹着Task1TCB数据结构;对应到下面的图中粉红色代表任务栈2,绿色代表Task1TCB,蓝色代表任务栈1。

    //任务栈1
    #define TASK1_STACK_SIZE                    20   //128
    StackType_t Task1Stack[TASK1_STACK_SIZE];					//StackType_t = uint32_t = 1 wordTCB_t Task1TCB;//任务栈2
    #define TASK2_STACK_SIZE                    20    //128
    StackType_t Task2Stack[TASK2_STACK_SIZE];
    

    Task2TCB结构体pxTopOfStack变量初始的数值为0x2x0d0,进入任务2的入口函数后,该数值变成0x2x110,在任务2的入口函数中会使用PSP线程栈空间,栈自顶向下增长; 当将任务2的相关寄存器保存至栈中时,由于vTaskDelay入口函数需要保存在栈中,那么保存在栈空间的内容会向下移动,在移动后发生了溢出,覆盖了Task1TCB中的uxPriority和xTicksToDelay变量,见下面洋红色区域。

    任务2切换任务1的过程是没有问题的,但是当任务1切换IDLE任务时,首先会将任务1在uxTopReadyPriority位图中清除,关键问题点来了,由于任务栈1中uxPriority数值已经不是1,改成了0x2000,0110,位图清除将不会成功。uxTopReadyPriority不会改成1,任然是3。然后会执行taskSELECT_HIGHEST_PRIORITY_TASK函数。

    #define taskSELECT_HIGHEST_PRIORITY_TASK()														  
    {																								   UBaseType_t uxTopPriority;																		/* 寻找最高优先级 */								                            portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								   /* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */                                       listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		    
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */
    

    uxTopPriority数值为1,那么会去pxReadyTasksLists[1]中查找任务,而在任务1切换IDLE中已经从就绪链表pxReadyTasksLists[1]中删除了任务1的挂载点,因此pxCurrentTCB会指向异常的地址0x0000,0000,后续切换就会异常。
    在这里插入图片描述

2.4.3 修改措施

为了验证上述分析,可以将任务栈大小由20改成22,重新编译仿真后,代码正常运行。此时新的栈空间起始地址会上移2个格,就不会覆盖Task1TCB的变量。综上过程,该程序异常是由于栈空间设置过小,导致了栈溢出,溢出后的内容覆盖了任务1的控制块,从而在任务1切IDLE时,找不到IDLE的TCB,触发了异常。

3. 总结

到目前为止,代码的状态越来越像一个操作系统了,任务之间的切换有了优先级,同时实现了阻塞延时,在延时过程中将任务加入至延时链表。与此同时,代码量也在提升,排查问题的难度在增加。这次代码移植的过程中就遇到了由于任务栈设定过小,导致任务切换中pxCurrentTCB指向了异常的地址,引发了硬件例外,程序的仿真结果与实际不符。经过不断的仿真排查,才找到问题的原因,将任务1和2的栈空间由20改成22之后,程序正常运行。

http://www.dtcms.com/a/504797.html

相关文章:

  • 开个做网站的公司网页版微信怎么加好友
  • 企业怎样选择域名做网站网站建设与维护论文
  • 做网站公司关键词阿里巴巴国际站入驻
  • 小规模企业做网站wordpress批量 添加别名
  • 天津网站建设揭秘创建网页教程
  • 上海专业做网站排名学生html美食静态网页代码
  • 蓝色科技企业网站模板免费下载老鬼seo
  • 如何创建自己公司的网站asp.net 网站启动慢
  • 网站引导页设计外发加工厂联系方式
  • 学做网站用谁的书个人发布房源的网站
  • 低成本做网站 百知人力资源管理就业方向
  • [嵌入式系统-150]:智能机器人(具身智能)内部的嵌入式系统以及各自的功能、硬件架构、操作系统、软件架构
  • 如何用flashfxp通过ftp访问网站服务器下载网站代码网站百度收录快
  • 企业英文网站建设的重要性广东省优质高职院校建设网站
  • 打工人日报#202510019
  • 深度学习周报(10.13~10.19)
  • 云南网站建设是什么泉州网站建设方案服务
  • 无锡做智能网站城建亚泰建设集团网站
  • 实战长尾关键词优化SEO效果
  • 具身智能路线
  • 网站制作公司交接网站不能上传附件
  • 天河做网站免费个人网站建设大全
  • 网站建设dujujiangxin海口网站制作
  • 山东省城乡建设部网站首页php实现网站消息推送
  • 毕业设计做网站还是系统好做网站图片大会导致慢
  • 给人做网站挣钱吗自定义wordpress首页标题
  • 1 建设网站目的公众号推送怎么制作
  • legacyforge插件的使用说明
  • 网站设计如何自学青岛工程有限公司
  • 濮阳市做网站jsp购物网站开发视频