【TencentOS tiny学习】源码分析(2)——调度器
文章目录
- 调度器的基本概念
- 启动调度器
- Cortex-M内核关中断指令
- 回归正题
- 看看任务栈的初始化
- 查找最高优先级任务
- 任务切换的实现
- SysTick
- SysTick初始化
- SysTick中断
温馨提示:本文不描述与浮点相关的寄存器的内容,如需了解自行查阅(毕竟我自己也不懂)
调度器的基本概念
TencentOS tiny
中提供的任务调度器是基于优先级的全抢占式调度,在系统运行过程中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被切出
,高优先级任务抢占
处理器运行。
TencentOS tiny
内核中也允许创建相同优先级的任务。相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务的情况下才有效。
为了保证系统的实时性,系统尽最大可能地保证高优先级的任务得以运行。任务调度的原则是一旦任务状态发生了改变,并且当前运行的任务优先级小于优先级队列中任务最高优先级时,立刻进行任务切换(除非当前系统处于中断处理程序中或禁止任务切换的状态)。
调度器是操作系统的核心
,其主要功能就是实现任务的切换
,即从就绪列表里面找到
优先级最高的任务,然后去执行
该任务。
启动调度器
调度器的启动由cpu_sched_start
函数来完成,它会被tos_knl_start
函数调用,这个函数中主要做两件事,首先通过readyqueue_highest_ready_task_get
函数获取当前系统中处于最高优先级的就绪任务,并且将它赋值给指向当前任务控制块的指针k_curr_task
,然后设置一下系统的状态为运行态KNL_STATE_RUNNING
。
当然最重要的是调用汇编代码写的函数cpu_sched_start
启动调度器,该函数在源码的arch\arm\arm-v7m
目录下的port_s.S
汇编文件下,TencentOS tiny
支持多种内核的芯片,如M3/M4/M7
等,不同的芯片该函数的实现方式不同,port_s.S
也是TencentOS tiny
作为软件与CPU硬件连接的桥梁
。以M4的cpu_sched_start
举个例子:
__API__ k_err_t tos_knl_start(void)
{if (tos_knl_is_running()) {return K_ERR_KNL_RUNNING;}k_next_task = readyqueue_highest_ready_task_get();k_curr_task = k_next_task;k_knl_state = KNL_STATE_RUNNING;cpu_sched_start();return K_ERR_NONE;
}
port_sched_startCPSID I ; set pendsv priority lowest; otherwise trigger pendsv in port_irq_context_switch will cause a context swich in irq; that would be a disasterMOV32 R0, NVIC_SYSPRI14MOV32 R1, NVIC_PENDSV_PRISTRB R1, [R0]LDR R0, =SCB_VTORLDR R0, [R0]LDR R0, [R0]MSR MSP, R0; k_curr_task = k_next_taskMOV32 R0, k_curr_taskMOV32 R1, k_next_taskLDR R2, [R1]STR R2, [R0]; sp = k_next_task->spLDR R0, [R2]; PSP = spMSR PSP, R0; using PSPMRS R0, CONTROLORR R0, R0, #2MSR CONTROL, R0ISB; restore r4-11 from new process stackLDMFD SP!, {R4 - R11}IF {FPU} != "SoftVFP"; ignore EXC_RETURN the first switchLDMFD SP!, {R0}ENDIF; restore r0, r3LDMFD SP!, {R0 - R3}; load R12 and LRLDMFD SP!, {R12, LR}; load PC and discard xPSRLDMFD SP!, {R1, R2}CPSIE IBX R1
Cortex-M内核关中断指令
从上面的汇编代码,我又想介绍一下Cortex-M
内核关中断指令,唉~感觉还是有点麻烦!
为了快速地开关中断, Cortex-M内核专门设置了一条 CPS 指令
,用于操作PRIMASK
寄存器跟FAULTMASK
寄存器的,这两个寄存器是与屏蔽中断有关的,除此之外Cortex-M
内核还存在BASEPRI
寄存器也是与中断有关的,也顺带介绍一下吧。
CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
寄存器 | 功能 |
---|---|
PRIMASK | 它被置 1 后,就关掉所有可屏蔽的异常,只剩下 NMI 和HardFault FAULT可以响应 |
FAULTMASK | 当它置 1 时,只有 NMI 才能响应,所有其它的异常都无法响应(包括HardFault FAULT) |
BASEPRI | 这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断 |
更多具体的描述看我以前的文章:RTOS临界段知识:https://blog.csdn.net/jiejiemcu/article/details/82534974
回归正题
在启动内核调度器过程中需要配置PendSV
的中断优先级为最低,就是往NVIC_SYSPRI14(0xE000ED22)
地址写入NVIC_PENDSV_PRI(0xFF)
。因为PendSV
都会涉及到系统调度,系统调度的优先级要低于
系统的其它硬件中断优先级,即优先响应系统中的外部硬件中断,所以PendSV的中断优先级要配置为最低,不然很可能在中断上下文中产生任务调度。
PendSV
异常会自动延迟上下文切换的请求,直到其它的 ISR
都完成了处理后才放行。为实现这个机制,需要把 PendSV
编程为最低优先级的异常。如果 OS
检测到某 ISR
正在活动,它将悬起一个 PendSV
异常,以便缓期执行上下文切换。也就是说,只要将PendSV
的优先级设为最低的,systick即使是打断了IRQ,它也不会马上进行上下文切换,而是等到ISR
执行完,PendSV
服务例程才开始执行,并且在里面执行上下文切换。过程如图所示:
然后获取MSP
主栈指针的地址,在Cortex-M
中,0xE000ED08
是SCB_VTOR
寄存器的地址,里面存放的是向量表的起始地址。
加载k_next_task
指向的任务控制块到 R2
,从上一篇文章可知任务控制块的第一个成员就是栈顶指针,所以此时R2
等于栈顶指针。
ps : 在调度器启动时,
k_next_task
与k_curr_task
是一样的(k_curr_task = k_next_task
)
加载R2
到R0
,然后将栈顶指针R0
更新到psp
,任务执行的时候使用的栈指针是psp
。
ps:
sp
指针有两个,分别为psp
和msp
。(可以简单理解为:在任务上下文环境中使用psp
,在中断上下文环境使用msp
,也不一定是正确的,这是我个人的理解)
以R0
为基地址,将栈中向上增长的8
个字的内容加载到CPU寄存器R4~R11
,同时R0
也会跟着自增
接着需要加载R0 ~ R3、R12以及LR、 PC、xPSR
到CPU寄存器组,PC指针指向的是即将要运行的线程,而LR寄存器则指向任务的退出。因为这是第一次启动任务,要全部手动把任务栈上的寄存器弹到硬件里,才能进入第一个任务的上下文,因为一开始并没有第一个任务运行的上下文环境,而在进入PendSV的时候需要上文保存,所以需要手动创造任务上下文环境(将这些寄存器加载到CPU寄存器组中)
,第一次的时候此汇编入口函数,sp是指向一个选好的任务的栈顶(k_curr_task
)。
看看任务栈的初始化
从上面的了解,再来看看任务栈的初始化,可能会有更深一点的印象。主要了解以下几点即可:
- 获取栈顶指针为
stk_base[stk_size]
高地址,Cortex-M
内核的栈是向下增长
的。 R0、R1、R2、R3、R12、R14、R15和xPSR的位24
是会被CPU自动
加载与保存的。- xPSR的
bit24必须置1
,即0x01000000。 - entry是任务的入口地址,即
PC
- R14 (
LR
)是任务的退出地址,所以任务一般是死循环而不会return
- R0: arg是任务主体的形参
- 初始化栈时sp指针会自减
__KERNEL__ k_stack_t *cpu_task_stk_init(void *entry,void *arg,void *exit,k_stack_t *stk_base,size_t stk_size)
{cpu_data_t *sp;sp = (cpu_data_t *)&stk_base[stk_size];sp = (cpu_data_t *)((cpu_addr_t)(sp) & 0xFFFFFFF8);/* auto-saved on exception(pendSV) by hardware */*--sp = (cpu_data_t)0x01000000u; /* xPSR */*--sp = (cpu_data_t)entry; /* entry */*--sp = (cpu_data_t)exit; /* R14 (LR) */*--sp = (cpu_data_t)0x12121212u; /* R12 */*--sp = (cpu_data_t)0x03030303u; /* R3 */*--sp = (cpu_data_t)0x02020202u; /* R2 */*--sp = (cpu_data_t)0x01010101u; /* R1 */*--sp = (cpu_data_t)arg; /* R0: arg *//* Remaining registers saved on process stack *//* EXC_RETURN = 0xFFFFFFFDLInitial state: Thread mode + non-floating-point state + PSP31 - 28 : EXC_RETURN flag, 0xF27 - 5 : reserved, 0xFFFFFE4 : 1, basic stack frame; 0, extended stack frame3 : 1, return to Thread mode; 0, return to Handler mode2 : 1, return to PSP; 0, return to MSP1 : reserved, 00 : reserved, 1*/
#if defined (TOS_CFG_CPU_ARM_FPU_EN) && (TOS_CFG_CPU_ARM_FPU_EN == 1U)*--sp = (cpu_data_t)0xFFFFFFFDL;
#endif*--sp = (cpu_data_t)0x11111111u; /* R11 */*--sp = (cpu_data_t)0x10101010u; /* R10 */*--sp = (cpu_data_t)0x09090909u; /* R9 */*--sp = (cpu_data_t)0x08080808u; /* R8 */*--sp = (cpu_data_t)0x07070707u; /* R7 */*--sp = (cpu_data_t)0x06060606u; /* R6 */*--sp = (cpu_data_t)0x05050505u; /* R5 */*--sp = (cpu_data_t)0x04040404u; /* R4 */return (k_stack_t *)sp;
}
查找最高优先级任务
一个操作系统如果只是具备了高优先级任务能够立即
获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。因为这个查找最高优先级任务的过程决定了调度时间是否具有确定性,可以简单来说可以使用时间复杂度
来描述一下吧,如果系统查找最高优先级任务的时间是O(N)
,那么这个时间会随着任务个数的增加而增大,这是不可取的,TencentOS tiny
的时间复杂度是O(1)
,它提供两种方法查找最高优先级任务,通过TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT
宏定义决定。
- 第一种是使用普通方法,根据就绪列表中
k_rdyq.prio_mask[]
的变量判断对应的位是否被置1。 - 第二种方法则是特殊方法,利用计算前导零指令
CLZ
,直接在k_rdyq.prio_mask[]
这个32
位的变量中直接得出最高优先级所处的位置,这种方法比普通方法更快捷,但受限于平台
(需要硬件前导零指令,在STM32中我们就可以使用这种方法)。
实现过程如下,建议看一看readyqueue_prio_highest_get
函数,他的实现还是非常精妙的~
__STATIC__ k_prio_t readyqueue_prio_highest_get(void)
{uint32_t *tbl;k_prio_t prio;prio = 0;tbl = &k_rdyq.prio_mask[0];while (*tbl == 0) {prio += K_PRIO_TBL_SLOT_SIZE;++tbl;}prio += tos_cpu_clz(*tbl);return prio;
}
__API__ uint32_t tos_cpu_clz(uint32_t val)
{#if defined(TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT) && (TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT == 0u)uint32_t nbr_lead_zeros = 0;if (!(val & 0XFFFF0000)) {val <<= 16;nbr_lead_zeros += 16;}if (!(val & 0XFF000000)) {val <<= 8;nbr_lead_zeros += 8;}if (!(val & 0XF0000000)) {val <<= 4;nbr_lead_zeros += 4;}if (!(val & 0XC0000000)) {val <<= 2;nbr_lead_zeros += 2;}if (!(val & 0X80000000)) {nbr_lead_zeros += 1;}if (!val) {nbr_lead_zeros += 1;}return (nbr_lead_zeros);
#elsereturn port_clz(val);
#endif
}
任务切换的实现
从前面我们也知道,任务切换是在PendSV
中断中进行的,这个中断中实现的内容总结成一句精髓的话就是 上文保存,下文切换,直接看源代码:
PendSV_HandlerCPSID IMRS R0, PSP_context_save; R0-R3, R12, LR, PC, xPSR is saved automatically hereIF {FPU} != "SoftVFP"; is it extended frame?TST LR, #0x10IT EQVSTMDBEQ R0!, {S16 - S31}; S0 - S16, FPSCR saved automatically here; save EXC_RETURNSTMFD R0!, {LR}ENDIF; save remaining regs r4-11 on process stackSTMFD R0!, {R4 - R11}; k_curr_task->sp = PSPMOV32 R5, k_curr_taskLDR R6, [R5]; R0 is SP of process being switched outSTR R0, [R6]_context_restore; k_curr_task = k_next_taskMOV32 R1, k_next_taskLDR R2, [R1]STR R2, [R5]; R0 = k_next_task->spLDR R0, [R2]; restore R4 - R11LDMFD R0!, {R4 - R11}IF {FPU} != "SoftVFP"; restore EXC_RETURNLDMFD R0!, {LR}; is it extended frame?TST LR, #0x10IT EQVLDMIAEQ R0!, {S16 - S31}ENDIF; Load PSP with new process SPMSR PSP, R0CPSIE I; R0-R3, R12, LR, PC, xPSR restored automatically here; S0 - S16, FPSCR restored automatically here if FPCA = 1BX LRALIGNEND
将PSP
的值存储到R0
。当进入PendSVC_Handler
时,上一个任务运行的环境即: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0
这些CPU寄存器的值会自动
存储到任务的栈中,此时psp指针已经被自动更新。而剩下的r4~r11
需要手动
保存,这也是为啥要在PendSVC_Handler
中保存上文(_context_save
)的原因,主要是加载CPU中不能自动保存的寄存器,将其压入任务栈中。
接着找到下一个要运行的任务k_next_task
,将它的任务栈顶加载到R0
,然后手动将新任务栈中的内容(此处是指R4~R11
)加载到CPU
寄存器组中,这就是下文切换,当然还有一些其他没法自动保存的内容也是需要手动加载到CPU
寄存器组的。手动加载完后,此时R0
已经被更新了,更新psp的值,在退出PendSVC_Handler
中断时,会以psp
作为基地址,将任务栈中剩下的内容(xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0
)自动加载到CPU寄存器。
其实在异常发生时,R14中保存异常返回标志,包括返回后进入任务模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针。此时的r14等于0xfffffffd,最表示异常返回后进入任务模式(毕竟PendSVC_Handler
优先级是最低的,会返回到任务中),SP以PSP作为堆栈指针出栈,出栈完毕后PSP
指向任务栈的栈顶。当调用 BX R14指令后,系统以PSP
作为SP
指针出栈,把接下来要运行的新任务的任务栈中剩下的内容加载到CPU寄存器:R0、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR
,从而切换到新的任务。
SysTick
SysTick初始化
systick是系统的时基,而且它是内核时钟,只要是M0/M3/M4/M7
内核它都会存在systick
时钟,并且它是可以被编程配置的,这就对操作系统的移植提供极大的方便。
TencentOS tiny
会在cpu_init
函数中将systick
进行初始化,即调用cpu_systick_init
函数,这样子就不需要用户自行去编写systick
初始化相关的代码。
__KERNEL__ void cpu_init(void)
{k_cpu_cycle_per_tick = TOS_CFG_CPU_CLOCK / k_cpu_tick_per_second;cpu_systick_init(k_cpu_cycle_per_tick);#if (TOS_CFG_CPU_HRTIMER_EN > 0)tos_cpu_hrtimer_init();
#endif
}
__KERNEL__ void cpu_systick_init(k_cycle_t cycle_per_tick)
{port_systick_priority_set(TOS_CFG_CPU_SYSTICK_PRIO);port_systick_config(cycle_per_tick);
}
SysTick中断
SysTick
中断服务函数是需要我们自己编写的,要在里面调用一下TencentOS tiny
相关的函数,更新系统时基以驱动系统的运行,SysTick_Handler
函数的移植如下:
void SysTick_Handler(void)
{HAL_IncTick();if (tos_knl_is_running()){tos_knl_irq_enter();tos_tick_handler();tos_knl_irq_leave();}
}
主要是需要调用tos_tick_handler
函数将系统时基更新,具体见:
__API__ void tos_tick_handler(void)
{if (unlikely(!tos_knl_is_running())) {return;}tick_update((k_tick_t)1u);#if TOS_CFG_TIMER_EN > 0u && TOS_CFG_TIMER_AS_PROC > 0utimer_update();
#endif#if TOS_CFG_ROUND_ROBIN_EN > 0urobin_sched(k_curr_task->prio);
#endif
}
不得不说TencentOS tiny
源码的实现非常简单,我非常喜欢
,在tos_tick_handler
中,首先判断一下系统是否已经开始运行,如果没有运行将直接返回,如果已经运行了,那就调用tick_update
函数更新系统时基,如果使能了TOS_CFG_TIMER_EN
宏定义表示使用软件定时器,则需要更新相应的处理,此处暂且不提及。如果使能了TOS_CFG_ROUND_ROBIN_EN
宏定义,还需要更新时间片相关变量,稍后讲解。
__KERNEL__ void tick_update(k_tick_t tick)
{TOS_CPU_CPSR_ALLOC();k_task_t *first, *task;k_list_t *curr, *next;TOS_CPU_INT_DISABLE();k_tick_count += tick;if (tos_list_empty(&k_tick_list)) {TOS_CPU_INT_ENABLE();return;}first = TOS_LIST_FIRST_ENTRY(&k_tick_list, k_task_t, tick_list);if (first->tick_expires <= tick) {first->tick_expires = (k_tick_t)0u;} else {first->tick_expires -= tick;TOS_CPU_INT_ENABLE();return;}TOS_LIST_FOR_EACH_SAFE(curr, next, &k_tick_list) {task = TOS_LIST_ENTRY(curr, k_task_t, tick_list);if (task->tick_expires > (k_tick_t)0u) {break;}// we are pending on something, but tick's up, no longer waittingpend_task_wakeup(task, PEND_STATE_TIMEOUT);}TOS_CPU_INT_ENABLE();
}
tick_update
函数的主要功能就是将k_tick_count +1
,并且判断一下时基列表k_tick_list
(也可以成为延时列表吧)的任务是否超时,如果超时则唤醒该任务,否则就直接退出即可。关于时间片的调度也是非常简单,将任务的剩余时间片变量timeslice
减一,然后当变量减到0时,将该变量进行重装载timeslice_reload
,然后切换任务knl_sched()
,其实现过程如下:
__KERNEL__ void robin_sched(k_prio_t prio)
{TOS_CPU_CPSR_ALLOC();k_task_t *task;if (k_robin_state != TOS_ROBIN_STATE_ENABLED) {return;}TOS_CPU_INT_DISABLE();task = readyqueue_first_task_get(prio);if (!task || knl_is_idle(task)) {TOS_CPU_INT_ENABLE();return;}if (readyqueue_is_prio_onlyone(prio)) {TOS_CPU_INT_ENABLE();return;}if (knl_is_sched_locked()) {TOS_CPU_INT_ENABLE();return;}if (task->timeslice > (k_timeslice_t)0u) {--task->timeslice;}if (task->timeslice > (k_timeslice_t)0u) {TOS_CPU_INT_ENABLE();return;}readyqueue_move_head_to_tail(k_curr_task->prio);task = readyqueue_first_task_get(prio);if (task->timeslice_reload == (k_timeslice_t)0u) {task->timeslice = k_robin_default_timeslice;} else {task->timeslice = task->timeslice_reload;}TOS_CPU_INT_ENABLE();knl_sched();
}
【TencentOS tiny学习】源码分析(2)——调度器相关推荐
- linux 进程调度源码分析,Linux调度器源码分析
代码分析根据3.10版本 通过对前面的学习我们知道Linux的调度分为两种 周期调度 完成周期性算法参数的更新和系统其它实际的检查 主调的 真正的调度过程 我们现在来看下主调的的代码框架. 入口 根& ...
- golang源码分析:调度器chan调度
golang调度机制chan调度 golang的调度策略中,碰见阻塞chan就会将该chan放入到阻塞的g中,然后再等待该chan被唤醒,这是golang调度器策略的主动调度策略之一,其中还有其他的主 ...
- xxl-job源码解读:调度器schedule
xxl-job源码解读:调度器schedule 本文基于xxl-job的2.3.1版本 基本说明 基本原理概述 调用器主要的用于判断定时任务的执行时间,按时调用触发器(trigger),再由触发器去获 ...
- threejs 源码解析_ThreeJS 物理材质shader源码分析(顶点着色器)
ThreeJS 物理材质shader源码分析(顶点着色器) Threejs将shader代码分为ShaderLib和ShaderChunk两部分,ShaderLib通过组合ShaderChunk的代码 ...
- 鸿蒙内核源码分析:调度机制篇
作者 | 深入研究鸿蒙,鸿蒙内核发烧友 出品 | CSDN(ID:CSDNnews) 头图 | CSDN 下载自东方 IC 阅读之前建议先读本系列其他文章,以便对本文任务调度机制的理解. 为什么要学这 ...
- Tomcat源码分析(十)--部署器 转载
本系列转载自 http://blog.csdn.net/haitao111313/article/category/1179996 我们知道,在Tomcat的世界里,一个Host容器代表一个虚机器资源 ...
- sis地址获取器_TencentOS tiny深度源码分析(2)—— 调度器
温馨提示:本文不描述与浮点相关的寄存器的内容,如需了解自行查阅 调度器的基本概念 TencentOS tiny中提供的任务调度器是基于优先级的全抢占式调度,在系统运行过程中,当有比当前任务优先级更高的 ...
- JVM源码分析--ClassLoader类加载器
本人原创,转载请注明出处:https://www.cnblogs.com/javallh/p/10224187.html 1.JDK已有类加载器: BootStrap ClassLoader (启动类 ...
- OkHttp3源码分析二 拦截器 上
在上一篇中,我们大概知道了OkHttp进行网络请求的大概的流程: 按着流程,一个一个的分析. RetryAndFollowUpInterceptor 构造方法: public RetryAndFoll ...
- SOFA 源码分析 — 连接管理器
前言 RPC 框架需要维护客户端和服务端的连接,通常是一个客户端对应多个服务端,而客户端看到的是接口,并不是服务端的地址,服务端地址对于客户端来讲是透明的. 那么,如何实现这样一个 RPC 框架的网络 ...
最新文章
- python post json数据_Python POST Json数据到Http
- 项目经理生存现状(漫画)
- JZOJ 100041. 【NOIP2017提高A组模拟7.12】列车调度
- 实验 4 [bx]和 loop 的使用
- 动手造轮子:基于 Redis 实现 EventBus
- Android之Unable to execute dex: Multiple dex files define 解决方法
- 红歌合唱之团结就是力量
- 551. 学生出勤记录
- 【Python】Pandas加载并查询Excel文件
- 4S店卖不出去的库存车都是怎么处理的?
- 数据结构 二叉树的递归建立和递归遍历
- python怎么排名次_2019:python第3次获得TIOBE最佳年度语言排名
- Windows10设置动态桌面壁纸
- yb3防爆电机型号含义_YB3防爆电机和YBX3防爆电机的区别
- 竹子的故事:送给那些坚持了很久却准备放弃的人 --- 厚积方能薄发
- Adobe Bridge使用教程:BR键盘快捷键大全
- 将Python代码打包成Windows 10的可执行文件
- 电脑下载软件用什么软件好?安卓手机下载软件用哪个软件好?IDM下载器说:在做的都是弟弟
- 抽样技术--简单随机抽样
- Docker——Docker 容器数据卷(Volumes)