


单击此处编辑母版标题样式,单击此处编辑母版文本样式,第二级,第三级,第四级,第五级,*,*,演变,对很多人来说,多任务并行的机制是神秘的,包括很多长期在操作系统下开发应用程序的人来说也是如此回顾一下从普通的,单任务顺序执行,到,多任务并行执行,的演变过程,有利于打破,“,多任务并行,”,的神秘感其实多任务并行机制一点也不复杂,从技术上说,它就是程序流折断加现场(在操作系统里叫上下文)保护这两部分技术不仅谈不上高深,甚至无法称其为,“,技术,”,,因为几乎每个开发者每天都在跟它们打交道:比较中断响应、调子程序那么它到底神秘在哪里呢?在于,程序流反向控制,机制,所谓,程序流反向控制,指的是,由子函数决定父函数的执行流程对于通常的程序来说,总是由父函数决定何时调用哪一个子函数的;而在并行多任务系统里,是由一个被称为,任务调度器,的子函数决定着何时调用哪一个父函数!,需要澄清一点是,这种子函数对父函数的调用并不是任意的,它只能将流程指向父函数的折断点,也就是最近一次调用任务调度器时的位置原因很简单,因为子函数根本不知道应该从父函数的哪个地方开始执行,除非它保存了父函数在折断时的上下文这种,“,由任务调度器保存任务流程折断点信息(上下文),并在将来某个时间恢复该上下文,然后继续该任务流程,”,的方式,就是多任务并行的核心机制。
如何在子函数中修改父函数的执行流程?修改堆栈!,键盘扫描,显示扫描,主程序,最简单的顺序执行方式,挨个调用任务,执行完一个任务后再执行下一个任务如果,一个任务长时间占用,CPU,那么其它任务对外部事件的响应全部停止主程序,显示扫描片段,1,显示扫描片段,2,显示扫描片段,3,显示扫描片段,4,延时处,1,键盘扫描,延时处,2,延时处,3,延时处,4,改进,将浪费的时间利用起来,仔细观察可发现,其实任务并非一直运行,大部分时间是在延时,.,如果将任务,从延时处折断,分拆成小片段,后插入到另一个任务中,取代原有的延时程序,就可以提高系统资源的利用率,.,键盘扫描片段,1,主程序,显示扫描片段,1,键盘扫描片段,2,显示扫描片段,2,键盘扫描片段,n,显示扫描片段,n,再改进,统一调度任务片段,改由主程序来调度各任务片段,.,此时任务只剩下逻辑上的概念,而不存在完整的任务函数,.,任务函数已被分拆成若干小片,.,不足之处是,任务片段之间的关系是固定的,.,例如执行完,键盘扫描,任务的片段,1,后,必定是运行,显示扫描,任务的片段,1.,键盘扫描片段,1,主程序,继续改进,实现流程控制,-,状态机,在主程序与任务之间增加一个接口,:,任务状态控制器,.,主程序 只与任务的状态控制器打交道,由状态控制器负责调用,任务的片段,以及控制,阶段的,变换,。
这种循环、条件转移等大尺度流程控制在状态控制器中完成、用特定编号命名每个程序流程阶段的执行方式,就是我们常说的,“,状态机,”.,任务状态控制,Switch(task_step),键盘扫描片段,2,键盘扫描片段,3,显示扫描片段,1,任务状态控制,Switch(task_step),显示扫描片段,2,显示扫描片段,2,其它任务片段,1,任务状态控制,Switch(task_step),其它任务片段,2,其它任务片段,2,状态机的优点是额外占用资源少,执行效率较高缺点是任务被拆得支离破碎,流程不直观而且让,任务在各个状态间跳来跳去,,其实处理,起来并不轻松状态机示例,Unsigned char ks_step=,读第,1,行键值,;,Void keyscan(),switch(ks_step),case,读第,1,行,:/,省略,“,键值,”,二字,.,ks_step=,读第,2,行,;,return,case,读第,2,行,:,ks_step=,读第,3,行,return,case,读第,3,行,:,.,ks_step=,读第,1,行,return,Unsigned char disp_step=,输出第,1,个数码管,;,Void keyscan(),switch(disp_step),case,输出第,1,个,:/,省略,“,数码管,”,三字,.,disp_step=,输出第,2,个,;,return,case,输出第,2,个,:,disp_step=,输出第,3,个,return,case,输出第,3,个,:,.,disp_step=,输出第,1,个,return,Void main(),while(1),keyscan();,display();,每次进入任务函数时只执行一个片段,然后返回,任务进度依靠状态指示器来保存。
状态指示器,每进入函数一次只执行一步即返回,让出,CPU,给其它任务,任务调度器,键盘扫描程序,显示扫描程序,其它任务,浮出水面 任务切换点与任务调度器,任务调度器,是多任务操作系统的核心,重新将任务小片整合起来,与顺序执行不同的是,在执行完每个任务片段后,调用任务调度器释放,CPU,,任务调度器负责分派下一个接管,CPU,的任务片段与状态机不同的是,状态机及主程序,也就是任务调者,是主动调用者,任务片段是受调用者,而任务调度器中,任务调度者是被调用者这种调用关系决定了任务又可以像以前顺序流程那样写成直观的任务函数,.,切换点,切换点,切换点,切换点,切换点,切换点,图中可看到,主程序已经消失,取而代之的是,任务调度器,任务,执行完任务片段后到达,切换点,,此时调用,任务调度函数将该,任务流折断,调度器将该任务流的,堆栈保存后将,CPU,交给下一个任务当该任务下次重新获行运行机会时,只需取出之前保存的堆栈,即可从切换点处继续运行响应,任务调度器示例,Void keyscan(),while(1),outrow(row);,getkey();,task_switch();,row+;,Void display(),unsigned char cs=0;,while(1),out_cs(cs);,out_data(dispbufcs);,task_switch();,cs+;,Void task_switch(),保存当前任务的堆栈,寻找下一个任务,用下个任务的堆栈替换当前的堆栈,return,任务总是从切换处继续运行。
流程可以自由拆分成任意片,同样是每次只执行一步即让出,CPU,,但任务函数比状态机优雅多了,任务调度的核心:堆栈迁移,有任务就有程序流,(PC),,有程序流就需要,堆栈,要折断任务流,就必须保存堆栈每个任务都设有一个,私有堆栈,,用于,保存任务流被折断,(,任务切换,),时,的堆栈内容堆栈是上下文切换时最重要的切换对象,这种对堆栈的切换叫作,“,堆栈迁移,”,事实上,在我接下来要讲的,OS,里,它是唯一需要切换的,部分,以后再提到堆栈迁移有两种方式,一种方法是,(,左图,),使用私栈作为堆栈,发生任务切换时,只需将栈指针即可到新任务的栈顶即可另一种是,(,右图,),使用,公栈,作为作堆栈,每切换一个任务,就将公栈的内容搬向私栈,并将新任务从私栈搬至公栈,然后修改栈指针指向新的栈底任务函数,任务堆栈,任务函数,任务堆栈,堆栈指针,SP,SP,在任务堆栈间切换,任务函数,任务堆栈,任务函数,任务堆栈,堆栈指针,SP,公栈,SP,总是指向公栈,任务切换时需在公栈与私栈间来回倒腾数据,优点:任务切换快,缺点:占内存,适用于较轻的系统中,优点:省内存,缺点:任务切换慢,适用于较重的系统中,栈指针切换,VS,堆栈搬移,使用,栈指针,切换方式,的优点,是堆栈迁移速度极快,移动两个字节即可完成整个迁移过程。
而,堆栈搬移方式,则较慢,每次需搬若干字节,(,如果在任务的第一层函数中切换任务,则需,2,字节,每加深一层函数增加,2,字节),且换入,/,换出的任务各需搬一次此外,还要计算栈深,/,栈顶位置整个过程约需增加,4080,个机器周期,(1,周期为,1,条,NOP,指令的执行时间)但使用栈指针,切换方式,时,由于整个运行周期都在私栈中,这就意味着需要足够深的栈来支撑调子函数,/,调中断,/,寄存器压栈等动作,即使是简单的任务,也需要,812,字节,栈深,而使用堆栈搬移方式时,私栈只要保存从栈底到任务切换时的栈深,而跟中断和最大调子函数深度无关,一般仅,24,字节,分配,8,字节,(,意味着任务切换可发生在第,4,层函数调用中,),已足够应付大多数应用实测数据表明,栈指针切换的方式适用于,函数调用少于,两层、中断期间不需寄存器压,栈,(,或使用,using,保护寄存器,现场,),的,场合中,操作系统可设计为超轻量,仅具有多任务能力,最多再加上睡眠控制而堆栈搬移法适用于函数嵌套较深,/,中断程序较复杂的环境中,操作系统中可加入进程睡眠控制、,CPU,睡眠控制、消息唤醒机制及内存动态分配机制等比较复杂的功能。
下面给出切换速度的实测数据:,栈指针切换方式,20,机器周期,堆栈搬移方式,100,机器周期,(2,字节栈深,每多一字节增加,10,周期,使用汇编优化后可减少到,6,周期,),小结,洋洋洒洒说了一堆,其实只是让大家将思维习惯转变一下,从传统的,由上至下,控制,任务流程,转变为,从下到上,切换,任务流程,记住,任务切换的过程,是通过修改堆栈来完成。