先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题,二维码如下:
在单片机裸机程序中,我们以函数为最小单位来划分代码功能的,所有函数之间都存在一个先后调用的关系(不是你调用我,就是我调用你,或者你我都被他调用);但是在嵌入式操作系统中,我们可以以任务为最小单位来看待一个程序代码(当然函数仍然是每个任务的最小单位),各个任务之间没有调用关系,它们可以说是各自为营。
可以把嵌入式系统程序看成各个单片机裸机程序的集合,跑操作系统的本质原因就是因为系统程序需要实现的功能很多,我们需要合理划分每个功能,保证相互之间没有关系,从而有一个框架清晰,逻辑清晰,后期维护和查找问题更便捷的系统。
那么操作系统是如何合理调度各个任务,保证每个任务都能合理正确的运行呢?这是所有操作系统最为重要的一个目的,现代操作系统绝大部分都是多任务系统,我们先看一看ucos-ii是如何实现的。
何为任务调度?就是终止当前正在执行的任务,转而去执行别的任务过程。
在探讨任务调度策略时,我们先假设读者对ucos-ii中任务的基本概念有了一定理解,这里也不在做介绍。任务有一个基本特性,任务优先级(存储在任务控制块结构体中,在初始化创建任务的时候会被赋值)是任务调度的重要参考,在ucos中,优先级值越低,优先级越高,越优先被系统调度。
触发任务调度的基本方式有两种,一种是任务级调度,另外一种是中断级调度。
所谓任务级调度,就是任务本身主动或被动触发了任务调度,比如说等待信号量过程中,当前信号量还没有产生,cpu不可能一直在这里死等(当然特殊情况,也会设计成在这里死等,比如像linux中的自旋锁机制),这样会造成别的任务不能得到快速响应,还怎么能称作实时操作系统呢?
任务调度的步骤,总结来说,可以分为两步,第一步是找到当前优先级最高的并且处于就绪状态的任务;第二步就是任务切换,从当前任务切换到优先级较高的任务去。通过调用API函数OS_Sched来实现,代码如下:
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u) { /* Schedule only if all ISRs done and ... */
if (OSLockNesting == 0u) { /* ... scheduler is not locked */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Increment context switch counter */
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
#endif
OS_TASK_SW(); /* Perform a context switch */
}
}
}
OS_EXIT_CRITICAL();
}
我们根据宏开关的定义,去掉未使能的代码,简写OS_Sched如下:
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0u) { /* Schedule only if all ISRs done and ... */
if (OSLockNesting == 0u) { /* ... scheduler is not locked */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
OSCtxSwCtr++; /* Increment context switch counter */
OS_TASK_SW(); /* Perform a context switch */
}
}
}
OS_EXIT_CRITICAL();
}
static void OS_SchedNew (void)
{
#if OS_LOWEST_PRIO <= 63u /* See if we support up to 64 tasks */
INT8U y;
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
#else /* We support up to 256 tasks */
INT8U y;
OS_PRIO *ptbl;
if ((OSRdyGrp & 0xFFu) != 0u) {
y = OSUnMapTbl[OSRdyGrp & 0xFFu];
} else {
y = OSUnMapTbl[(OS_PRIO)(OSRdyGrp >> 8u) & 0xFFu] + 8u;
}
ptbl = &OSRdyTbl[y];
if ((*ptbl & 0xFFu) != 0u) {
OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(*ptbl & 0xFFu)]);
} else {
OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(OS_PRIO)(*ptbl >> 8u) & 0xFFu] + 8u);
}
#endif
}
同样,根据宏开关,简写OS_SchedNew如下:
static void OS_SchedNew (void)
{
INT8U y;
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
}
我们发现这里只有2句赋值语句,OSPrioHighRdy是ucos-ii中用来标记当前系统任务中优先级最高的任务的全局变量,那么ucos是如何找到优先级最高的任务的呢?我们重点关注一下全局数组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 */
};
初看这个定义,绝大部分读者可能跟我一样,晕头转向,云里雾里,什么鬼(WTF!!)?而且我可能比各位读者还要更笨一些,我可是前前后后死磕了好几遍,才看懂这边的定义,看这个数组之前,我们先来看一下ucos是如何标记任务优先级的。
ucos中,将任务用数组OsRdyTbl[8],人为划分成了8组,OsRdyTbl[0]代表任务组0,OsRdyTbl[1]代表任务组1…OsRdyTbl[7]代表任务组7;每个数组中的每一位代表一个优先级任务,比方说对于OsRdyTbl[0]的第0位代表任务优先级为0的任务,OsRdyTbl[0]的第1位代表任务优先级为1的任务,以此类推;OsRdyTbl[1]的第0位代表任务优先级为8的任务(18+0),OsRdyTbl[1]的第1位代表任务优先级为9的任务(18+1)…OsRdyTbl[7]的第0位代表任务优先级为56的任务(78+0),OsRdyTbl[7]的第1位代表任务优先级为57的任务(78+1);同时ucos用OSRdyGrp表示哪一组优先级,OSRdyGrp的第0位表示第0组优先级状态(如果该位为1,表示第0组有存在任务就绪)…第7位表示第7组优先级状态。
因此我们只要知道OSRdyGrp的值,以及OsRdyTbl[]数组的值,就可以知道当前任务就绪的状态了,比方说,如果由我来设置最初版本的ucos系统(当然现在知道这样设计是存在问题的,虽然从逻辑上来看,也能实现功能,但是从实时性上来看,是不合理的)
u8 grp, tbl;
/* 先找出属于哪一组 */
for (i = 0; i < 8; i++)
{
temp = OSRdyGrp >> i;
if (temp & 0x1)
break;
}
if (OSRdyGrp) /* 这个判断条件可以不加上,因为系统中必定会存在一个空闲任务 */
grp = i;
/*再找出当前那一组上哪一个优先级
*因为OsRdyTbl[0]的优先级最高,其次是OsRdyTbl[1],最后是OsRdyTbl[7]
*所以我们采用从0开始索引查找
*/
for (j = 0; j < 8; j++)
{
for (i = 0; i < 8; i++)
{
temp = OsRdyTbl[j] >> i;
if (temp & 0x1)
goto find_out;
}
}
find_out:
tbl = i;
OSPrioHighRdy = grp << 3 + tbl;
根据我们自己所实现的查找最高优先级的代码,反过来理解ucos-ii系统中的巧妙实现方法,它是如何通过一个数组,经过查表就能找到最高优先级的呢?
我们知道OSRdyGrp代表了哪一组任务优先级,第0位代表的任务组肯定是最高优先级的,因此我们先判断OSRdyGrp最低位出现为1的那一组,那么这一组中对应的任务肯定是优先级最高的;同理,OsRdyTbl[]数组中的每一位也代表一个任务优先级,最低位出现为1的肯定是优先级最高的(为什么是这样的,因为ucos系统OSRdyGrp和OsRdyTbl[]就是这样来定义的),找到最开始出现为1的,就可以根据公式:
OSRdyGrp最低位出现为1的位数 << 3 + OsRdyTbl[OSRdyGrp最低位出现为1的位数]最低位出现为1的位数
这里两个最低位出现为1的位数不是很好理解,请读者仔细分析这段话。
因此,ucos定义的数组就是表示最低位出现为1的数,比如说
优先级的值 二进制 低位最开始出现1的位数
0 0000 0000 ----> 0
1 0000 0001 ----> 1
2 0000 0010 ----> 2
3 0000 0011 ----> 1
4 0000 0100 ----> 2
5 0000 0101 ----> 1
......
128 1000 0000 ----> 7
......
255 1111 1111 ----> 0
讲到这里,各位读者应该明白了查找较高优先级任务的原理
2. 任务切换
在找到较高任务的优先级时,就需要实现任务的切换了,上面的过程中,我们只是找到了当前处于就绪状态的最高优先级任务的优先级值,如何实现任务切换呢,请看代码(仍然是OS_Sched函数中的部分片段,说明的是:在不同版本的ucos-ii上,代码的细节会有所差别,不过原理目的都是一样的)
if (OSLockNesting == 0u) { /* ... scheduler is not locked */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Increment context switch counter */
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
#endif
OS_TASK_SW(); /* Perform a context switch */
}
}
我们一句一句分析:
A. 找到当前优先级对应的任务控制块,根据优先级的值,索引数组
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]
B. 判断最高优先级是否为当前任务,如果就是当前任务的话,不需要切换;否则需要切换;
C. 如果要切换,记录当前任务被切换的次数,以及任务总共被切换的次数
OSTCBHighRdy->OSTCBCtxSwCtr++;
OSCtxSwCtr++;
D. 实现任务切换,调用宏OS_TASK_SW()实现,实际上是调用的汇编函数OSCtxSw,有关这个汇编函数的实现方式,我们在另外的一篇文章中会重点分析,这里我们只需要知道调用这个函数后就会实现任务切换即可
OS_TASK_SW()
中断级调度,是调用C函数OSIntExit实现的,如下:
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3u /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0u;
#endif
if (OSRunning == OS_TRUE) {
OS_ENTER_CRITICAL();
if (OSIntNesting > 0u) { /* Prevent OSIntNesting from wrapping */
OSIntNesting--;
}
if (OSIntNesting == 0u) { /* Reschedule only if all ISRs complete ... */
if (OSLockNesting == 0u) { /* ... and not locked. */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSCtxSwCtr++; /* Keep track of the number of ctx switches */
#if OS_TASK_CREATE_EXT_EN > 0u
#if defined(OS_TLS_TBL_SIZE) && (OS_TLS_TBL_SIZE > 0u)
OS_TLS_TaskSw();
#endif
#endif
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
}
OS_EXIT_CRITICAL();
}
}
根据宏开关,简写如下:
void OSIntExit (void)
{
OS_CPU_SR cpu_sr = 0u;
if (OSRunning == OS_TRUE) {
OS_ENTER_CRITICAL();
if (OSIntNesting > 0u) { /* Prevent OSIntNesting from wrapping */
OSIntNesting--;
}
if (OSIntNesting == 0u) { /* Reschedule only if all ISRs complete ... */
if (OSLockNesting == 0u) { /* ... and not locked. */
OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
OSCtxSwCtr++; /* Keep track of the number of ctx switches */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
}
OS_EXIT_CRITICAL();
}
}
我们发现,这个函数的实现基本上和OS_Sched的函数实现类似,也是先找到当前优先级最高的任务,然后启动任务切换。唯一不同的就是,调用了不同的函数实现了任务切换,这里就是调用了汇编函数OSIntCtxSw()来实现的,函数如下:
OSIntCtxSw
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
我们发现,这个函数也是使能了一次PendSV异常,跟汇编函数OSCtxSw的实现一样,都是软件触发了一次了PendSV异常,在PendSV异常处理函数中,实现上下文的切换,参照文章《ucos-ii嵌入式操作系统任务调度(二)----任务切换瞬间cpu做了什么》