一、什么是任务调度器
1.1 多任务并行机制
对很多人来说,多任务并行机制挺神秘的,甚至对很多有多线程并发编程经验的工程师来说也是如此。前面我用五篇文章谈了C++多线程并发编程的原理与技巧,但对操作系统的多任务并行机制并没有过多阐述。现在回顾下从单任务顺序执行到多任务并行执行的演变过程,有利于打破多任务并行机制的神秘感。
多任务并行机制从技术上来说就是程序流折断 + 现场(在操作系统里叫上下文)保护,这两部分我们也并不陌生,回顾下常用的中断响应、调子程序等。那么多任务并行机制的神秘性在哪里呢?在于程序流反向控制机制,就是由子函数决定父函数的执行流程。对于通常的程序来说,总是由父函数决定何时调用哪个子函数,而在并行多任务系统中,却是由一个被称为任务调度器的子函数决定何时调用哪一个父函数。
需要说明的一点是,这种子函数对父函数的调用并非任意的,它只能将流程指向父函数的折断点,也就是最近一次调用任务调度器时的位置。原因很简单,因为子函数根本不知道应该从父函数的哪个地方开始执行,除非它保存了父函数在折断时的上下文。这种“由任务调度器保存任务流程折断点信息(上下文),并在将来某个时间恢复该上下文,然后继续该任务流程”的方式,就是多任务并行的核心机制。
如何在子函数中修改父函数的执行流程呢?下文将会给出答案。
1.2 任务调度器
前面一篇文章《有限状态机》谈到了状态机作为一个事件驱动模型,将一个大任务分割为多个小任务,特别是对于协议栈数据处理这类比较复杂的大任务,使用状态机而非操作系统可以占用更少的资源获得更高的执行效率。但对于多个相互独立的任务来说,虽然也可以采用多个任务状态机分别管理不同的任务,将每个任务拆分为多个不同的状态并控制任务流程的状态迁移处理起来并不轻松。就像有限状态机那篇文章谈到的,不方便实现任务的动态调度,难以实现对特定任务的实时响应。下面给出一个状态机实现多任务流程控制的图示:
状态机需要我们自己将任务分割为一个个小的任务片段,增加了我们程序开发的难度。那么,任务分割是否可以交给操作系统自己处理呢?任务调度器便可以实现任务分割功能。每次调用任务调度器时,任务调度器会先保存程序流折断点的上下文信息(保存在该任务的私有堆栈中),然后将该任务流折断将CPU交给下一个任务。下一个任务也是从折断点开始执行的,先将目标任务折断点的上下文信息装填到CPU的主堆栈中,CPU就可以从该任务的折断点继续往下执行了。任务分割、程序流折断、现场上下文保护等这些任务都是由任务调度器自动完成的,为我们开发多任务并行的程序带来了极大的便利。下面给出一个任务调度器实现多任务流程控制的图示:
从上面两个图的对比也能看出,状态机中的任务片段是由主程序主动调用的,属于传统的由上至下的控制任务流程,任务调度器中的任务片段则是由任务调度器被动调用的,属于从下到上的切换任务流程,这也印证了前面谈到的程序流反向控制机制。
1.3 堆栈迁移
前面提到程序折断点上下文的现场保护需要保存堆栈,每个任务都设有一个私有堆栈,用于保存任务流被折断(任务切换)时的堆栈内容。堆栈是上下文切换时最重要的切换对象,这种对堆栈的切换叫作“堆栈迁移”。
堆栈迁移有两种方式:一种方法是(左图)使用私栈作为主堆栈,发生任务切换时,只需将栈指针切换到新任务的栈顶即可,另一种是(右图)使用公栈作为主堆栈,每切换一个任务,就将公栈的内容搬向旧任务的私栈,并将新任务从私栈搬至公栈,然后修改栈指针指向新的栈顶。
栈指针切换和堆栈搬移两种方式的优缺点在上图中简单列出了,分别从时间和空间两个角度对比下。栈指针切换只需要移动两个字节,而堆栈搬移则需要搬移若干字节(每增加一层函数调用还需要增加至少两个字节)且换入/换出各需搬移一次,可见栈指针切换消耗时间短得多。一般时间与空间难以两全,在内存空间的占用方面就正好相反了。栈指针切换方式,每个私栈都需要有足够的栈深支撑调子函数、调中断、寄存器压栈等动作,至少占用8-12字节栈深,而使用堆栈搬移方式时,私栈只要保存从栈底到任务切换时的栈深,中断和调子函数的栈深可由公栈承担,所以私栈分配4-8字节栈深基本够用。
二、任务调度器工作原理
一般任务调度器应用于操作系统设计中,多任务调度器根据使用场景主要分为两大类:抢占式多任务调度器与非抢占式多任务调度器。
抢占式多任务调度器主要应用于MCU等需要及时响应外设事件的场景,强调特定任务响应的实时性,所以一个常用的调度策略是给每个任务分配一个不同的优先级,每次调度运行处于就绪态的优先级最高的任务,这就是RTOS的任务调度算法原理。
非抢占式多任务调度器主要用于CPU等处理复杂运算和资源I/O等没那么注重实时响应的场景,这种场景更注重针对每个任务的运行状况合理分配CPU等资源,既要保证每个任务都能分配到合理的执行时间和空间资源不至于某个任务迟迟得不到执行机会,又要最大限度的利用计算机资源尽量减少计算存储等资源的浪费。比较经典的是Linux系统的CFS完全公平调度策略,这个比较复杂,读者感兴趣可以自行了解。
考虑到RTOS使用的抢占式多任务调度算法相对简单,下面以UCOS的高优先级抢占式任务调度算法为例介绍下任务调度器的设计原理。任务调度器分为任务切换和任务选择两大部分,任务选择自然是选择接下来要切换到哪一个任务,UCOS中是选择处于就绪态下优先级最高的任务,任务切换则是将旧任务折断点的上下文信息保存到旧任务私栈,然后将待切换的新任务折断点上下文信息从该任务私栈搬移到公栈,然后从新任务折断点开始继续执行就完成了任务的切换。
但任务切换需要触发事件,UCOS通过SysTick定时器产生定时中断作为任务调度器的驱动事件,任务切换涉及到公栈与私栈数据的搬移,所以每个任务也需要有一个数据结构来保存其折断点上下文信息、优先级等重要信息。下面先从描述任务的数据结构任务控制块TCB谈一下UCOS是如何管理任务的。
2.1 任务描述与管理
UCOS中是使用一个叫任务控制块(TASK CONTROL BLOCK)的结构体来描述一个任务的,下面给出UCOS TCB数据结构的代码如下(已经删除了部分不重要的编译选项和成员变量):
// Micrium\Software\uCOS-II\Source\ucos_ii.h
/*
*********************************************************************************************************
* TASK CONTROL BLOCK
*********************************************************************************************************
*/
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list */
#if (OS_EVENT_EN)
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */
#endif
#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
INT32U OSTCBDly; /* Nbr ticks to delay task or, timeout waiting for event */
INT8U OSTCBStat; /* Task status */
INT8U OSTCBStatPend; /* Task PEND status */
INT8U OSTCBPrio; /* Task priority (0 == highest) */
INT8U OSTCBX; /* Bit position in group corresponding to task priority */
INT8U OSTCBY; /* Index into ready table corresponding to task priority */
OS_PRIO OSTCBBitX; /* Bit mask to access bit position in ready table */
OS_PRIO OSTCBBitY; /* Bit mask to access bit position in ready group */
} OS_TCB;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
上面的TCB首地址与第一个成员变量OSTCBStkPtr(当前任务堆栈的栈顶指针)的地址一致,是为了在任务切换时可以直接通过TCB访问到OSTCBStkPtr,不需要进行指针偏移,提高了对任务堆栈的访问效率。OSTCBNext与OSTCBPrev可以看出,多个任务的TCB以双向链表的形式组织在一起,便于任务的前后遍历。OSTCBEventPtr与OSTCBMsg主要用于任务间通信的,后面再介绍,这里先略去。OSTCBDly用于任务睡眠和超时等待的,OSTCBStat与OSTCBStatPend用于标记任务所处状态的,每个任务也借助状态机模型被分为不同的状态,方便管理。OSTCBPrio是记录任务优先级的,后面的OSTCBX、OSTCBY、OSTCBBitX、OSTCBBitY则是辅助计算优先级的参数,任务优先级的计算不是本文重点,本文先略去了。
所有的任务控制块TCB都是存放在任务控制块列表数组OSTCBTbl[OS_MAX_TASKS]中的,系统通过任务控制块优先级表OSTCBPrioTbl[OSTCBPrio]查询到任务控制块的地址。任务控制块优先级表OSTCBPrioTbl[OSTCBPrio]是以任务优先级为索引,里面保存的是任务控制块的首地址,据此可以通过任务优先级号快速找到当前任务在任务控制块中的首地址,而不必到任务控制块链表中去一步一步查找,加快了任务的切换时间,提高了操作系统的效率。任务控制块、任务堆栈、任务控制块优先级链表三者的指针对应关系见下图:
常用的任务管理函数和功能描述如下,具体代码不是本文重点就略去了:
函数 | 描述 |
---|---|
INT8U OSTaskCreate (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT8U prio) | 传递进去的参数分别为任务函数指针、任务函数参数、任务堆栈栈顶指针、任务优先级,创建任务就是将任务代码、任务堆栈、任务控制块等内存区域联系在一起,并且初始化任务控制块与任务堆栈中的相应字段 |
INT8U OSTaskDel (INT8U prio) | 当一个任务不需要运行时,可以调用该函数将该任务删除,参数prio优先级可以唯一标识该任务 |
INT8U OSTaskSuspend (INT8U prio) | 有些任务因某些原因需要暂停运行,但以后还要继续运行,可以调用该函数挂起该任务 |
INT8U OSTaskResume (INT8U prio) | 前面挂起的任务如果要继续运行,可以调用该函数恢复运行 |
INT8U OSTaskChangePrio (INT8U oldprio, INT8U newprio) | 创建任务时为该任务设定了优先级,如果后面对该任务的优先级需求有变更,可以调用该函数动态更改该任务的响应优先级 |
2.2 任务调度选择算法
前面提到UCOS调度算法是选择处于就绪态中优先级最高的任务运行,通过前面任务描述的介绍可知,一个任务可以由一个优先级唯一标识,以任务优先级为索引获得任务控制块的首地址(OSTCBPrioTbl[OSTCBPrio]),从而获得任务堆栈栈顶指针,保存或者恢复任务程序流折断点的上下文信息。但还有个前提条件是处于就绪态的任务,那么什么是就绪态呢?一个任务为了方便管理,使用状态机模型将任务划分为五个状态,它们之间的状态转换图如下:
从上图可以看出就绪态是任务状态中的一种,而任务调度器只调度就绪态的任务。为了更快的从就绪态任务中找到最高优先级的任务,我们还需要一个数据结构任务就绪表来辅助管理任务。任务就绪表是一个8位字节数组,每一位表示一个任务是否处于就绪状态(1-就绪态,0-非就绪态),由一个8 * 8的二维数组位图表示64个优先级的任务是否处于就绪态,数据结构图示如下:
任务就绪表的主要作用就是标识任务是否处于就绪态,并能方便的从就绪态任务中找到优先级最高的任务,所以对任务就绪表的操作主要有三种:某任务对应位置1、某任务对应位清0、查找到当前任务就绪表中优先级最高的任务优先级(上面二维数组中被置1的最小数字位)。前面提到任务控制块中OSTCBX、OSTCBY、OSTCBBitX、OSTCBBitY这几个成员变量就是辅助操作任务就绪表的,具体的位操作运算不是本文重点先略去了,至此我们了解了任务调度器如何从就绪任务中找到最高优先级任务的,找到了待切换到的目标任务,接下来就看如何进行任务切换了。
2.3 任务切换过程
处理器是如何保存程序流折断点上下文信息的,又是如何进行程序切换的呢?这需要先介绍下处理器的寄存器组了,下面给出ARM Crotex-M3寄存器组示意图如下:
通用寄存器R0~R12保存程序运算过程中需要的数据和中间结果的,程序计数器PC是个指路器,指向下一条要执行指令的地址(任务代码区),所以改变PC的值就能改变程序流的走向,操作系统是通过把待运行程序的地址赋予程序计数器PC来实现程序切换的。
连接寄存器LR保存调用子程序或异常的返回地址,以便子程序执行完毕后返回其调用点,也即ARM处理器通过LR寄存器实现对断点和调用点的记录。堆栈指针寄存器SP(包括MSP与PSP)指向任务堆栈的栈顶,任务堆栈是内存中一种后进先出的数据结构,常用于保存中断断点、子程序调用返回点、CPU现场数据、程序间传递参数等,通用寄存器中操作的数据主要来自于内存中的任务堆栈区,所以程序折断点的上下文信息可以保存在内存中的任务堆栈中。特殊功能寄存器主要保存跟程序状态和中断相关的信息。
任务切换最主要的三个相关寄存器:PC指向目标任务代码,LR指向子程序或异常的返回地址,SP指向任务堆栈的栈顶,处理器中的运行环境(寄存器组)与内存中的运行环境(任务堆栈与任务代码)之间的关系如下图示:
任务堆栈可以看作是该任务的虚拟处理器(在Linux系统中有一个TSS任务状态段的数据结构实现相似的功能),保存了处理器的运行环境。当需要运行某个任务时就把该任务堆栈复制到实际的处理器寄存器组中就可以了;当需要切换任务时,则把当前处理器寄存器组中的上下文数据复制到当前任务堆栈,再把另一个需要运行的任务堆栈中的上下文数据复制到实际处理器寄存器组中就可以了。
前面介绍任务描述与管理时提到任务创建函数将任务代码、任务堆栈、任务控制块(构成任务的三要素)等内存区域联系在一起,内部通过调用OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)完成任务堆栈初始化,其中将任务代码地址赋值给虚拟程序计数器PC,将任务代码参数赋值给虚拟通用寄存器R0,返回内存中初始化后的任务堆栈栈顶地址。既然任务切换的关键是在实际处理器寄存器组与任务堆栈间搬移数据,而任务堆栈指针SP是访问任务堆栈的唯一依据,那么任务切换的关键就是把程序的任务堆栈指针OS_TASK_STK赋予处理器的堆栈指针SP,通过堆栈指针SP的切换实现任务的切换。
2.4 任务调度的触发
任务调度的选择和任务切换原理以及搞清楚了,什么时候执行任务调度呢?继续回到抢占式任务调度的定义中找答案”每次调度运行处于就绪态的优先级最高的任务“,从这句话可以看出,任务调度的条件涉及两个:就绪态与最高优先级,也就是说这两个因素中的任何一个有变化,都应该执行任务调度程序。换句话说,有新的任务加入就绪态、有旧的任务离开就绪态、原本处于就绪态中的任务优先级被更改等情况,都应该调用任务调度程序。
那么怎么判断任务状态或优先级是否发生改变了呢?换言之就是怎么判断任务就绪表是否产生变更呢?这就要用到操作系统的心跳,系统滴答定时器SYSTICK,每个滴答中断都会在其中断服务程序中检查任务就绪表是否变更,是否需要执行任务调度。下面给出两个任务间通过SYSTICK触发调度的简单模式图示:
上面是比较简单的情况,但如果在产生SYSTICK中断时系统正在响应一个中断会怎样?从抢占式任务保证对外设事件响应的实时性角度考虑,响应中断的优先级自然要高于任务切换的优先级,此时SYSTICK中断应该延迟等待外设中断响应完成后再执行,ARM为SYSTICK提供了一个可以悬起并延迟执行的低优先级中断PendSV,如果需要执行任务切换则悬起PendSV异常,它会自动延迟上下文切换请求,直到其他的ISR中断服务请求都完成处理后才放行,为实现这个机制,需要把PendSV编程为最低优先级的异常。使用PendSV控制上下文切换的图示如下:
从上面的过程可以看出,任务切换的核心是PendSV异常的触发和响应,任务切换函数完成PendSV异常的触发,PendSV异常处理函数完成堆栈数据的搬移。上图涉及到两次任务调度,前一次没有中断发生,直接在PendSV中执行上下文切换;后一次有中断发生,在ISR中断服务请求执行完毕后再在PendSV中执行上下文切换。两种任务调度方式虽然都是在PendSV中完成的,但前一种方式需要调度程序自己保存旧任务上下文数据,后一种方式则由ISR中断服务请求函数完成了保存旧任务上下文数据的任务,所以UCOS就有两种任务调度器,分别为任务级上下文调度和中断级任务调度。
2.5 任务调度代码
通过前面的介绍,我们已经了解任务在计算机中是如何被描述和管理的、下一个要切换的任务是如何被选择出来的、任务是如何切换的、任务调度是在何时被触发或调用的,下面用UCOS的代码从任务调度函数入手,看任务的调度在代码中是如何实现的。前面也说了,UCOS的任务调度器有两种:任务级调度器和中断级调度器, 下面分别给出UCOS中的两种任务调度函数的代码如下:
// Micrium\Software\uCOS-II\Source\os_core.c
/*
*********************************************************************************************************
* 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).
*
* Arguments : none
* Returns : none
*
* Notes : 1) This function is INTERNAL to uC/OS-II and your application should not call it.
* 2) Rescheduling is prevented when the scheduler is locked (see OS_SchedLock())
*********************************************************************************************************
*/
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 */
OS_TASK_SW(); /* Perform a context switch */
}
}
}
OS_EXIT_CRITICAL();
}
/*
*********************************************************************************************************
* EXIT ISR
*
* Description: This function is used to notify uC/OS-II that you have completed serviving an ISR. When
* the last nested ISR has completed, uC/OS-II will call the scheduler to determine whether
* a new, high-priority task, is ready to run.
*
* Arguments : none
* Returns : none
*
* Notes : 1) You MUST invoke OSIntEnter() and OSIntExit() in pair. In other words, for every call
* to OSIntEnter() at the beginning of the ISR you MUST have a call to OSIntExit() at the
* end of the ISR.
* 2) Rescheduling is prevented when the scheduler is locked (see OS_SchedLock())
*********************************************************************************************************
*/
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 */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
}
}
}
OS_EXIT_CRITICAL();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
上面的调度函数中OS_SchedNew()函数主要是用来获得当前处于就绪态中最高优先级任务的优先级号OSPrioHighRdy,该变量是一个全局变量,所以OS_SchedNew()函数直接操作全局变量而没有参数和返回值。通过OSPrioHighRdy作为索引,在OSTCBPrioTbl[OSPrioHighRdy]中获得切换目标任务控制块的首地址OSTCBHighRdy,也即任务堆栈栈顶指针的地址。
前面找到了切换目标任务的堆栈指针,接下来的任务切换主要由OS_TASK_SW()或OSIntCtxSw()函数完成,OS_TASK_SW()通过宏定义指向了汇编函数OSCtxSw(),所以实际上两种调度器的任务切换分别由OSCtxSw()与OSIntCtxSw()汇编函数实现。
// Micrium\Software\uCOS-II\Ports\ARM-Cortex-M3\Generic\RealView\os_cpu.h
#define OS_TASK_SW() OSCtxSw()
- 1
- 2
这两个汇编函数(OSCtxSw()与OSIntCtxSw())的实现原理正如前面提到的,悬起或触发一个PendSV异常,具体的堆栈搬移工作在PendSV服务函数中完成,具体代码如下:
// Micrium\Software\uCOS-II\Ports\ARM-Cortex-M3\Generic\RealView\os_cpu_a.asm
;/**************************************************************************************
;* 函数名称: OSCtxSw
;* 功能描述:任务级上下文切换
;* 参 数: None
;* 返回值: None
;***************************************************************************************/
OSCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常(causes context switch)
LDR R5, =NVIC_PENDSVSET
STR R5, [R4]
POP {R4, R5}
BX LR
;/**************************************************************************************
;* 函数名称: OSIntCtxSw
;* 功能描述:中断级任务切换
;* 参 数: None
;* 返回值: None
;***************************************************************************************/
OSIntCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;触发PendSV异常(causes context switch)
LDR R5, =NVIC_PENDSVSET
STR R5, [R4]
POP {R4, R5}
BX LR
NOP
;/**************************************************************************************
;* 函数名称: OSPendSV
;* 功能描述: OSPendSV is used to cause a context switch.
;* 参 数: None
;* 返回值: None
;***************************************************************************************/
PendSV_Handler
CPSID I ; Prevent interruption during context switch
MRS R0, PSP ; PSP is process stack pointer
CBZ R0, PendSV_Handler_Nosave ; Skip register save the first time
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11}
LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1] ; R0 is SP of process being switched out
; At this point, entire context of process has been saved
PendSV_Handler_Nosave
PUSH {R14} ; Save LR exc_return value
LDR R0, =OSTaskSwHook ; OSTaskSwHook();
BLX R0
POP {R14}
LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy;
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr;
LDM R0, {R4-R11} ; Restore r4-11 from new process stack
ADDS R0, R0, #0x20
MSR PSP, R0 ; Load PSP with new process SP
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
end
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
从上面的代码可以看出,OSCtxSw()与OSIntCtxSw()代码基本一样,因为两者需要完成的任务一样,都是触发PendSV异常,但两者的工作模式不同,前一个是用户级线程模式,后一个是特权级Handler模式,所以两个函数的PendSV_Handler执行动作有所区别,前者先保存当前寄存器组数据到当前任务堆栈,后者跳过保存寄存器过程(在进入ISR前已保存过),直接装填新任务堆栈中的上下文信息到处理器寄存器组。
到这里UCOS任务调度器的原理和实现代码基本解释清楚了,如果想了解更完整的代码逻辑,可以查看UCOS的完整代码。
评论记录:
回复评论: