在本系列的第一篇文章《实时性迷思(1)——快是优点么?》中,我们介绍了实时性的基本模型:
并得出两个重要的结论:
- 实时性只关注“是否能在实时性窗口内完成对应事件的处理”,而与事件处理的快慢无直接关系;
- 从应用整体的角度来看,实时性窗口内越靠前的时间越珍贵;
这个模型本身并不复杂,但 “你以为你懂了” 和 “能够回答实际问题” 或者是“能够得出有意义的推论” 还是有相当的距离的。比如上一篇文章《实时性迷思(2)——“时间片轮转”的沙子》,我们就推导出了“惊人”的结论:高频率的时间片轮转会浪费大量处理器时间,从最终效果来看对系统的实时性是有害的!
今天我们继续来借助实时性模型来研究一个看似铁板钉钉的问题:
- 当应用在运行时有大比例的时间屏蔽了中断,系统的实时性还有救么?
- 当应该频繁的开关中断,系统的实时性还有救么?
这里的两个问题,其实都没有切中要害,如果硬要回答的话,有经验的老鸟会首先说:你提供的信息不足。
这又是为什么呢?(懒得看中间过程的小伙伴可以直接看文章最后的结论)
【从一个例子开始】
复杂的理论不必多说,我们首先来看一个极端的例子:
代码语言:javascript复制int main(void)
{
...
while(1) {
__disable_irq(); //! 关闭全局中断
do_some_work(); //! 经过测量,占用了7个周期
__enable_irq(); //! 开启全局中断
}
}
这个代码本身并不复杂,事实上,它在前后台系统中非常典型。作为例子,这里有几个要点需要首先明确:
- 这只是一个例子,它存在的意义是为我们提供一个讨论的起点,请不必在意和猜测它在实际应用中的意义;
- 假设 __disable_irq() 消耗一个周期;当它执行完成后,全局中断会被关闭;
- 假设 __enable_irq() 消耗一个周期;当它执行完成后,全局中断会被打开;
- 假设 这里的 while(1) {} 导致的循环跳转(无条件跳转)会消耗一个周期(其实Cortex-M3/M4就是这样);
- 函数 do_some_work() 消耗7个周期。
基于上述假设,我们很容易发现,在一次循环中:
- 从执行 do_some_work() 开始到 __enable_irq() 执行结束,总共 7 1=8 个周期——在这期间,中断都是被屏蔽的;
- 自从“无条件跳转”开始到 __disable_irq() 执行结束,总共 1 1=2 个周期——在这期间,全局中断是可以被响应的;
- 整个循环占用10个周期:其中8个周期中断被屏蔽。又由于这是main函数内的超级循环,因此可以大体推断出:在整个应用执行期间 80% 的时间中断是被屏蔽的。
这符合本文一开头所提出的两个问题的条件,即:大比例的时间屏蔽了中断 和 频繁的开关中断。
【是时候搬出模型了……】
那么,在这个例子中,实时性会受到怎样的影响呢?我们不妨结合模型,看一个最坏情况,即,刚开始执行 do_some_work() 的时候,某一事件发生——实时性窗口开始计时:
再图中,由于中断被屏蔽而导致事件无法被响应,这段时间被称为“事件无法响应时间”,结合模型容易得出:
结论1:
只要“事件无法响应时间”与“处理事件所需时间”的总和超过了“实时性窗口”,当前事件处理的实时性就无法保证了。
正如前面几篇文章一再强调的那样,时间的实时性窗口是来自物理世界的,基于物理时间计算的绝对值。这里CPU周期其实是一个相对值——系统频率越高,8个周期对应的物理时间就越短;系统频率越低,8个周期对应的物理时间就越长(当然还要考虑处理事件所需的时间也会随着频率的变化而变化)。对现今大部分动辄几十兆赫兹,或者上百兆赫兹的单片机系统来说,8个周期可能连1us都不到(作为参考当系统是 8MHz时,8个周期正好1us)。
结论2:
当且仅当系统频率已知时,我们才能根据CPU的周期数计算出“事件无法响应时间”和“处理时间所需时间”——也只有都换算成相同单位时,与实时性窗口的比较才有意义。
一个应用中往往有多个具有实时性要求的事件,在已知系统频率的情况下,我们可以将“事件无法响应时间” 拉到每一个实时性窗口中一一比较,只要任何一个不满足,我们就可以宣告整个系统实时性的破产。然而,如果所有单独比较的结果都令人满意,我们是否就可以宣告:由屏蔽中断所导致的“事件无法响应时间”足够短,不会对系统的实时性造成影响呢?——高兴的太早了。
【CPU资源磨刀霍霍……】
一个实时性应用中往往不止一个事件有实时性要求,因此,判断系统的实时性是否所有保证从来都不是只单纯的在每一个实时性窗口内做比较就能解决的。就像上面所说的那样,由于屏蔽中断的时候,任何事件都可能发生,因此,由屏蔽导致的“事件无法响应时间”必须带入到每一个实时性窗口中去进行比较。
仅仅只是这样,仍然不够。由于CPU资源有限,我们还必须确认在“最差情况下”扣除了中断屏蔽期间所占用的CPU资源后,仍然有足够的CPU资源来满足其它实时性窗口的需求。关于如何计算每个实时性任务的CPU资源占用,可以通过文章《实时性迷思(2)——“时间片轮转”的沙子》来复习,仍然有印象的同学可以直接看下面这张图片来唤醒记忆:
这里有一个误区非常值得阐明:即,在前面的例子中,我们说“系统有80%的时间都在屏蔽中断”,这是否意味着,屏蔽中断占用了 80%的CPU资源——只剩下20%的CPU时间用于实时性处理了呢?
也许你愣住了。但答案很类似脑经急转弯——并不是这样,因为在我们讨论的前后台系统中,其实隐含了一个假设——实时性的响应是通过中断来进行的——既然是中断,就是可抢占的,因此,即便8个周期内无法响应,只要那一个周期开了口子,CPU的资源就被实时性处理程序抢走了。
思考这个问题,实际上直接引出了第三个重要的结论:
结论3:
“事件无法响应时间” 不看积累下来的总量,而只看单次最大能连续拖延实时性相应多久。
要理解这个结论,其实并不困难。这就好比PWM,在较长的时间内,占空比相同而频率不同的PMW,其高电平的总时间是相同的(占空比相同);但频率不同的PMW实际上每次高电平的持续时间是不同的——频率越低,当然每个周期内高电平时间越长。
套用到屏蔽中断对实时性的影响上来说:
推论1:
屏蔽中断并不可怕,哪怕积累下来的时间占比很大,只要每次屏蔽的时间足够短,就能有效的减小对系统实时性的影响——换句话说,高频率的开关中断很可能还是有益实时性的(关键还看推论2)。
推论2:
如果系统中存多个对实时性响应的屏蔽(比如裸机中的屏蔽中断、RTOS中的关闭调度),根据木桶原理,只以单次屏蔽时间最长的那个来评估对系统实时性的影响。
【问出正确的问题】
文章的开始部分,我们提出了两个问题:
- 当应用在运行时有大比例的时间屏蔽了中断,系统的实时性还有救么?
- 当应该频繁的开关中断,系统的实时性还有救么?
现在我们知道,这两个问题都忽略了一个重要的信息,即:这个系统中单次屏蔽中断最长的时间是多少?一旦我们获得了这个时间,我们就可以问出正确的问题:
- 已知当前系统中,最大的中断屏蔽时间长度为 Tmask;系统频率为 F;对已有的实时性事件处理来说,系统的实时性是否仍然能够得到保证?
对每一个具体的系统来说,求解过程也很简单:
- 由于屏蔽中断的时候,任何实时性事件都有可能发生,因此我们要重新计算系统的CPU资源占用——评估它是否接近或超过了 100%
- 计算每一个实时性任务的CPU占用时,都要把“事件无法响应”Tmask 加到 “处理事件所需时间” 里——作为分子去除以作为分母的“实时性窗口”:
【小结】
如果上述讨论让你头疼,那么记住下面的内容基本都不会有错:
- 频繁开关中断并不可怕;
- 别管关闭中断的时间总比例是多大,这没意义;
- 找到系统中关闭中断时间最长的那个代码,测量它占用的时间——它才是罪魁祸首;
- 使用“以小换大策略”——借助一切可能的手段,使用小的屏蔽来替换掉长时间的屏蔽——无论是屏蔽中断还是RTOS里的屏蔽调度,道理都是一样的。
- RTOS里尽可能用 mutex,而不要长时间关调度——因为mutex几乎是RTOS可以提供的屏蔽时间最短的手段了。
- 裸机自求多福。