实时性迷思(1) —— “快是优点么?”
共 4210字,需浏览 9分钟
·
2021-03-26 18:26
【序】
也就是经过那一次,我突然发现自己之前对实时性的认知可谓徒有其表,甚至从未做对实时性模型本身的定量分析——所幸,那次研究报告如期交付,工作变动也如愿以偿。然而,3年后我发现“我又双叕天真了”——那是有一次,我正跟人讨论嵌入式基本范式,就突然一个瞬间,脑海中原本毫不相关的一些模型猛地被联系到了一起(音效请脑补):
我甚至本能的立即意识到:之前自己在某篇文章中“言之凿凿”的推论过程其实存在巨大漏洞——当然,那本书从未出版过,而且会闲到对我进行深究的人估计也没有几个。
Lv1:“实时性” = “越快越好”,认为用好中断是保证实时性的关键;这类朋友通常最擅长的是裸机下的“前后台系统”;
Lv2:“实时性” = RTOS,认为选一个好的RTOS,或者会用RTOS就可以保证实时性;这一阶段的朋友对RTOS充满了好奇,以编写自己的RTOS为“ 终(zhong)极(er)目标”;
Lv3:“实时性” = 任务拆分,这一阶段已经能正确的理解实时性窗口的概念,意识到实时性并不意味着越快越好,但也认为“在可能的情况下”“快一点响应事件没啥坏处”;这一阶段的朋友可能已经可以在裸机和RTOS之间自由的反复横跳,无论是裸机下的状态机还是RTOS下的线程都已了如指掌、任务间通信更是游刃有余;
Lv4:这一阶段开始思考实时性模型的特点,并逐渐意识到模型本身其实隐含了足以颠覆过往所有关于实时性认知的秘密;到达这一阶段的朋友通常觉得没必要、也没心思继续思考实时性更本质的数学意义——因为此时获得的结论已经足够了应付几乎所有的工程开发了。顺便说一下,我就在这里。
Lv5:到了这个阶段,不仅脑洞大开、战斗力惊人、估计打针也没法阻止你抓破脖子了吧——以上只是暴露年龄的玩笑,但肯定可以水几篇SCI论文了……
【击碎 “唯快不破” 的神话】
基于物理世界客观法则的限制,很多应用在制定需求说明的时候,从某一个事件发生的时刻计算,会规定一个死线(Dead Line),即:一旦事件发生了,如果不在这个死线之前完成整个对事件的处理,就视作失败;
这里,从事件发生到死线这段时间长度,习惯上称为实时性窗口。当事件发生时,只有在死线内任意时刻完成了对事件的处理,才能称为实时性得到了满足;
容易注意到,处理事件的过程也需要消耗时间——一般称为事件处理时间;
图1 实时性基本模型
图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 “越快处理越好” 导致其它任务无法满足实时性要求
当你使用“越快越好”策略时,你不会有额外的收益,而实际上是走了别人的路,让人无路可走——典型的损人不利己;
当你在别人需要的时候,在自己实时性得到保证的前提下,尽可能让出对你没有额外价值的靠前的时间,实际上是一种“利他主义”;
当所有的任务都采用这种利他策略时,就变成了“人人为我,我为人人”的合作策略——这种情况下,如果数学证明整个系统一定存在一个方案来满足所有任务的实时性需求,那么利他策略一定能找到这样的解决方案。
既然单纯的“越快越好”不可取,且“实时性窗口内”越靠前的时间越有价值,是否意味着,其实“越靠后越好呢”?
每一个事件处理任务都清楚的知道“距离事件发生已经过去了多长时间”;
为了做到“卡着上课铃进教室”,不到最后时刻,绝对不执行任务处理。
根据这一算法,我们推演得到以下的尴尬情形:
图 6 过于谦让的后果……
不妨分析下过程:首先,task_a()执行,在了解到距离自己的最后时刻还有6ms的实时后毅然的决定把宝贵的时间留给他人;于是,CPU来到了下一个任务函数,基于类似的原因,task_b()也摆摆手……最终第一轮三个任务都决定再等一等……
如此谦让(浪费)了3ms以后,任务B终于决定下场——在执行了3ms任务处理后,成功的将随后的任务C逼上了绝路……随着A的沦陷,大型翻车现场成就达成……
【小结】