摘要
在前一篇博文实时操作系统的任务睡眠中提到,FreeRTOS在任务调度的流程中,会用一个循环遍历的方法查找最高优先级就绪态任务,这是一种较为简单的方案,有较大的优化空间。本文先对FreeRTOS的任务调度算法进行分析,接着借鉴了UCOSII系统的两级查表方法,对该流程进行优化,最后用两种方法测试同一测试用例,可以清楚的看到优化的效果。
FreeRTOS中默认将任务分为5个优先级(0~4,该值可以修改宏configMAX_PRIORITIES以配置),所以需要一个5个元素的数组pxReadyTaskList[configMAX_PRIORITIES],数组的每个元素表示对应优先级链表头结点,处于就绪态的具有相同优先级的任务会挂接到相同的链表上。其数据结构如下图所示:
实时操作系统都是运行最高优先级的任务,所以调度器的一项重要任务就是查找最高优先级就绪态任务。为此,FreeRTOS定义了一个全局变量uxTopReadyPriority,它记录了当前系统里最高优先级的就绪态任务。每当有新的任务从其它状态如睡眠转为就绪态时,系统就会判断,新任务的优先级是否高于uxTopReadyPriority,如果是的话,将uxTopReadyPriority修改为当前的任务优先级。
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )
{
--uxTopReadyPriority;
}
这种方法在系统最多只支持5个优先级的时候似乎效率很好,但是有个小漏洞。考虑一下,假设系统里只有两个任务,一个优先级是1,一个优先级是255。两个任务频繁的切换,这样,每次当255的任务从就绪态转为睡眠,调度器要查找到下一个可以执行的任务,需要循环255次!
static unsigned long OSRdyGrp;
static unsigned char OSRdyTbl[32];
static unsigned long const OSMapTbl[] = {0x1, 0x2, 0x4, 0x8,
0x10, 0x20, 0x40, 0x80,
0x100,0x200, 0x400, 0x800,
0x1000, 0x2000, 0x4000, 0x8000,
0x10000, 0x20000, 0x40000, 0x80000,
0x100000, 0x200000, 0x400000, 0x800000,
0x1000000,0x2000000, 0x4000000, 0x8000000,
0x10000000, 0x20000000, 0x40000000, 0x80000000};
char const OSUnMapTbl[] = {
0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
};
需要注意,我们这里是假设系统支持0~255一共256个优先级,OSRdyGrp需要 256/8 =32位的数值来对应于OSRdyTbl的32个表项,
#include <stdio.h>
int bit_map(int n)
{
int i;
for (i = 7;i >= 0;i--)
if (n & (1 << i))
break;
if (i < 0)
return 0;
return i;
}
int _tmain(int argc, _TCHAR* argv[])
{
int n;
int tab[256]={0};
for (n=0;n<=0xff;n++)
{
tab[n] = bit_map(n);
}
for(n=0;n<=0xff;n++)
{
if(n%0x10==0)
printf("\n");
printf("%3d," , tab[n]);
}
printf("\n");
return 0;
}
在vTaskStartScheduler里将ildetask的对应的bitmap置位
OSRdyGrp |= 1;
OSRdyTbl[0] |= 1;
在任务转为就绪态时要将对应的bitmap置位
#define prvAddTaskToReadyQueue( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ) \
if( ( pxTCB )->uxPriority > uxTopReadyPriority ) \
uxTopReadyPriority = ( pxTCB )->uxPriority; \
OSRdyGrp |= OSMapTbl[(( pxTCB )->uxPriority >> 3)];\
OSRdyTbl[(( pxTCB )->uxPriority >> 3)] |= OSMapTbl[(( pxTCB )->uxPriority & 7)]; \
vListInsertEnd( ( xList * ) &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xGenericListItem ) )
在任务从就绪态移除时将对应bitmap清掉
void prvRemoveFromReadyQueue( tskTCB * pxTCB)
{
OSRdyTbl[(( pxTCB )->uxPriority >> 3)] &= ~OSMapTbl[(( pxTCB )->uxPriority & 7)];
if (!OSRdyTbl[(( pxTCB )->uxPriority >> 3)])
OSRdyGrp &= ~OSMapTbl[(( pxTCB )->uxPriority >> 3)];
vListRemove( &( pxTCB->xGenericListItem ) );
}
最后就是任务调度时的查表了,在vTaskSwitchContext里
if (listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ))
{
OSRdyTbl[(( pxCurrentTCB )->uxPriority >> 3)] &= ~OSMapTbl[(( pxCurrentTCB )->uxPriority & 7)];
if (!OSRdyTbl[(( pxCurrentTCB )->uxPriority >> 3)])
OSRdyGrp &= ~OSMapTbl[(( pxCurrentTCB )->uxPriority >> 3)];
if (pxCurrentTCB->uxPriority == uxTopReadyPriority)
{
if (OSRdyGrp & 0xff000000)
row = 24;
else if (OSRdyGrp & 0x00ff0000)
row = 16;
else if (OSRdyGrp & 0x0000ff00)
row = 8;
row = row + OSUnMapTbl[((OSRdyGrp >> row) & 0x000000ff)];
column = OSUnMapTbl[OSRdyTbl[row]];
uxTopReadyPriority = (row << 3) + column;
}
}
实验方法如下,在STM32F103VET6平台上,创建2个任务,一个优先级为255,它阻塞在一个消息队列上,每收到消息就做一点简单的计算处理,并再次进入阻塞;另一个任务优先级为1,它不停的想消息队列发送消息。这个实验目的就是频繁的切换任务,每循环0xffff次,打印一条消息
static int a,b,c;
void do_something() //一个简单而无实际意义的函数,只为说明做了一点事情
{
a = b;b = c; c = a;
}
void Task1Func(void* p)
{
static int cnt = 0,round = 0;
int val = 0xfe;
while (1)
{
do_something();
if (cnt++ % 0xffff == 0)
USART_OUT(USART1,"Task 1 round %d\n",++round);
xQueueSendToBack(xQueue,&val,0xfff);
}
}
void Task2Func(void* p)
{
static int cnt = 0,round = 0;
int rec;
while (1)
{
xQueueReceive(xQueue,&rec,( portTickType )0xffff);
do_something();
if (cnt++ % 0xffff == 0)
USART_OUT(USART1,"Task 2 round %d\n",++round);
}
}
xQueue = xQueueCreate(1,sizeof(int));
xTaskCreate(Task1Func,( const signed char * )"Task1",64,NULL,1,NULL);
xTaskCreate(Task2Func,( const signed char * )"Task2",64,NULL,255,NULL);
先看原始的FreeRTOS查找算法的效率如何,
每循环0xffff次,需要大约9.5秒
再看修改为查表算法的运行结果:
每循环0xffff次,需要大约2.45秒。很显然,要高效很多,虽然这是个设计的很极端的实验,但也能说明问题。
看到这里,你可能在有个疑问,FreeRTOS作为一个十分优秀的开源实时操作系统,为何会采用一个如此简单低效的调度查优办法,而不像其它的RTOS大多都是采用查表的方法,以提高效率。楼主认为,FreeRTOS的设计者这样做是增加灵活性,可移植性,因为在FreeRTOSConfig.h文件中,有一个宏configMAX_PRIORITIES,它定义了系统支持的优先级数量,这个宏理论上可以改为大于1的任意数字。但是如果要采用查表的方法,就很难兼容了,例如,某个项目需求特殊,就非要需要支持1000个任务,那该如果查表?