操作系统中的任务与任务切换
1. 为什么要有任务?
我们常常见到操作系统,也常常要学习操作系统,从一个小白的角度看,任何事情都有其对立的东西,或者说事务都是一步步发展的,既然有操作系统这个东西,那么也必然有无操作系统的软件。
如果大家都有学习把玩过单片机,比如51单片机或者是STM32单片机,那么应该是都有无操作系统时代这样的经历。无操作系统时代,可以看作是有一个任务的操作系统。无操作系统时,软件的流程是怎么样子的?
无操作系统,裸机环境
while(1)
{
taskA();
taskB();
taskC();
taskD();
}
带操作系统环境
taskA();
taskB();
taskC();
taskD();
我们常常见到操作系统,也常常要学习操作系统,从一个小白的角度看,任何事情都有其对立的东西,或者说事务都是一步步发展的,既然有操作系统这个东西,那么也必然有无操作系统的软件。 如果大家都有学习把玩过单片机,比如51单片机或者是STM32单片机,那么应该是都有无操作系统时代这样的经历。无操作系统时代,可以看作是有一个任务的操作系统。无操作系统时,软件的流程是怎么样子的?
无操作系统,裸机环境
while(1)
{
taskA();
taskB();
taskC();
taskD();
}
带操作系统环境
taskA(); taskB(); taskC(); taskD();
所以我们需要一个操作系统,来提高我们处理器的效率,把处理器的每一刻都去做有意义的事情。
相当于是间接的提高了处理器的性能。
综上所述,有操作系统与无操作系统,最大的区别就是操作系统下可以宏观上并行的执行多个任务,并可以在任务之间切换,选择合适的任务在处理器执行。
所以我们来实现的一个操作系统,首先就是实现以下功能
- 可以创建任务
- 可以在不同的任务之间进行切换
有了上面的功能,就可以说是实现了从无操作系统到操作系统的转变
操作系统就是一个上帝视角,每一个任务都是一个无线循环的函数功能
void taskA()
{
initA();
while(1)
{
aaa();
}
}
void taskB()
{
initB();
while(1)
{
bbb();
}
}
2. 任务切换怎么实现
2.1 任务切换过程
不知道大家对函数,处理器运行相关的知识有多少的了解。任务切换有点类似与函数调用的过程,但是也会有很多不同
void taskA()
{
initA();
while(1)
{
aaa();
bbb();
ccc();
ddd();
}
}
void taskB()
{
initB();
while(1)
{
xxx();
yyy();
zzz();
}
}
假设上面有2个无限循环执行的任务,可能的执行过程是这样的
aaa -> xxx -> bbb -> ccc -> yyy -> zzz -> xxx -> ddd -> aaa -> yyy
如上的执行过程,处理器在2个循环中来回执行,一定是发生了任务切换的过程
处理器执行一个函数的时候,有几个重要的要素:
- 堆栈
- PC 程序指针
- LR 程序返回指针
- 其他的通用寄存器
每一个函数有输入自己的堆栈内容,咋进入函数时候,开辟出堆栈空间,堆栈存放以下内容:
- 函数的参数
- 函数内定义的局部变量
因为每一个函数的参数和局部变量都是该函数私有的,并且不能被随意修改,所以在进行任务切换时候,要考虑一下堆栈
接着就是 PC 程序指针,处理器执行那一段代码程序,就是有PC指针决定,处理器总是从PC指针存放的地址去取指令执行。
所以让处理器从一个任务切换到另一个任务去执行,可能会与PC指针有关系了
2.2 任务切换需要作什么
我们想想一下如何从一个任务切换到另一个任务?
1、保护堆栈、保存堆栈
一般的处理器都会包含一个SP堆栈寄存器,该寄存器存放了当前堆栈的地址。我们现在要进行任务切换,一段时间过后还要从其他任务切换回来。所以我们需要在切换之前吧堆栈的指针保存,在切换过程中吧目标任务的堆栈指针恢复
2、跳转到目标任务
我们从当前任务切换到目标任务,需要在切换之前把当前运行的地址也保存下来,同理目标任务也已经保存了运行地址,所以切换目标任务就是把目标任务的保存的运行地址恢复,并且是恢复到PC指针,让处理器执行目标任务
3、保存通用寄存器
通用寄存器在函数执行过程中存放了一些有用的信息,在任务切换之前要保存起来,在切换到目标任务的时候要恢复回来
4、上面的信息保存在哪里
为每一个任务定义并创建一个结构体,用于保存上述重要的信息
2.3 准备切换任务的材料
上面介绍了任务切换需要什么,既然知道需要什么,那我们就开始着手准备
- 定义一个保存任务的信息的结构体,包含上面需要的几个部分:
- 堆栈指针
- PC指针
- 通用寄存器存放
struct task{ void *sp; void *pc; uint64_t regs[32];}
- 创建一个任务
创建任务,就需要填充任务的结构体,定义一个任务初始化函数,提供以下参数
- 任务结构体
- 堆栈地址
- 任务入口地址
int task_init(struct task *t, void *sp_addr, void *pc_addr)
{
t->sp = sp_addr;
t->pc = pc_addr;
return 0;
}
上述函数完成了一个任务的初始化
现在,我们准备好了任务需要的材料,就可以开始进行切换了
2.4 进行切换任务实现
具体的任务切换又可以分成几个种类:
-
从系统启动到第一个任务开始运行,当前没有任务,切换到第一个任务
task_switch_to(struct task *task_to);
-
当前正在一个任务中,切换到另一个任务
task_switch_from_to(struct task *task_from, struct task *task_to)
我们应该去思考如何填充实现上面的函数,当然了,这一部分是与具体的处理器体系结构相关联最大的部分
可以去了解下常用的RTOS在各个架构下是如何实现上述的功能,尤其是AARCH64架构,因为我目前的操作系统就是基于aarch64处理器架构的
关于上面2个任务切换函数的实现,已经上传到github的项目代码中,可以浏览查看
//根据C语言和汇编语言的调用过程,第一个参数存放在x0寄存器中 //我们函数传递的是任务结构体指针 task_switch_to: ldr x1, [x0] //任务的pc地址是第一个参数,读取到x1寄存器 mov x30, x1 //把pc地址放入到x30寄存器,x30寄存器是lr寄存器,稍后切换完成以后返回时候就会返回到任务中执行 add x0, x0, 8 //sp参数在结构体中偏移是八字节 ldr x1, [x0] //取出并设置sp地址 mov sp, x1 ret //函数返回,跳转到x30寄存器存放地址去执行
如果对上面的步骤不是很清楚,可以使用vscode配合gdb进行调试,单步执行代码跟踪处理器执行的过程,尝试一下是否可以正确完成任务切换
目前已经在github的项目中已经实现了上面的代码,可以下载并尝试调试和执行
项目地址:https://github.com/jhbdream/armv8_os.git
欢迎关注 star



