摘要
在之前的一篇博文实时操作系统的任务调度示例之抢占中,以实验和代码的形式讲解了不同优先级任务同时出在就绪态中,高优先级的任务总是先得到运行。这里就留下了一个问题,如果多个出于就绪态的任务具有相同优先级,它们之间互相不能抢占,那应该是谁得到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
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是在空转,不是sleep。sleep会主动让出CPU,而空转是占用CPU的。作为对比,将程序里的m_delay改为sleep函数(FreeRTOS里就是vTaskDelay),运行结果如下:

实验2
为了更清楚的演示时间片对于程序的影响,我们将configTICK_RATE_HZ来配置为10,即时间片为100ms。同时,每个Task的循环里打印一句话之后再原地打转时间改为delay 25ms。此时程序的运行结果就如下所示:
时间片周期是怎么确定的?
前面已经说过,任务时间片什么时候到期是由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中断,同时重新装载计数值,开始下次计数。




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






时间片的设置
