实时操作系统的任务调度示例之时间片

时间:2022-01-14 19:48:27

摘要

在之前的一篇博文实时操作系统的任务调度示例之抢占中,以实验和代码的形式讲解了不同优先级任务同时出在就绪态中,高优先级的任务总是先得到运行。这里就留下了一个问题,如果多个出于就绪态的任务具有相同优先级,它们之间互相不能抢占,那应该是谁得到CPU?答案有两种,有的系统实现是根据进入就绪态的时间对相同优先级的任务进行排序,先运行第一个,等第一个运行完成主动让出CPU之后再运行第二个,依次类推。这方式并不太常见,因为相同优先级的任务往往希望能够共享CPU,也就是第二种方式:按时间片轮转运行。即多个任务轮流得到操作系统的调度,每次得到调度的任务执行时间为一个“时间片”,如下图所示:


实时操作系统的任务调度示例之时间片

在图中,一共有3个用户任务Task1/Task2/Task3在轮流执行,t1/t2/t3/t4/t5任意两个相邻之间的时间间隔就是一个时间片,黑色箭头表示Tick中断发生,红色为操作系统内核执行调度程序。什么是Tick?在不同的资料里叫法不同,有的叫“时钟节拍”,有的叫“心跳”,“滴答”等等,它们的作用是一样的,就是在每个时间片到期的时候,触发一次tick中断,给调度器一个机会来运行本身,寻找下一个需要分配CPU的任务。在FreeRTOS里,Tick中断的频率是由FreeRTOSConfig.h里的宏configTICK_RATE_HZ来配置。它的意义是“1秒内执行多少次tick中断”,例如将其设置为100,则1秒内执行100次tick中断,时间片长度就是10ms。

本文用几个小实验来清晰的展示时间片轮转调度的运行情况,以及时间片的配置对于应用程序的影响。后面分析了整个过程的软硬件实现原理,最后给出了关于时间片配置的一些建议。

使用的系统是FreeRTOS V7.2.0,硬件平台是STM32F103VET6


实验1

