前言
以前写的一些乱七八糟的东西,都比较乱,本着完美主义(吃饱撑的)都是写一点过两天不满意又删掉,现在用到uC/OS-II来做开发,觉得还是有必要写些东西。虽然很憧憬一些高质量的博文,但是毕竟接触这一行时间不久(三个月左右),所以只能一点点探索着来写。这个系列的博文大部分也是参照了网上流传的一些教程来写的,也并非全部原创,只是作为学习笔记,把看到的学到的东西做自己的理解并写下来。
前后台系统(Foreground/Background System),也被称作是超循环(Super-Loops)。应用程序本身是一个死循环,并有一些中断函数来处理相关的操作,这个大概就可以理解为前后台系统了。个人觉得,其实在以前的裸机开发的过程中,很多都是用到了前后台系统,只是当时对这种概念并不了解。例如之前做的光立方,在死循环中跑对应的流水灯,并用外部中断监听按键来进行不同动画的切换。例如毕设做的自平衡小车,在死循环中跑显示屏内容的刷新,并利用外部中断来监听加速度陀螺仪的中断输出,进而进行姿态数据的计算处理给电机驱动相应的输出来控制电机保持平衡。这些(应该)都可以称得上前后台系统。
那么,所谓的前后台,后台即那个在主程序跑的死循环,前台即中断所处理的内容。当需要时间相关性很强的关键操作,一般都是由中断完成的,恰如上述例子中光立方的按键监听和自平衡小车的姿态感应控制。
这种系统好处和坏处是什么呢。个人觉得好处就是足够简单,基本上就是单片机裸机开发了,很多产品也是基于前后台系统的,微波炉,电话,和一些玩具,平常在死循环中处于待机状态,而其他一系列操作都是通过中断实现。坏处呢,就是时间没办法掌控,像是做个小设计当然基本不用考虑时间的问题,但是比较大点的项目,一个死循环时间可能会很长,中断给出的数据可能要很久才到处理它的地方,也就是响应慢。同时,一旦处理的东西多了,在一整个死循环里就会显得非常混乱,后续开发会非常困难。
那么以我个人之见,在开发一些大型的项目时,单纯的前后台系统肯定是一种愚蠢的方法。我们需要一些别的方法,不仅能使开发更方便结构更清晰,同时也要满足所开发项目的一些要求。
代码的临界段,也称作是临界区。定义它的目的是什么呢,就是要清晰一个概念,这段代码运行时,是不允许被打断的,不允许任何形式甚至不允许中断来打断它。那怎么样保证它不会被打断呢,在进入这段代码之前,先把中断关闭,执行完之后,再把中断打开,保证执行的这段时间没有中断来进行干扰即可。
但是值得注意的一点是,关闭中断的这段时间不宜过长,不能什么乱七八糟的都往这里塞。关闭中断的时间要尽可能短,否则可能会影响时序导致影响整个系统的运行。
这就就好像你在看一场对你来说非常重要的直播的时候,肯定先推掉一切会影响你看直播的活动,什么喝酒撸串,什么唱歌搓澡,甚至恨不得把手机关机掉,等看完直播了再把手机打开。但是你不能一时兴起跑去深山老林浪一个月再回来,家人朋友找你得找疯了,这就是时间要尽可能短。
任何为任务所用的实体,都可以称作是资源。资源可以是输入输出设备,比如打印机,键盘之类的,也可以只是一个变量,数组,结构体等。
而可以被一个以上任务所使用的资源,就是共享资源。但是值得注意的,为了防止数据破坏,每个任务在与共享资源打交道时,必须要独占资源。这个就叫做互斥(mutual exclusion)。
就像公司打印机一样,整个公司共用一台,但是你在用的时候不能还有其他人也在用,肯定时你先用完了再给下一个人用。
任务,也称作时一个线程,是一个简单的程序。改程序可以认为CPU只被自己本身所使用。实时操作系统的设计过程,就包括了如何把一整个问题,分割成几个任务来完成。多个任务的协作,来实现整个问题的解决,而每一个任务都有其优先级,CPU寄存器,以及自己的栈空间。优先级和栈空间的作用和意义,会之后再说。
任务共有五种状态,休眠态,就绪态,运行态,中断态,挂起态。休眠态相当于任务驻留在内存之中,但不被内核所调用。就绪态意味着任务已经准备好可以运行了,但是其优先级低于正在运行的任务,所以正在等优先级高的任务释放CPU的控制权。运行态就是正在运行着使用CPU的任务,掌握着CPU的控制权。中断态就是在运行过程中由于中断,CPU提供相应中断服务时该任务的状态。挂起态就是正在等待某一个资源或者事件,比如正在等待某个脉冲,正在等在某个资源被释放,正在等待延时等等。
下图中就表现了各个任务状态之间时如何转换的。其中WAITING就是等待态,也就是挂起态,它一旦得到所需要的资源就会转变为就绪态,而正在执行的任务一旦需要某些资源但暂时没有获得时就会进入挂起态。READY就是就绪态,当任务被启动时并拥有相应的资源,就会进入就绪态等待CPU控制权,一旦获得了CPU控制权就会进入运行态。RUNING就是运行态,当任务就绪后获得了CPU控制权就会进入运行态。ISR就是中断态,是在任务运行时,CPU临时去处理中断的时候该任务的状态。DORMANT就是休眠态了,任务如果被删除,就会进入休眠态。
需要注意的几点:
1.任务获得资源后不会直接进入运行态,一定是先就绪,再运行;
2.中断推出后,不一定会回到原有的任务中,而是在任务列表选取优先级最高的任务进行运行,如果在中断让更高优先级的任务就绪,那么中断结束最高优先级的任务会开始执行;
3.任务被删除,这个删除不是说任务就不存在了,只是进入了休眠态,CPU不会再管它,即使由它需要的资源被释放了,它也不会进入就绪态。等于说,这个任务不是挂掉了,而是被打入冷宫了。想要用到这个任务,再把它唤醒就行了。
而以上任务改变不同的状态中,有一点,就是任务从运行态到非运行态。CPU不可能空着的,即使没有我们启动的任务,也总会有一个空闲任务。在uC/OS-II里,作者就保留了优先级最低的两个任务优先级。最低的只做一个简单的加一操作,第二低的是用来算CPU使用率的。而任务从运行态到非运行态,必然有另一个任务从就绪态到运行态,说白了就是CPU控制权的转让。而这个转让过程,就是任务切换。
任务切换(Task Switch),也称作上下文切换(Context Switch)。运行的任务把当前在CPU的状态临时保存到自己的任务栈中,就完成了该任务状态的保存,等下次它获得CPU控制权时不至于丢了之前做到哪里了。而保存完之后,就是要接替CPU使用权的任务把自己之前存在任务栈中的放到CPU寄存器里,继续上次被打断的任务。
任务就像是程序员一样,完成自己该完成的工作。他可能不知道整个工程是做什么的,什么时候他应该干什么。只是有人叫他的时候他就开始干活,叫他停的时候就把自己的东西清出来位子让给别人。那实现这套过程,总得有个人来管理众多的程序员该做什么,什么时间做,这个时候就需要技术总监。而在操作系统中,内核基本上就充当了技术总监的工作。
内核(Kernel)就是负责管理各个任务的,或者说为每个任务分配CPU使用时间,并负责各任务间的通信。使用了内核,这个实际上是完成功能之外的东西,是需要额外增加了应用程序的负担。同时每个任务要有自己私有的存放临时数据的任务栈,这些都是需要耗费ROM的。而内核本身对CPU的占用,一般占了整个时间的2%到5%。
调度(Scheduler),还有一个另外的英文名字叫dispatcher。说到schedule,其实就是日程表安排表。所以顾名思义,调度就是内核来决定,现在该哪个任务来干活了,哪个任务该一边歇着了。多数实时内核的调度是基于优先级调度法的。
现在举一个例子,来讲所谓的优先级是什么,以及可剥夺型内核和不可剥夺型内核是什么。
假设有四个人A,B,C,D,优先级从高到低。B开始使用打印机,这时候ACD也来了要用打印机,虽然可能D比A,C都要先来,但是D优先级最低,所以还是要先给A用,然后给C用,然后才能给D。这就是优先级制度。那B怎么办,老大A过来了,这打印机我让还是不让?
假如这时候A拍拍B的肩膀说,没事你先用,你用完了再给我用,不急。然后B就先用完打印机再给A用,这个就是不可剥夺型内核。打印机我既然再用着,就先用完再考虑给谁用。
假如这时候A拍拍B的肩膀说,小B你先停停,我这的比较重要先,拖不得,我先用完再还你用。于是B把打印机让给A,A用完之后,B再接着用。这就是可剥夺型内核。虽然我现在正在用的,但是我上司来了,或者说有更重要的事情要用了,我就得先让出来给别人用。
但是现在问题来了,B打印到了一半A又打印,之后又给B打印,打印机是公共的,没办法把数据临时拿出来让B保存。那么A中间插一脚,打印的东西不久全乱了吗。本来要打印B“BBBBB”的,A中间插一脚要打印“AAAAA”,结果出来的数据可能就是“BBBAAAAABB”。在不可剥夺型内核中几乎不会出现这样的问题,因为A不会临时把B的使用权抢夺出来,但是这种情况在可剥夺型内核发生了该怎么办?处理方法还是挺多的,先举个例子,打印机上有个纸条,规定其他东西抢不抢我不管,反正打印机规定了,有人在用着这个,就不允许别人打断,这个就是互斥。详细的以后再说。
可重入性(Reentrancy)就是说某一个函数可以被一个或者一个以上的任务调用,而不担心数据被破坏。上述的打印机就是典型的不可重入性。因为虽然需要打印的内容每个人不同,但打印出来都是到一摞纸上,最尴尬的是这摞纸是不允许从中间分开的。
下面这段代码就是不可重入型函数,因为在代码段中包含了全局变量Temp。如果一个以上的任务同时调用这个函数,交替进行执行,就会造成Temp值的不可预测。
int Temp;
void swap(int *x, int *y)
{
Temp = *x;
*x = *y;
*y = Temp;
}