谈一谈RTOS的核心之一:调度器
调度器
上下求索,方可得道之精髓
引言
我还在学校的时候,实验室有一个学长在B站发布了这么一个视频,B站链接在这里,并且将代码开源在github,取名为suos,对于当时的我来说,实时操作系统是一个很新鲜的东西,特别是当看到代码里面的两个函数中的while(1),这完全是超出认知的东西。如今四年已经过去了,不敢说对RTOS的远离了解的很清楚,但是也是管中窥豹,略知一二,今天就记录一下对RTOS的调度器的理解,本文主要是简述原理,suos虽然说很简单,功能也少,但是在调度器这里确实实现的还算清楚,通俗易懂,因此借用学长的代码总结一番
C语言的栈调用过程
要想理解调度,必须先了解C语言函数调用的过程,裸机操作中,内存中专门会分配一块区域作为栈区,C函数在调用过程中,分为以下几个步骤:
- 1、调用函数时,首先将当前函数的返回地址(调用下一条指令的位置)压入堆栈。这个返回地址指向了调用函数的下一条指令,以便在函数执行完后能够返回到正确的位置。
- 2、如果函数有参数,将参数的值按照从右到左的顺序依次压入堆栈。
- 3、将当前函数的局部变量在堆栈上分配空间。这些局部变量包括在函数内部定义的变量和临时变量。
- 4、执行函数体内的代码。
- 5、如果函数内部调用了其他函数,重复上述步骤,将控制权传递给被调用函数。
- 6、当函数执行完毕时,将函数的返回值放在指定的寄存器中(如EAX寄存器),并且将函数的栈帧(包含局部变量和临时变量的空间)从堆栈中移除,释放内存。
- 7、从堆栈中弹出之前保存的返回地址,将控制权返回到调用函数的位置,继续执行调用函数后面的代码。
- 8、需要注意的是,堆栈的大小是有限的,所以函数调用的层级过多或者函数内部使用过多的局部变量可能导致堆栈溢出的问题。
上面是裸机的函数调用方式,在RTOS中,任务之间出现抢占时,希望记录这个任务当前的运行状态,因此就需要将当前的程序计数器和栈指针保存下来,下一次再运行时进行恢复,这就是调度器保存恢复现场的中的两个重要参数:程序计数器和堆栈指针
堆栈块
通过C语言的栈调用过程我们知道,裸机运行过程中存在栈调用,但是在RTOS中可能同时存在好几个任务,这些任务在运行过程中可能随时被切换,因此每个任务必须有一块自己的栈空间,只能自己占用,保证自己任务的正常运行,在任务挂起时保存起来,在任务运行的时候再恢复。这就是调度器保存恢复现场的中的一个重要参数:堆栈 。多提一嘴,在嵌入式领域经常会碰到堆栈溢出的问题,一般来说导致这种问题的原因就是另一个任务的栈空间溢出占用了另一个任务的栈空间导致任务崩溃。
CPU寄存器
寄存器是位于CPU内部的高速存储器,用于存储临时数据和执行算术和逻辑操作。寄存器在C语言运行过程中的几个用途:
- 存储变量和计算结果:寄存器是CPU内部最快的存储器,用于存储频繁使用的变量和临时计算结果。通过将变量或计算结果保存在寄存器中,可以加快对它们的访问速度,提高程序的执行效率。
- 函数调用和参数传递:寄存器在函数调用期间用于保存函数的参数和局部变量。一些寄存器(如参数寄存器)专门用于传递函数参数,而其他寄存器(如通用寄存器)用于保存局部变量和临时值。使用寄存器传递参数和保存局部变量可以减少内存访问的开销,提高函数调用的效率。
- 存储指针:寄存器可以用于存储指针变量,例如指向数组、结构体或函数的指针。通过将指针存储在寄存器中,可以更快地进行指针操作和访问相关的数据结构。
- 保存程序状态:某些特殊的寄存器,如标志寄存器,用于保存程序的状态信息。这些寄存器中的位表示条件状态、进位标志、零标志等,对于控制程序流程和执行条件语句非常重要。
总而言之,CPU寄存器保存的值是程序运行过程中的,需要用寄存器值来恢复下一次的程序运行状态,这也是调度器保存恢复现场的中的一个重要参数:CPU寄存器
调度过程
我们从上述可知,在产生调度时,需要保存恢复的四个重要参数:程序计数器、堆栈指针、堆栈、CPU寄存器,每次产生调度时,保存当前任务的这些参数(现场),并且恢复下一个这些参数,多个任务这样轮回切换,就产生一种多个任务在并行进行的错觉。
时间片
上面我们知道了任务调度的过程,但是又产生一个问题,谁来调度,多久调度一次。通常情况下,会开启一个硬件定时器,定时运行RTOS内核,RTOS每间隔固定时间判断是否应该启动调度。在FREERTOS中,RTOS运行在滴答定时器的中断函数中,如果需要调度,则触发一次PENSV中断,在PENSV中断函数中进行一次任务调度
学长SUOS代码解读
suos中,核心的调度代码都是汇编实现的,在程序启动时启动一个定时器,直接在定时器中进行调度
;定时器0中断服务函数 进程调度在此实现
Timer0_ServiceFunction:
MOV SP_Backups, SP ;将当前用户的SP指针保存
MOV SP, #(SystemStack - 1) ;将系统栈设置为当前栈
LCALL Save_CPU_Context ;保存现场环境
LCALL Control_Process ;调用进程调度函数 调用后系统将切换现场信息和断点
LCALL Recovery_CPU_Context ;恢复现场信息
MOV SP, SP_Backups ;恢复栈指针
RETI
保存恢复现场环境参数也是由汇编实现,很简单的变量赋值:
;保存CPU现场信息,保存上文
Save_CPU_Context:
MOV PSW_Backups, PSW
MOV ACC_Backups, A
MOV B_Backups, B
MOV R0_Backups, R0
MOV R1_Backups, R1
MOV R2_Backups, R2
MOV R3_Backups, R3
MOV R4_Backups, R4
MOV R5_Backups, R5
MOV R6_Backups, R6
MOV R7_Backups, R7
MOV DPL_Backups, DPL
MOV DPH_Backups, DPH
RET
;恢复CPU现场信息,切换下文
Recovery_CPU_Context:
MOV PSW, PSW_Backups
MOV A, ACC_Backups
MOV B, B_Backups
MOV R0, R0_Backups
MOV R1, R1_Backups
MOV R2, R2_Backups
MOV R3, R3_Backups
MOV R4, R4_Backups
MOV R5, R5_Backups
MOV R6, R6_Backups
MOV R7, R7_Backups
MOV DPL, DPL_Backups
MOV DPH, DPH_Backups
RET
调度过程使用C语言实现,主要分为两个部分:1、更新进程状态,寻找下一个准备启动的任务函数名为Refresh_Process。2、将缓存的参数值保存在上一任务的TCB中,并且从下一个任务TCB恢复参数到缓存参数中,函数名为Switch_Process
void Control_Process(void)
{
EA = 0;
Refresh_Process();
Switch_Process();
EA = 1;
}
void Refresh_Process(void)
{
uint8_t i;
for (i = 0;i<=PCB_Number;i++)
{
if (PCB_IndexTable[i]!=(void*)0 && PCB_IndexTable[i]->ProcessState == Block) ////PCB非空 进程被阻塞
{
///*信号量的阻塞唤醒设计有问题*/
if (PCB_IndexTable[i]->BlockEvent == 0 && PCB_IndexTable[i]->s == SemaphoreNull)
PCB_IndexTable[i]->ProcessState = Ready;
else if (PCB_IndexTable[i]->BlockEvent>0)
{
--PCB_IndexTable[i]->BlockEvent;
if (PCB_IndexTable[i]->BlockEvent == 0) PCB_IndexTable[i]->ProcessState = Ready;
}
/*
if (PCB_IndexTable[i]->BlockEvent==0&&*(PCB_IndexTable[i]->s)>0)//计时器为0且信号量>0 唤醒
{
PCB_IndexTable[i]->ProcessState = Ready;
continue;
}
if (PCB_IndexTable[i]->BlockEvent<=60000)
--PCB_IndexTable[i]->BlockEvent;
else ;//待添加的阻塞检查事件
if (PCB_IndexTable[i]->BlockEvent==0&&*(PCB_IndexTable[i]->s)>0)//计时器为0且信号量>0 唤醒
{PCB_IndexTable[i]->ProcessState = Ready;continue;}
*/
}//if end
}//for end
}
void Switch_Process(void)
{
uint8_t i;
// 0.判断是否需要切换进程:若没有处于就绪态的进程则无需切换
bit Unwanted = 1; // 不需要切换进程标志 为1时不切换
for (i = 0;i<=PCB_Number;i++)
if (PCB_IndexTable[i]->ProcessState == Ready)
Unwanted = 0;
if (Unwanted) return;
// 1.将用户栈空间复制到自己的PCB,腾出来给下一个进程
for (i=0;i<StackSize;i++)
PCB_IndexTable[PCB_Current]->ProcessStack[i] = UserStack[i];
// 2.将CPU环境复制到PCB,腾出来给下一个进程
PCB_IndexTable[PCB_Current]->PSW = PSW_Backups;
PCB_IndexTable[PCB_Current]->ACC = ACC_Backups;
PCB_IndexTable[PCB_Current]->B = B_Backups;
PCB_IndexTable[PCB_Current]->R0 = R0_Backups;
PCB_IndexTable[PCB_Current]->R1 = R1_Backups;
PCB_IndexTable[PCB_Current]->R2 = R2_Backups;
PCB_IndexTable[PCB_Current]->R3 = R3_Backups;
PCB_IndexTable[PCB_Current]->R4 = R4_Backups;
PCB_IndexTable[PCB_Current]->R5 = R5_Backups;
PCB_IndexTable[PCB_Current]->R6 = R6_Backups;
PCB_IndexTable[PCB_Current]->R7 = R7_Backups;
PCB_IndexTable[PCB_Current]->DPL = DPL_Backups;
PCB_IndexTable[PCB_Current]->DPH = DPH_Backups;
PCB_IndexTable[PCB_Current]->SP = SP_Backups;
// 3.选择下一个就绪的进程并切换
//
i = PCB_Current;
while(1) /*所有进程都阻塞了这么办?*/
{
if (i==PCB_Number)
i=0; // 循环查找
if (PCB_IndexTable[i]->ProcessState == Ready)
break; // 找到下一个就绪的进程了,退出寻找
i++;
}
if (PCB_IndexTable[PCB_Current]->ProcessState == Running) //如果当前进程为运行态
PCB_IndexTable[PCB_Current]->ProcessState = Ready; //当前进程设置为就绪
PCB_Current = i;
PCB_IndexTable[PCB_Current]->ProcessState = Running;//找到的进程设置为运行状态
// 4.将找到的进程CPU的现场信息复制到中转站
PSW_Backups = PCB_IndexTable[PCB_Current]->PSW;
ACC_Backups = PCB_IndexTable[PCB_Current]->ACC;
B_Backups = PCB_IndexTable[PCB_Current]->B;
R0_Backups = PCB_IndexTable[PCB_Current]->R0;
R1_Backups = PCB_IndexTable[PCB_Current]->R1;
R2_Backups = PCB_IndexTable[PCB_Current]->R2;
R3_Backups = PCB_IndexTable[PCB_Current]->R3;
R4_Backups = PCB_IndexTable[PCB_Current]->R4;
R5_Backups = PCB_IndexTable[PCB_Current]->R5;
R6_Backups = PCB_IndexTable[PCB_Current]->R6;
R7_Backups = PCB_IndexTable[PCB_Current]->R7;
DPL_Backups = PCB_IndexTable[PCB_Current]->DPL;
DPH_Backups = PCB_IndexTable[PCB_Current]->DPH;
SP_Backups = PCB_IndexTable[PCB_Current]->SP;
// 5.将栈空间从PCB复制到用户栈
for (i=0;i<StackSize;i++)
UserStack[i] = PCB_IndexTable[PCB_Current]->ProcessStack[i];
}
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。