第三章 风水轮流转 【承前启后的废话】 本章探讨怎样实现多任务,并重点研究切换任务需要做什么。 关于:在嵌入式系统中,为何要基于操作系统开发程序?它相对于典型的前后台程序有什么优点? 什么是任务?什么是多任务?等等。这类问题,本章不作额外说明。 笔者假定读者已经理解这些基本问题。如不理解,请自行百度、谷歌。 大多数书籍讲述多任务的方法是直接告诉读者系统怎么做,如μC/OS-II的任务由什么组成,怎样调度等等。 笔者不打算这么做,而是打算像做一道证明题那样,一步步告诉读者需要怎么做,为什么要这么做。 为让初学者清楚“怎样实现多任务”,笔者决定让自己做回“初学者”,一步步研究到底如何实现多任务。 “怎样实现多任务”的“子问题”是“怎样实现多任务的切换”,这也是一个关键问题。 “怎样实现多任务的切换”的“降阶问题①”是“怎样实现两个任务的切换”。 所以,本章先从“怎样实现两个任务的切换”这个特殊化、简单化的问题着手研究,之后层层深入。 最后,再分析“μC/OS-II的任务切换”及“怎样实现多任务”。 ------------------------------------------------------------------------------------------------------------------------------------------- ① 你不能理解某个问题,通常意味着,你还无法理解更简单的问题;你不能解决某个问题,一般可以说,你还没能解决更简单的问题。 笔者认为,将问题“降阶”、“特殊化”应该是一个数理工作者(或工程师)惯用的“武器”。 将问题降阶、特殊化的目的是为了更快的解决问题,同时,在研究降阶问题往往能给我们一些启示,这些启示并非直接研究一般问题就能够轻易得到。 ------------------------------------------------------------------------------------------------------------------------------------------- · 怎样实现两个任务的切换【例化问题】 为方便研究“怎样实现两个任务的切换”,我们对它进行“例化”,给出具体问题。 如果,在一个系统中①,它运行着A、B两个任务,执行顺序如下: (1)运行A任务,延时50ms。 (2)运行B任务,延时50ms。 (3)运行A任务,延时50ms。 (4)运行B任务,延时50ms。 问:系统应如何处理,才能成功切换任务?(只须给出与“切换任务”相关的信息) ------------------------------------------------------------------------------------------------------------------------------------------- ① 这里所指的系统,只含有一个单核单线程CPU,单核单线程CPU在任意时刻只能执行一条指令。 ------------------------------------------------------------------------------------------------------------------------------------------- 【理论分析】 下面,我们对以上第(1)至(4)步进行分析: (1):不需作其它处理。(直接运行A任务①,因为,到此看不出需要处理什么。) (2):运行B任务前,将CPU的PC值更新为B任务地址②,不需作其它处理。(理由同上) (3):运行A任务前,将CPU寄存器③更新为上一次运行中断时的CPU寄存器。(为使A任务“接着原来”运行) 然而,“上一次运行中断时的CPU寄存器”并没有被保存,这样: (2)就需修改为:运行B任务前,将A任务运行中断时的CPU寄存器值存到全局变量④中。 (4):运行B任务前,将CPU寄存器更新为上一次运行中断时的CPU寄存器。(为使A任务“接着原来”运行) 然而,“上一次运行中断时的CPU寄存器”并没有被保存,这样: (3)就需修改为:运行A任务前,将B任务运行中断时的CPU寄存器值存到全局变量中, 并将CPU寄存器更新为上一次运行中断时的CPU寄存器。 以上内容实际上是分析过程的“草稿”,读者如果能耐心看完,相信,能够明白所以然。 为更清楚的说明问题,将“草稿”进一步整理如下: (1):不需作其它处理。 (2):运行B任务前,将A任务运行中断时的CPU寄存器值存到全局变量中。 (3):运行A任务前,将B任务运行中断时的CPU寄存器值存到全局变量中, 并将CPU寄存器更新为A任务上一次运行中断时的数据。 (4):运行B任务前,将CPU寄存器更新为上一次运行中断时的CPU寄存器。 说明:如果第(4)步后,系统任务还没结束,需要再切换任务,那么,第(4)步需改为: 运行B任务前,将A任务运行中断时的CPU寄存器值存到全局变量中, 并将CPU寄存器更新为B任务上一次运行中断时的数据。 再次将“草稿”进一步整理如下: (1):不需作其它处理。 (2):运行B任务前,将A任务运行中断时的CPU寄存器值存到全局变量中。 (3):运行A任务前,将B任务运行中断时的CPU寄存器值存到全局变量中, 并将CPU寄存器更新为A任务上一次运行中断时的数据。 (4):运行B任务前,将A任务运行中断时的CPU寄存器值存到全局变量中, 并将CPU寄存器更新为B任务上一次运行中断时的数据。 通过上面的分析,我们知道切换任务时,需要做的是:(本文称此为Solution 1) ·将当前任务的CPU寄存器值存到RAM中。 ·将当前任务更新为即将运行任务⑤。 ·将CPU寄存器更新为即将运行任务上一次运行中断时的数据。 ------------------------------------------------------------------------------------------------------------------------------------------- ① 即调用A任务里的首个需要运行的函数。 ② 任务的地址即相应的首个运行函数的函数名地址。如,void taskA() {},将taskA赋值给PC。 ③ CPU寄存器包括了PC、SP、寄存器组等。 ④ 用过μC/OS-II的读者,知道μC/OS-II将CPU寄存器值存到栈中,即存放在局部变量里。但,在这里,由于没有特殊处理,则只能存到全局变量里。 ⑤ 这个在分析过程中并没有提到,但,它是隐含需要的,因为: ·本次切换的第三步要更新CPU寄存器,就需要知道“即将运行任务”是哪个。 ·下次切换的第一步、第三步,系统也需要知道到“当前任务”。 就本简单问题而言,可以这么说,由于 A、B轮流切换,所以,系统需要记录当前任务,以便知道切换到哪个任务。 ------------------------------------------------------------------------------------------------------------------------------------------- 【实践测试】 实际上,将以上理论付诸于实践,并不能实现多任务。 原因是:A任务被挂起时,SP指向某个RAM地址(如0x2000)。切换到B任务,B任务依然要使用SP,然而这个SP值并不是B任务在被挂起时的SP值,而是A任务被挂起时的SP值(如,前面提到的0x2000)。 根据前面章节的研究,我们知道局部变量被分配在与SP相关的RAM中,即栈中。 若代码并不“刻意”改变SP的值,则整个栈是连续的。也就是,有且只有一个堆栈。 但,如果要实现多任务,则需要有多个堆栈支持,使得各个任务的RAM数据不会相互影响。 系统可以这么处理:(本文称此为Solution 2) ·建立任务①时 ·为任务分配相应的堆栈,即确定栈顶地址及堆栈大小。 ·切换任务时 ·将SP指向即将运行任务的栈顶地址(“刻意”改变SP值,使得各个任务使用相应的堆栈)。 ·将当前任务的SP保存到全局变量中②。 我们综合Solution 1及Solution 2,可以得到: ·建立任务时 ·为任务分配相应的堆栈,即确定栈顶地址及堆栈大小。(来自Solution 2) ·切换任务时 ·将当前任务的CPU寄存器值存到RAM中。(来自Solution 1) ·将当前任务的SP③保存到全局变量中。(来自Solution 2) ·将当前任务更新为即将运行任务。(来自Solution 2) ·将SP指向即将运行任务的栈顶地址。(来自Solution 2) ·将CPU寄存器更新为即将运行任务上一次运行中断时的数据。(来自Solution 1) 将Solution 3付诸于实践,前文提到的简单问题得以解决④。 ------------------------------------------------------------------------------------------------------------------------------------------- ① OSTaskCreate、OSTaskCreateExt。 ② 这个在分析过程中并没有提到,但,它是隐含需要的,因为:下次切换,系统要“将SP指向即将运行任务的栈顶地址”,所以,需要保存SP。 为何要保存为全局变量,是因为,只有这样才能找回它。否则,如果分配在局部变量中,难以或者说无法找回它。(加标识有可能找回) ③ 细心的读者将发现:这一步的“SP”已包含在上一步的“CPU寄存器”中,所以,这一步可以省去。确实是这样。 但因为,在μC/OS-II中,分为两步:将CPU寄存器保存在栈中,将SP保存在TCB控制块中。所以,在这,笔者并没有省去这步。以方便后续分析。 ④ 仅以上处理是不够的,但,笔者在此只讨论了最基本的、需要处理的东西,以让读者容易明白。 ------------------------------------------------------------------------------------------------------------------------------------------- |