从零开始写一个操作系统(三) —— 任务切换器

在最开始的章节中我们编译了ez-rtos的项目工程源代码,只不过我们是在模拟器中跑的仿真,所以我们没办法在物理世界中看到真实的 LED 灯闪烁。如果有条件拥有 STM32 开发板的同学,可以将代码下载到开发板上看到如下的效果,你将会观察到两个 LED 灯以 DELAY_US 这个宏定义所定义的时间间隔进行交替闪烁。

本章节我们就将介绍怎么开发多任务操作系统中最基本的任务切换器功能,并且实现如下 LED 灯交替闪烁的功能。

知乎视频图标

CPU 能运行多个任务?

在学习计算机组成原理之前,似乎很多人觉得电脑能同时打开多个软件是一件理所当然的事情。然而并不是如此。

做过单片机开发的同学就知道单片机里面如果不引入操作系统,那么我们在 main() 函数中写的那些代码会从单片机通电后立即开始执行。那么问题来了,main() 函数只有一个,因此单片机的 CPU 通电以后只能执行 main() 函数这一个任务。

电脑CPU也是如此。熟悉 X86 OS开发的同学应该知道:电脑在上电以后会先执行 BIOS 中的上电自检程序检测所有硬件是否功能正常,然后再去 MBR 主引导扇区中读取 512 bytes 的 BootLoader,将其加载到 0x7c00 这个内存位置,并且 BIOS 将会从这个地方开始执行 OS 的 BootLoader。因此电脑在没有 OS 的情况下,也只能从 BootLoader 开始一直顺序执行下去。你可以认为 BootLoader 就是类似于单片机中的 main() 函数。

那么问题来了,只有一个 main() 函数,但是我又想让很多个任务(task)同时执行该怎么办呢?

答案就是引入多任务切换器,通过操作系统提供的多任务切换器,每隔一段时间切换下一个任务,只要保证每个任务都能每隔一个很短的时间就得到执行,那么我们就能够很逼真的感觉到这么多的任务仿佛在同时运行。

操作系统怎么切换任务?

请注意上一句话中的三处黑体字,它表明了我们的任务切换器应该要做上述三种操作。下面我们就来逐个剖析。

切换

任何 CPU 都有一个 PC 寄存器,这个寄存器指示着当前 CPU 正在执行内存中哪个地址上的指令。我们只需要修改 PC 寄存器就能过改变操作系统当前执行的指令。如果 CPU 此时此刻正在执行函数 A() 中的指令,我们将 PC 寄存器的值改成函数 B() 中的指令,那 CPU肯定会老老实实去执行 B 函数的指令了。

任务切换的思路我们已经有了。剩下就是“每隔一段时间”这个该怎么做的问题了。

每隔一段时间

STM32 以及主流的 CPU 都有提供定时器(Timer),定时器会在定时结束后发起一个时间中断,我们可以在这个中断中进行修改 PC 寄存器的任务切换操作。

下一个任务

现在还剩一个问题,就是我们的操作系统需要把 PC 修改为下一个任务的函数指针,那么去哪找到下一个任务并且获得它的函数指针呢?我们在 C语言中定义了那么多函数,究竟哪些属于需要被操作系统的任务切换器调度的任务?这就需要操作系统维护一个任务列表,操作系统因此就可以从这个任务列表中依次找到每个任务。这个列表通常叫做 tcb_list,后面会更具体介绍 tcb 这个单词的含义。

意外的降临

电脑不只是傻乎乎的运行事先存储在硬盘中的程序,他还需要根据外界提供给他的输入进行各种判断,然后提供合适的反馈。例如我们通过键盘打字,通过鼠标点击给计算机一些输入,然后计算机通过显示器和音响喇叭给我们一些反馈。

在单片机上,也会有许许多多的传感器,他们接受外界的环境信息,然后通过控制电机或者 LED 灯给我们用户反馈。

这就需要一种机制让 CPU 能够获取到外部的输入,然后针对不同的输入进行一些逻辑策略的判断,反馈输出。如果是刚学单片机的初学者可能很快就能想到写一个 while 循环不停的判断某个 GPIO 的电平高低来判断外界的输入到底是什么,然后循环体内再去写具体的函数判断逻辑,但是这就有一个问题,while 是一个死循环,CPU 得一直浪费时间执行这个死循环,没办法做其他事情。因此还有另一种手段,就是 CPU 提供的事件中断功能。

但是这个世界瞬息万变,事件中断的来临的时机我们无法预估,一旦有意外的事件来临,我们需要第一时间去处理这个事件中断,处理完了再让 CPU 判断是否到达了需要切换任务的时间点,若到了切换时间点则做我们任务切换的工作。(例如在医疗行业的心电监护仪,汽车中的安全气囊控制器,中断的响应优先级绝对是最高的,因为事关生命安全)。所以我们需要一种机制让任务切换的优先级低一点。

这在 Cortex-CM3 中有一个叫做 PendSV 中断就是专门做这个事情的。

SV 的全称是 Supervisor,指的是一些系统调用。PendSV 和普通 SV 的区别是它多了一个 pend,说明 PendSV 的事件处理函数可以被挂起,等到有合适的时机再去执行(也就是没有任何其他中断执行的情况下才会去执行它),因此通常 PendSV 的中断抢占优先级也被设为最低。抢占优先级最低意味着如果一旦出现其他中断来临,那么其他中断可以抢占 CPU 的使用权,优先执行完其他较为重要的中断处理以后再回来继续任务切换。

任务切换器的初始化

上面提到了任务切换需要有一个定时器实现定时切换任务,还需要维护一个任务列表,然后还需要利用 PendSV 将任务切换的函数作为一个低优先级的事件处理函数,那么任务切换器的初始化也就是做这些工作而已。

我们打开 ./os/task.c 和 ./os/task.s 以及它的头文件来详细学习一下代码该怎么写

通常阅读一个 C语言 项目,都是先阅读头文件看看它导出了哪些声明。

可以看到在代码顶部定义了最大可以执行的任务数量,我们这里为了简单起见设置为 8 个,如果你需要同时执行更多任务可以自行调大该值。

然后定义了 Task_Control_Block_t 这个结构体,这就是 TCB 任务控制块的全称,他记录了任务的一些执行信息,这里先暂时不用管里面的成员。

接着往后看,有一个成员类型为 TCB 的数组名为 tcb_list,这就是之前所说的任务列表了。

看到这里就好了,因为我们这一阶段主要了解任务管理的初始化操作。

我们看 init_task() 函数,首先调用 create_task 函数创建了一个 idle 任务,并且将 current_TCB 变量,也就是当前执行的任务的 TCB 设为我们创建的第一个 idle 任务,然后又内联了一段汇编代码,这个汇编代码将 PSP 的值设置为 0x00000000。

#define STACK_IDLE_SIZE 32
stack_t stack_idle[STACK_IDLE_SIZE];
void init_task() {
	create_task(task_idle, 0, stack_idle, STACK_IDLE_SIZE);
	current_TCB = &tcb_list[0];
	__asm {
		// PSP = 0
		MOV R0, 0x0
		MSR PSP, R0
	}
}

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注