Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1490093
  • 博文数量: 465
  • 博客积分: 8915
  • 博客等级: 中将
  • 技术积分: 6365
  • 用 户 组: 普通用户
  • 注册时间: 2010-07-30 15:05
文章分类

全部博文(465)

文章存档

2017年(33)

2016年(2)

2015年(4)

2014年(29)

2013年(71)

2012年(148)

2011年(178)

分类: IT业界

2011-12-23 16:53:23

让模块善始善终

在软件行业,模块化设计早已深入人心,因为通过模块化这种“分而治之”的方法能有效地降低设计的复杂度。如何获得更好的模块化设计不是这里要讨论的重点,本书只关注模块的初始化与终止化这两个关键点。

在大多的嵌入式系统中,模块的运行是从调用它的初始化函数开始的。与模块大多有初始化函数相比,忽视为模块设计终止化函数这种现象却很普遍。与运行在桌面操作系统上的软件不同的是,通常整个嵌入式设备就只有一个应用软件在运行,因此对软件启停不少会采用开关设备电源或按下重启按钮这种“粗暴的”方式来完成,久而久之大家将为模块设计终止化函数当做了多余。

首先,从完整性的角度来看,一个模块如果提供初始化函数,那么也应当设计终止化函数。其次,为每一个模块设计终止化函数,意味着提供了一种“优雅地”关闭系统的手段,进一步的内涵是,通过这种方式将为我们创造检测系统资源泄漏的时机。

让我们站在堆管理模块的角度来检查优雅终止模块所带来的好处。这里假设堆管理模块具备记录每一次内存分配所发生的位置信息这一功能。以它为例,是因为内存泄漏是嵌入式软件开发中比较让人头痛的问题。

如果一个系统中所有模块的终止行为都不经过各自的终止化函数的话,堆管理模块就无法通过它所记录的分配位置信息来了解是否存在内存泄漏问题。如果只为堆模块提供终止化函数也同样无法发现内存泄漏问题,因为其他模块可能在初始化时分配内存,且这些内存在整个软件生命周期中都需要使用。在系统终止时,如果这些内存不被各模块自行释放的话,堆管理模块无法在它的终止化函数中判断哪些内存发生了泄漏。

如果为每一个模块都设计终止化函数,就能做到更容易检测内存泄漏。如果那些在初始化函数中分配内存的模块在终止化时进行内存释放操作,且假设所有使用了动态内存的模块的终止化函数是在堆管理模块的终止化函数之前被调用的,那么当堆管理模块的终止化函数被调用时,就可以根据所记录的信息找到没有释放的内存(泄漏点)。

依此类推,其他的资源也可以采用这一方法发现泄漏。比如,在定时器管理模块的终止化函数中可以检查是否所有的定时器都已回收了。

在模块的终止化函数中检查所管理资源是否存在泄漏需要解决一个问题,即各个模块的依赖关系,以保证各资源管理模块的终止化函数是在使用它(所管理资源)的模块之后被调用的。第14章所引入的模块分层与分级的概念将有助于解决模块间的依赖关系。

本书操作系统篇中的多个章节采用了这一设计原则以防范资源泄漏,读者在阅读这些章节时将加深对这一设计原则的理解。

统计信息在好多方面将发挥作用。首先,它可被用于软件调优。软件在开发过程中不可能完全了解现场运行环境,这导致系统可能无法运行在最佳状态(性能、可使用性等)。要让系统处于最佳运行状态,必须获得软件在现场的运行环境,这需要通过一定的数据,设计必要的统计信息将有助于获得所需的现场数据。

其次,统计信息还有助于查错。打个比方,如果一个软件系统是从设备外部接收消息并对之进行处理和响应的,如果设计有进入系统消息数量的统计信息,那么当某些情形下出现负荷异常高时,通过它就可以判断负荷异常是由外部引起的还是由内部造成的。大多难以定位错误根源的软件缺陷,正是因为“蛛丝马迹”太少了,而统计信息有助于捕获它们。

统计信息通常采用为每一个统计项定义一个整型变量的形式,图13.7是一个定时器模块所设计的相关统计信息。从第2948行可以看出,statistic_t类型就是整型的typedef。第70717880行则定义了相应的统计变量。当相应的情形出现时,则对对应的统计变量进行加一操作。统计信息的显示就是将统计变量的值通过一定形式输出。由此看来,增加统计信息的成本不论从内存空间还是处理器时间上其开销都是很经济的。

embedded/code/platform/common/inc/primitive.h

00029: typedef unsigned int u32_t;

00047: // for statistic

00048: typedef u32_t statistic_t;

00049:

embedded/code/platform/timer/v3/src/timer.c

00068: typedef struct {

00069:     dll_t dll_;

00070:     statistic_t hit_;

00071:     statistic_t redo_;

00072:     csize_t reentrance_;

00073:     csize_t level_;

00074: } bucket_t;

00075:

00076: typedef struct {

00077:     statistic_t notimer_;

00078:     statistic_t traversed_;

00079:     statistic_t abnormal_;

00080: } timer_statistic_t;

00081:

