【UCOSii源码解读】优先级算法及内核源码分析

系列文章

  1. UCOSii启动流程以及平台相关文件分析
  2. 优先级算法及内核源码分析
  3. 任务管理
  4. 时间管理
  5. 事件控制块
  6. 内存管理
  7. 任务间通讯与同步

在这里插入图片描述

(一)任务,线程,进程

相信你也产生过这样一个问题,任务,线程,进程究竟有着怎样的关系。这个确实有点让人迷糊。在RTOS里面,我们都叫任务,到了LINUX又有进程又有线程,然而他们之间很多模型比如互斥,信号量等模型又十分相似,这就有点傻傻分不清了。

什么叫多任务系统?多任务系统指可以同一时间内运行多个应用程序,每
个应用程序被称作一个任务。

任务定义:任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列
共同达到某一目的的操作。

进程定义:进程是指一个具有独立功能的程序在某个数据集上的一次动态
执行过程,它是系统进行资源分配和调度的最小单元。

线程定义:线程是进程内独立的一条运行路线,是处理器调度的最小单
元,也可以成为轻量级进程。

看了定义,有点晕,还是通俗的说一下它们的区别吧。①通常一个任务是
一个程序的一次执行,一个任务包含一个或多个完成独立功能的子任务,这个
独立的子任务就是进程或线程。②一个进程可以拥有多个线程,每个线程必须
有一个父进程。

任务
任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到
某一目的的操作。通常一个任务是一个程序的一次执行,一个任务包含一个或
多个完成独立功能的子任务,这个独立的子任务就是进程或线程。例如,一个
杀毒软件的一次运行是一个任务,目的是从各种病毒的侵害中保护计算机系
统,这个任务包含多个独立功能的子任务(进程或线程),包括实时监控功能、
定时查杀功能、防火墙功能及用户交互功能等。任务、进程和线程之间的关系
如图 1 所示
在这里插入图片描述
所以一定要给RTOS中的任务说他是一个进程还是一个线程的话,个人感觉是进程,毕竟他们都有TCB块,以及独立的系统资源。

(二)任务状态及任务转换图

在这里插入图片描述
睡眠态(DORMANT)指任务驻留在程序空间之中,还没有交给μC/OS-Ⅱ管理,(见程序清单L3.1或L3.2)。把任务交给μC/OS-Ⅱ是通过调用下述两个函数之一:OSTaskCreate()或OSTaskCreateExt()。当任务一旦建立,这个任务就进入就绪态准备运行。任务的建立可以是在多任务运行开始之前,也可以是动态地被一个运行着的任务建立。如果一个任务是被另一个任务建立的,而这个任务的优先级高于建立它的那个任务,则这个刚刚建立的任务将立即得到CPU的控制权。一个任务可以通过调用OSTaskDel()返回到睡眠态,或通过调用该函数让另一个任务进入睡眠态。
调用OSStart()可以启动多任务。OSStart()函数运行进入就绪态的优先级最高的任务。就绪的任务只有当所有优先级高于这个任务的任务转为等待状态,或者是被删除了,才能进入运行态。
正在运行的任务可以通过调用两个函数之一将自身延迟一段时间,这两个函数是OSTimeDly()或OSTimeDlyHMSM()。这个任务于是进入等待状态,等待这段时间过去,下一个优先级最高的、并进入了就绪态的任务立刻被赋予了CPU的控制权。等待的时间过去以后,系统服务函数OSTimeTick()使延迟了的任务进入就绪态(见3.10节,时钟节拍)。

正在运行的任务期待某一事件的发生时也要等待,手段是调用以下3个函数之一:OSSemPend(),OSMboxPend(),或OSQPend()。调用后任务进入了等待状态(WAITING)。当任务因等待事件被挂起(Pend),下一个优先级最高的任务立即得到了CPU的控制权。当事件发生了,被挂起的任务进入就绪态。事件发生的报告可能来自另一个任务,也可能来自中断服务子程序。
正在运行的任务是可以被中断的,除非该任务将中断关了,或者μC/OS-Ⅱ将中断关了。被中断了的任务就进入了中断服务态(ISR)。响应中断时,正在执行的任务被挂起,中断服务子程序控制了CPU的使用权。中断服务子程序可能会报告一个或多个事件的发生,而使一个或多个任务进入就绪态。在这种情况下,从中断服务子程序返回之前,μC/OS-Ⅱ要判定,被中断的任务是否还是就绪态任务中优先级最高的。如果中断服务子程序使一个优先级更高的任务进入了就绪态,则新进入就绪态的这个优先级更高的任务将得以运行,否则原来被中断了的任务才能继续运行。
当所有的任务都在等待事件发生或等待延迟时间结束,μC/OS-Ⅱ执行空闲任务(idle task),执行OSTaskIdle()函数。

