摘要:调度,Schedule 也称为 Dispatch,是操作系统的一个重要模块,它负责抉择零碎要解决的下一个工作。调度模块须要协调处于就绪状态的工作对资源的竞争,按优先级策略从就绪队列中获取高优先级的工作,给予资源使用权。
本文分享自华为云社区《LiteOS 内核源码剖析系列六 - 工作及调度(5)- 工作 LOS_Schedule》,原文作者:zhushy。
本文咱们来一起学习下 LiteOS 调度模块的源代码,文中所波及的源代码,均能够在 LiteOS
开源站点 https://gitee.com/LiteOS/LiteOS 获取。调度源代码散布如下:
- LiteOS 内核调度源代码
包含调度模块的公有头文件 kernelbaseincludelos_sched_pri.h、C 源代码文件 kernelbaseschedsched_sqlos_sched.c,这个对应单链表就绪队列。还有个 ` 调度源代码文件 kernelbaseschedsched_mqlos_sched.c,对应多链表就绪队列。本文次要分析对应单链表就绪队列的调度文件代码,应用多链表就绪队列的调度代码相似。
- 调度模块汇编实现代码
调度模块的汇编函数有 OsStartToRun、OsTaskSchedule 等,依据不同的 CPU 架构,散布在下述文件里:archarmcortex_msrcdispatch.S、archarmcortex_a_rsrcdispatch.S、archarm64srcdispatch.S。
本文以 STM32F769IDISCOVERY 为例,剖析一下 Cortex- M 核的调度模块的源代码。咱们先看看调度头文件 kernelbaseincludelos_sched_pri.h 中定义的宏函数、枚举、和内联函数。
1、调度模块宏函数和内联函数
kernelbaseincludelos_sched_pri.h 定义的宏函数、枚举、内联函数。
1.1 宏函数和枚举
UINT32 g_taskScheduled 是 kernelbaselos_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)和 STATIC INLINE BOOL OsPreemptableInSched(VOID)。区别是,前者判断是否能够抢占调度时,先关中断,防止以后的工作迁徙到其余核,返回谬误的是否能够抢占调度状态。
1.2.1 内联函数 STATIC INLINE BOOL OsPreemptable(VOID)
咱们看下 BOOL OsPreemptable(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 BOOL OsPreemptableInSched(VOID)
函数 STATIC INLINE 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 VOID LOS_Schedule(VOID)
函数 STATIC INLINE 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、调度模块罕用接口
这一大节,咱们看看 kernelbaseschedsched_sqlos_sched.c 定义的调度接口,蕴含 VOID OsSchedPreempt(VOID)、VOID OsSchedResched(VOID)两个次要的调度接口。两者的区别是,前者须要把当前任务放入就绪队列内,再调用后者触发调用。后者间接从就绪队列里获取下一个工作,而后触发调度去运行下一个工作。这 2 个接口都是外部接口,对外提供的调度接口是上一大节剖析过的 STATIC INLINE VOID LOS_Schedule(VOID),三者有调用关系 STATIC INLINE VOID LOS_Schedule(VOID)—>VOID OsSchedPreempt(VOID)—>VOID OsSchedResched(VOID)。
咱们剖析下这些调度接口的源代码。
2.1 抢占调度函数 VOID OsSchedResched(VOID)
抢占调度函数 VOID OsSchedResched(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:%xn", 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)
抢占调度函数 VOID OsSchedPreempt(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)
函数 VOID OsTimesliceCheck(VOID)在反对工夫片调度时才失效,该函数在 tick 中断函数 VOID OsTickHandler(VOID)里调用。如果以后运行函数的工夫片应用结束,则触发调度。⑴处获取以后运行工作,⑵判断 runTask->timeSlice 工夫片是否为 0,不为 0 则减 1。如果减 1 后为 0,则执行⑶调用 LOS_Schedule()触发调度。
#ifdef LOSCFG_BASE_CORE_TIMESLICE
LITE_OS_SEC_TEXT VOID OsTimesliceCheck(VOID)
{⑴ LosTaskCB *runTask = OsCurrTaskGet();
⑵ if (runTask->timeSlice != 0) {
runTask->timeSlice--;
if (runTask->timeSlice == 0) {⑶ LOS_Schedule();
}
}
}
#endif
3、调度模块汇编函数
文件 archarmcortex_msrcdispatch.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 在文件 kernelinitlos_init.c 中的运行函数 VOID OsStart(VOID)启动零碎阶段调用,传入的参数为就绪队列中最高优良级的 LosTaskCB *taskCB。咱们接下来剖析下该函数的汇编代码。
⑴处设置 PendSV 异样优先级为 OS_NVIC_PENDSV_PRI,PendSV 异样个别设置为最低。全局变量 g_oldTask、g_runTask 定义在 archarmcortex_msrctask.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 指向工作栈的栈指针,工作栈当初保留的是上下文,对应定义在 archarmcortex_mincludearchtask.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 对应 UINT32 PC,此时寄存器 R12 指向工作栈上下文的 UINT32 xPSR。执行⑺处指令,指针持续加 18 个 4 字节长度,即对应 S0 到 S15 及 UINT32 FPSCR; UINT32 NO_NAME 等上下文的 18 个成员。此时,寄存器 R12 指向工作栈的栈底,紧接着把寄存器 R12 写入寄存器 psp。
最初,执行⑻处指令,把 R5 写入 lr 寄存器,开中断,而后跳转到 R6 对应的上下文的 PC 对应的函数 VOID OsTaskEntry(UINT32 taskId),去执行工作的入口函数。
.type OsStartToRun, %function
.global OsStartToRun
OsStartToRun:
.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 OsTaskSchedule
OsTaskSchedule:
.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 osPendSV
osPendSV:
.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)、UINT32 LOS_IntUnLock(VOID)、VOID LOS_IntRestore(UINT32 intSave)调用了汇编函数,这些汇编函数别离是本文要剖析的 ArchIntLock、ArchIntUnlock、ArchIntRestore。咱们看下这些汇编代码,PRIMASK 寄存器是繁多 bit 的寄存器,置为 1 后,就关掉所有可屏蔽异样,只剩下 NMI 和硬 Fault 异样能够响应。默认值是 0,示意没有敞开中断。汇编指令 cpsid I 会设置 PRIMASK=1,敞开中断,指令 cpsie I 设置 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/Lite…。
点击关注,第一工夫理解华为云陈腐技术~