实时嵌入式软件开发的25个常见错误(四)

 

#8 第一个正确答案不是唯一的答案

没有经验的程序员特别容易认为他们得到第一个正确答案是唯一的答案。开发嵌入式系统软件经常会让人感到灰心。花了数天才搞明白怎样设置寄存器以让硬件按照自己的意愿工作。在某个时候,噢!它终于能跑了。一旦跑起来了,程序员就会删去所有调试代码,然后将这些代码作为好代码合入模块中。永远不要再修改这些代码了,因为花了这么长时间调试,没有人愿意去破坏它。

很不幸的是,第一次的成功往往不是最好的解决方法。它的确是重要的一步,因为比起让系统跑起来,改善一个能跑的系统更为容易。人们却很少再去改进一个已经运转起来的系统,特别是这个系统看起来已经跑的不错。但是,我还要拐弯抹角的说,这可能是一个需要耗费巨大系统资源的糟糕设计,比如占用太多处理器时间或存储器,或者是如果它优先级高的话,使系统时序异常。

一个优秀的设计,通常至少有两个以上的方案。大多数情况下,最好的设计是其他方案的折中考虑的结果。如果一个开发人员只能给出唯一的解决方案,那就应该咨询其他专家以得到另外的方案。

 

#7 #include "globals.h"