(三)任务控制块

一旦任务建立了,任务控制块OS_TCBs将被赋值。任务控制块是一个数据结构,当任务的CPU使用权被剥夺时,μC/OS-Ⅱ用它来保存该任务的状态。当任务重新得到CPU使用权时,任务控制块能确保任务从当时被中断的那一点丝毫不差地继续执行。OS_TCBs全部驻留在RAM中。读者将会注意到笔者在组织这个数据结构时,考虑到了各成员的逻辑分组。任务建立的时候,OS_TCBs就被初始化了。

378 *********************************************************************************************************
379 * 任务控制块 (TASK CONTROL BLOCK)
380 *********************************************************************************************************
381 */
382
383 typedef struct os_tcb {
384 OS_STK *OSTCBStkPtr; //当前TCB的栈顶指针
385
386 #if OS_TASK_CREATE_EXT_EN > 0 //允许生成OSTaskCreateExt()函数
387 void *OSTCBExtPtr; //指向用户定义的任务控制块(扩展指针)
388 OS_STK *OSTCBStkBottom; //指向指向栈底的指针
389 INT32U OSTCBStkSize; //设定堆栈的容量
390 INT16U OSTCBOpt; //保存OS_TCB的选择项
391 INT16U OSTCBId; //否则使用旧的参数
392 #endif
393
394 struct os_tcb *OSTCBNext; //定义指向TCB的双向链接的后链接
395 struct os_tcb *OSTCBPrev; //定义指向TCB的双向链接的前链接
396
397 #if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0) || (OS_SEM_EN > 0) || (OS_MUTEX_EN > 0)
398 //当以上各种事件允许时
399 OS_EVENT *OSTCBEventPtr; //定义指向事件控制块的指针
400 #endif
401
402 #if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0)
403 void *OSTCBMsg; //满足以上条件,定义传递给任务的消息指针
404 #endif
405
406 #if (OS_VERSION >= 251) && (OS_FLAG_EN > 0) && (OS_MAX_FLAGS > 0)
407 #if OS_TASK_DEL_EN > 0
408 OS_FLAG_NODE *OSTCBFlagNode; //定义事件标志节点的指针
409 #endif
410 OS_FLAGS OSTCBFlagsRdy; //定义运行准备完毕的任务控制块中的任务?
411 #endif
412
413 INT16U OSTCBDly; //定义允许任务等待时的最多节拍数
414 INT8U OSTCBStat; //定义任务的状态字
415 INT8U OSTCBPrio; //定义任务的优先级
416
417 INT8U OSTCBX; //定义指向任务优先级的低3位,即=priority&0x07
418 INT8U OSTCBY; //定义指向任务优先级的高3位,即=priority>>3
419 INT8U OSTCBBitX; //定义低3位就绪表对应值(0~7),即=OSMapTbl[priority&0x07]
420 INT8U OSTCBBitY; //定义高3位就绪表对应值(0~7),即=OSMapTbl[priority>>3]
421
422 #if OS_TASK_DEL_EN > 0 //允许生成 OSTaskDel() 函数代码函数
423 BOOLEAN OSTCBDelReq; //定义用于表示该任务是否须删除
424 #endif
425 } OS_TCB;

其中的元素就不一一介绍了,最关键的以下一个元素
(1)任务的关键 OS_STK == 任务的堆栈,用于保存任务的信息,最主要的是保存在程序的运行的SP指针。任务切换的实质就是SP指针的变化,通过SP指针的变化,可以跳转到你想要去的任何的一块不受保护的地址去。
(2)任务的链表: struct os_tcb *OSTCBNext; 指向下一个任务,
此处使用链表是可以通过指针访问下一个任务的内容,可以使用这个双向链表放置到某些队列当中,
(3)事件控制块:OS_EVENT *OSTCBEventPtr;
是一个技术组件,用于后面的消息和消息队列,邮箱和信号量等的设计。

(四)UCOS任务优先级算法查表法(重难点)

