写点什么

LiteOS 内核源码分析:任务 LOS_Schedule

发布于: 2021 年 04 月 06 日

​​​​摘要: 调度,Schedule 也称为 Dispatch,是操作系统的一个重要模块,它负责选择系统要处理的下一个任务。调度模块需要协调处于就绪状态的任务对资源的竞争,按优先级策略从就绪队列中获取高优先级的任务,给予资源使用权。


本文分享自华为云社区《LiteOS内核源码分析系列六-任务及调度(5)-任务LOS_Schedule》,原文作者:zhushy 。


本文我们来一起学习下 LiteOS 调度模块的源代码,文中所涉及的源代码,均可以在 LiteOS 开源站点https://gitee.com/LiteOS/LiteOS 获取。调度源代码分布如下:


  • LiteOS 内核调度源代码


包括调度模块的私有头文件 kernel\base\include\los_sched_pri.h、C 源代码文件 kernel\base\sched\sched_sq\los_sched.c,这个对应单链表就绪队列。还有个`调度源代码文件 kernel\base\sched\sched_mq\los_sched.c,对应多链表就绪队列。本文主要剖析对应单链表就绪队列的调度文件代码,使用多链表就绪队列的调度代码类似。


  • 调度模块汇编实现代码


调度模块的汇编函数有 OsStartToRun、OsTaskSchedule 等,根据不同的 CPU 架构,分布在下述文件里: arch\arm\cortex_m\src\dispatch.S、arch\arm\cortex_a_r\src\dispatch.S、arch\arm64\src\dispatch.S。


本文以 STM32F769IDISCOVERY 为例,分析一下 Cortex-M 核的调度模块的源代码。我们先看看调度头文件 kernel\base\include\los_sched_pri.h 中定义的宏函数、枚举、和内联函数。


1、调度模块宏函数和内联函数


kernel\base\include\los_sched_pri.h 定义的宏函数、枚举、内联函数。


1.1 宏函数和枚举


UINT32 g_taskScheduled 是 kernel\base\los_task.c 定义的全局变量,标记内核是否开启调度,每一位代表不同的 CPU 核的调度开启状态。


⑴处定义的宏函数 OS_SCHEDULER_SET(cpuid)开启 cpuid 核的调度。⑵处宏函数 OS_SCHEDULER_CLR(cpuid)是前者的反向操作,关闭 cpuid 核的调度。⑶处宏判断当前核是否开启调度。⑷处的枚举用于标记是否发起了请求调度。当需要调度,又暂不具备调度条件的时候,标记下状态,等具备调度的条件时,再去调度。


⑴  #define OS_SCHEDULER_SET(cpuid) do {     \        g_taskScheduled |= (1U << (cpuid));  \    } while (0);
⑵ #define OS_SCHEDULER_CLR(cpuid) do { \ g_taskScheduled &= ~(1U << (cpuid)); \ } while (0);
⑶ #define OS_SCHEDULER_ACTIVE (g_taskScheduled & (1U << ArchCurrCpuid()))
⑷ typedef enum { INT_NO_RESCH = 0, /* no needs to schedule */ INT_PEND_RESCH, /* pending schedule flag */ } SchedFlag;
复制代码

1.2 内联函数


有 2 个内联函数用于检查是否可以调度,即函数 STATIC INLINE BOOL OsPreemptable(VOID)和

STATICINLINE BOOL OsPreemptableInSched(VOID)。区别是,前者判断是否可以抢占调度时,先关中断,避免当前的任务迁移到其他核,返回错误的是否可以抢占调度状态。


1.2.1 内联函数 STATIC INLINE BOOLOsPreemptable(VOID)


我们看下 BOOLOsPreemptable(VOID)函数的源码。⑴、⑶属于关闭、开启中断,保护检查抢占状态的操作。⑵处判断是否可抢占调度,如果不能调度,则标记下是否需要调度标签为 INT_PEND_RESCH。


STATIC INLINE BOOL OsPreemptable(VOID){⑴  UINT32 intSave = LOS_IntLock();⑵    BOOL preemptable = (OsPercpuGet()->taskLockCnt == 0);    if (!preemptable) {        OsPercpuGet()->schedFlag = INT_PEND_RESCH;    }⑶  LOS_IntRestore(intSave);    return preemptable;}
复制代码


1.2.2 内联函数 STATIC INLINE BOOLOsPreemptableInSched(VOID)


函数 STATICINLINE BOOL OsPreemptableInSched(VOID)检查是否可以抢占调度,检查的方式是判断 OsPercpuGet()->taskLockCnt 的计数,见⑴、⑵处代码。如果不能调度,则执行⑶标记下是否需要调度标签为 INT_PEND_RESCH。对于 SMP 多核,是否可以调度的检查方式,稍有不同,因为调度持有自旋锁,计数需要加 1,见代码。


STATIC INLINE BOOL OsPreemptableInSched(VOID){    BOOL preemptable = FALSE;
#ifdef LOSCFG_KERNEL_SMP⑴ preemptable = (OsPercpuGet()->taskLockCnt == 1);#else⑵ preemptable = (OsPercpuGet()->taskLockCnt == 0);#endif if (!preemptable) {⑶ OsPercpuGet()->schedFlag = INT_PEND_RESCH; }
return preemptable;}
复制代码


1.2.3 内联函数 STATIC INLINE VOIDLOS_Schedule(VOID)


函数 STATICINLINE VOID LOS_Schedule(VOID)用于触发触发调度。⑴处代码表示,如果系统正在处理中断,标记下是否需要调度标签为 INT_PEND_RESCH,等待合适时机再调度。然后调用 VOID OsSchedPreempt(VOID)函数,下午会分析该函数。二者的区别就是多个检查,判断是否系统是否正在处理中断。


STATIC INLINE VOID LOS_Schedule(VOID){    if (OS_INT_ACTIVE) {⑴      OsPercpuGet()->schedFlag = INT_PEND_RESCH;        return;    }    OsSchedPreempt();}
复制代码


2、调度模块常用接口


这一小节,我们看看 kernel\base\sched\sched_sq\los_sched.c 定义的调度接口,包含 VOIDOsSchedPreempt(VOID)、VOID OsSchedResched(VOID)两个主要的调度接口。两者的区别是,前者需要把当前任务放入就绪队列内,再调用后者触发调用。后者直接从就绪队列里获取下一个任务,然后触发调度去运行下一个任务。这 2 个接口都是内部接口,对外提供的调度接口是上一小节分析过的 STATIC INLINE VOID LOS_Schedule(VOID),三者有调用关系 STATICINLINE VOID LOS_Schedule(VOID)--->VOID OsSchedPreempt(VOID)--->VOIDOsSchedResched(VOID)。


我们分析下这些调度接口的源代码。

2.1 抢占调度函数 VOID OsSchedResched(VOID)


抢占调度函数 VOIDOsSchedResched(VOID),我们分析下源代码。


⑴验证需要持有任务模块的自旋锁。⑵处判断是否支持调度,如果不具备调度的条件,则暂不调度。⑶获取当前运行任务,从就绪队列中获取下一个高优先级的任务。验证下一个任务 newTask 不能为空,并更改其状态为非就绪状态。⑷处判断当前任务和下一个任务不能为同一个,否则返回。这种情况不会发生,当前任务肯定会从优先级队列中移除的,二者不可能是同一个。⑸更改 2 个任务的运行状态,当前任务设置为非运行状态,下一个任务设置为运行状态。⑹处如果支持多核,则更改任务的运行在哪个核。紧接着的一些代码属于调度维测信息,暂时不管。⑺处如果支持时间片调度,并且下一个新任务的时间片为 0,设置为时间片超时时间的最大值 LOSCFG_BASE_CORE_TIMESLICE_TIMEOUT。⑻设置下一个任务 newTask 为当前运行任务,会更新全局变量 g_runTask。然后调用汇编函数 OsTaskSchedule(newTask, runTask)执行调度,后文分析该汇编函数的实现代码。


VOID OsSchedResched(VOID){    LosTaskCB *runTask = NULL;    LosTaskCB *newTask = NULL;
⑴ LOS_ASSERT(LOS_SpinHeld(&g_taskSpin));
⑵ if (!OsPreemptableInSched()) { return; }
⑶ runTask = OsCurrTaskGet(); newTask = OsGetTopTask(); LOS_ASSERT(newTask != NULL); newTask->taskStatus &= ~OS_TASK_STATUS_READY;
⑷ if (runTask == newTask) { return; }
⑸ runTask->taskStatus &= ~OS_TASK_STATUS_RUNNING; newTask->taskStatus |= OS_TASK_STATUS_RUNNING;
#ifdef LOSCFG_KERNEL_SMP⑹ runTask->currCpu = OS_TASK_INVALID_CPUID; newTask->currCpu = ArchCurrCpuid();#endif
OsTaskTimeUpdateHook(runTask->taskId, LOS_TickCountGet());
#ifdef LOSCFG_KERNEL_CPUP OsTaskCycleEndStart(newTask);#endif
#ifdef LOSCFG_BASE_CORE_TSK_MONITOR OsTaskSwitchCheck(runTask, newTask);#endif
LOS_TRACE(TASK_SWITCH, newTask->taskId, runTask->priority, runTask->taskStatus, newTask->priority, newTask->taskStatus);
#ifdef LOSCFG_DEBUG_SCHED_STATISTICS OsSchedStatistics(runTask, newTask);#endif
PRINT_TRACE("cpu%u (%s) status: %x -> (%s) status:%x\n", ArchCurrCpuid(), runTask->taskName, runTask->taskStatus, newTask->taskName, newTask->taskStatus);
#ifdef LOSCFG_BASE_CORE_TIMESLICE if (newTask->timeSlice == 0) {⑺ newTask->timeSlice = LOSCFG_BASE_CORE_TIMESLICE_TIMEOUT; }#endif
⑻ OsCurrTaskSet((VOID*)newTask); OsTaskSchedule(newTask, runTask);}
复制代码


2.2 抢占调度函数 VOID OsSchedPreempt(VOID)


抢占调度函数 VOIDOsSchedPreempt(VOID),把当前任务放入就绪队列,从队列中获取高优先级任务,然后尝试调度。当锁调度,或者没有更高优先级任务时,调度不会发生。⑴处判断是否支持调度,如果不具备调度的条件,则暂不调度。⑵获取当前任务,更改其状态为非就绪状态。


如果开启时间片调度并且当前任务时间片为 0,则执行⑶把当前任务放入就绪队列的尾部,否则执行⑷把当前任务放入就绪队列的头部,同等优先级下可以更早的运行。⑸调用函数 OsSchedResched()

去调度。


VOID OsSchedPreempt(VOID){    LosTaskCB *runTask = NULL;    UINT32 intSave;
⑴ if (!OsPreemptable()) { return; }
SCHEDULER_LOCK(intSave);
⑵ runTask = OsCurrTaskGet(); runTask->taskStatus |= OS_TASK_STATUS_READY;
#ifdef LOSCFG_BASE_CORE_TIMESLICE if (runTask->timeSlice == 0) {⑶ OsPriQueueEnqueue(&runTask->pendList, runTask->priority); } else {#endif⑷ OsPriQueueEnqueueHead(&runTask->pendList, runTask->priority);#ifdef LOSCFG_BASE_CORE_TIMESLICE }#endif
⑸ OsSchedResched();
SCHEDULER_UNLOCK(intSave);}
复制代码


2.3 时间片检查函数 VOID OsTimesliceCheck(VOID)


函数 VOIDOsTimesliceCheck(VOID)在支持时间片调度时才生效,该函数在 tick 中断函数 VOID OsTickHandler(VOID)里调用。如果当前运行函数的时间片使用完毕,则触发调度。⑴处获取当前运行任务,⑵判断 runTask->timeSlice 时间片是否为 0,不为 0 则减 1。如果减 1 后为 0,则执行⑶调用 LOS_Schedule()触发调度。


#ifdef LOSCFG_BASE_CORE_TIMESLICELITE_OS_SEC_TEXT VOID OsTimesliceCheck(VOID){⑴  LosTaskCB *runTask = OsCurrTaskGet();⑵  if (runTask->timeSlice != 0) {        runTask->timeSlice--;        if (runTask->timeSlice == 0) {⑶          LOS_Schedule();        }    }}#endif
复制代码


3、调度模块汇编函数


文件 arch\arm\cortex_m\src\dispatch.S 定义了调度的汇编函数,我们分析下这些调度接口的源代码。汇编文件中定义了如下几个宏,见注释。


.equ OS_NVIC_INT_CTRL,           0xE000ED04     ; Interrupt Control State Register,ICSR 中断控制状态寄存器.equ OS_NVIC_SYSPRI2,            0xE000ED20     ; System Handler Priority Register 系统优先级寄存器.equ OS_NVIC_PENDSV_PRI,         0xF0F00000     ; PendSV异常优先级.equ OS_NVIC_PENDSVSET,          0x10000000     ; ICSR寄存器的PENDSVSET位置1时,会触发PendSV异常.equ OS_TASK_STATUS_RUNNING,     0x0010         ; los_task_pri.h中的同名宏定义,数值也一样,表示任务运行状态,
复制代码


3.1 OsStartToRun 汇编函数


函数 OsStartToRun 在文件 kernel\init\los_init.c 中的运行函数 VOIDOsStart(VOID)启动系统阶段调用,传入的参数为就绪队列中最高优秀级的 LosTaskCB *taskCB。我们接下来分析下该函数的汇编代码。


⑴处设置 PendSV 异常优先级为 OS_NVIC_PENDSV_PRI,PendSV 异常一般设置为最低。全局变量

g_oldTask、g_runTask 定义在 arch\arm\cortex_m\src\task.c 文件内,分别记录上一次运行的任务、和当前运行的任务。⑵处代码把函数 OsStartToRun 的入参 LosTaskCB*taskCB 赋值给这 2 个全局变量。


⑶处往控制寄存器 CONTROL 写入二进制的 10,表示使用 PSP 栈,特权级的线程模式。UINT16 taskStatus 是 LosTaskCB 结构体的第二个成员变量,⑷处[r0 , #4]获取任务状态,此时寄存器 r7 数值为 0x4,即就绪状态 OS_TASK_STATUS_READY。然后把任务状态改为运行状态 OS_TASK_STATUS_RUNNING。


⑸处把[r0]的值即任务的栈指针 taskCB->stackPointer 加载到寄存器 R12,现在 R12 指向任务栈的栈指针,任务栈现在保存的是上下文,对应定义在 arch\arm\cortex_m\include\arch\task.h 中的结构体 TaskContext。往后 2 行代码把 R12 加 36+64=100,共 25 个 4 字节长度,其中包含 S16 到 S31 共 16 个 4 字节,R4 到 R11 及 PriMask 共 9 个 4 字节的长度,当前 R12 指向任务栈中上下文的 UINT32 R0 位置,如图。



⑹处代码把任务栈上下文中的 UINT32 R0; UINT32 R1; UINT32 R2; UINT32 R3; UINT32 R12; UINT32 LR;UINT32 PC; UINT32 xPSR;的分别加载到寄存器 R0-R7,其中 R5 对应 UINT32 LR,R6

对应 UINT32PC,此时寄存器 R12 指向任务栈上下文的 UINT32 xPSR。执行⑺处指令,指针继续加 18 个 4 字节长度,即对应 S0 到 S15 及 UINT32 FPSCR; UINT32 NO_NAME 等上下文的 18 个成员。此时,寄存器 R12 指向任务栈的栈底,紧接着把寄存器 R12 写入寄存器 psp。


最后,执行⑻处指令,把 R5 写入 lr 寄存器,开中断,然后跳转到 R6 对应的上下文的 PC 对应的函数 VOIDOsTaskEntry(UINT32 taskId),去执行任务的入口函数。


.type OsStartToRun, %function.global OsStartToRunOsStartToRun:    .fnstart    .cantunwind⑴  ldr     r4, =OS_NVIC_SYSPRI2    ldr     r5, =OS_NVIC_PENDSV_PRI    str     r5, [r4]
⑵ ldr r1, =g_oldTask str r0, [r1]
ldr r1, =g_runTask str r0, [r1]#if defined(LOSCFG_ARCH_CORTEX_M0) movs r1, #2 msr CONTROL, r1 ldrh r7, [r0 , #4] movs r6, #OS_TASK_STATUS_RUNNING strh r6, [r0 , #4] ldr r3, [r0] adds r3, r3, #36 ldmfd r3!, {r0-r2} adds r3, r3, #4 ldmfd r3!, {R4-R7} msr psp, r3 subs r3, r3, #20 ldr r3, [r3]#else⑶ mov r1, #2 msr CONTROL, r1
⑷ ldrh r7, [r0 , #4] mov r8, #OS_TASK_STATUS_RUNNING strh r8, [r0 , #4]
⑸ ldr r12, [r0] ADD r12, r12, #36#if !defined(LOSCFG_ARCH_CORTEX_M3) ADD r12, r12, #64#endif
⑹ ldmfd r12!, {R0-R7}#if !defined(LOSCFG_ARCH_CORTEX_M3)⑺ add r12, r12, #72#endif msr psp, r12#if !defined(LOSCFG_ARCH_CORTEX_M3) vpush {s0}; vpop {s0};#endif#endif
⑻ mov lr, r5 cpsie I bx r6 .fnend
复制代码


3.2 OsTaskSchedule 汇编函数


汇编函数 OsTaskSchedule 实现新老任务的切换调度。从上文分析抢占调度函数 VOID OsSchedResched(VOID)时可以知道,传入了 2 个参数,分别是新任务 LosTaskCB *newTask 和当前运行的任务 LosTaskCB*runTask,对于 Cortex-M 核,这 2 个参数在该汇编函数中没有使用到。在执行汇编函数 OsTaskSchedule 前,全局变量 g_runTask 被赋值为要切换运行的新任务 LosTaskCB *newTask。


我们看看这个汇编函数的源代码,首先往中断控制状态寄存器 OS_NVIC_INT_CTRL 中的 OS_NVIC_PENDSVSET 位置 1,触发 PendSV 异常。执行完毕 osTaskSchedule 函数,返回上层函数抢占调度函数 VOID OsSchedResched(VOID)。PendSV 异常的回调函数是 osPendSV 汇编函数,下文会分析此函数。汇编函数 OsTaskSchedule 如下:


.type OsTaskSchedule, %function.global OsTaskScheduleOsTaskSchedule:    .fnstart    .cantunwind    ldr     r2, =OS_NVIC_INT_CTRL    ldr     r3, =OS_NVIC_PENDSVSET    str     r3, [r2]    bx      lr    .fnend
复制代码


3.3 osPendSV 汇编函数


接下来,我们分析下 osPendSV 汇编函数的源代码。⑴处把寄存器 PRIMASK 数值写入寄存器 r12,备份中断的开关状态,然后执行指令 cpsid I 屏蔽全局中断。⑵处把当前任务栈的栈指针加载到寄存器 r0。⑶处把寄存器 r4-r12 的数值压入当前任务栈,执行⑷把寄存器 d8-d15 的数值压入当前任务栈,r0

为任务栈指针。


⑸处指令把 g_oldTask 指针地址加载到 r5 寄存器,然后下一条指令把 g_oldTask 指针指向的内存地址值加载到寄存器 r1,然后使用寄存器 r0 数值更新 g_oldTask 任务的栈指针。


⑹处指令把 g_runTask 指针地址加载到 r0 寄存器,然后下一条指令把 g_runTask 指针指向的内存地址值加载到寄存器 r0。此时,r5 为上一个任务 g_oldTask 的指针地址,执行⑺处指令后,g_oldTask、g_runTask 都指向新任务。


执行⑻处指令把 g_runTask 指针指向的内存地址值加载到寄存器 r1,此时 r1 寄存器为新任务 g_runTask 的栈指针。⑼处指令把新任务栈中的数据加载到寄存器 d8-d15 寄存器,继续执行后续指令继续加载数据到 r4-r12 寄存器,然后执行⑽处指令更新 psp 任务栈指针。⑾处指令恢复中断状态,然后执行跳转指令,后续继续执行 C 代码 VOID OsTaskEntry(UINT32 taskId)进入任务执行入口函数。


.type osPendSV, %function.global osPendSVosPendSV:    .fnstart    .cantunwind⑴  mrs     r12, PRIMASK    cpsid   I
TaskSwitch:⑵ mrs r0, psp
#if defined(LOSCFG_ARCH_CORTEX_M0) subs r0, #36 stmia r0!, {r4-r7} mov r3, r8 mov r4, r9 mov r5, r10 mov r6, r11 mov r7, r12 stmia r0!, {r3 - r7}
subs r0, #36#else⑶ stmfd r0!, {r4-r12}#if !defined(LOSCFG_ARCH_CORTEX_M3)⑷ vstmdb r0!, {d8-d15}#endif#endif⑸ ldr r5, =g_oldTask ldr r1, [r5] str r0, [r1]
⑹ ldr r0, =g_runTask ldr r0, [r0] /* g_oldTask = g_runTask */⑺ str r0, [r5]⑻ ldr r1, [r0]
#if !defined(LOSCFG_ARCH_CORTEX_M3) && !defined(LOSCFG_ARCH_CORTEX_M0)⑼ vldmia r1!, {d8-d15}#endif#if defined(LOSCFG_ARCH_CORTEX_M0) adds r1, #16 ldmfd r1!, {r3-r7}
mov r8, r3 mov r9, r4 mov r10, r5 mov r11, r6 mov r12, r7 subs r1, #36 ldmfd r1!, {r4-r7}
adds r1, #20#else ldmfd r1!, {r4-r12}#endif⑽ msr psp, r1
⑾ msr PRIMASK, r12 bx lr .fnend
复制代码


3.4 开关中断汇编函数


分析中断源代码的时候,提到过开关中断函数 UINT32 LOS_IntLock(VOID)、UINT32LOS_IntUnLock(VOID)、VOID LOS_IntRestore(UINT32 intSave)调用了汇编函数,这些汇编函数分别是本文要分析的 ArchIntLock、ArchIntUnlock、ArchIntRestore。我们看下这些汇编代码,PRIMASK 寄存器是单一 bit 的寄存器,置为 1 后,就关掉所有可屏蔽异常,只剩下 NMI 和硬 Fault 异常可以响应。默认值是 0,表示没有关闭中断。汇编指令 cpsid I 会设置 PRIMASK=1,关闭中断,指令 cpsieI 设置 PRIMASK=0,开启中断。


⑴处 ArchIntLock 函数把寄存器 PRIMASK 数值返回并关闭中断。⑵处 ArchIntUnlock 函数把寄存器 PRIMASK 数值返回并开启中断。两个函数的返回结果可以传递给⑶处 ArchIntRestore 函数,把寄存器状态数值写入寄存器 PRIMASK,用于恢复之前的中断状态。不管是 ArchIntLock 还是 ArchIntUnlock,都可以和 ArchIntRestore 配对使用。


.type ArchIntLock, %function    .global ArchIntLock⑴  ArchIntLock:        .fnstart        .cantunwind        mrs     r0, PRIMASK        cpsid   I        bx      lr        .fnend
.type ArchIntUnlock, %function .global ArchIntUnlock⑵ ArchIntUnlock: .fnstart .cantunwind mrs r0, PRIMASK cpsie I bx lr .fnend
.type ArchIntRestore, %function .global ArchIntRestore⑶ ArchIntRestore: .fnstart .cantunwind msr PRIMASK, r0 bx lr .fnend
复制代码


小结


本文带领大家一起剖析了 LiteOS 调度模块的源代码,包含调用接口及底层的汇编函数实现。感谢阅读,如有任何问题、建议,都可以留言给我们: https://gitee.com/LiteOS/LiteOS/issues 。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 04 月 06 日阅读数: 20
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
LiteOS内核源码分析:任务LOS_Schedule