【序】
不知道你发现没有,平时我们讨论嵌入式软件开发时总绕不开与实时性(Real Time)相关的话题。相信不少朋友和我一样是通过实时性操作系统(Real Time Operating System, RTOS)第一次接触到实时性概念的——我记得那还是大学时代、参加机器人竞赛的时候。工作以后自信地以为加深了不少对实时性的本质认识——现在看来其实还未摸到门道。就这样浑浑噩噩一直到毕业后的第八年,因为工作变动的原因,我被迫要在一周内要做一个实时性原理相关的研究报告,也就在那时,我体会到了疯狂练功走火入魔的感觉:走路在思考、吃饭在看资料、头一直发烧一样的微微发热、甚至连睡觉都在梦中推演模型——头发一把一把的掉,幸好有截稿时间,否则真的要秃了。
也就是经过那一次,我突然发现自己之前对实时性的认知可谓徒有其表,甚至从未做对实时性模型本身的定量分析——所幸,那次研究报告如期交付,工作变动也如愿以偿。然而,3年后我发现“我又双叕天真了”——那是有一次,我正跟人讨论嵌入式基本范式,就突然一个瞬间,脑海中原本毫不相关的一些模型猛地被联系到了一起(音效请脑补):
我甚至本能的立即意识到:之前自己在某篇文章中“言之凿凿”的推论过程其实存在巨大漏洞——当然,那本书从未出版过,而且会闲到对我进行深究的人估计也没有几个。
今天,即便我非常确信——在前方至少还有几道数学的深谷阻碍着我触碰“实时性”的圣杯——然而我并不是计算机科学家,现有结论对我来说已经足够装逼
- Lv1:“实时性” = “越快越好”,认为用好中断是保证实时性的关键;这类朋友通常最擅长的是裸机下的“前后台系统”;
- Lv2:“实时性” = RTOS,认为选一个好的RTOS,或者会用RTOS就可以保证实时性;这一阶段的朋友对RTOS充满了好奇,以编写自己的RTOS为“ 终(zhong)极(er)目标”;
- Lv3:“实时性” = 任务拆分,这一阶段已经能正确的理解实时性窗口的概念,意识到实时性并不意味着越快越好,但也认为“在可能的情况下”“快一点响应事件没啥坏处”;这一阶段的朋友可能已经可以在裸机和RTOS之间自由的反复横跳,无论是裸机下的状态机还是RTOS下的线程都已了如指掌、任务间通信更是游刃有余;
- Lv4:这一阶段开始思考实时性模型的特点,并逐渐意识到模型本身其实隐含了足以颠覆过往所有关于实时性认知的秘密;到达这一阶段的朋友通常觉得没必要、也没心思继续思考实时性更本质的数学意义——因为此时获得的结论已经足够了应付几乎所有的工程开发了。顺便说一下,我就在这里。
- Lv5:到了这个阶段,不仅脑洞大开、战斗力惊人、估计打针也没法阻止你抓破脖子了吧
——以上只是暴露年龄的玩笑,但肯定可以水几篇SCI论文了……
在理解了实时性的模型以后,我(本能的排除了自己比较笨这个可能性,然后)意识到:其实这一过程完全没必要如此漫长和曲折——很多结论和道理是如此简单——不仅书本上有,而且解释和学习起来都不费什么力气。可能这就是“挠破头”想通某个道理之后,回头再看时忍不住要“苦笑”时的感受吧。
按照约定,为了将经验和知识分享给大家,从本文开始,我将以几篇文章的篇幅:从基础模型开始,由浅入深、由理论到实践,推演关于实时性的几个重要结论——从而直接跳跃到Lv4的认知阶段。如果你看了这个系列后有什么话想说的、想问的,还请在评论区写下您的留言。求评论、求转发、求收藏。
【击碎 “唯快不破” 的神话】
图1展示了一个标准的实时性模型:
- 基于物理世界客观法则的限制,很多应用在制定需求说明的时候,从某一个事件发生的时刻计算,会规定一个死线(Dead Line),即:一旦事件发生了,如果不在这个死线之前完成整个对事件的处理,就视作失败;
- 这里,从事件发生到死线这段时间长度,习惯上称为实时性窗口。当事件发生时,只有在死线内任意时刻完成了对事件的处理,才能称为实时性得到了满足;
- 容易注意到,处理事件的过程也需要消耗时间——一般称为事件处理时间;
图1 实时性基本模型
考虑一个有趣的问题:对一个实时性任务来说,实时性窗口内的时间,其价值是一样的么?换句话说,横竖处理事件消耗的时间是不变的,早点做迟点做都是做,有什么区别么?
图2 实时性窗口内不同时间段完成事件响应
对比图2所示的三种情况,可以很清楚的得出结论:理论上,从满足实时性的角度出发,在时间窗口内任意时段完成对事件的处理都满足实时性要求;早做没有任何额外的好处,“踩着上课铃到校”也没有任何惩罚——简单说就是早做迟做无所谓。
你说“我不管,我不管”,既然什么时候做都一样为什么不能“尽早做”?“你也说了尽早做没啥不好”,“中断来了,服务程序执行了,我想让它迟点执行也做不到啊?”
为了回答这个问题,我们不讲大道理,先看一个常见的例子:
- 超级循环里有三个任务A、B和C;
void main(void)
{
...
while(1) {
task_a();
task_b();
task_c();
}
}
- 每个任务都使用轮询的方式在等待一个来自芯片外界的事件发生(先不考虑存在中断的情况);
- 当一个任务函数被执行时会检查对应的事件是否已经发生,如果确实已经发生,则执行后续的处理;反之则立即退出任务函数——释放处理器;
- A、B、C三个事件的实时性窗口分别为10ms, 6ms和4ms;处理三个事件的处理程序分别需要4ms、3ms和0.4ms。如图3所示:
图3 三个事件的实时性窗口和事件处理时间示意图
- 需要强调的是,task_a()、task_b()和task_c()三个函数的策略本质上都是一样的——“一旦检测到事件立即处理,绝不迟延”!
基于上述事实,容易发现:假如某一时刻,A、B、C三个函数都处于触发状态(等待处理的状态),而超级循环恰巧进入task_a()执行——这种情况其实比想象中容易发生,比如从task_a()退出到task_c()执行完成期间,事件A触发了;从task_b()退出到task_c()执行完成期间,事件B触发了;在task_c退出()之后恰巧事件C又触发了……此时,任务A会立即响应,消耗4ms的时间来完成事件处理;当从task_a()函数退出时,剩余给task_b()的时间窗口只有2ms(6ms-4ms),而事件B的处理函数需要3ms——显然事件B的实时性是无法得到保证的——当然事件C已经死得透透了……
图 4 “越快处理越好” 导致其它任务无法满足实时性要求
通过上面的例子,我们知道“越快处理越好”是值得反思的——至少会存在情况导致系统在某些时刻无法满足实时性要求;那么从模型上来说,如何理解这一现象呢?
让我们重新来看图1所示的模型:
实际上,如果单纯从一个实时性任务自身出发来看,的确在实时性窗口内,任意时间完成事件的处理都是一样的;然而,通过前面的举例我们其实可以发现,当一个系统中存在多个实时性任务时,虽然一个实时性窗口内的任意时间对任务自己都是等价的,但越靠前的时间对“别人”来说是越宝贵的:
- 当你使用“越快越好”策略时,你不会有额外的收益,而实际上是走了别人的路,让人无路可走——典型的损人不利己;
- 当你在别人需要的时候,在自己实时性得到保证的前提下,尽可能让出对你没有额外价值的靠前的时间,实际上是一种“利他主义”;
- 当所有的任务都采用这种利他策略时,就变成了“人人为我,我为人人”的合作策略——这种情况下,如果数学证明整个系统一定存在一个方案来满足所有任务的实时性需求,那么利他策略一定能找到这样的解决方案。
图 5 一种可能的解决方案(不是唯一)
作为一个系统开发者,我们显然是需要从全局考虑的,因此完全没有必要从单个实时性任务的自私视角来看问题,因此结论就变得更为直接:实时性窗口内越靠前的时间价值越高,从总体上来看“单纯”越快越好的策略对实时性是有害的。
既然单纯的“越快越好”不可取,且“实时性窗口内”越靠前的时间越有价值,是否意味着,其实“越靠后越好呢”?
为了验证另外一个极端“越慢越好(越靠后越好)”是否是正确的,我们不妨以同样的例子来推演一下,仅仅更新task_a()、task_b()和task_c()的执行策略:从“越快越好”变为“越慢越好”——这实际上意味着:
- 每一个事件处理任务都清楚的知道“距离事件发生已经过去了多长时间”;
- 为了做到“卡着上课铃进教室”,不到最后时刻,绝对不执行任务处理。
根据这一算法,我们推演得到以下的尴尬情形:
图 6 过于谦让的后果……
不妨分析下过程:首先,task_a()执行,在了解到距离自己的最后时刻还有6ms的实时后毅然的决定把宝贵的时间留给他人;于是,CPU来到了下一个任务函数,基于类似的原因,task_b()也摆摆手……最终第一轮三个任务都决定再等一等……
如此谦让(浪费)了3ms以后,任务B终于决定下场——在执行了3ms任务处理后,成功的将随后的任务C逼上了绝路……随着A的沦陷,大型翻车现场成就达成……
从结论上看,另外一个极端“越慢越好”也是走不通的。那么究竟如何才能从模型分析的角度出发得出一个令人信服的、容易理解的、满足所有任务实时性需求的方法呢?关于这一点,我们下次再聊。
【小结】
从系统全局来看,实时性窗口内的时间越靠前越有价值,应该尽可能留给别的更紧急的任务来使用。事件发生时“越快处理越好”的策略直接占用了它人的“生命线”——当所有的任务都试图“损人不利己”时,那么整个系统没有一个任务是可以保证自己的实时性不被它人破坏的。从结论上看简单的“越快越好”策略在实时性系统中是不允许的。