系统上有两个任务(可以认为是线程),高优先级任务P1,低优先级任务P2。P1被定时器T1每毫秒的超时事件激活一次工作,每次工作必须在1毫秒之内完成。现在有条发给P1的消息在P1上创建一个对象,创建对象共耗时2ms,从而挤占了两个超时事件,导致P1有段时间工作不稳定。
问题:为了解决上面的问题,能否将对象的创建过程交给P2完成,然后将创建好的对象指针交给P1?
系统上有两个任务(可以认为是线程),高优先级任务P1,低优先级任务P2。P1被定时器T1每毫秒的超时事件激活一次工作,每次工作必须在1毫秒之内完成。现在有条发给P1的消息在P1上创建一个对象,创建对象共耗时2ms,从而挤占了两个超时事件,导致P1有段时间工作不稳定。
问题:为了解决上面的问题,能否将对象的创建过程交给P2完成,然后将创建好的对象指针交给P1?
P1和P2 的会话通道都是现成的,不需要考虑。问题的关键在于创建对象的过程交给另一个任务完成是否能解放P1。
把这段时间的一些讨论心得整理成下面的小短文享给大家。
给大家分享一下基站实时软件的特点---(原创)
首先看下面场景:
系统上有两个任务:高优先级任务P1,低优先级任务P2,资源可共享(可以理解为线程)。有毫秒定时器T1,每毫秒发消息给P1让P1干活, P1必须在1毫秒之内把活完成。
现在有条从外部发给P1的消息,使P1创建一个对象,创建对象共耗时2ms,从而挤占了两个超时事件,导致P1有段时间工作不稳定。
问:为了解决上面的问题,能否将对象的创建过程交给P2完成,然后P2将创建好的对象指针交给P1?
在解答以上问题前,我们必须知道操作系统的调度是基于tick中断的。Tick中断是操作系统的脉搏。任务切换的条件是发生了tick中断,其他中断,系统调用等。
Tick的精度的设置决定了操作系统的实时性高低,Tick周期设置的越短,实时性越好,但是操作系统的任务切换开销在整个系统中占得比重就越大,从而造成了CPU资源的浪费。例如vxworks的Tick设置一般缺省设置是1/60s,在通信设备上一般设置成1/1000s以适应对一些毫秒级定时器的需求。
再回到上面的题目,如果“对象创建过程”不包含除tick中断以外的任务切换条件,那么这个过程不管放在哪个任务里,都会持续的消耗CPU资源,直到发生tick中断或者任务执行完后主动释放CPU。如果把T1考虑进来,如果T1是绕开操作系统的一套定时器,这个移植方案也是行的。在FSP上T1正是这样的一套定时器。
说到实时系统里的定时器,往往有重造轮子之嫌。
上层软件开发有丰富的定时器API可供选择:sleep,alarm,signal,settimer甚至select…
但是在实时系统上,上面的这些操作系统提供的API并不总能满足需求。
首先是数量级的需求,只要是开发协议栈就避不开定时器,一个协议栈运转起来可能会创建成百上千个定时器,如果把这些定时器完全交给操作系统去管理,这对操作系统的性能影响是相当可观的。为了避免这些影响,我们往往会基于这些底层API写一套上层的定时器应用框架,把对系统的影响程度降到最低。
对于一些毫秒级精度的协议,我们也会考虑用系统级的定时器。如果精度再高一点,例如微秒级,我们就得绕开操作系统定制一套定时中断API了。LTE的PS就是基于这样的一套API运转的。
PS对时间很敏感。原因是LTE调度周期只有1ms。每一毫秒我们都要有数据及控制信息进出。这些进出都是基于软件的计算。软件的计算围绕着一个个业务模型。
这里我们先不谈具体的业务场景,先看下面这个小故事:
|
奇怪的卖花人 有个奇怪的卖花人,他有8个花瓶,依次编号0,1,2,3,4,5,6,7。每个花瓶最多插1朵花。 他有个习惯,每天只往一个花瓶插花卖花,例如第一天往花瓶0里插1朵花,然后只卖花瓶0里的花,第二天就往1里插,卖1的,以此类推。等到第九天他再回来检查0号花瓶,如果0号花瓶花还在,他这一天就没有插花的任务,只有卖花的任务。一朵花如果被卖了28次还没有被卖出去,卖花者就会把这朵花丢掉。 他还有个习惯,他一旦卖了某朵花,比这朵花更早插到其他花瓶的花会被贴上打折标签。 现在卖花人觉得贴标签太麻烦,请你为卖花人设计一套软件算法,帮助卖花人在卖花时知道该花是否要打折。 |
上面这个故事是PS里某个小需求的抽象版,具体到真实环境上,卖花人数量变成了800个,每个瓶子可插4朵带编号的花,还有其他七七八八的上下文,从而交织出一个复杂的业务场景。软件场景的分析设计过程可能会很复杂,但是真正去实现,代码量往往可能仅仅几十行。接下来不可避免的工作就是优化。
上层开发很少会谈及优化。就像下面这个短文里论述的:优化:你最大的敌人“Optimization: Your Worst Enemy”http://www.flounder.com/optimization.htm
但是具体到我们的产品里:1ms,1Ghz的CPU上1,000,000 clock cycle。 假设你有一个大循环遍历从1~1000,为了让系统正常工作,我们预留出20%的cpu资源做buffer。这样留给这个大循环的单次循环的clock cycle就是800(实际环境远比这个假设复杂)。
800个clock cycle!你增加的任何一行代码抢去的CPU资源都是奢侈的。为了利用好这仅有的800个clock cycle,产生的代码也是奇特的。
假设一条if语句,if(x||y||z)最好情况是只判断x得到true,最坏情况是判断了x,y,z得到true或false。在DSP上的u-plane代码里有时候你会看到这个判断会被写成if(x|y|z),貌似永远用最坏情况来取结果。事实上因为DSP上有多路运算单元,很多操作可以并行执行,所以上面的语句会比前面那个语句的最坏情况要好。而当这条if语句放在一个大循环里,前者的最坏情况可能会是一条错误。
下面的代码是针对上面提的“卖花”需求在真实环境下一个小接口updateFlag的四个不同实现版本的实现。你更倾向于用哪套代码?你认为哪套实现的效率最高?
|
Enum
class A2
//this is the current version on svn m_flag[flagid] = 0;
}
class A4 m_flag[flagid] = (_cmpgtu4(_hill(AgeArrayTmp1), dwtmp) << 4) |_cmpgtu4(_loll(AgeArrayTmp1), dwtmp);
|
看汇编结果:
A1耗120 cycles
A2耗32 cycles
A3耗7 cycles
A4耗14cycles。
你能猜对吗?
再看下面两个版本的查询代码,其中A也是经常惹争议的,如果对照右边的汇编结果,可能会令你对它重做评价。
|
enum {SIZEFORTEST = 20}; T1* findA(const T1& value)// Total cycle { T1* p=NULL; for(int i =0; i<SIZEFORTEST; ++i) { if(value == m_pArray[i] ) { p=m_pArray[i]; } } return p; } |
Total cycles :6+min_trip_cnt*1 = 26 |
|
T1* findB(const T1& value) { for(int i =0; i<SIZEFORTEST; ++i) { if(value == m_pArray[i] ) { return &m_pArray[i]; } } return NULL; } | Total cycles :9+min_trip_cnt*9= Between 18 and 189 |
但是到这里并不算完,在真实的环境上我们还要考虑三级内存架构。
数据从external memory读到L2 cache大约要耗费100个cycle,从L2 cache读到L1 cache大约是10个cycle,从L1到CPU就1个cycle。栈放在哪里,对象放在哪里,代码段放在哪里都会影响你的程序性能。
在实时系统里我们经常封装一层内存管理模块,是什么原因?
正如“Optimization: Your Worst Enemy”提到的----为了避免内存碎片。还记得当年流行的“磁盘碎片整理”的功能吧,那是内存碎片的“硬盘版模型”。到了我们的PS,我们对内存的管理更是精细,要考虑3级内存,要考虑cache line,我们的调优工程师不停的测量和调优,这个过程犹如名剑师在打磨一把宝剑。
除了要避免内存碎片,我们还要重视另一个碎片:时间碎片。
我们的大部分程序跑在一个单进程的上下文里,这个进程被各种消息激活。消息的优先级,消息到来的时间,处理这条消息占得时间片大小都可能会对整个系统的调度产生影响。如果低优先级的消息穿插分布在整个时域上,很有可能会挤占高优先的消息。这是一种优先级翻转。
例如MSG_H1优先级最高,MSG_H2优先级居中,MSG_L是优先级最低,MSG_H1对MSG_H2有依赖,如果MSG_L先到,在处理MSG_L的过程中,MSG_H2和MSG_H1都到了,当MSG_L处理完毕后,如果没有特殊的调整,我们可能会先处理MSG_H1,从而得不到正确的输出。为了使系统能正常工作,我们提高MSG_H2的优先级。如果MSG_H1是个定时器消息,这就意味着超时消息被MSG_L和MSG_H2往后挤,影响了定时精度。
更合理的解决方案应该是,通过调整MSG_L的到来时机或者内部调整把它的处理时间挂到划分好的低优先级处理时序中,让出更多的完整时间来处理高优先级的消息。
在遇到将多个小消息合并成大消息,或者要添加一些低优先级消息等场景的时候,一定要把这些内容考虑进去。