将configTICK_RATE_HZ配置为100,即时间片为10ms。在main函数里创建3个优先级相等的task,每个Task的执行函数里就是循环打印一句话之后再原地空转delay 100ms.
xTaskCreate(TaskFunc1,( const signed char * )"Task1",64,NULL,tskIDLE_PRIORITY+3,NULL);
xTaskCreate(TaskFunc2,( const signed char * )"Task2",64,NULL,tskIDLE_PRIORITY+3,NULL);
xTaskCreate(TaskFunc2,( const signed char * )"Task3",64,NULL,tskIDLE_PRIORITY+3,NULL);
vTaskStartScheduler();
void TaskTunc1(void* p){  static int cnt = 0;  while (1)  {vTaskEnterCritical();  //进入临界区,避免多任务同时访问串口USART_OUT(USART1,"Task1 %d\n",++cnt);vTaskExitCritical();   //离开临界区m_delay(100);   }}


三个Task的任务执行函数几乎是一样的,就不重复贴了,程序运行起来,串口输出的log是这样子的:

实时操作系统的任务调度示例之时间片
从log看,大约每个100ms周期里,Task1、Task2、Task3都输出了一次log,可知3个Task得到了相等的CPU执行时间。 同时观察每100ms里,3个task输出log的间隔大约都是10ms左右,如第一次,15:08:44.078,Task得到CPU,输出一行log,之后开始进入空转;15:08:44.088,tick中断发生,调度器程序决定Task2为下一个要执行的task,同样的,它输出log之后也进入了空转;15:08:44.098,tick中断再次发生,开始执行Task3。这之后的70ms里,没有log输出,因为这段时间里3个Task都在空转。
请注意,这里的task是在空转,不是sleep。sleep会主动让出CPU,而空转是占用CPU的。作为对比,将程序里的m_delay改为sleep函数(FreeRTOS里就是vTaskDelay),运行结果如下:

实时操作系统的任务调度示例之时间片
可以看到周期还是100ms,但每个task输出log之后立马让出CPU,给其它的task运行,而不用再等待时间片耗尽由调度器来强制换出。,从时间戳上看3个task几乎是同时输出log

实验2

为了更清楚的演示时间片对于程序的影响,我们将configTICK_RATE_HZ来配置为10,即时间片为100ms。同时,每个Task的循环里打印一句话之后再原地打转时间改为delay 25ms。此时程序的运行结果就如下所示:
实时操作系统的任务调度示例之时间片
log的输出频率变成了25ms一次,每个Task得到CPU之后都会运行100ms(这个时间片够大吧),输出4行log再由调度器切换出去。

时间片周期是怎么确定的?

前面已经说过,任务时间片什么时候到期是由tick中断决定的。tick中断到底是个什么鬼?在FreeRTOS移植的过程中(如对移植过程有疑问请百度在STM32中移植FreeRTOS 纯净版),有两个重要的中断响应函数需要修改。如下所示:实时操作系统的任务调度示例之时间片
xPortSysTickHandler就是tick中断的响应函数,xPortPendSVHandler也很重要,下一节就要用到它。
那么tick中断在哪里配置的?多久触发一次?为什么修改configTICK_RATE_HZ会修改中断周期?在FreeRTOS的启动调度函数里“xPortStartScheduler”,调用了一个“prvSetupTimerInterrupt”,它的实现只有两句话:实时操作系统的任务调度示例之时间片
将里面的宏全部替换掉,就得到下面的表达式(请注意configTICK_RATE_HZ在这里有使用到,这里假设它为100,也就是时间片为10ms):实时操作系统的任务调度示例之时间片
这两个寄存器是意义为:实时操作系统的任务调度示例之时间片
systick使用内部FCLK时钟源,它的频率为72M。它装载的计数值是(72M/100-1),按照向下计数的方式,每个时钟减1,当减到0时,就触发一下systick中断,同时重新装载计数值,开始下次计数。

任务时间片到期以后,到底发生了什么?


时间片到期以后,中断处理函数xPortSysTickHandler被执行,它的最主要动作就写了一个寄存器:
实时操作系统的任务调度示例之时间片
这个寄存器的解释为:
实时操作系统的任务调度示例之时间片

这个寄存器位的作用就是“悬起PendSV”,其实就是触发一下PendSV中断,在里面执行任务上下文切换的动作。那么为什么不在systick中断里直接切换上下文,而要延迟到PendSV中断里去做? 这是因为SysTick是高优先级的中断,它的优先级高于普通用户IRQ,当有一个用户IRQ在执行的过程发生了Systick中断的话,会被Systick抢占。由于嵌入式系统要保证用户IRQ的响应时间尽量小,所以systick中断要做尽量少的事情就要返回,将较为耗时的上下文切换动作延迟到PendSV中断里去执行,它是一个低优先级的中断,它可以等到所有用户IRQ完毕了之后再执行。整个过程如下所示:
实时操作系统的任务调度示例之时间片

PendSV中断的Handler就是xPortPendSVHandler。楼主为它做了详细的注释,请耐心观看.没办法,汇编代码大多数都是这些看起来很无聊的动作,但是它们很重要,往往也不能轻易修改

实时操作系统的任务调度示例之时间片

里面有一行bl vTaskSwitchContext,它是一个子函数,用来寻找下一个要执行的函数(如何快速查找最高优先级的任务请参考这篇博文ucosii实时操作系统的任务调度),它里面核心的代码如下:
实时操作系统的任务调度示例之时间片

最后就是ListGET_OWER_OF_NEXT_ENTRY宏了,看它的名字也知道是寻找下一个链表里的元素
实时操作系统的任务调度示例之时间片

时间片的设置

最后说说configTICK_RATE_HZ应该设置为多大。和在PC上一样,我们开发嵌入式产品,终级目的也是执行应用程序,也就是一个一个的Task函数,做(少量的)数学计算和(大部分)控制逻辑,调度器的代码对于应用程序来说算是“无用功”,太多频繁的执行任务调度,有一点“喧宾夺主”的意思。而太少的执行调度程序也不好,就像实验2所展示的那样,就已经与代码里的实现不相符了,代码里的每个task都是期望“每25ms输出一次打印”。但现在的情况是一次task连续输出4次之后执行耗尽了时间片,之后需要等待200ms才能再次获得执行,而且,如果相同优先级的任务更多的话,那需要等待的时间也就是越长。同时,有的系统的定时器时间和sleep时间也是在tick中断进行累加和超时判断的,该tick的周期越长,则这些时间的精度也就越低。在楼主经历的嵌入式项目中,大多是将tick周期设为1~50ms,根据处理器的性能和期望的时间精度来做权衡。
实时操作系统的任务调度示例之时间片