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

news2024/9/24 3:20:17

前言

本文以野火的教程和代码为基础,对 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.coloradmin.cn/o/900718.html

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

相关文章

符号随机梯度下降算法SIGNSGD

考虑随机优化问题&#xff1a; 符号随机梯度下降(SIGNSGD)算法&#xff1a; 假设基础&#xff1a; 收敛定理&#xff1a; 联邦优化&#xff1a;

[Java优选系列第2弹]SpringMVC入门教程:从零开始搭建一个Web应用程序

想和你们分享我眼里的代码世界&#x1f5fa;️ 优选系列持续更新中&#x1f4ab; 一直在等你&#xff0c;你终于来啦&#x1f496; 绿色代表解释说明 黄色代表重点 红色代表精髓 SpringMVC是一个基于Java的Web框架&#xff0c;它使用了MVC&…

旅行越野SUV——捷途旅行者即将与大家见面!

从2020年开始,国内车市踊跃出许多性格鲜明的车型,例如坦克300、极氪001、蔚来ET5旅行版以及五菱悦野,虽然它们属于小众车型,但奈何销量都非常亮眼,这也从另一角度证明现如今的年轻消费群体很喜欢特点鲜明的汽车产品。前段时间,捷途汽车发布的捷途旅行者,一款定位旅行越野SUV的新…

JVM的前世今生之类加载过程

1. 什么是JVM VM是JavaVirtualMachine&#xff08;Java虚拟机&#xff09;的缩写&#xff0c;JVM是一种用于计算设备的规范&#xff0c;它是一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。它可以实现跨操作系统运行&#xff0c;即一…

CMake语法复习

前言 此文总结了库的制作和一些CMake常用的一些语法。 一&#xff1a;创建静态库和动态库 静态库的生成和使用 动态库的生成和使用 二&#xff1a;使用CMake来生成Makefile&#xff0c;生成可执行文件 顶层目录下的CMakeLists.txt project(HELLO) add_subdirectory(libhell…

设计模式之备忘录模式(Memento)的C++实现

1、备忘录模式的提出 在软件功能开发过程中&#xff0c;某些对象的状态在转换过程中&#xff0c;由于业务场景需要&#xff0c;要求对象能够回溯到对象之前某个点的状态。如果使用一些共有接口来让其他对象得到对象的状态&#xff0c;便会暴露对象的实现细节。备忘录模式是在不…

ps打开出现dll文件丢失怎么回事?该如何修复

今天在看到一个ps的相关问题&#xff0c;可能很多同学也遇到过&#xff0c;安装好ps软件之后&#xff0c;打开会提示无法启动此程序。然后会有各种文件的丢失一般都是xxx.dll文件的格式&#xff0c;并提示尝试重新安装该程序&#xff0c;以解决此问题。不但是PS&#xff0c;还有…

226、仿真-基于51单片机楼道教室走道智能灯光光照人体感应检测控制Proteus仿真设计(程序+Proteus仿真+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件设计 二、设计功能 三、Proteus仿真图 四、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&…

Hlang--用Python写个编程语言-函数与基本数据结构实现

文章目录 前言语法表述解析器修改词法解析函数节点函数节点解析List的解析实现解释器节点函数操作String和List处理总结前言 okey,经过一段时间的努力,接下来要实现的是函数。当然还有对应的基本数据结构,那么之后的话,我们的工作就开始进一步转换了。 那么在这块我们要实…

java:数据库连接池

概念 举个例子来说吧&#xff0c;假设我们开了一家餐馆&#xff0c;客人来了&#xff0c;我们就请一个服务员&#xff0c;使用完后再把他开除了&#xff0c;下个客人再来了&#xff0c;我们再请一个&#xff0c;使用完再开除。 这是不是我们现在使用 JDBC 连接数据库的场景&a…

电脑提示缺少vcomp140.dll怎样修复-vcomp140.dll丢失怎么办?

在使用电脑软件或者玩游戏时候&#xff0c;我们有时候会遇到提示系统丢失DLL状况&#xff0c;相信很多用电脑的都遇到过&#xff0c;我曾多次遇到提示丢失DLL&#xff0c;每次都要折腾了几小时&#xff0c;终于&#xff01;在我多次折腾后&#xff0c;&#xff0c;有幸得到这个…

ForkJoinPool 你真的明白和用对了吗

ForkJoinPool 是一个功能强大的 Java 类&#xff0c;用于处理计算密集型任务&#xff0c;使用 ForkJoinPool 分解计算密集型任务&#xff0c;并并行执行它们&#xff0c;能够产生更好的性能。它的工作原理是将任务分解成更小的子任务&#xff0c;使用分而治之的策略进行操作&am…

mysql数据传输到mssql

一、找开Navicat Premium 12 此时目标数据库会创建一个同名的表

ElasticSearch DSL语句(bool查询、算分控制、地理查询、排序、分页、高亮等)

文章目录 DSL 查询种类DSL query 基本语法1、全文检索2、精确查询3、地理查询4、function score &#xff08;算分控制&#xff09;5、bool 查询 搜索结果处理1、排序2、分页3、高亮 RestClient操作 DSL 查询种类 查询所有&#xff1a;查询所有数据&#xff0c;一般在测试时使…

【C++11新特性】lambda表达式

文章目录 1. lambda表达式概念2. lambda表达式语法3. lambda表达式应用 1. lambda表达式概念 lambda表达式是一个匿名函数&#xff0c;恰当使用lambda表达式可以让代码变得简洁&#xff0c;并且可以提高代码的可读性。 见见lambda表达式的使用 现在要对若干商品分别按照价格和…

Java并发----创建线程的三种方式及查看进程线程

一、直接使用 Thread // 创建线程对象 Thread t new Thread() {public void run() {// 要执行的任务} }; // 启动线程 t.start(); 例如&#xff1a; // 构造方法的参数是给线程指定名字&#xff0c;推荐 Thread t1 new Thread("t1") {Override// run 方法内实现…

LED驱动型IC芯片的原理介绍

一、LED驱动器是什么 LED驱动器&#xff08;LED Driver&#xff09;&#xff0c;是指驱动LED发光或LED模块组件正常工作的电源调整电子器件。由于LED PN结的导通特性决定&#xff0c;它能适应的电源电压和电流变动范围十分狭窄&#xff0c;稍许偏离就可能无法点亮LED或者发光效…

AraNet:面向阿拉伯社交媒体的新深度学习工具包

阿拉伯语是互联网上第四大最常用的语言&#xff0c;它在社交媒体上的日益增加为大规模研究阿拉伯语在线社区提供了充足的资源。然而&#xff0c;目前很少有工具可以从这些数据中获得有价值的见解&#xff0c;用于决策、指导政策、协助应对等。这种情况即将改变吗&#xff1f; …

Java日志框架-JUL

JUL全称Java util logging 入门案例 先来看着入门案例&#xff0c;直接创建logger对象&#xff0c;然后传入日志级别和打印的信息&#xff0c;就能在控制台输出信息。 可以看出只输出了部分的信息&#xff0c;其实默认的日志控制器是有一个默认的日志级别的&#xff0c;默认就…

串口通讯

USART是全双工同步通讯 在同步通信中&#xff0c;数据信号所传输的内容绝大多数属于有效数据&#xff0c;而异步通信中包含了各种帧的标识符&#xff0c;所以同步通讯的效率更高。但是同步通信对时钟要求苛刻&#xff0c;允许的误差小。而异步通信则允许双方的误差较大 比特率…