00082: static timer_statistic_t g_statistic;

运用这一设计原则,需要我们在设计过程中时刻思考哪些信息对软件查错和调优有帮助。统计项的设计并不要求一步到位,可以在软件生命周期的任何阶段根据需要而增加。

程序代码是设计的物质外壳,再好的思想必须最终通过代码去表达,而这就离不开对函数、变量和参数进行恰当的命名,以便准确地传达设计意图。让我们通过一个例子来说明命名对设计的重要性。

假设需要设计一个双向链表的操作函数,其功能是删除链表头结点并将之当做函数的返回值返回,那如何给这一函数取名呢?

dll_get_head()[1]这个命名可能会在读者的脑海中浮现,但这个命名并不好,因为“get”所表达的是“取”而没有“删除”的意思。当他人读到对dll_get_head()函数的调用时,将理解为“只是引用头结点但并不将它从链表中删除”,这显然没有精确地传达设计本意。一个没有传达设计本意的命名是注定要让人困惑的。

dll_extract_head()呢?同样不好,因为“extract”也不能表达从链表中删除结点的意思。用过WinZip英文版的读者或许注意到了,其中解压文件就用了“extract”这个词。

比较合适的命名是dll_pop_head()。“pop”这一动词源于退栈操作,当从栈中“弹出”一个元素时,意味着这一元素将从栈中被删除并返回。将“pop”引入双向链表的函数名中能准确地传达设计意图。

除了函数名的命名很重要外,函数参数和变量的命名同样重要,因为它们能起到“点睛”的作用。对于图13.8中的两个函数,从参数名就能完全明白如何用,任何解释用法的注释都显得多余。

embedded/code/platform/common/inc/dll.h

void dll_insert_before (dll_t *_p_dll, dll_node_t *_p_ref,

    dll_node_t *_p_inserted);

void dll_insert_after (dll_t *_p_dll, dll_node_t *_p_ref,

    dll_node_t *_p_inserted);

软件设计的最终产物不能是一堆难读的代码;相反,代码应当努力做到让人读起来“行云流水”。好的设计在看完它的接口函数和数据结构后就知道如何使用它,因为它们的命名向人传达了模块的行为。从这一点说来,花时间斟酌命名是值得的,因为它节省了他人用于理解的时间。

在不少资料中强调注释对于编码的重要性,甚至提出程序应有三分之一的篇幅是注释。对于“三分之一”这一提法,作者并不赞同。原因是:

n  我们在读程序时的第一反应是读代码而不是注释。如果代码能清楚地表达意思,那就没有写注释的必要,即使写了那也一定是多余。如果注释占了整个项目源程序的三分之一,作者怀疑其中很多都是废话,只是为了做到“占三分之一”而已。

n  注释与代码很容易在维护的过程中失步,因此会出现注释所发出的声音与源代码实现完全不同这种尴尬。一旦注释被发现不精确它就会被人遗忘,也就起不到注释应有的效果。

与“三分之一”提法不同的是,作者认为注释应当尽可能少,并将写注释所省下来的时间用于推敲命名。请注意,千万不要误认为“少注释是好程序的充分条件”,而应当理解为“少注释是好程序的必要条件”。

诚然,这并不是在否定注释。大部分情形下,通过命名就能清楚地表达程序实现的局部思想,而注释应当放眼于全局去写以起到提纲挈领的作用,或者某些行为打破了常规(比如,switch语句块内的casebreak不成对)也可以考虑通过注释加以解释。如果命名实在无法做到“传神”或打破了常识的话,也可以考虑采用少量的注释进行弥补。

人天生就是审美家,软件工程师在进行软件设计时这种天性会自然地发挥作用。将设计感觉当做是一个设计原则,多少让人觉得有点不靠谱,但这一原则也正体现了软件设计中的艺术成分。

就作者的经验来看,如果做设计时觉得别扭,那工作效率一定不高;反之,则工作效率奇高。软件设计真正花时间的是思考,而不是编码。思考的目的是从纷繁的现象中试图找到问题的本质,或者从众多的因素中找出关键。设计时之所以出现“审美告警”,一定是有什么没有考虑清楚。这种情形下停下来做进一步的思考,将有助于理清思路,以最终获得更好的设计。

相信每个软件工程师或多或少都能感觉到“审美告警”,而信号的强弱与软件工程师的设计水平可能是正相关的。软件工程师如果重视这种信号,则这种信号的灵敏度也会慢慢提高。因为重视它意味着将进行更多的思考,而思考多了就更容易形成自己的设计原则和思想;相反,如果长期忽视它的存在,则最终可能会造成这种信号的消失。忽视“审美告警”的存在,或许意味着我们并不关心所设计的主题,其质量也别指望好到哪儿去,更有甚者会酝酿出将来的一个“毒瘤”。

 

 

本文选自《专业嵌入式软件开发——全面走向高质高效编程(DVD光盘1)》一书 

图书详细信息:http://blog.chinaunix.net/space.php?uid=13164110&do=blog&id=3046048


[1] 函数名中的dllDouble-Linked List,即双向链表的简写。

阅读(502) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~