UCOS采用的抢占式优先级算法,不带时间片的,这意味着,一个优先级上应该只能站着一个任务。那么确定最高优先级是啥就是任务切换中必不可少的一部分。因为这个算法江北频繁调用所以这个算法的好坏将直接影响到这个操作系统的性能。
这个算法是UCOS的精华所在
在这里插入图片描述
按照这个算法我们先正向走一遍。怎样根据就绪表算出最高优先级。

首先我们必须要搞清楚几个关键的变量究竟是什么:
在这里插入图片描述

在这里插入图片描述

OSRdyGrp 这是一个INTU8也就是8位无符号整形变量,这个变量的每一个bit都代表就绪表每一组是否有就绪的任务。
OSRdyTbl是一个有八个元素的一维数组,每一个元素代表一个组,每一个bit代表一个优先级。

那么系统是怎么找到最高的优先级的呢。
先看一下源码是怎么写的:

818 void OS_Sched (void) //任务调度函数
819 {
820 #if OS_CRITICAL_METHOD == 3 //中断函数被设定为模式3
821 OS_CPU_SR cpu_sr;
822 #endif
823 INT8U y; //定义一个8位整数y
824
825
826 OS_ENTER_CRITICAL(); //关闭中断
827 //如果中断嵌套次数>0,且上锁(调度器)嵌套次数>0,函退出,不做任何调度
828 if ((OSIntNesting == 0) && (OSLockNesting == 0)) {
829 //如果函数不是在中断服务子程序中调用的,且调度允许的,则任务调度函数将找出进入就绪态的
830 //最高优先级任务,进入就绪态的任务在就绪表中OSRdyTbl[ ]中相应位置位.
831 y = OSUnMapTbl[OSRdyGrp];
832 OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
833 //找到最高优先级任务后,函数检查这个优先级最高的任务是否是当前正在运行的任务,以避免不
834 //必要的任务调度,多花时间
835 if (OSPrioHighRdy != OSPrioCur) {
836 //为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是通过将
837 //以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来实现的
838 OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
839 OSCtxSwCtr++; //统计计数器OSCtxSwCtr加1,以跟踪任务切换次数
840 OS_TASK_SW(); //最后宏调用OS_TASK_SW()来完成实际上的任务切换
841 }
842 }
843 OS_EXIT_CRITICAL(); //打开中断
844 

OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
最关键的其实就是这句话。其实非常容易理解。在一个8x8的表中,已知这个每一位代表一个优先级这个时候,已知行y,列x,就可以得到prio=8*y+x;首先看OSRdyGrp的第一个1,找到最高优先级所在的组y,再找这个组中的第一个1定位最高优先级。关键就在于如何快速的求这个y和x.这个就是OSUnMapTbl表存在的意义了。

我们可以看到在计算y的过程中查了一次表,在计算x的过程中也查了一次表,仔细研究就会发现其实都是为了解决同一个问题。
对于一个8bit的数0bxxxxxxxx怎样从高位到低位找出第一个1。y 实际上就是OSRdyGrp第一个1的位置,x是OSRdyTbl[y]第一个1的位置
那我们正常的想法该怎么写呢
for(i=0;i<8&(x&0x01<<i==0);i++)
通过这样的方法就可以求出到底是 第几位出现了第一个1,但是我们发现这样效率很低,一趟下来最倒霉需要比较8次,两个叠加最倒霉就是64次,这样会拖慢RTOS的反应时间。那么怎么办呢,就有了UCOS的神来之笔。
查表法,以空间换时间最典型的例子。比如说我一个数
OSRdyGrp=0b00000010查表发现是OSUnMapTbl[OSRdyGrp]=1;好的这就知道,要到第二行去找。
再比如OSRdyGrp=0b00000110 OSUnMapTbl[OSRdyGrp]=1;还是到第二行去找。
这样就定位到了在那一组(行)中。然后同样的方法可以定位到任务在一组中的第几个。就知道x和y的值了,然后就可以 算出优先级的值了。
prio=8*y+x (y<<3+x)

既然正向可以看懂的话那么反着将每一个任务改成就绪状态也非常容易理解了吧。

OSRdyGrp |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];

prio & 0x07相当于prio%8,可以自己算一算,数学就是这么个神奇的东西。

(五)任务调度分析

