【学习日记】【FreeRTOS】时间片的实现

前言

本文以野火的教程和代码为基础,对 FreeRTOS 中时间片的概念作了解释,并且给出了实现方式,同时发现并解决了野火教程代码中的 bug。

一、时间片是什么

在前面的文章中,我们已经知道任务根据不同的优先级被放入就绪列表中不同下标的链表中,先执行优先级高的链表。而且我们知道,在一条相同优先级的就绪链表中可以存放多个 TCB。

那么,你有没有想过,当一条相同优先级的就绪链表中存放了多个 TCB 时,怎么执行?有些人可能会想:相同优先级的话就顺序执行呗,执行完任务1就接着执行任务2,这样一直往下执行就可以啦。

那如果我们在一个任务中放入了耗时很长的代码,或者更极端一些,放入一个死循环,那我们的任务岂不是就永远无法执行完毕,也就永远没办法切换到相同优先级的其他任务了。

所以,我们在这里引入了对于时间片的支持。**时间片(time slice)实际上就是为了让 CPU 资源能较为公平地分配到每一个任务中。在相同的优先级中,每个任务按照固定顺序依次执行一个时间片的时间,然后切换到下一个任务。**如果一个任务在时间片结束之前没有执行完,它将被挂起,任务的上下文将被保留,并在下一个循环中重新获得时间片后继续执行上次没有执行完的代码。

通过划分时间片,操作系统能够公平地分享处理器时间给多个任务,使得它们能够以看似同时的方式进行处理,提高了系统的并发性和响应性能。

二、时间片代码的实现

其实在前面的文章中,我们的代码已经可以支持时间片了,但是为了让代码效率更高,还是需要对代码进行一些优化。主要是以下这些代码:

  • SysTick 中断 xPortSysTickHandler()
  • 时基计数函数 xTaskIncrementTick()
  • 时间片的核心实现

1. SysTick 中断 xPortSysTickHandler() 的修改

之前的代码是每进入一次 SysTick 中断后都调用一次 taskYIELD() 进行任务的切换。

但实际上,进入中断后并不一定需要切换任务。需要进行任务切换的有以下这些情况:

  • 当前执行的这个任务的优先级不是最高的
  • 当前执行的这个任务的优先级虽然是最高的,但是有其他优先级和它一样的任务在等待执行

所以我们在 SysTick 中断中调用时基计数函数 xTaskIncrementTick(),并在 xTaskIncrementTick() 对上面所说的情况进行判断,返回是否需要进行任务切换,再在 SysTick 中断看情况是否进行任务切换。

  • 代码:
/*
*************************************************************************
*                             SysTick中断服务函数
*************************************************************************
*/
void xPortSysTickHandler( void )
{/* 关中断 */vPortRaiseBASEPRI();{/* 更新系统时基 */if( xTaskIncrementTick() != pdFALSE ){taskYIELD();}}/* 开中断 */vPortClearBASEPRIFromISR();
}

2. 时基计数函数 xTaskIncrementTick() 的修改

  • 添加一个返回值 xSwitchRequired,标识是否需要进行任务切换,初始化为 pdFALSE,表示不需要任务切换
  • 遇到这些情况,就修改 pdFALSE 为 pdTRUE,表示需要进行任务切换
    • 当前执行的这个任务的优先级不是最高的
    • 当前执行的这个任务的优先级虽然是最高的,但是有其他优先级和它一样的任务在等待执行
  • 定义了 configUSE_PREEMPTION 为 1,表示使用任务抢占,也就是优先级
  • 定义了 configUSE_TIME_SLICING 为 1,表示使用时间片

实际上,野火的教程中出现了两个 bug:

  • 添加了返回值 xSwitchRequired 却没有 return
  • configUSE_TIME_SLICING 的定义被注释掉了,也就是说,野火的代码实际上没有完全实现时间片的功能
    我们把上面这些 bug 修复,得到了以下的代码:
#define configUSE_PREEMPTION            1
#ifndef configUSE_TIME_SLICING#define configUSE_TIME_SLICING 1
#endif//系统时基计数
BaseType_t xTaskIncrementTick( void )
{TCB_t * pxTCB;TickType_t xItemValue;    BaseType_t xSwitchRequired = pdFALSE;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 );#if (  configUSE_PREEMPTION == 1 ){if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){xSwitchRequired = pdTRUE;}}#endif /* configUSE_PREEMPTION */}}}/* xConstTickCount >= xNextTaskUnblockTime */#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ){if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ){xSwitchRequired = pdTRUE;}}#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) *//* 任务切换 *///portYIELD();return xSwitchRequired;
}

3. 时间片的核心实现——列表索引的妙用

那么如果要实现时间片,也就是说在进行任务切换的时候,我们要获取到同一优先级链表下当前任务的下一个任务的指针,然后再切换到下一个任务去执行。这要怎么实现呢?

我们调用任务切换函数后,就会产生 PendSV 中断,中断中会调用上下文切换函数,而在上下文切换函数中,会调用选择优先级最高任务的函数,这个函数中首先确定最高的优先级,然后更新当前任务指针(listGET_OWNER_OF_NEXT_ENTRY())。

实际上,listGET_OWNER_OF_NEXT_ENTRY() 就是实现时间片的关键。
还记得我们在进行链表实现时,链表中有一个 pxIndex 的索引吗,listGET_OWNER_OF_NEXT_ENTRY() 使用这个索引,记住了上一次执行的任务的位置,这样就能在每次调用时获取当前任务的下一个任务的指针。

  • 代码:
/* 获取链表节点的OWNER,即TCB */
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )										\
{																							\List_t * const pxConstList = ( pxList );											    \/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点,如果当前链表有N个节点,当第N次调用该函数时,pxInedex则指向第N个节点 */\( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							\/* 当前链表为空 */                                                                       \if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	\{																						\( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						\}																						\/* 获取节点的OWNER,即TCB */                                                             \( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											 \
}

三、时间片代码实现的验证

我们定义两个优先级较低(优先级为 2)的任务,并在这两个任务中使用循环延时改变电平,然后再定义一个优先级较高(优先级为 3)的任务,在这个任务中使用阻塞延时改变电平。代码如下:

/* 任务1 优先级为2 */
void Task1_Entry( void *p_arg )
{for( ;; ){flag1 = 1;//vTaskDelay( 1 );delay (100);		flag1 = 0;delay (100);//vTaskDelay( 1 );        }
}/* 任务2 优先级为2 */
void Task2_Entry( void *p_arg )
{for( ;; ){flag2 = 1;//vTaskDelay( 1 );delay (100);		flag2 = 0;delay (100);//vTaskDelay( 1 );        }
}/* 任务2 优先级为3 */
void Task3_Entry( void *p_arg )
{for( ;; ){flag3 = 1;vTaskDelay( 2 );//delay (100);		flag3 = 0;vTaskDelay( 2 );//delay (100);}
}
  • 结果如下:
    任务3 延时 2 ms:
    在这里插入图片描述
    任务3 延时 10 ms:
    在这里插入图片描述
    可以看到,在任务3 没用接过 CPU控制权改变电平时,任务1 任务2 平均分配 CPU 使用时间,时间片的实现成功。

而如果我们的时间片功能没有实现,那么任务3 在进入阻塞延时后将交出 CPU 使用权,开始执行任务1。由于任务1 的延时是循环延时而不是阻塞延时,那么 CPU 将一直执行任务1 直到任务3 的阻塞延时结束再跳到任务3 执行,而任务2 永远不会得到执行。

四、关于野火代码 bug 的讨论

在野火的官方例程中,configUSE_TIME_SLICING 的定义被注释掉了,也就是说,野火的代码实际上没有完全实现时间片的功能。

以上文定义的验证代码为例,注释掉configUSE_TIME_SLICING,导致在任务3 交出 CPU 控制权给其他任务后,如果其他任务里面是一个循环,那么在任务3 重新获得 CPU 控制权前将一直执行这个任务;而得益于 listGET_OWNER_OF_NEXT_ENTRY(),在任务3 下一次交出 CPU 控制权后,将会执行和刚才任务不同的另一个任务。

也就是出现了一种怪异的现象:表面看起来优先级较低的任务1 和 2 有轮流执行,似乎实现了时间片算法。但实际上正确的表现应该是在任务3 交出 CPU 控制权后(此时任务3 中变量电平稳定为1 或者 0),任务1 任务2 在任务3 中变量电平稳定期间可以多次切换,实现真正的时间片;而不是直到任务3 改变电平后再进行任务1 和任务2 之间的相互切换。

同样是任务 3 阻塞延时为2ms:

  • 注释后(没有真正实现时间片的功能):
    在这里插入图片描述

  • 没有注释(真正实现时间片功能):
    在这里插入图片描述
    以上就是本文的全部内容啦!

后记

如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/46193.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机竞赛 python的搜索引擎系统设计与实现

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; python的搜索引擎系统设计与实现 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;5分创新点&#xff1a;3分 该项目较为新颖&#xff…

1、攻防世界第一天

1、网站目录下会有一个robots.txt文件&#xff0c;规定爬虫可以/不可以爬取的网站。 2、URL编码细则&#xff1a;URL栏中字符若出现非ASCII字符&#xff0c;则对其进行URL编码&#xff0c;浏览器将该请求发给服务端&#xff1b;服务端会可能会先对收到的url进行解码&#xff0…

深度解读波卡 2.0:多核、更有韧性、以应用为中心

本文基于 Polkadot 生态研究院整理&#xff0c;有所删节 随着波卡 1.0 的正式实现&#xff0c;波卡于 6 月 28 日至 29 日在哥本哈根举办了年度最重要的会议 Polkadot Decoded 2023&#xff0c;吸引了来自全球的行业专家、开发者和爱好者&#xff0c;共同探讨和分享波卡生态的…

ByteV“智农”平台--数字乡村可视化

“智农”平台基于自主可控的数字孪生技术、物联网技术、大数据技术&#xff0c;构建全流程的新型农业一体化管理平台&#xff0c;围绕产运销管理全流程&#xff0c;实现生产->存储->包装->运输->销售的全链条管理。融合农业数据管理、农业数据预警显示、多维数据综…

达梦数据库8用户管理以及忘记sysdba密码修改办法

达梦数据库8用户管理&达梦数据库v8忘记sysdba密码&#xff0c;修改办法。 达梦数据库8用户管理1.创建用户的语法:2.锁定/解锁用户3.修改用户的密码&#xff08;同样要符合密码策略PWD_POLICY&#xff09;4.修改用户默认表空间5.删除用户6.同样地可以使用DM管理工具进行创建…

【工具】 删除Chrome安装的“创建快捷方式”

创建Chrome的快捷方式&#xff0c;可以放在桌面&#xff0c;想用时双击就可以打开网页&#xff0c;比书签&#xff08;brookmark&#xff09;结构化管理更方便。 但是&#xff0c;安装一时爽&#xff0c;卸载有问题。 如果用 windows 控制面板\所有控制面板项\程序和功能 卸载…

Facebook 应用未启用:这款应用目前无法使用,应用开发者已得知这个问题。

错误&#xff1a;Facebook 应用未启用:这款应用目前无法使用&#xff0c;应用开发者已得知这个问题。应用重新启用后&#xff0c;你便能登录。 「应用未经过审核或未发布」&#xff1a; 如果一个应用还没有经过Facebook的审核或者开发者尚未将应用发布&#xff0c;那么它将无法…

java学习004

常用数据结构对应 php中常用的数据结构是Array数组&#xff0c;相对的在java开发中常用的数据结构是ArrayList和HashMap&#xff0c;它们可以看成是array的拆分&#xff0c;一种简单的对应关系为 PHPJAVAArray: array(1,2,3)ArrayListlArray: array(“name” > “jack”,“…

基于YOLOv8模型和PCB电子线路板缺陷目标检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOv8模型PCB电子线路板缺陷目标检测系统可用于日常生活中检测与定位PCB线路板瑕疵&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的目标检测&#xff0c;另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检…

算法leetcode|72. 编辑距离(rust重拳出击)

文章目录 72. 编辑距离&#xff1a;样例 1&#xff1a;样例 2&#xff1a;提示&#xff1a; 分析&#xff1a;题解&#xff1a;rust&#xff1a;二维数组&#xff08;易懂&#xff09;滚动数组&#xff08;更加优化的内存空间&#xff09; go&#xff1a;c&#xff1a;python&a…

内网穿透实战应用-windows搭建WebDAV服务,并内网穿透公网访问【无公网IP】

windows搭建WebDAV服务&#xff0c;并内网穿透公网访问【无公网IP】 文章目录 windows搭建WebDAV服务&#xff0c;并内网穿透公网访问【无公网IP】1. 安装IIS必要WebDav组件2. 客户端测试3. cpolar内网穿透3.1 打开Web-UI管理界面3.2 创建隧道3.3 查看在线隧道列表3.4 浏览器访…

设计模式之代理模式(Proxy)的C++实现

1、代理模式的提出 在组件的开发过程中&#xff0c;有些对象由于某种原因&#xff08;比如对象创建的开销很大&#xff0c;或者对象的一些操作需要做安全控制&#xff0c;或者需要进程外的访问等&#xff09;&#xff0c;会使Client使用者在操作这类对象时可能会存在问题&…

Vue2到3 Day7 全套学习内容,众多案例上手(内付源码)

简介&#xff1a; Vue2到3 Day1-3 全套学习内容&#xff0c;众多案例上手&#xff08;内付源码&#xff09;_星辰大海1412的博客-CSDN博客本文是一篇入门级的Vue.js介绍文章&#xff0c;旨在帮助读者了解Vue.js框架的基本概念和核心功能。Vue.js是一款流行的JavaScript前端框架…

【Java 动态数据统计图】动态数据统计思路案例(动态,排序,数组)一(112)

需求&#xff1a;&#xff1a; 有一个List<Map<String.Object>>,存储了某年某月的数据&#xff0c; 数据是根据用户查询条件进行显示的&#xff1b;所以查询的数据是动态的&#xff1b;需按月份统计每个年月数据出现的次数&#xff0c;并且按照月份排序&#xff1…

c#设计模式-结构型模式 之 代理模式

前言 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时&#xff0c;访问对象不适合或者不能直接 引用目标对象&#xff0c;代理对象作为访问对象和目标对象之间的中介。在学习代理模式的时候&#xff0c;可以去了解一下Aop切面编程AOP切面编程_aop编程…

项目实战 — 博客系统③ {功能实现}

目录 一、编写注册功能 &#x1f345; 1、使用ajax构造请求&#xff08;前端&#xff09; &#x1f345; 2、统一处理 &#x1f384; 统一对象处理 &#x1f384; 保底统一返回处理 &#x1f384; 统一异常处理 &#x1f345; 3、处理请求 二、编写登录功能 &#x1f345; …

vue引入 import { decode } from ‘js-base64‘

vue引入 import { decode } from ‘js-base64’ package.json 里面加上 需要用的地方 加上 import { decode } from ‘js-base64’ let params decode(loook)最后 npm install

sh 脚本循环语句和正则表达式

目录 1、循环语句 1、for 2、while 3、until 2、正则表达式 1、元字符 2、表示次数 3、位置锚定 4、分组 5、扩展正则表达式 1、循环语句 循环含义 将某代码段重复运行多次&#xff0c;通常有进入循环的条件和退出循环的条件 重复运行次数 循环次数事先已知 循环次…

爱荷华州的一个学区正在使用ChatGPT来决定禁止哪些书籍

为了响应爱荷华州最近颁布的立法&#xff0c;管理员们正在从梅森市学校图书馆移除禁书&#xff0c;官员们正在使用ChatGPT帮助他们挑选书籍&#xff0c;根据公报和大众科学. 由州长金雷诺兹签署的禁令背后的新法律是教育改革浪潮的一部分&#xff0c;共和党立法者认为这是保护…

OLED透明屏案例:揭示技术创新的无限可能性

OLED透明屏作为一项创新性技术&#xff0c;在各个领域展现出了令人惊叹的应用潜力。 那么&#xff0c;尼伽便通过介绍一些具体的OLED透明屏案例&#xff0c;探索其在智能家居、汽车行业、商业展示、航空航天、教育与培训以及医疗健康等领域的成功应用。 这些案例将展示OLED透明…