一个包含所有系统常量,变量定义,数据类型定义,和/或函数原型的头文件是代码不能复用的信号。在代码走读中,只需要花五秒钟时间看看是否有这样的文件存在就能知道代码能否复用(请看错误 #25 复用代码不是为复用而设计)。立即发现这个问题的诀窍就是看看是否有一个头文件的存在,通常叫 global.h,但有的也叫 project.h,defines.h,和 prototype.h。这些文件包含所有的数据类型,变量,#define 定义,函数原型,以及其他的应用需要的头信息。

程序员说这样会使他们的生活更为轻松,因为在每一个模块中,他们所需要做的就是在他们的 .c 文件中只包含一个 .h 文件。很不幸的是,这个偷懒的代价就是显著增加开发和维护的时间,同时也因为高度耦合(请看 #18)而使得不能在其他项目中复用这里的任何代码。

正确的方法是遵守严谨的模块化规范。任何模块都分为两个文件, .c 和 .h 文件。.h文件中仅包含模块为外部提供的接口信息,而 .c 文件中包含模块的其他所有内部信息。关于强制的模块化规范的更详细信息在后面的错误 #2 无命名和风格规范中。

 

#6 编码完成后写文档

每个人都知道大多数情况下系统文档是隐晦难懂的。很多组织都尽力使任何东西都文档化,但是文档并不是在正确的时间内完成。问题就是文档经常是在代码写完后补上的。

文档必须在编码之前或者之中完成,而永远不能在之后。在编码开始之前,应该先进行详细设计并写文档。这将成为以后用户和系统文档的基础。完全按照文档来编码。发现文档中任何不明确的地方,应该先纠正文档。这不仅保证了文档及时更新,还保证了程序员按照文档来编码。

在编码过程中更新文档就象代码走读。通常,程序员在写代码的过程中发现他们的BUG。比如,“如果成功,函数返回1”。程序员就会想,如果不成功话,返回什么呢?他们就会去看他们的代码,然后就发现不成功情况没有正确处理。

 

 

 #5 无代码走读

很多程序员,包括初学者和专家,保护他们的代码就像发明家保护他们的专利思想一样。很不幸的是这种情况破坏了应用的健壮性。通常,这些程序员知道他们的代码凌乱,所以他们害怕别人看见这些代码并发表意见。因此,他们就象人们不让父母看到自己凌乱的房间一样藏起他们的代码。

为保证软件的健壮性,正式的代码走读(也叫软件检视)必不可少。代码走读应该定期进行,并针对所有合入系统中的代码。正式的代码走读应该由多个人来执行,在纸上做下记录并进行跟踪。软件工程研究表明,一天代码走读发现的BUG超过一个星期的调试。

程序员也应该养成自己回看代码的习惯。很多程序员在计算机上敲进代码,然后运行,看看有什么问题,如果出错,就开始调试它,甚至没有任何纸面的跟踪记录。花一天时间人工跟踪代码能节省数天甚至是数星期的痛苦调试。

 

#4 不分青红皂白的使用中断

中断可能是实时系统中优先级倒置的最大原因,使得系统不能满足它的时序要求。原因是中断会抢占其他所有的一切,并且时间是不确定的。如果它抢占一个定期发生的事件,就可能产生不可预料的行为。在一个完美的实时系统中,不应该有中断的存在。

很多程序员将百分之八九十的处理放到中断服务程序中。完整的I/O命令处理和周期循环体是中断服务程序中常见的东西。程序员认为中断服务程序可以减轻操作系统的负荷,使系统运行的更好。当然这样相对报文切换来讲是可以减轻系统的负荷,但是基于下面两个主要理由系统并没有必要运行的更好:

Ÿ   时间不能调度以提供执行保证,因为可能产生rate-monotonic或者earliest-deadline-first等实时调度算法问题。

Ÿ   中断服务程序只能通过全局变量来交换数据。下一个错误会解释这为什么不好。

   相反,尽可能使每个线程周期执行,避免不定期事件。如果使用一个采用固定优先级调度算法的实时操作系统,就使用rate-monotonic算法为每一个线程安排优先级。如果使用一个采用动态调度的实时操作系统,就使用earlist-deadline-first 算法。如果没有实时操作系统,可以使用[4]中描述的非抢占的earlist-deadline-first 算法。

 

#3 使用全局变量!

全局变量经常遭到软件工程师的反对,因为它与面向对象设计的封装原则背道而驰,使软件更难以维护。把这个原则放到实时软件开发中,在系统中避免使用全局变量就更为重要。

在大多数实时操作系统中,进程为线程或者是轻量的进程。进程共享相同的地址空间以减少执行系统调用和报文切换时的负荷。同时带来的负面影响就是一个全局变量将自动为所有进程共享。因此,使用同一个模块内定义的一个全局变量的两个进程就要共享同一个值。这样的冲突会破坏功能,而不仅仅是软件的可维护性。

很多实时软件程序员把这个作为一个使用相同存储器的好方法。但是,在这种情况下,就要小心保证互斥操作以防止竞争而产生的难以预料的问题。不幸的是,很多防止竞争的结构,如信号量,并没有很好的实时性,并且会产生难以预料的阻塞。同时,象优先级反转协议(the priority ceiling protocol),明显使用过度。

互斥和竞争在任何操作系统的课本中都有讲到。

 

#2 无命名和风格规范

在非实时系统开发中,这个错误应该放在 #1。

没有命名和风格规范下开发软件,就象盖房子没有任何建筑图纸。没有规范,组织中每个程序员都按照他们自己的喜爱行事。当其他人要读这些代码时问题就出来了(如果项目中正确的按照错误“#5 无代码走读”中所说的做代码走读,问题就立刻出来了,而不会等到后来)。举个例子,设想两个不同的程序员开发同一个模块。第一个程序员花了一个小时能理解并校正代码,让另一个程序员来做就需要一天,也就是说要增加800%的人力资源。

影响代码可读性的主要因素就是命名规范。如果遵守严谨的命名规范,看到一个名字就知道它的意义,在哪里定义,它是一个变量,常量,宏,函数,数据类型,还是其他的声明。这个规范必须写出来,就象设计图纸上的图例,以让任何读这个代码的人都知道这个规范。

下面引用了Maryland大学实时系统软件工程实验室(SERTS)强制执行的命名规范。那些花很少时间学习了这些规范的研究人员都很欣赏他们写出来的代码的可读性,特别是在他们读过其他没有任何编程规范的人写出来的代码后。一个组织是喜欢这套规范还是他们自己的规范都不要紧,重要的是能给出一个选择这套命名规范的好理由,能够把这套规范写出来并发给所有的开发人员,并能严格遵守。

SERTS 命名规范

要想让软件的可维护性在一个很高的水平,仅仅开发出一个个模块是不够的。维护软件的一个最大成本就是花时间去理解原来的程序员在代码中的思路。想降低这个成本,严谨的风格和命名规范就必须在整个组织中得到严格的执行。这里就给出了一套已经被证明是很不错的命名规范。

一个组织应该坚持让所有的开发人员都使用一套命名规范。代码走读的一部分工作就要检查规范的执行情况。如果有必要,一个公司可以对没有遵守这套命名规范的开发人员采取经济制裁。这看起来似乎很可笑,但是如果想到因为开发人员没有遵守规范而在下一年花费公司50000美元时就不会这样认为了。如果开发人员宁愿使用自己的规范,那就不幸了。就像艺术家必须严格遵守指导书以使自己的设计能够得到建筑审查人员的批准一样,一个软件工程师必须严格遵守公司建立的规范以使他们的程序得到QA部门的批准。

衡量软件可维护性的基本标准如下:

Ÿ   如果客户投诉软件错误,多快能够找到这个错误?

Ÿ   如果客户要求增加一个新功能,多快能够完成?

Ÿ   一旦错误定位,需要修改多少行代码?

   显然,上述问题很大程度上依赖实际应用的情况和问题的难度。但是,如果把两段具有相同功能,需要相同修改的代码放在一起,哪段程序遵守的规范能够让工作更快的完成?这就是衡量软件可维护性的主要准则,不仅仅是比较设计,还应该比较风格和规范。

   命名规范尤其重要。软件的可维护性与使用的命名规范直接相关。看到任何名字,就可能立刻区分出是常量,变量,宏,还是函数,也知道它是局部的,全局的,内部的,还是外部的。统一采用表1中的命名规范就能够从名字中得到这些信息。

   表1:为提高C语言程序的软件可维护性采用的SERTS命名规范

Synbol

Description

xyz.c

模块“xyz”的源程序文件。

xyz.h

模块“xyz”的头文件。

任何在这个文件中定义的名字都必须有一个 xyz 或者 XYZ 的前缀,并且必须是模块为外部提供的接口。

xyz_t

模块 xyz 的主要数据类型。

在文件 xyz.h 中定义。

xyzAbcde_t

模块 xyz 的次要数据类型“Abcde”。

在文件 xyz.h 中定义。

xyzAbcde()

使用数据类型 xyz_t 中元素的函数“Abcde”。

XYZ_ABCDE

模块 XYZ 中的常量。

必须在文件 xyz.h 中定义。

XYZ_ABCDE()

模块 XYZ 中定义的宏。

必须在文件 xyz.h 中定义。

xyz_abcde

模块 xyz 为外部提供的全局变量。

必须在文件 xyz.c 中定义,并且在 xyz.h 中用 extern 声明。全局变量应该尽量避免使用。

ABCDE

_ABCDE

_ABCDE_FGH

模块内部使用的局部常量。 必须在文件 xyz.c 的最前面定义。

第三种格式允许使用多个单词,如_ABCDE_FGH。如果仅仅是“ABCDE_FGH”,就表示模块“abcde”。

abcde

局部变量。必须在函数内部定义。

结构内部的定义也使用这种方式。

_abcde

内部全局变量。必须在文件 xyz.c 的最前面用“static”定义。

_abcde()

内部函数。必须用“static”定义。原型放在文件 xyz.c 的最前面。

函数体必须在放在文件 xyz.c 的后面,并且在所有外部函数体之后。

_abcde_t

内部定义的数据类型。必须放在文件 xyz.c 的最前面。

abc_e

模块 abc 为外部提供的枚举数据类型。

每个枚举数据类型的入口必须使用相同的规范定位为常量。

 

   函数通常应该象表2所示命名,每个为外部提供的函数都存在一个意义相对的函数。成双的定义函数名有两个重要好处。它能强制开发人员保证设计的完整性,允许开发人员同时创建两个函数,并用一个函数去测试另一个。另外确信两个成双的函数具有相对的意义。比如,当采用表1中的命名规范时,就能保证 send 相对函数不是 read,create 的相对函数不是 finish。再举个例子,如果同时创建读和写两个函数,就可以先写再读来验证两个函数。另外,在不同的模块中使用相同的规范也是应该的。

2:常见的成双定义的函数名

xyzCreate <--> xyzDestroy

xyzInit <--> xyzTerm

xyzStart <--> xyzFinish

xyzOn <--> xyzOff

xyzAlloc <--> xyzFree

xyzSnd <--> xyzRcv

xyzRead <--> xyzWrite

xyzOpen <--> xyzClose

xyzStatus <--> xyzControl

xyzNext <--> xyzPrev

xyzUp <--> xyzDown

xyzStop <--> xyzGo

  

   这样来写软件,在需要分解时就能很快完成。函数命名时应该按照“从大到小”的顺序,而不是按照自然读的顺序。比如,如果模块 xyz 有一个子结构 xyzFile_t ,那么使用这个结构的函数就应该按照下面方式来命名:

   xyzFileCreate

   xyzFileDestroy

   xyzFileRead

   xyzFileWrite

   而不是:

   xyzCreateFile

   xyzDestroyFile

   xyzReadFile

   xyzWriteFile

   注意到任何函数名的最后一个单词是表示函数执行动作的动词,中间是表示动作使用的模块的名词。

   这样的规范就很明显的表示 xyzFile 是模块 xyz 的子模块,而在第二种命名中就没有这么明显。而且,如果模块 xyz 规模不断增加而设计人员决定进一步分解它,就很容易将整个子模块 xyzFile 和相应的函数分开成一个独立模块,比如叫做 xyzfile。只要全局搜索和替换 xyzFile 为 xyzfile 就完成了全部修改,几分钟内分解就完成了。如果没有按照这套规范来命名,就需要花很长的时间来把名字转换为新模块中使用的名字。

   对模块命名时使用一些意义模糊的短名字是可以接受的,因为这个名字将要作为所有命名的前缀。仅仅用意义明确的缩写作为函数名。如果没有这样的缩写,就使用全名。如果使用了缩写,就应该在整个项目中都使用。

   比如,规范中使用 xyzInit 作为模块 xyz 的初始化函数,就不要再用 xyzInitialize。另外一个例子,可以使用 snd 和 rcv,或者是 send 和 receive,但不能混用。其他常见的缩写有 intr 为 interrupt,fwd 为 forward,rev 为 reverse,sync 为 synchronization,stat 为 status,ctrl 为 control。但是,意义不明确的缩写是不推荐的,因为降低了可读性,如 trfm 也许是 transform 的缩写。这种情况下,函数名不缩写是一个更好的选择,如 xyzTransform()。另外一个过分使用缩写的例子,比较一下 xyzFileCreate() 和 xyzFileCrt()。后面一个使用了不经常用的缩写,在软件维护阶段读代码时就不太好懂了。相比不能明确表达函数的实际意思,使用一个稍稍长点的函数名更好一些。

 

#1 没有测量执行时间

很多程序员在进行实时系统的设计,但是他们对他们任何模块的代码执行时间都一无所知。举个例子,一个公司让我们定位他们系统中偶尔发生的错误。依照我们的经验,这是一个时序或者是同步上的错误。因此我们首先提了一个小小的要求,一张系统中进程和中断服务程序的清单以及相应的执行时间。这个清单对他们是很容易得到的,所以他们欣然提供给我们。但是,他们并没有测量执行时间,而仅仅由设计人员在代码执行之前估计了一下。

我们的首要任务就是测量各个进程和中断服务程序的执行时间,然后很快就定位系统发生错误的原因是因为系统负荷过重。这个公司也说:“对,我们也很清楚这个!”但是,他们听到空闲进程竟然占了20%的执行时间时非常惊奇。(如果测试所有的任务,就包括空闲任务)问题是他们所有对执行时间的估计都是错误的。甚至有一个中断服务程序按照估计是几百微秒,而实际执行时间竟达6毫秒!

当开发一个实时系统时,应该测量每一步的执行时间。这包括代码的每一行,每个循环,每个函数等等。这应该是一个不间断的过程,就象测试功能一样。在测量执行时间的同时,对照一下估计的结果。如果测量出来的时间不可思议,就要分析它,计算每一步的时间。

有些程序员等到所有的事情都做完了才开始测量时间。这样的话系统经常会出现很多时序上的问题,而仅仅测量一下时间就会为解决这些提供足够的线索。“实时系统”中的关键词就是时间

 

参考

[1] M. Hassani and D. Stewart, "A Mechanism for Communicating in Dynamically Reconfigurable Embedded Systems",?inProc. of High Assurance Systems Engineering Workshop, Washington DC., August 1997.

 

[2] B. L. Jacob, "Cache design for embedded real-time systems", Embedded Systems Conference Summer, Danvers  MA, June 1999.

 

[3] S. Shlaer and S. J. Mellor, "Recursive design of an application-independent architecture",?IEEE Software, v.14, n.1, pp. 61?2, Jan/Feb 1997.

 

[4] D. Stewart, "Designing Software Components for Real-Time Applications",?inProc. of Embedded Systems Con-ference, San Jose, CA, September 1999.

 

[5] D.B. Stewart, R.A. Volpe, and P.K. Khosla, "Design of dynamically reconfigurable real-time software using port-based objects",?IEEE Trans. on Software Engineer-ing, v.23, n.12, Dec. 1997.

 

[6] M. Steenstrup, M. Arbib, and E.G. Manes. Port Automata and the Algebra of Concurrent Processes,Journal of Computer and System Sciences, v. 27, n.1, pp. 29-50, Jan. 1983.

 

你可能感兴趣的:(实时嵌入式软件开发的25个常见错误(四))