点击蓝色字体
关注我们哦

阅读本文之前,需要具备以下两点条件:
了解arm汇编或者x86汇编 熟悉c语言程序开发
调度器是什么
任务调度器的存在,主要是为了充分利用计算机的硬件资源,要让计算机尽可能"同时"多干一点点活。这些活其实是由处理器执行某段代码来完成的,一般我们把这段程序称为进程。进程通过负责完成某件事情,但是如果这件事情太过于复杂的话,就必须招几个小弟来帮忙干活了,这些小弟我们把它叫做线程。特别地,在嵌入式RTOS里面,没有进程这个概念,而线程则被称为任务
按理说,一个处理器在任意时刻,只能执行一个任务。它想要真正地并行运行任务,必须要具备多个处理器,比如说同/异构多核cpu。
如果非要让一个处理器兼顾所有的任务,唯一的做法就是让每个任务各自在处理器上执行一小会,然后再换下一个任务上处理器,直到所有的任务都执行结束。任务调度器就是实现这种任务伪并行的软件模块,与调度器配合工作的还有调度算法,本文暂不分析调度算法。
怎么实现任务调度器
一个任务一旦获得处理器而运行,是通过处理器的寄存器来运行指令并进行数据的读写处理的。也可以说任务的行为是通过寄存器来体现的。注意:这里的寄存器,不是指芯片的外设寄存器,而是芯片内核的寄存器。
内核的寄存器是计算机非常底层的机制了,哪怕是C语言这种号称最接近机器语言的高级语言,也并不能直接控制内核寄存器。所以在实现一款任务调度器的时候,必须是要使用汇编语言(当然机器语言也可以,只不过没人会这么干)。
那么当任务在处理器上运行的时候,我们把此时内核寄存器上的具体值称为任务上下文,也有很多人把它叫做任务情景
。一旦任务要退出处理器暂停运行时,我们就要迅速地把任务上下文保存在一个私有的内存,而且要记录好这个私有内存的位置。这样一来,当我们想要重新让这个任务运行的时候,就可以找到记录私有内存的位置,然后把事先保存的任务上下文恢复到内核寄存器上面了。
每次任务切换的过程,都必然会遇到任务上下文保存和任务上下文恢复的过程。这也是任务调度的核心所在。
如何保存任务上下文
这是一个很泛、很广的内容,因为牵扯到很多语言的底层机制和芯片的架构知识,没有办法展开来讲 。这里主要是从本质上跟大家探讨两个核心的问题:
1、内核具体有哪些寄存器?
2、内核寄存器哪些需要保存?
先来看第一个问题,内核寄存器的值跟芯片的体系架构息息相关,像ARM、X86、RISC-V都不一样,这必须是要阅读相关芯片架构的技术文档。
再来看第二个问题,实际上,我们保存内核寄存器的值的时候,有一个笨方法,那就是不管三七二十一,我把所有内核寄存器全都给保存了。
这固然可以,但是对于操作系统来说,任务调度是非常频繁的工作, 在任务调度器里面加入无意义的指令会降低操作系统的性能,我们应该尽可能简化它的负担。那么在判断哪些寄存器需要保存的时候,不得不提到的就是ABI/EABI了。
ABI/EABI是什么鬼
先来看ABI,ABI的全称是Application Binary Interface,即应用程序二进制接口。这个与API可不一样,API是指应用程序可编程接口,而ABI规定的是更加底层的一套规则,是属于编译方面的约定,比如参数如何传递、返回值如何存储、系统调用的实现方式、目标文件或者数据类型等等。
而EABI则是指嵌入式系统中的ABI。
这里为什么要强调ABI规则?
主要原因就是C编译器是按照这套规则来编译C程序的,假如我们完全是使用C语言来开发程序,那就无需考虑ABI规则,编译器会帮我们处理好,但是如果我们自己手动去写汇编代码,而且要提供给C语言调用的话,就必须按ABI规则去写。
比如在X86架构里面,有一份官方规范SysV_ABI_386-v4文档里面,有这么一段话:
All registers on the Intel386 are global and thus visible to both a calling and a called function. Registers %ebp, %ebx, %edi, %esi, and %esp “ belong ” to the calling function. In other words, a called function must preserve these registers' values for its caller. Remaining registers “ belong ” to the called function. If a calling function wants to preserve such a register value across a function call, it must save the value in its local stack frame.
大体意思是在i386体系架构上,函数调用时,这5个寄存器ebp、ebx、edi、esi归主调函数所有,其余寄存器归被调函数所用。
也就是说,不管被调函数中是否使用了这5个寄存器,在被调函数执行后,这5个寄存器的值不变。也就是被调函数要为主调函数保护好这5个寄存器的值。当然,你也可以一股脑地把所有通用寄存器都保存(虽然会浪费cpu性能,但确实省事)。
我们经常在STM32里面看到的任务调度器就是这样。任务在放弃执行时,通过Pendsv异常来触发中断,当CM3开始响应一个中断时,内核会自动把 8个寄存器(R0-R3,R12,LR,PC,xPSR)的值压入栈,中断退出后自动恢复。(具体可以参考<<Cortex-M3权威指南>>,第九章--中断的具体行为),所以在STM32的任务调度器中,我们常常可以看见它保存了除自动恢复以外的所有通用寄存器。比如RT-thread在stm32上的任务调度器,就保存了几乎所有的通用寄存器。如下表:
struct exception_stack_frame{/* 异常发生时,自动加载到 CPU 寄存器的内容 */rt_uint32_t r0;rt_uint32_t r1;rt_uint32_t r2;rt_uint32_t r3;rt_uint32_t r12;rt_uint32_t lr;rt_uint32_t pc;rt_uint32_t psr;};struct stack_frame{/* 异常发生时,需手动加载到 CPU 寄存器的内容 */rt_uint32_t r4;rt_uint32_t r5;rt_uint32_t r6;rt_uint32_t r7;rt_uint32_t r8;rt_uint32_t r9;rt_uint32_t r10;rt_uint32_t r11;struct exception_stack_frame exception_stack_frame;};
总结
到这里,就把任务调度器的本质跟大家分享完了,因为调度器涉及到很多芯片架构特定的知识,贴代码意义不大。毕竟贴了STM32的,不贴一下i386的说不过去吧,贴了i386的,不贴MIPS的也太不爱国...
能把调度器的本质说清楚,并且让大家能理解就很欣慰了。
END
往期推荐
扫描二维码
获取更多精彩
just enjoy!


喜欢本文点个在看
