随着实时软件在可靠性和安全性要求极高的环境和系统中的广泛使用,对软件可靠性的依赖正在以前所未有的速度增长,实时软件的可靠性设计与保证在实时系统中占据着越来越重要的位置。可靠性是实时软件的一个重要指标。
通常,将强实时系统定义为具有严格时限且紧急重要的系统,并当作关键任务来处理。可采用静态分析、保留资源及冗余配置的方法,使关键任务的时限能得到满足。但是,实时软件在故障表现、失效机理、唯一性及复杂性等方面与常规软件有着本质的差别,且与实时系统平台相关。实时软件的可靠性设计有别于传统软件。在实时软件的设计过程中,常采用相应的技术和工具,进行避错设计。避错是提高实时软件可靠性的基本方法,但只能达到一定的限度。要想进一步提高可靠性,一般要在避错设计的基础上进行容错设计。
一、避错设计
避错设计是传统的可靠性设计技术,在长期的可靠性实践中产生了大量的理论并积累了丰富的经验,是实现实时软件高可靠性目标的有力保证。它在充分应用软件工程技术、方法、工具和加强软件工程管理的基础上,针对实时软件的具体特点,采用形式化设计、抗干扰设计、软硬件相结合等技术和方法。此外,彻底的软件测试是保证实时软件可靠性的重要手段。
1.实时操作系统与BIOS设计
实时操作系统和BIOS的设计,不仅是保证系统响应及时性及实时任务处理等的根本,而且是设计实时应用软件的基础。
(1)实时操作系统设计
实时操作系统不仅要覆盖系统的常规功能,还必须具有任务或时间的实时处理能力,要求能支持半导体盘、Watchdog和XIP程序的上电直接运行等。实时操作系统设计的基本目标是,提高系统引导速度、运行快速性和代码可靠性。
为提高系统和代码的运行速度,一般避免使用可靠性较低的软硬驱设备,将重复执行的代码放置在可靠性较高且不易丢失数据的固态存储器中。这样不仅提高了系统的引导速度,还提高了可靠性。而且,ROM盘可以以I/O或局部映射的方式访问,不占用实模式主存空间,并可在保护模式下对其进行访问。
XIP方式的提出,是建立在程序运行的高可靠性和新的存储结构要求的基础上,操作系统应是XIP格式的。目前,一些XIP格式的实时操作系统已开发成功,它们具有精简的内核和OM的可配置性。选择实时操作系统时,可根据要求,使用相应的配置工具,即可获得合适的配置结果。例如,为保证兼容性和系统配置的简洁性,可将一个XIP DOS配置到64KB之内,仅包括一个实时运行内核和最小化的命令解释器。
对可靠性要求更高的实时系统,常采用容错操作系统。它不仅在软件结构上保证了可靠性,而且对容错计算管理层中的错误检测与诊断、系统重组与降级、错误恢复与重构,以及软件故障检测、限制与恢复等提供支持,从而进一步提高了可靠性。
(2)BIOS设计
由于BIOS直接与硬件挂接,因此其设计直接影响系统的性能。通常,为保证BIOS的功能及系统运行的可靠性,BIOS的设计应遵循简洁性、可裁剪性、可调试性、可调整性和独立性,以及可靠性和可维护性等原则。遵照这些原则,首先,BIOS在软件结构上采用模块化设计思想并在建立一个可覆盖所有应用的功能模块集的基础上,建立模块的筛选层;其次,在BIOS代码中,为各个阶段可能的软硬件调试与维护预留适当的调试出口、寄存器状态监视、嵌入式dbug等,为调试提供方便。当然,这些功能也应是可筛选的。此外,在确保BIOS代码覆盖系统基本硬件资源和确保BIOS代码集基本配置的基础上,BIOS代码应尽量小,以便在内存的F段节省出一定的空间,以备特殊用途。
为了能从根本上保证程序指令的高可靠执行,实时软件已逐步从加载运行的重定位*.ee格式,转向就地运行的预定位*.bin格式。因此,不论实时操作系统,还是BIOS,都应该是XI格式的。它也是程序固化和就地执行的前提。当然,用高级语言、或用高级语言与汇编语言混合编制实时应用软件时,代码与数据的分离和定位是XIP格式的关键。
2.实时应用软件设计
目前,软件开发有很多有效的形式化方法。它们针对不同的软件系统、管理模式和开发使用环境发挥不同的作用。其中,基于生命周期模型的开发方法对软件工程管理和提高软件产品可靠性最有效。它遵循软件生命周期的划分,明确规定每个阶段的任务。在软件开发中,按照工程管理的原则清晰地划分软件的时间和任务阶段,严格按要求完成每个阶段的任务并产生该阶段文档,同时对相应阶段进行评审或测试,进而保证软件产品的可靠性。在目前尚无更有效的软件工程方法和开发模型的情况下,严格遵循软件工程原理,按照软件生命周期模型进行软件工程设计,是保证实时软件可靠性的基础。
(1)健壮性设计
实时软件仅有正确性远远不够,还必须具有一定的防止错误输入的能力,在发生故障时应能有效地控制事故的蔓延,并进行报警输出处理,使之具有较强的健壮性。
提高软件健壮性的措施有:
这些措施虽然明显地提高了软件的健壮性,但没有从根本上解决问题,且工作量极大。在常规软件设计中,数据结构与其操作分离,使它们之间存在着潜在的不一致性,不利于改进软件健壮性。面向对象程序设计把数据结构与其操作封装在一个对象中,不允许其它类直接访问它的数据,改变了传统的数据访问方式,从而彻底消除了潜在的不一致性,提高了软件健壮性。
(2)抗干扰设计
实时软件一般是嵌入式软件,其可靠性常常受到嵌入环境和外部干扰的制约。因此,进行软件抗干扰设计势在必行。容错设计、冗余设计、抽象复算、指令复机、纠错编码、设备重复、自动诊断、自动重组、自动修复系统等技术都是有效的抗干扰设计方法。
实时软件因受干扰而使程序"跑飞"或"死锁"时,可重新启动,并初始化。程序的限界运行也是处理程序因干扰而"跑飞"的有效方法。程序运行的时间监视是处理因干扰导致"死锁"的一种有效方法。限时运行方法常用于已知子程序或程序功能块运行时间的情况。软件陷阱是在程序中的适当地方加入陷阱入口/出口语句,当因干扰而发生程序"跑飞"时,就可能落入预设的陷阱。陷阱的出口由设计人员预先设定,这样程序运行就进入可控阶段。有时,实时软件的数据采集会因环境的电磁等干扰而使所采集的数据中含有干扰成分。为此,可在实时软件中植入数字滤波器,对数据进行平滑处理,以提高数据精度。
此外,软件工具的应用也是实时软件可靠性的重要保证。目前,在软件需求分析、软件设计、软件测试、正确性证明、软件验收、软件维护及软件工程管理等各个阶段都有相关的工具予以支持。软件工具根据任务需求,严格按既定的标准和规范工作。在软件开发中,应尽可能使用优秀的软件开发工具。同时,对软件的部分功能和性能测试,软件工具也是极其有效、甚至必不可少的。
在软件生命周期的各个阶段、尤其在需求分析和软件设计等重要阶段进行严格的评审和测试,是发现错误、提高可靠性的有效办法。此外,软件可靠性问题不仅来自于软件设计,更大程度上来自于无约束的随意修改。因此,在实时软件的可靠性设计中,要严格技术状态管理,建立软件修改报告制度,按规定履行更改手续,保持软件技术状态的一致性、可操作性和可检查性。
3.重入和并发
并行环境下的重入程序设计比单纯的递归调用更严格。如果设计不当,轻则产生数据计算错误,重则引起系统死锁,而且对时间敏感。但是,调试过程不一定能发现问题。
对可重入程序设计,可通过以下方法来改进其可靠性:
并行是指程序执行的时间与其它程序有重叠现象,即多重任务与多进程并发运行。在并行处理中,公共资源保护是并发程序设计的重点。
4.结构冲突与回溯
实时软件的输入和输出数据之间可能很少、甚至没有结构上的对应关系,从而导致结构冲突。基本的解决办法是将冲突部分相互隔离开来,建立多个程序结构,再利用中间文件把程序结构联系起来,构成整体。Jackson法是目前较好的解决办法。此外,中间文件法、多道管理程序控制法、程序转换法和伙伴程序设计方法等也都是较好的办法。
在实时软件设计中所选用的循环和选择结构,在条件测试时,往往需要进行回溯。这样不仅影响其实时性,还可能导致软件的不可靠。软件设计中的回溯一般可用下列步骤解决:
二、容错设计
完全或部分消除软件系统、尤其是软件故障后果特别严重的系统(如宇航系统等)的故障,是软件容错设计的基本目标。采用软件容错的目的在于,采取更积极的措施来降低因软件错误而造成的不良影响。实现软件容错的基本活动包括故障检测、损坏估计、故障恢复和缺陷处理。由于容错设计既容忍错误的存在,又能极大地提高可靠性,因此一开始就受到了实时软件、尤其是高可靠性要求的实时软件的青睐。软件容错自70年代受到重视以来,众多的研究基本沿袭了硬件容错的思想。目前,软件容错主要有以下两种方法:
1.N版本程序设计
N版本程序设计是一种提高软件可靠性的屏蔽冗余技术,是由不同设计人员用不同算法与编程语言来实现的具有较高独立性但功能相同的软件。在每个程序中设置一个或多个交*检测点,这N个软件在几台耦合疏松的计算机中分别运行,每执行到一个交*检测点时便产生一个比较向量,并将比较向量传递给表决器,由表决器按多数表决或其它约定的策略做出输出决策。这种N版本程序设计方法于1972年由Elmendorf提出。随后,Arizenis和Chen分别于197年和1978年将其投入实际应用。
表决器是N版本程序结构的关键。由于表决程序规模较小,程序结构也不复杂,因此可以设计得很可靠,并可用程序正确性证明技术来证明其正确性。值得注意的是,表决器不仅能完成简单的表决功能,还必须允许因计算机字长等限制所造成的、不同程序的计算误差,并能在给定的误差范围内进行表决。表决器通常还要进行故障记录,这些记录将作为软件维护的依据。此外,表决器还应知道N个程序之间最大的运行时间差d,多数程序已输出结果后,表决器等待d时间后,如果还没有检测到输出,即判定该程序发生了差错。
根据多数表决的原理,N版本程序结构至少可以容忍(N-1)/2个程序中任意方式的软件故障。如果每个程序中的故障不会引起系统失效,则它可容忍多达N-2个故障。因此,N版本程序设计能有效地改善软件系统的可靠性。
小型程序通常没必要采用N版本程序结构。对于较大的程序,这种结构(即使N=3)对内存空间和执行时间的要求往往会超过用户的承受能力,从而限制了N版本程序结构的应用。但是,N版本程序设计技术是保证软件可靠性的最强有力的方法,对超高可靠性要求的实时系统是必要的。
N版本程序结构最好在多处理器系统上应用,N个程序可分配在不同的处理器上运行,使软硬件故障都能被容忍。N版本程序也可在同一处理器上先后执行,由于每个程序对硬件的使用部分或使用次序不同,一个硬件故障将在程序中表现为不同的差错或不表现差错。因此,即使在同一处理器上运行,N版本程序除能改善软件的可靠性外,还能容忍一定的硬件故障。
2.恢复块设计
同硬件容错设计中的动态冗余技术类似,以一个静态冗余的N版本程序结构为核心,再用S个程序作后备,随时准备替换N版本程序中出现差错的程序,这样就构成了一个混合的动态冗余系统。但是,如果N版本程序结构退化到一个极端的情况,就可以得到一个特别有意义的动态冗余结构——恢复块结构。
程序的执行过程可以看成由一系列操作所构成,这些操作又可由更小的操作构成。恢复块设计就是选择一组操作作为容错设计单元,从而把普通的程序块变成恢复块。被选择用来构造恢复块的程序块可以是模块、子程序、程序段或过程等。一个恢复块含有若干个功能相同、设计有异的程序块,每一时刻有一个程序块处于运行状态。一旦出现故障,则以备用块加以替换,从而构成动态冗余。这种方法可在一台计算机上使用。
恢复块结构的可靠度极大地依赖于软件故障的恢复覆盖率,过低的恢复覆盖率将使恢复块结构失去意义。恢复块结构能较好地改善软件系统的可靠性。
恢复块结构中的恢复块可以不必存在内存中,并且一般情况下只运行一遍。因此,恢复块系统中的时间和空间要求很容意被接受。这就使恢复块系统有可能得到广泛应用。但是,如果覆盖率过低,则它对系统的可靠性几乎没有改进。
实时软件的可靠性设计是软件可靠性工程的重要内容。实时软件的避错设计是可靠性设计的根本。除本文给出的设计方法和保证技术外,形式化的实时软件设计、故障/安全性设计及软硬件相结合的可靠性设计都是非常有效的方法。
而容错设计是对避错设计的补充和提高。容错技术包括软件容错和硬件容错,由于时间约束的关系,一般采用硬件容错,主要是硬件冗余。容错技术的执行过程可分为分析错误、定位错误和错误恢复,其关键是要实时地分析错误。定位错误是个弹性很大的过程。对于实时性不太强的系统,可定位到很小的粒度(如插件、芯片等);对于强实时系统,需要及时进行处理,因此来不及精确定位,一般定位到机器一级。系统恢复的困难在于,如何使备份机快速投入运行,使任务执行不中断。因此,对不同软件的容错设计应区别对待。
著作权声明:本文由www.Walzer.cn原创,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!
一、质量的相对概念
1、多数比较上进的程序员,都希望自己的代码作品是优雅的、高质量的、别人看到能赞赏不已的。但事实上,紧迫的进度压力使程序员没有太多时间思考,匆忙赶出功能后,赶快测试发布赶快交付给客户。因此有人提出需要重构,有人提出各种测试方法,计算“每千行代码缺陷率”,以追求“零缺陷”为目标。总之多数技术人员认为“质量越高越好”。这里有个典型例子《养成重构的习惯有多重要》,原文和后面的回帖都很有代表性。
2、现在我们假设一种场景,筷子的质量。
首先你到了五星级酒店,它的筷子必须是如象牙般优雅,笔直而对称,没有任何瑕疵斑点,有合适的重量手感,等等,也就是说五星级酒店对筷子的质量要求是很高的,否则客户会发飚。
然后你到了一家路边的快餐店,顺手拿过来一双“一次性筷子”,拆开后发现毛刺很多容易扎手,甚至筷子有点弯曲,但你还是凑合着用了,或者实在无法忍受就扔掉再拿一把,因为这是在路边快餐店,用户对筷子的要求是低的。
如果你把快餐店的筷子卖到酒店里会发生什么情况?质量太低客人无法接受。如果五星级酒店的筷子卖到快餐店会发生什么情况?用户不需要那么好,也不愿意付那么多钱。所以同样做一个筷子,却对质量有不同要求。
所以说:质量是相对的。
3、基于第2点,所以一味追求“高质量代码”,把“高质量目标”凌驾于“企业赢利目标”之上,是多数技术人员所犯的错误。
二、对质量目标进行逐级分解和控制
多数成熟度不高的软件公司会有一定的质量控制方法,但将其用于所有的项目和所有的软件层面。我认为这是一种资源浪费。适度降低对外围层次、用户需求弱相关、使用频度低模块的质量控制,会给项目带来进度和成本上的收益。
比如下面这个案例。这是一个比较成功的网游公司中,项目代码分层控制情况
层次功能 | 质量要求 | 开发者 | 编程语言 | 代码开放度 | |
5 | 任务脚本、战斗剧情、数值设定等外围脚本 | 逻辑正常,跑起来不会导致程序崩溃就行。手工测试 | 项目策划组的非编程专业人员,简单培训后即可编写 | 自定义的、类似python的低难度脚本 | 所有人可见 |
4 | 项目外围代码 | 详细设计由主程评审,代码未评审。手工测试 | 项目程序组的非主力人员,多数是普通大学本科毕业生 | 某种业内标准的脚本语言(商业原因不便透露) | 所有人可见 |
3 | 跨项目使用的游戏主要逻辑,核心代码。如好友系统、聊天系统、帮派系统等 | 详细设计由技术总监亲自评审,部份模块有做单元测试. | 项目程序组的主力人员,主要由重点大学本科生构成 | 同上,业内标准的脚本语言 | 所有人可见 |
2 | 跨项目使用的开源游戏引擎,研究/优化/集成/针对特殊需求进行二次开发 | 技术总监亲自带队,测试方法未知 | 研究部成员。基本都是211大学硕士或海归,计算机或数学专业 | C/C++ | 研究部成员及工程项目的经理、程序经理、测试经理可见。其余人员不可见 |
1 | 上面3/4层所用的脚本解释器、服务器分布式框架等 | 技术总监亲自开发和测试。测试方法未知 | 技术总监和几个创业元老,清一色海归 | C/C++ | 仅技术总监和公司股东/创业元老,及几个项目经理可见 |
大家特别注意每个层次的质量要求,从“不使程序崩溃、逻辑正确不使程序崩溃即可”,到“技术总监亲自开发测试,不许别人碰里面代码”分级管理,越是核心部分对代码质量要求越高,从开发人员的级别/背景/资历/审核人员级别/测试方法上可以体现出来。而4和5两层比较外围的代码, 只要实现功能就可以了, 我阅读了这些代码并在其中开发过一小段时间,里面到处充斥着“坏味道”的代码,程序员都是边改边骂,但这并不影响这个游戏有60万的活跃用户和300万以上的注册用户,给公司带来强劲的现金流。而这套对质量进行分级控制的方法,则是技术总监传授讲解给我的。
(表格中代码开放度仅供参考,小公司是输不起的,看看pudn.com上那些把老东家代码拿出来开源的人渣就知道了)
大家知道项目的时间-质量-成本铁三角, 如果把上面5层代码的铁三角列个表格出来,大致如此(我们假设在每个软件层面投入的成本是一致的)
层次 | 质量 | 进度 | 成本 |
5 | 5 | 1 | 3 |
4 | 4 | 2 | 3 |
3 | 3 | 3 | 3 |
2 | 2 | 4 | 3 |
1 | 1 | 5 | 3 |
平均 | 3 | 3 | 3 |
越是核心层(1层), 其需要修改的代码越少,但是对代码执行的时间和空间开销越小, 稳定性要求越高. 越外围的代码(5层), 针对需求而开发和改动的代码量越大. 选取上表中的1层、5层、平均画图来表示是这样的:
因此,精益求精、重构只适用于靠近核心的代码层;而对于外围代码层, 由于赶工而导致代码质量低、放松测试条件,则是完全合理的。
三、结论
所以,在做软件工程的质量控制时,应该把握软件的关键层面,抓住质量控制的瓶颈。横向而言,就是开发框架、引擎、核心功能之类的层级;纵向而言,就是用户使用频率最高的模块、和竞争对手做差异化竞争的功能等。对于外围代码和次要模块代码,前者一般不容易出错得太离谱(被开发框架限制住),后者使用频度低,则可以适当牺牲质量以求开发速度。
因此,处于外围代码开发的兄弟们就不要成天抱怨、不要提出各种重构要求了。我也曾经在“坏味道”的代码,确切地说是“粪坑”中扑腾,深知其中感受。但就像魔兽世界里组队打某些副本BOSS,有的人职责是拉住BOSS的仇恨(拉住客户),有的人职责是砍BOSS(解决核心模块),有的人则需要群杀不断刷出来的小怪(快速开发外围逻辑)。如果不是这样的配合,那就会团灭;如果不是这样的配合,下个月的工资可能就发不出来,不是吗?
这个是比较早, 09年4月份的事情了。整理文档翻出来,觉得还有点意思.
当时CLIENT-SERVER的通讯封包格式有两种方案
a. 以7E为开头和结尾, PAYLOAD中所有7E的字节, 都在其后扩展一个BYTE, 写为7E, 7D, (称为转义). 封包中不带CHECKSUM, CRC等校验用的字段
b. 以7E为开头和结尾, 带一个CHECKSUM字段, PAYLOAD中不进行7E->7E 7D的转义.
几个同事就这个通信封包格式, 采用方案一或方案二, 开会激烈讨论了个把小时。
我原先反对转义方案的出发点比较模糊, 只是觉得原先转义的方案"不优雅"; 后来才想清楚了不优雅的"本质"在哪里.
所有的代码, 可以在抽象意义上分作两大块, 两者的着重点是不同的.
(1) 正常运行的代码. 首要追求高效性,
这个"高效性"如果从逻辑的角度来解释, 那么一方面是"高效"地对正确的数据执行正确的算法(方法/策略), 另一方面是"高效"地找出异常, 然后丢给异常处理代码去处理.
(2) 处理异常的代码. 首要追求健壮性.
就是程序必须能从异常中自我恢复. 由于代码多数时间跑的是"正常"逻辑, 少数情况下才不得不处理"异常", 所以"异常"处理的代码中, 首要任务是健壮, 跑不死, 而高效性则是次要的.
那么回到转义的策略上来看,原先的7E -> 7E 7D, 使得装包和拆包的时候, 时间上都必须挨字节扫描过去, 空间上必须另开一块内存, 这些"不优雅"的工作是为了应对网络传输时包数据丢失. 包数据丢失是一个异常情况,而转义策略本质上就是不论好包坏包,一棍子打死, 统统要经过转义算法. 用上面的观点解释, 即"为了异常情况下的健壮性,牺牲了正常情况下的高效性".
而用Header + Length + CheckSum + Payload + Tailer的做法, 逻辑上是这样的
这是在上面"正常->高效性 & 异常->健壮性"指导思想下的做法. 那么现在就剩最后一个问题, 计算checksum和转义的工作相比, 哪一个更快? 如果转义处理的效率, 比checksum更高,那么上面的假设就不成立了.
所以我做了个实验, 代码如下
上面这段代码,在SAMSUNG 2442 400MHz的CPU, WM 6.1系统上运行结果是
copy 1024 bytes * 100K times, use 11677 ms
check sum 1024 bytes *100K times, use 7504 ms
所以, 一个正确的数据包, 经过CHECKSUM计算的时间, 比其经过转义计算的时间要快得多, 仅为其64%. 这是手机上的情况, 服务器上的百分比不太清楚是什么样,但至少有一点是肯定的,就是用CHECKSUM的方案比用转义的方案,在正常逻辑情况下速度更快、内存开销更少。当服务器同时处理十万数量级网络数据包的时候, 性能提升还是比较可观的。
这篇文章的重点不在于哪个方案更严谨,或者上面的逻辑对不对,而是在于这么一个思想:
(1) 正常运行的代码. 首要追求高效性,
(2) 处理异常的代码. 首要追求健壮性.