if (ticks > 0u) { /* 0 means no delay! */ OS_ENTER_CRITICAL(); y = OSTCBCur->OSTCBY; /* Delay current task */ OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX; if (OSRdyTbl[y] == 0u) { OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY; } OSTCBCur->OSTCBDly = ticks; /* Load ticks in TCB */ OS_EXIT_CRITICAL(); OS_Sched(); /* Find next task to run! */ }
依然是这一部分,接下来的重点是这个函数:OS_Sched()
这个函数实在是太重要了,因此我不得不慎重。
首先看一下官方的注释:
*********************************************************************************************************
* SCHEDULER
*
* Description: This function is called by other uC/OS-II services to determine whether a new, high
* priority task has been made ready to run. This function is invoked by TASK level code
* and is not used to reschedule tasks from ISRs (see OSIntExit() for ISR rescheduling).
*
*********************************************************************************************************
从上面的说明可以看出,这个函数的作用,主要是用来调度当前已经进入了就绪状态的最高优先级任务,然后切换进去。
函数定义如下:
1 void OS_Sched (void) 2 { 3 #if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */ 4 OS_CPU_SR cpu_sr = 0u; 5 #endif 6 9 OS_ENTER_CRITICAL(); 10 if (OSIntNesting == 0u) { /* Schedule only if all ISRs done and ... */ 11 if (OSLockNesting == 0u) { /* ... scheduler is not locked */ 12 OS_SchedNew(); 13 OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; 14 if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */ 15 #if OS_TASK_PROFILE_EN > 0u 16 OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */ 17 #endif 18 OSCtxSwCtr++; /* Increment context switch counter */ 19 OS_TASK_SW(); /* Perform a context switch */ 20 } 21 } 22 } 23 OS_EXIT_CRITICAL(); 24 }
关于任务调度的部分肯定是原子操作,不允许任何中断存在,因此必须要关闭中断。
其次,任务调度不能发生在中断中以及任务调度器上锁的情况,因此必须加以判定。
上面的内容比较简单,去掉那些判断和宏开关,我们需要关注的重点主要在以下的部分:
1 OS_SchedNew(); 2 OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; 3 if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */ 5 OS_TASK_SW(); /* Perform a context switch */ 6 }
首先看这个函数:OS_SchedNew()
函数定义如下:
1 static void OS_SchedNew (void) 2 { 3 #if OS_LOWEST_PRIO <= 63u /* See if we support up to 64 tasks */ 4 INT8U y; 5 6 7 y = OSUnMapTbl[OSRdyGrp]; 8 OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]); 9 #else /* We support up to 256 tasks */ 10 INT8U y; 11 OS_PRIO *ptbl; 12 13 14 if ((OSRdyGrp & 0xFFu) != 0u) { 15 y = OSUnMapTbl[OSRdyGrp & 0xFFu]; 16 } else { 17 y = OSUnMapTbl[(OS_PRIO)(OSRdyGrp >> 8u) & 0xFFu] + 8u; 18 } 19 ptbl = &OSRdyTbl[y]; 20 if ((*ptbl & 0xFFu) != 0u) { 21 OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(*ptbl & 0xFFu)]); 22 } else { 23 OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(OS_PRIO)(*ptbl >> 8u) & 0xFFu] + 8u); 24 } 25 #endif 26 }
因为我们的系统最高支持64个任务,所以去掉那些我们不需要关注的地方,函数定义简化如下:
static void OS_SchedNew (void) { INT8U y; y = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]); }
看着好像很简单,整个函数就两句话,但是,只要能把这两句话给弄明白了,关于调度的东西基本上就都没问题了。
关于变量OSRdyGrp与数组OSRdyTbl[]的意义,相信各位都已经十分理解,分别代表组就绪状态和任务就绪状态,那么新出来的这个数组OSUnMapTbl[]又代表什么呢?它和任务就绪表有什么关系?
跟踪OSUnMapTbl数组的定义可以发现,这是一个常数数组,它里面的内容是只读的,定义如下:
INT8U const OSUnMapTbl[256] = { 0u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x00 to 0x0F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x10 to 0x1F */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x20 to 0x2F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x30 to 0x3F */ 6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x40 to 0x4F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x50 to 0x5F */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x60 to 0x6F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x70 to 0x7F */ 7u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x80 to 0x8F */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x90 to 0x9F */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xA0 to 0xAF */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xB0 to 0xBF */ 6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xC0 to 0xCF */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xD0 to 0xDF */ 5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xE0 to 0xEF */ 4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u /* 0xF0 to 0xFF */ };
这个表还不小,一眼看去脑袋都大了,它到底是个什么玩意儿?别慌,听我慢慢讲解……
依然举个例子,现在我们的系统只有两个任务(0和12),当前的任务优先级是0,然后这个任务进入了延时,这个时候根据前面了解的东西:
y = OSTCBCur->OSTCBY; /* Delay current task */ OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
这个优先级为0的任务已经被设置为了未就绪状态,也就是把它的就绪表清空了,对应的OSRdyTbl[0]肯定是0,由于只有两个任务,因此对应的OSRdyGrp的最后一个bit位,也肯定是0,。
然后我们还有一个优先级为12的任务已经准备就绪, 那么代码执行到了这里,任务的OSRdyTbl[1]必然等于0x10,组号OSRdyGrp必然等于0x02,把2带进这个常数表,得到的结果是:1
这个“1”有什么意义?
y = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
结论:这两句代码真正的功能,就是从系统中,把当前已经就绪了的任务里,优先级最高的那个任务给找出来,而这个任务也就是我接下来要切换进去的那一个。
假设现在我们系统中只有两个任务,一个优先级为0(未就绪),一个优先级为12(就绪),现在我们就来看看,他到底是怎么把12这个数据给找出来的。
上面说了,当一个任务的优先级是确定数的时候,他的组号、组内坐席号,偏移量等都是确定的。
当只有任务12就绪时,这个时候组号OSRdyGrp必然等于0x02,那么把它带入那个常数表中,得到结果y为1。
在把1带入数组OSRdyTbl[1]中,等到结果的结果是0x10(参考上一节),把0x10带入常数表,得到的结果OSUnMapTbl[OSRdyTbl[y]] == 4
这个y等于1,把它向左移动3个bit,得到的结果是8(二进制00001000)……最后的结果8 + 4 = 12。
没想到它真的把我需要的优先级给算出来了,到底是怎么做到的?
-----------------------------------------------------------------------------------------------------------------------
其实,UCOSII使用的这种方法被叫查表法,根据一定的规律,直接计算出当前优先级最高的那个任务号。
那个看似莫名其妙的常数表OSUnMapTbl,其实它所代表的意思,是0~255个数字中,1所在的最低位:
比如1,二进制00000001,在它的最低位出现了1,那么带入常数表一查,发现OSUnMapTbl[1] = 0,也就是第0位出现了1。
比如2,二进制00000010,在它的次低位出现了1,那么带入常数表一查,发现OSUnMapTbl[2] = 1,也就是第1位出现了1。
比如3,二进制00000011,在它的最低位出现了1,那么带入常数表一查,发现OSUnMapTbl[3] = 0,也就是第0位出现了1。
……
比如12,二进制00001100,在它的第2位出现了1,那么带入常数表一查,发现OSUnMapTbl[12] = 2,也就是第2位出现了1。
比如63,二进制011111111,在它的最低位出现了1,那么带入常数表一查,发现OSUnMapTbl[3] = 0,也就是第0位出现了1。
因为有了这个表,算法上才可能做到无论有多少个任务进入了就绪的状态,我都能轻轻松松地取出优先级最高的那一个,根据变量OSRdyGrp的状态,我可以找到那些组有就绪的任务,
如果第0组和第3组内都有就绪的任务,那么OSRdyGrp肯定等于0x05,但是我根本不用关心第3组的状态,忽略即可,因为第0组明显优先程度更大,我只需要继续前往第0组内寻找便可。
进入第0组内部后,假如优先级为1和优先级为5的任务都就绪了,变量OSRdyTbl[0] == 0x22(二进制00100010),我肯定要执行优先级为1的任务,那么我就直接把这个数据带进那个常数表中去查寻,看看最低位出现1的位置,也就是就绪的任务到底是哪一个,
只要找到了它,别的任务就算是就绪状态,我也不用管了……查表法便是基于这个原理。
y = OSUnMapTbl[OSRdyGrp]; OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
再看一下这两句代码,第一句代码的意思是:找到就绪任务中优先级最高的组号,比如1组和3组都就绪了,我需要的结果是:1。
第二句代码中的这句话OSUnMapTbl[OSRdyTbl[y]]的意思是:找到在这个组中,优先级最高的任务的坐席号,也就是偏移量,比如任务12和任务13都就绪了,我需要的结果是:4(代表任务12的偏移)。
整个第二个代码的意思是:把组号和组内坐席号组合起来,形成最后的任务优先级。
如果想不明白的话,可以参考上一章:
ptcb->OSTCBY = (INT8U)(prio >> 3u); ptcb->OSTCBX = (INT8U)(prio & 0x07u);
这两句话的意思,在建立任务的时候,把一个好好的优先级给拆开,现在终于是把它们给重新合上了。
现在回过头看,OS_SchedNew这个函数的作用是什么?
很明显,它的作用就是寻找到,在当前已经就绪的任务中,优先级最高的那一个任务。
题外话,思考一个问题,为什么要用查表法呢?
如果是自己来做这个策略,有没有其他的方法?
当然有,如果是我来做,或许可以建立一个大表,里面装有所有任务的就绪状态,然后写一个for循环,每次进行任务切换的时候,从低到高依次判断,如果任务状态位bit是1,那么就证明这个任务是就绪了的,立即跳出去进行任务切换,如果任务状态bit是0,那就证明这个任务没有就绪,继续进行下一个判断,我想这样肯定更容易理解一些。
不过这样做有一个问题,如果当前我就绪的任务优先级是0,那么在第一个循环就能找到任务,然后任务切换,时间比查表法块很多,如果我就绪的任务是255呢?那么我可能就需要循环255次才能找到就绪的任务,那么时间肯定会很长。
用这种方法会导致寻找就绪任务需要的时间完全不能确定,有时候短,有时候长,然而这对于一个系统而言,最怕的就是这种不确定因素。
查表法和循环法就完全不同了,它虽然死板一些,但不管当前系统有多少任务,不管当前有多少任务是处于就绪状态,它每次计算出最高优先级的任务的时间是一定的,这种确定性对于系统很重要。