前文
之前很早就写过关于这方面的文章,但过于简陋,于是趁着有点时间再次整理了下,并以代码的角度进行讲解一下。【由于水平有限,如有错误请批评指正】
正文
0 本文写的一个简单OS所实现的目标
系统内核对外提供一些接口,主要有系统初始化,系统开启,任务创建及任务延时等。其功能原型如下:
void OS_Init(void);
void OS_Start(void);
void Task_Creak(void (*pfun)(void),INT8U *pStack,INT8U Task_ID);
void OSTimeDly(INT8U ticks);
1任务入口地址:
在程序中,对某个功能函数进行调用时,一般会采用【程序名(参数)】这种方式进行调用,如下:
int add(int a, int b)
{
return a +b;
}
int main()
{
sum = add(1,2);//调用函数
return 0;
}
而在OS中,是不直接采用【程序名(参数)】这种方式调用任务函数的,而是对传统函数调用在底层上的模拟(想了很久都没有找到合适的话来解释,如果我没讲明白,可以看接下来的具体代码解释)。了解过C语言的函数调用原理或者编译原理的同学都知道,程序有一个运行时结构,当在调用函数时,程序会终止对主调函数(对应上面例子中的main()函数)的执行,转而去执行被调函数(对应上面的例子的add()函数),于是变会将被调函数的入口地址赋值给PC指针(编译器的工作),而之前,程序会对被调函数执行完后的返回地址进行压栈(编译器的工作),当被调函数执行完后,该值会赋给PC指针(编译器的工作),以便继续执行中断的主调函数,不过由于退栈的缘故,被调函数的运行环境会在被调函数执行完毕后销毁掉。这也对应了操作系统原理中所说的【上下文环境的切换】,当然也不完全相同,只是类似,操作系统的上下文切换是由操作系统控制的,本质是对上述行为的模拟,不是由编译器控制的,再者上述的调用过程会销毁被调函数的运行环境,而操作系统的目的是要在多个任务函数上进行连续切换执行,因此需要对各任务函数的运行环境进行保存,这就是下面将讲到的任务私有堆栈的工作。
其实在C语言中(包括大部分语言)函数的函数名就表示函数的入口地址,该地址和变量地址一样需要使用一个指针变量进行存储,这就是所说的函数指针。上面的第一个例子可以改为:
typedef int(*FUN)(int a, int b);//定义一个合适的函数指针类型
int add(int a, int b)
{
return a +b;
}
int main()
{
FUN ADD = add;
sum = ADD (1,2);//调用函数
return 0;
}
为了简便介绍,默认的任务函数为无参无返回值函数(当然,为了使任务函数更加灵活,也可以为任务函数进行传参,如果支持传参,需要在汇编部分将参数赋值给相应的寄存器),一个简单的任务函数。
typedef int(*TASKFUN)(void);//定义一个任务函数类型
void task1(void)//写一个任务函数,task1便是入口地址
{
while(1)//任务函数必须是死循环函数,不然会跑飞
{
/*任务代码*/
OSTimeDly(100);//我们写的是一个实时操作系统,不是分时系统,因此需要主动交出执行权限,该函数会在后面给出
}
}
任务函数必须是死循环函数,因为写一个简单入门OS,不支持销毁销毁任务,想了解更加完善的OS可以看UCOS的源码,我也是一开始写了一个简单的OS后去看UCOS,会发现UCOS的最基本的内核和我们写的是差不多的,无非是功能更加完善。我也尝试对UCOS进行了相应裁剪工作(将一些不常用的功能剔除,或者将一些功能简化)并可正常运行,我一直都是围绕着这个简单的OS内核模型来思考的,因此我觉得写一个简单的OS模型还是有好处的,可以在用一些成熟的操作系统的时候,面对庞大的源码,也不至于思维混乱。即使在学习Linux这种分时操作系统也时有所帮助。
2任务私有堆栈:
上面已经提到,操作系统的目的是要在多个任务函数上进行连续切换执行,因此需要对各任务函数的运行环境进行保存,因此需要为每个任务必须有属于自己的堆栈,而对运行环境的保存的数据结构就是任务私有堆栈,也称为人工堆栈。人工堆栈大小需要谨慎选择,不能短也不能太长,短了会是溢出会可能修改其他任务的人工堆栈,产生调度紊乱。太长会浪费空间,尤其是像51这种硬件资源本就少的单片机。在这里,堆栈的空间的预留是通过数组来划分的,即静态分配,当然也可以采用动态分配。在建立任务时,要对堆栈初始化(这也很关键),将任务入口地址压到最底部(不同的单片机情况不同,这里以51为例,后面的也是),然后sp指向正确的堆栈位置(不同的单片机情况不同,要保存的寄存器个数不同),个人在设计中发现,为了不让sp越界,最好将堆栈最底部单元预留出来,避免浪费可以用来保存任务信息,比如堆栈使用情况。
#define MAX_TASK 2
INT8U Stack[MAX_TASK][27];
3任务控制块:
在操作系统中任务是以任务控制块来管理的,和人工堆栈一样每个任务也有属于自己的任务控制块。一旦任务被创建,系统便会为任务提供一个任务控制块,任务代码与任务控制块关联后便成为系统的一个进程。任务控制块是由结构体描述的,由于内核的精简,任务控制块只由两个成员组成,代码如下:
typedef struct{
//INT8U OSTCBStkPtr; //当前TCB堆栈指针
INT8U OSTCBSP;
//INT8U *OSTCBPREV; //前一个TCB指针
//INT8U *OSTCBStkNext;//后一个TCB指照
//INT8U OSTCBPSW;j
// INT8U *OSTCBSP;
// INT8U OSTCBPrio; //优先级
// INT8U OSTCBRdy; //就绪状态
INT8U OSTCBDly; //延时节拍
}OS_TCB;
idata volatile OS_TCB os_tcb[MAX_TASK];//定义任务控制块表
根据系统需求成员定义不同(一些成熟的OS会利用预编译来确定需要的成员,比如当配置时关闭一些功能,利用预编译判断不加入一些成员变量,从而达到节约内存的效果),在这里,需求是自由延时服务的OS,只需要一个保存任务SP的成员变量和保存延时时间的成员变量,一些在一些成熟OS上常用的成员变量在这里可以注释掉。
5任务状态:
在操作系统中方便任务的管理,将任务分为不同的状态,为了精简系统,系统任务状态分为三种,如下图所示。
操作系统使用任务就绪表来记录任务的就绪情况,调度时从就绪表中获取优先级最高的就绪任务。
6 就绪表和查找表
利用就绪表和查找表可以在任务调度切换阶段,找到就绪表中优先级最高的任务,为节约数据空间,任务状态使用一个字节的位来储存任务的就绪状态,即就绪表为一个一字节变量,且位号代表优先级别,约定位0位最高级别,位7为最低状态。