818 void OS_Sched (void) //任务调度函数
819 {
820 #if OS_CRITICAL_METHOD == 3 //中断函数被设定为模式3
821 OS_CPU_SR cpu_sr;
822 #endif
823 INT8U y; //定义一个8位整数y
824
825
826 OS_ENTER_CRITICAL(); //关闭中断
827 //如果中断嵌套次数>0,且上锁(调度器)嵌套次数>0,函退出,不做任何调度
828 if ((OSIntNesting == 0) && (OSLockNesting == 0)) {
829 //如果函数不是在中断服务子程序中调用的,且调度允许的,则任务调度函数将找出进入就绪态的
830 //最高优先级任务,进入就绪态的任务在就绪表中OSRdyTbl[ ]中相应位置位.
831 y = OSUnMapTbl[OSRdyGrp];
832 OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
833 //找到最高优先级任务后,函数检查这个优先级最高的任务是否是当前正在运行的任务,以避免不
834 //必要的任务调度,多花时间
835 if (OSPrioHighRdy != OSPrioCur) {
836 //为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是通过将
837 //以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来实现的
838 OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
839 OSCtxSwCtr++; //统计计数器OSCtxSwCtr加1,以跟踪任务切换次数
840 OS_TASK_SW(); //最后宏调用OS_TASK_SW()来完成实际上的任务切换
841 }
842 }
843 OS_EXIT_CRITICAL(); //打开中断

为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是通过将以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来实现的[L3.8(4)]。接着,统计计数器OSCtxSwCtr加1,以跟踪任务切换次数[L3.8(5)]。最后宏调用OS_TASK_SW()来完成实际上的任务切换[L3.8(6)]。
任务切换很简单,由以下两步完成,将被挂起任务的微处理器寄存器推入堆栈,然后将较高优先级的任务的寄存器值从栈中恢复到寄存器中。在μC/OS-Ⅱ中,就绪任务的栈结构总是看起来跟刚刚发生过中断一样,所有微处理器的寄存器都保存在栈中。换句话说,μC/OS-Ⅱ运行就绪态的任务所要做的一切,只是恢复所有的CPU寄存器并运行中断返回指令。为了做任务切换,运行OS_TASK_SW(),人为模仿了一次中断。多数微处理器有软中断指令或者陷阱指令TRAP来实现上述操作。中断服务子程序或陷阱处理(Trap hardler),也称作事故处理(exception handler),必须提供中断向量给汇编语言函数OSCtxSw()。OSCtxSw()除了需要OS_TCBHighRdy指向即将被挂起的任务,还需要让当前任务控制块OSTCBCur指向即将被挂起的任务。
OSSched()的所有代码都属临界段代码。在寻找进入就绪态的优先级最高的任务过程中,为防止中断服务子程序把一个或几个任务的就绪位置位,中断是被关掉的。为缩短切换时间,OSSched()全部代码都可以用汇编语言写。为增加可读性,可移植性和将汇编语言代码最少化,OSSched()是用C写的。

(六)开关调度锁函数原理分析

void  OSSchedLock (void)
{
#if OS_CRITICAL_METHOD == 3u                     /* Allocate storage for CPU status register           */
    OS_CPU_SR  cpu_sr = 0u;
#endif



    if (OSRunning == OS_TRUE) {                  /* Make sure multitasking is running                  */
        OS_ENTER_CRITICAL();
        if (OSIntNesting == 0u) {                /* Can't call from an ISR                             */
            if (OSLockNesting < 255u) {          /* Prevent OSLockNesting from wrapping back to 0      */
                OSLockNesting++;                 /* Increment lock nesting level                       */
            }
        }
        OS_EXIT_CRITICAL();
    }
}

定位到
OS_ENTER_CRITICAL();
OS_EXIT_CRITICAL();开关中断

#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
#endif

开关中断最底层使用汇编写的(以cortex-m3为例):
OS_CPU_SR_Save
MRS R0, PRIMASK ;加载PRIMASK寄存器地址
CPSID I ;PRIMASK=1,关闭所有中断(除了nmi and hardfault)
BX LR ;返回

OS_CPU_SR_Restore
MSR PRIMASK, R0 ;
BX LR ;

至此内核最关键的部分都分析完成了,最核心的调用就是
引发调度的
void OS_Sched (void) //任务调度函数,一般人为切换任务调用
OSIntExit (void)//一般在系统时钟中断中调用周期性使用

OSSchedLock (void)//通过开关中断,开关任务调度
OSSchedUnLock (void)
与直接开关没什么区别。
OS_ENTER_CRITICAL();
OS_EXIT_CRITICAL();开关中断

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

与光同程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值