高可靠性嵌入式系统固件设计策略
本文针对如何编写易理解、易维护的优秀代码进行了讨论,为程序员提供了一些非常实用的编程指导。文中指出,函数功能应该最小化,代码封装便于程序维护,消除冗余能够提高程序的可靠性,适当的重构能够降低维护过程中程序熵增大的速度,提高程序的清晰度,而遵循一定的标准并采用适当的检验工具则会进一步保证代码的可靠性。
一些非正式调查显示,60%到70%的固件编写者都持有电子工程师学位,这一学历背景在帮助理解所开发的应用的物理层,以及错综复杂的硬件时,起到了很好的作用。但大多数电子工程课程都忽视了软件工程的教育。当然,教师们会教授如何编程,他们希望每个学生都能精通代码构造,但在他们所提供的教育中,缺乏对构造可靠系统所必须的软件工程关键原则的教育。
也许如今最广为人知,但却最少被采用的软件设计准则就是保持函数短小精悍。我曾在一次固件讲座中询问听众,多少人在编写代码时限制了函数长度,结果几乎没人举手。但事实上我们清楚,好的代码不可能很长。
如果你编写的函数超过了50行,即一页,那么这个函数已经太长。事实上,对于一个超过8或10个阿拉伯数字的字符串,我们能够记住的时间很可能无法超过1分钟。那么又怎能奢望我们能理解一个由成千上万的ASCII字符构成的函数?对于那些跨了许多页的程序而言,即使是试图跟随程序流程都很困难,甚至几乎不可能,因为我们必须不断地翻页,才能看懂那一个个嵌套着的循环是用来干什么的。
一个函数应该只实现一个功能。如果一段代码过于缠绕不清,拼命地想完成许多不同的功能,那么这样的代码就过于复杂,不可能具备可靠和可维护的特性。我见过太多这样的函数,它们利用多达50个参数来选择成打的交互模式,这样的函数几乎都不能可靠地工作。用独立的方式表达独立的想法,将每个想法写成完全清楚的函数。经验告诉我们,当你很难找到一个能够清楚表达函数意义的名字时,说明这个函数的功能已经太多了。
封装代码
提倡面向对象编程(OOP)的人们一直在倡导“封装、继承和多态”的要求,尽管OOP并不适用于所有应用,封装却可以说放之四海而皆准。封装的意思是将数据以及对其进行操作的代码捆绑进一个实体,这意味着任何其他代码都不能直接访问这些数据。
如果你所开发的应用中对ROM限制非常严格,每个字节都要仔细斟酌,那么这种应用基本无法封装。在几个对成本极端敏感的应用中(例如电子贺卡),将内存需求降到最低就至关重要。但是我们必须承认,这类应用的开发成本本身就非常昂贵。无论何时,只要你陷入了受字节限制的开发条件,那么开发成本就很可能高得惊人。
大家都知道,利用C++和Java编程时可以进行封装。事实上,用C和汇编开发时,同样可以进行封装。封装时要注意,所有全局变量都必须在使用到该变量的函数或模块内定义,并保证该变量不被其他程序访问。但封装并不仅仅意味着数据隐藏。一个完全封装好的对象具有很高的内聚性(cohesion),无需涉及任何与其无关的行为就能完成任务。同时,这样的对象还具备异常安全和多线程安全性。我们可以把这样的对象或函数看作一个完整的功能性黑盒子,只需很少的外部支持,或者根本无需任何支持。
一个封装得较好的序列号处理程序可能需要一个中断服务程序,用以向循环缓冲器传送它接收到的字符,需要一个get_data()程序从数据结构中提取数据,同时还需要一个is_data_available()函数,用于测试接收到的字符。它还能处理缓冲溢出、序列号缺失、奇偶错误以及所有其它可能出现的错误条件,因此,这种程序是可重入的。
对代码进行封装之后的一个必然结果就是消除了代码之间的依赖性。高内聚必然伴随着低耦合,换句话说,就是封装后的代码对其它行为的依赖性较小。我们都曾读过这样的代码,一些看起来简单的操作却与成打的其它模块纠缠不清。这时,即使只做最简单的设计修改,维护人员也不得不在成千上万行代码中跟踪变量和功能,而这肯定会把他们逼疯。
消除冗余
斯坦福大学的研究人员对160万行Linux代码进行的研究发现,即使是无害的冗余也往往和程序缺陷(bug)高度关联(参看www.stanford.edu/~engler/p401-xie.pdf)。
研究人员将冗余定义为一段无效的代码,例如:1. 将一个变量赋给它自己;2. 初始化或设置一个变量后却从不使用它;3. 死码;4. 在复杂的条件判断语句中,一个子语句的逻辑条件已经被在其之前的子语句涵盖,因而该语句永远不会被求值。这些研究员十分聪明,他们并未将某些特殊情况列入冗余范畴,例如用于设置一个存储映射I/O端口的代码。这类操作看起来多余,但其实是有用的。
即使那些不会造成程序缺陷的无害冗余也会引发问题,因为这些包含冗余的函数中出现硬错误的可能性比不含冗余代码的函数要高出50%。冗余代码的出现意味着设计人员思路不清,因此很有可能在附近出现其它错误。
此外,还要小心成块拷贝的代码。我个人十分赞成代码重用,也鼓励开发人员继续开发那些已经测试过的大块源代码,但是很多时候,设计人员往往在拷贝代码时,并没有对被拷贝代码的含义做足够的研究。你真的能确保,即使这段代码是来自该程序的另一部分,所有的变量也都是按你期望的方式初始化的吗?会不会有极小的可能在程序中出现互斥,引发死锁或者竞争?
我们拷贝代码是为了节约开发时间,但这是有代价的。我们必须比研究自己正在编写的新代码更仔细地研究这些拷贝来的代码。当lint或者编译器警告发现了未使用的变量时,必须引起注意。这可能是一个信号,意味着程序中潜伏着更多严重的错误。
减少实时代码
实时代码不但易出错、编写成本高昂,而且调试成本可能更高。如果可能,最好将对执行时间要求严格的段落转移到一个单独的任务或者程序段中去。如果在整个程序中处处渗透着时间问题,那么会令每一个字都变得难以调试。
如今我们所构建的系统比过去庞大得多,也复杂得多,但我们所采用的调试工具的调试能力却不如十年前的调试工具。尽管处理器的速度已经暴增至接近无穷快,在线仿真器还是我们的首选调试器,其中包括实时跟踪电路、事件定时器,甚至性能分析工具。今天,我们身边处处充斥着BDM或者JTAG调试器。这些工具用于解决程序上的问题还可以,但他们基本上没有提供任何资源用于解决时域问题。
还要请大家记住以下几个在安排开发时间表上的经验规则:一个系统负荷达到90%的系统,其开发时间是负荷小于或等于70%系统的两倍;如果系统的负荷增加到95%,那么开发时间将增大到三倍。实时应用项目的开发是十分昂贵的,高负荷的实时项目尤其如此。
编写优雅流畅的代码
这里强调的是流畅,而非跳转。要构造流畅的程序,就应该避免使用continue、goto、break或过早的return。这些语句本来都是十分有用的构造,但他们通常会降低函数的透明性。极限编程以及其它一些敏捷编程方法强调了重构(即重新编写劣质代码)的重要性。这其实并非新概念,理顺并维护编写恶劣的代码模块比维护一段结构漂亮的代码模块要昂贵得多。
重构的狂热追求者们要求我们重新编写所有那些可以被改善的代码,这就显得过于吹毛求疵。我们的工作是要用一种可盈利的方式创造能够成功的产品。追求完美这一目标不应凌驾于所有其它的考虑之上。但仍有一些函数写得太差,必须重写。
如果你害怕编辑某个函数,或者如果每次你对这个函数做一点修改,它就会出问题,那么就该重构这个函数。如果你作为一个专业开发人员,凭着你经过良好训练的直觉发现,我们最好别碰这段代码,因为没人敢与之周旋。那么就说明是时候该放下所有其它事,重写这段代码,让它变得易懂、易维护。
热力学第二定律告诉我们,任何闭合系统如果向更加无序的方向发展,其熵就会增加。程序也遵循这一令人沮丧的事实。一次又一次的维护过程常常会增加程序的无序性,使下一次的改变更加困难。正如Ron Jeffries所指出的,维护而不重构会给每次得到的软件版本增加一个“混乱因子(m)”,从而增大代码的熵。这样,每次修改软件得到的新版本的维护代价可以看做(1+m)(1+m)(1+m). . .,或者(1+m)×n,其中n是修改发布的次数。并且随着我们对程序修整和随意缩略的次数越来越多,维护代价会呈指数上升。这也是为什么有些程序员的小聪明会激怒管理者,让他们觉得“这个程序简直一团糟,没法维护”。
重构也需要付出代价,但它能消除混乱因子,使得修改发布软件的代价变成线性的1+r+r+r . .
Luke Hohmann倡导“降低老版本的熵”,他指出我们为了发布产品,常常过于频繁地做出一些快速修整,这就增大了维护成本。因此,必须还清妄自修整软件带来的技术债务。维护不仅仅是填鸭式地向软件中添加新功能,它还包含降低软件在维护过程中自然产生的熵。
同时,重构还能将含混不清的逻辑关系理顺。如果现有的代码缠绕不清、乱七八糟,那么就应重新编写它,以便更好地说明其含义。此外,还应删除那些层层嵌套的循环或条件语句。因为谁也没有那么聪明,能够明白那一层层嵌套的IF。程序越透明,就越容易正确。
遵守代码编写标准并借助检查工具
请根据你公司的固件标准来编写代码,并利用正式的代码检查工具来增强代码与标准的符合度并寻找缺陷。在检查结束之后再进行测试。用检验工具寻找缺陷比人工调试大约便宜20倍。他们能够捕捉到你通过传统测试从未检查到的各种问题。许多研究都表明,传统调试只能检查约一半代码!如果在产品发布前不使用检验工具,那么发布的很可能是一个处处隐含着缺陷的产品。
有趣的是用于构造安全性要求高的软件的DO-178B标准严重依赖于所使用的工具来保证每一行代码都被执行,这些代码覆盖工具确实是个奇迹,但它们仍然无法取代检查。
代码编写标准和检查是相辅相成的,任何一方离开另一方都不可能取得成功。而没有标准和检查就不可能构造出完美的固件。
本文小结
当我才刚刚开始我的职业生涯时,一位比我入行早的同事教了我一招:如果这该死的程序能够工作,那么就别管它。这一招他称之为基本工程原则。这是一个很有诱惑力的概念,我将其贯彻了许多年。这一原则在硬件设计中似乎还算有效,但将其用于固件设计,就将是一场灾难。我认为,“还工作得不错”就意味着项目成功的想法是部分软件危机的根源所在,然而专业人士应该明白,对一个软件发展的要求与对其功能和操作的要求同等重要。让我们共同努力,写出既漂亮又便于维护,而且能够可靠工作的程序!
评论
查看更多