2021SC@SDUSC
一. 实时操作系统
在介绍TencentOS Tiny查找最高优先级进程之前,首先我们来了解一下什么是实时操作系统,在维基百科上,实时操作系统(RTOS)的定义如下:
实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。
实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个进程需要执行,实时操作系统会马上(在较短时间内)执行该进程,不会有较长的延时。这种特性保证了各个进程的及时执行。
这里需要注意,大多数实时操作系统都是嵌入式操作系统,但并不是所有的嵌入式操作系统都是实时的。
实时操作系统中都要包含一个实时进程调度器,这个进程调度器与其它操作系统的最大不同是强调:严格按照优先级来分配CPU时间,并且时间片轮转不是实时调度器的一个必选项(尽管TencentOS Tiny中也使用了时间片轮转)。
二. 查找最高优先级进程
一个操作系统如果只是具备了高优先级进程能够立即获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。因为这个查找最高优先级进程的过程决定了调度时间是否具有确定性,可以简单来说可以使用时间复杂度来描述一下吧,如果系统查找最高优先级进程的时间是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中我们就可以使用这种方法)。
实现过程如下,
__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);
#else
return port_clz(val);
#endif
}
三. 实现进程切换
在TencentOS Tiny中,进程切换是在PendSV中断中进行的,这个中断的实现可以一句话概括就是:保存上文,切换下文,以下便是关于PendSV中断如何实现的源代码:
PendSV_Handler
CPSID I
MRS R0, PSP
_context_save
; R0-R3, R12, LR, PC, xPSR is saved automatically here
IF {FPU} != "SoftVFP"
; is it extended frame?
TST LR, #0x10
IT EQ
VSTMDBEQ R0!, {S16 - S31}
; S0 - S16, FPSCR saved automatically here
; save EXC_RETURN
STMFD R0!, {LR}
ENDIF
; save remaining regs r4-11 on process stack
STMFD R0!, {R4 - R11}
; k_curr_task->sp = PSP
MOV32 R5, k_curr_task
LDR R6, [R5]
; R0 is SP of process being switched out
STR R0, [R6]
_context_restore
; k_curr_task = k_next_task
MOV32 R1, k_next_task
LDR R2, [R1]
STR R2, [R5]
; R0 = k_next_task->sp
LDR R0, [R2]
; restore R4 - R11
LDMFD R0!, {R4 - R11}
IF {FPU} != "SoftVFP"
; restore EXC_RETURN
LDMFD R0!, {LR}
; is it extended frame?
TST LR, #0x10
IT EQ
VLDMIAEQ R0!, {S16 - S31}
ENDIF
; Load PSP with new process SP
MSR PSP, R0
CPSIE I
; R0-R3, R12, LR, PC, xPSR restored automatically here
; S0 - S16, FPSCR restored automatically here if FPCA = 1
BX LR
ALIGN
END
将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,它是系统的时基,而且它是内核时钟,只要是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
中断服务函数:
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 > 0u
timer_update();
#endif
#if TOS_CFG_ROUND_ROBIN_EN > 0u
robin_sched(k_curr_task->prio);
#endif
}
在tos_tick_handler
中,首先判断一下系统是否已经开始运行,如果没有运行将直接返回,如果已经运行了,那就调用tick_update
函数更新系统时基
因此我们再来看一下用来更新系统时基的tick_update
函数
__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 waitting
pend_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();
}