Chinaunix首页 | 论坛 | 博客
  • 博客访问: 101418
  • 博文数量: 15
  • 博客积分: 1649
  • 博客等级: 上尉
  • 技术积分: 168
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-12 10:43
文章分类

全部博文(15)

文章存档

2011年(5)

2010年(10)

分类:

2010-11-12 23:52:15


By: shan_ghost

QUOTE:
原帖由 flyeleph 于 2008-11-7 23:32 发表
39楼 发表于 2008-11-7 23:32   
最近刚完成一个项目。项目是在老外原有的代码上增加新的功能。以前写代码时习惯了在用指针前先检查一下指针是否为空。所以在这个项目也是这样做的。
但老外直接跟我们说不要那样干,那样会隐藏你的BUG。
想想也是有道理的。从逻辑上来讲,有时候根本不允许所用的指针为空,如果用判断来知道这个指针是空的,这时又要怎么办呢?输出一句话让调试的人
知道有错误了? 他们的做法是只在新申请的内存时检查是否为空,如果为空就做些内存清理的工作,腾出些空间。在其他用指针的环境下都不检查
指针是否为空。该调用方法的就调用方法,该往内存中写的就写。如果指针是空就让程序 crash 掉。这些能及时发现问题。如果不能及时发现问题,那只
能证明所做的测试还不够。



说的好;不过从跟贴看来,还是有不少人根本就没看懂这段话的意思。


首先一点必须搞明白的就是: 为什么说检查空指针会隐藏bug?不是已经检查到指针为空并且采取措施了吗?

那些脑子一下子转不过来弯的,可说都是绊倒在这点上了。


————————————
作为死修电视的,俺(以及俺的同行们)常常会有这样的经历: 有些电视出了故障,保险管时不时被烧;检查之,发现熔了老长一段(甚至玻璃壳都有了裂纹);换个新的又没了问题——查一时又查不到原因,怎么办?

对新手来说,一般就放弃了: 换上个新的先凑合着用。反正保险管便宜,烧了就换一个呗。
甚至于,某些野蛮人敢拿根铜丝给人家连上——看起来能用了,不是吗。

后果必然是: 某天,显像管或者其他什么重要东东烧了——甚至引起火灾,把户主多年的血汗统统付之一炬都不罕见。


——————————————————————————————————
烧保险管是一个信号,这个信号明明白白告诉我们: 其他地方有致命缺陷了——击穿,打火,短路,功率管热稳定性差……
如果不把这个致命缺陷找到并修复,早晚要闹出大乱子。


——————————————————————
同样,未知的空指针也是一个信号,说明代码的其他部分有致命缺陷: 缓冲溢出,野指针,栈被破坏,逻辑异常,多线程出现了数据竞争等等等等……

换句话说,菜鸟仅仅看到空指针,有点经验的中、老鸟看到的则是致命缺陷。

不把致命缺陷排除,房子总有一天要被烧掉,程序也总有一天要崩溃。


同样的道理,对于操作系统核心或神六神七之类“高级”玩意儿,该无视这个致命缺陷勉强运行呢,还是该马上报错修正缺陷?

当一个进程试图退出、操作系统核心却发现对应的进程列表指针为空时,是死皮赖脸运行下去,把一切的一切都搅得乌七八糟然后死的很难看好呢,还是清清楚楚记录下当前内存映像提交然后蓝屏重启好?

指针用过马上置空,目的绝不是为了方便之后可以检查到并防止程序crash(或者,如楼主题目所说“避免指针在释放之后被引用”)——恰恰相反,这个做法的目的正是为了在程序引用了已释放的指针时能够立即crash掉。

高质量的系统,不是糊弄出来的。

东西做的底层、高端,不是隐瞒错误的理由——相反,对神六神七之类政治敏感的“高端”任务来说,糊弄出问题倒是请君体验新式步枪威力的好理由。

=============================================================================

By: 思一克

实际上,直接CRASH是一个追求的最高目标. 可惜由于硬件软件,体系结构等限制, 远达不到.

比如,strcpy(d, s), 如果d是一个被free(d)的地址, 系统往往也能工作. 但这里实际是一个巨大的恶劣的BUG. 无法CRASH的原因是硬件对内存的保护是按页的, 无法按字节.

有BUG就直接CRASH应用是一个目标. 不能完全达到.

我原来是修理电视的. 最好的故障是什么? 是子系统全坏掉. 最恶劣的故障是系统神经病似地故障---时断时续,时大时小,变化莫测.---- 对于软件说也一样.

-------------
实际上, 编译系统和OS配合, 故意将地址NULL(0)留出来并设置为内存保护段. 当程序使用NULL中内容时候, 让程序CRASH掉. 目的就是为了DEBUG, 而且是非常容易地DEBUG.

所以可以说, strcpy和许多库FUNCTION看似没有检查参数指针是否是NULL, 其实是系统替你检查了. 好处是, 不影响速度. 同时有可以给用户报了错误. 你想呀, strcpy是多么底层的被大量调用的, 检查那东西浪费了效率. 程序中的废话在高层地方出现对效率影响很小, 在底层出现影响很大.

BTW, strcpy的库绝大多数不是LZ那样用C编的, 而是用汇编指令(许多CPU都用专门移动,COPY串的指令). C编的效率要低许多许多.

-------------------------------------------
还有, 你编程序应该设计的结果是:
有BUG就CRASH, 但CRASH后不影响没有遭遇BUG的部分的正常运行. 实际上UNIX给了你这个机制, 就是进程.

还有,
不要将应用程序的CRASH(Segmentation fault后推出运行)理解成汽车撞树,飞机CRASH地面.

实际上, 这里发生的一切都在OS 的掌控之下,检查出问题了,让你的程序停止下来, 温柔地停下来.
有点相当与你靠驾驶执照时候, 交通警察是考官, 你犯了错误后的情况.

而撞树相当与OS没有检查,错误的程序还在继续,直到系统不动了,或错误数据产生了.DOS原来许多地方就如此.

==============================================================================

By: shan_ghost
成功的团队很多,而且往往各有各的成功之道。
但有一点品质肯定是它们所共有的: 分工明确,权责分明。

废话少说,言归正传。


昨天在70楼的帖子中论述了总体设计层面上对不同错误的不同看待和处理方式。

可能有人要怒了: 你的方法严重依赖于try catch!

是的,是的。这点我承认。但您的头脑能不能稍微灵活点呢?

比如,甭看不起COM,看看微软是怎么做的。


在下以前做的某个项目中也有类似的设计。
我们定义了若干个错误级别,如WARNING、MINOR_ERROR、MAJOR_ERROR、CRITICAL_ERROR以及FINAL_FAULT。

如何界定错误等级呢?

举例来说: 连接数据库失败,或者正在访问数据库时把网线拔了,这不是错误,连WARNING都不算。
程序应该做的,是准确识别网络返回码,通过合理流程优雅的回退当前事务——当然,要给用户事务日志里写一笔。
一旦网络故障排除,程序自然会恢复正常。

类似的,网络信号差、外接数字仪表易受干扰、被控单板机工作不稳定等等,这些都不是错误,而是必须预设逻辑正确处理的正常情况。
不同点在于: 必须根据实际情况区别对待,以不同的方式将不稳定的设备稳定利用起来。

把这种东西和程序逻辑错误混为一谈的,显然是需要多读点书了。



那么,什么样的问题才算错误呢?
假设功能模块A设计是给部长级别的人用的,经过完整的权限检查,在底层某个写库模块出了个“数据库访问权限不足”故障,这就是错误。
原因很简单: 前面的权限检查要么没发挥作用,让无权的人访问到了不该他接触的东西;要么数据库的权限设置出了问题,把本该有权限的人给挡到了外面。
当然,不恰当的重用了某些访问模块,也可能会导致类似问题——这种时候,类似问题通常完全不影响逻辑,也不会影响使用——但显然我们不能冒这个险。
此外,软件配置不当,连到了错误的数据库,也可能造成此故障(但这又说明数据库连接模块没有尽到自己的职责)。

无论如何,这种东西即使在运行时被精确识别,是不应该也没办法处理的。

很明显,这个时候就应该抛异常。
不过,基于种种原因,我们禁用了异常,设计了另一套代替机制: WARNING、MINOR_ERROR、MAJOR_ERROR、CRITICAL_ERROR以及FINAL_FAULT。

根据具体情况的不同,同样的错误可能对应到不同级别的异常:
WARNING:    程序设计有错,但只影响当前函数的此次调用,可当作失败处理;但必须写调试日志以跟踪错误。
MINOR_ERROR:程序设计错误影响到了整个子模块,此模块应停止一切活动,把这个错误直传上去,让上级模块撤销此次操作。
MAJOR_ERROR:程序设计错误影响到了中间控制模块,此模块应停止一切活动把错误直传上去,让框架撤销操作
CRITICAL_ERROR: 程序设计错误影响到了负责总控的框架,整个框架不应再作任何操作,应结束程序并重新启动服务。
FINAL_FAULT:发生了无法恢复的重大故障,应结束程序且不得重新启动(或重启动后禁止写任何数据以免造成数据无法恢复),等待专业人员修复(这个级别从未被用到过)。

假如发生了strcpy参数为空指针这样的故障,我们会在捕获到后,根据可能受影响的指针(即包括与这个指针同时声明的指针)界定影响范围(定义错误级别),凡在影响范围的模块都必须无条件停止除释放资源外的一切工作,以避免闹出无意格式化硬盘的笑话。
本质上,这就是对try...catch机制的一种模拟。

请注意一点: 即使是最低的WARNING故障,也意味着代码逻辑有问题,也是不可容忍的。
因此,在调试版里,所有故障全部被当作assert fault处理。

只有调试日志中没有任何内容时,才说明程序逻辑过关,可以推出试用了。


显然,既然可以且必须保证调试日志中没有任何内容,那么绝大部分情况下,用assert代替以上繁杂的错误级别就是完全可行的。
因为你显然不会触发到它。

举例来说,c/c++基础库里用了多少assert?
它们的strcpy会不会返回arg_error?
用着这些“不可靠”代码,为何你的系统不会崩溃?
莫非你们自己重写了全套c/c++基础库?

众所周知,linux就是用充满了assert的c基础库写的;即使频繁因为bug打补丁,为何它仍然是最稳定的服务器平台?
它的稳定,是因为用了assert还是相反?



和基于异常的系统相比,我们这个错误级别系统的缺点是:
1、无法抵抗绝对无法在运行时识别的故障(比如野指针)——这个缺点凡非基于异常的系统都有;基于异常的系统同样也有无法处理的情况,比如指针越界已经彻底捣毁了栈结构。
   但是,只要不是糊涂蛋,就不会看不到不同策略下整体所能达到的稳定程度(比如基于异常的系统可以恢复野指针导致的轻度崩溃,其他方式则都难以达到同样的高度),而是总拿些特殊问题来胡搅蛮缠。
2、使用繁复,难以推广


另一方面,假设纳入本系统管理的故障有M个,无法纳入管理的故障有N个;
假设第一次测试发现了110个故障,其中100个属于M,10个属于N;
反复测试,直到软件质量稳定,设新发现的故障有50个属于M,5个属于N。
那么,可以认定: 无法纳入管理的bug占可以纳入管理的bug数目的10%。
根据测试理论,可以通过历次测试的bug曲线估算出项目中仍然存在的bug数(比如,10个);其中约10%的bug无法控制——这点任何人都无能为力,只能通过测试解决。
当然,这1个bug确实有极小的导致崩溃的可能。

这种系统的可靠性会如何呢?大家可以想象一下。

结论: 错误确实无可避免,但至少我们可以界定模块的质量,测出它的可靠性,然后采取种种措施保证总体的稳定与可靠。


后来,因为新人加入,虽然我们可以解释清楚逻辑错误和必然发生因而必须处理的情况之间的区别,但他们还没有能力准确界定错误级别。
怎么办?
我们取消了错误级别这种做法,全部代之以assert。
因为不这样做,他们就不会使用错误系统,而是严重倾向于使用防错逻辑——这显然容易导致错误扩散,为定位带来麻烦。

将错误界定为错误,bug修正率几乎可以达到100%,且很少因为修改bug而引入新的bug;使用防错逻辑,一方面导致错误被掩盖、扩散,另一方面显然会为定位造成很大麻烦,甚至导致错误越改越多。

大概来说,这种情况下,bug可能只有80%能被修正。
于是,165个bug,过去全部被修复;预计还有10个bug,1个无法控制(虽然bug可以控制不等于功能正常);
而现在,165个bug,只有165*80%=132个真正被修复;不考虑这种编码方案会隐藏bug,还是会有165-132+10=43个bug隐藏,其中4个无法控制。

事实上,由于错误隐藏以及bug修复引入新的bug,代码质量前后差距相差几达数十倍。
一个重要原因是:那些被“防错”代码“控制”的bug,其实根本就没有真正被控制;仅仅是在表面上被隐藏起来而已。
strcpy得到空指针、然后根据错误返回不精确地处理掉就是典型的例子;这种东西会带来更多难以发现的bug。
因此,实际无法控制的缺陷肯定远超过20个,甚至可能更多(换句话说:这种糊涂蛋系统里,已经几乎没有可信的东西了)。

将所有错误用assert标识出来,放弃错误控制流程;于是,经过几轮测试,预估仍然存在的bug很快再次被压缩到20个左右——但这次全部无法控制。
不过,一共20个无法控制的bug,显然远胜于 (20个不可控制的bug) + (20/10% = 200个据说已经被控制的bug)
(后来,我们破除了头脑中对异常的成见,连最后这20个缺陷也用异常保护起来了)

隐藏错误导致测试难以起到作用,是造成这种现象的根本原因。

暴露bug,这就是assert的意义。




所以说,没有问题是不能解决的;但必须要有正确的心态和灵活的头脑。

我这里仅仅说了try catch和错误级别,如果有死脑筋认为只有这两招可以防错的话,请看微软那位老兄举的execl加速计算代码的例子:
由于必须区分有改动和无改动的单元格,并准确识别公式计算会涉及到的那些单元格,这显然需要极端复杂的代码。
怎么保证它的有效性?
他们写了另外一个只在调试期执行的、缓慢而简单的全部计算程序,用assert检查这个程序和正式代码的计算结果有无差异。

这种机制在运行期有什么用?它甚至都不会被编译到发行代码里!
它的作用就是: 不允许把bug留到运行期。

看看 write clear code 这本书吧。昨天贴的关于assert的小故事,只是它的第二章的十分之一不到。
剩余的内容,都是如何让编译器帮助排除各种错误的。
莫非有人能脑筋死到必须让我把这整本书给您复述一遍?还是你根本就没打算理解这些东西,仅仅就是想吵个架而已?


另,看看这则关于google的报道:

一般每个新业务上线的第一年,通常会发生1000次个别主机的故障、数千次硬盘故障;一次电力输送问题,会导致500至1000太主机失效约6小时;20 次机柜损坏,每次会造成40至80台主机下线;5次机柜摇晃,会导致一半的网络封包在传送过程中遗失;整个业务至少一次重新上线,在两天之内的任何时间, 影响5%到主机。整个业务中还有一半的几率会过热,可能导致5分钟内让几乎所有服务器当机,恢复则需要花费1到2天地时间。

没关系。即使硬件如此不稳定,Google也不会崩溃。
只要他们的程序逻辑没有问题。

至于某个坦承自己的项目会“有个逻辑错误,偶尔的发生, 影响到了3-5格模块的内容, 其他的逻辑错误都是返回错误。但是没有的异常错误, 最后在strcpy 里面爆发了。你想定位就慢慢花时间吧。”的“坏人”^_^,在下老早就已经帮他指出了问题所在。
对于任何一边偷偷向在下的观点投降,一边拿在下自己的观点掺杂上他自己一直没弄懂的问题过来缠杂不清的"坏人",在下将不得不秉承一贯作风,从此置之不理了。


最后: 这本来是专注于assert的讨论,但现在显然已经远远超出范围,甚至都波及到硬件稳定性了。
在下认为关于assert的一切都已经被阐述明白,没什么需要多说的了;那么,就让这个“面条”话题就此结束吧。

*********************
晕,俺的话就那么难以理解吗?


1、错误必须正确分类
网线断掉、硬件不稳定等等都是正常情况设计师必须预先考虑到这些,设置合理的处理/恢复逻辑。
    代码中的逻辑错误与以上相同,在总体设计层面上也是正常情况,没有为这些东西准备措施的就是面条设计师。

2、不同错误不同对待
    正常情况式的异常将纳入系统异常流程逻辑错误必须立即让它爆发,然后在影响范围之外纳入系统异常流程


举例来说:
     strcpy得到一个空指针,这就是典型的逻辑错误
     对有经验的设计者来说,这意味着如下几种可能:
            1、调用者的上级模块有问题,且调用者本身没有严格检查参数
            2、调用者本身逻辑有问题,比如使用了未初始化的内存或野指针
            3、之前的流程出现了严重故障,内存写越界导致程序跑飞

     如果相关代码继续运行,必然招致如下后果(全部或其中之一):
            1、错误被稀里糊涂的丢弃,业务流向稀里糊涂,最终导致用户数据莫名其妙地消失
            2、后续逻辑继续引用未初始化的内存或野指针,捣毁栈结构甚至导致程序跑飞
            3、跑飞的代码……谁知道它会做什么……这种故障没有任何人能准确界定原因(某责任人满头大汗地“偶发,偶发……”)。

所以,对这种情况的处理不能简单返回一个arg_error了事,而是必须想办法隔离相关模块,在更高的、不受影响的层次上恢复运行。


3、在错误精确分类的基础上再考虑如何恢复
看在下曾用过的这个设计:

WARNING:    程序设计有错,但只影响当前函数的此次调用,可当作失败处理;但必须写调试日志以跟踪错误。
MINOR_ERROR:程序设计错误影响到了整个子模块,此模块应停止一切活动,把这个错误直传上去,让上级模块撤销此次操作。
MAJOR_ERROR:程序设计错误影响到了中间控制模块,此模块应停止一切活动把错误直传上去,让框架撤销操作
CRITICAL_ERROR: 程序设计错误影响到了负责总控的框架,整个框架不应再作任何操作,应结束程序并重新启动服务。
FINAL_FAULT:发生了无法恢复的重大故障,应结束程序且不得重新启动(或重启动后禁止写任何数据以免造成数据无法恢复),等待专业人员修复(这个级别从未被用到过)。

各位觉得这个设计怎样?
如果strcpy空指针问题撞上这个系统,显然不会崩溃,而是在经验老道的程序员手下准确界定范围不触动可能导致无法预料的总爆发的任何东西直线返回,然后在可能恢复错误最低层次重试

如果你们也有这种系统,返回错误码俺没意见;可惜在下看不出各位有使用过类似系统的任何迹象。

在下说过,这是对异常系统的一种模拟。如果你们不打算或不能用异常,只要有这个,效果和异常相差无几
也许有些朋友是在这种系统下工作,那么返回错误无可厚非;但你要站出来说“我们不用assert,我们不用异常”——不客气的说:你们那位可怜的设计师的脸面被你们给丢尽了;同时希望你们回去好好看看设计,不要误导初学者。

在下以前也在华为做过。当时我们的系统也使用了异常,但同时还定义了类似的异常级别系统,如 WARNING MINOR_ERROR 等等。
这样,上层模块接到下层抛出的异常,就可以大致估计影响范围,然后决定在何种层次上恢复正常。
我们同样大量使用assert;但assert被我们定义为 CRITICAL_ERROR,并且会保留在发行代码中。


4、试图恢复无法预知的错误必将导致错误扩散甚至更为严重的伤害;
   并且这种错误几乎无法界定原因

所以,在下更早的项目组宁可放弃原有的错误分类系统,也要迁就新手,让他们把逻辑错误当逻辑错误处理。

换句话说,正规软件开发是要估计代码稳定程度的。
隐藏错误的系统将导致测试经理评估出一个虚假的bug级别;像strcpy空指针伴生野指针之类的bug绝不会无缘无故消失,总有一天它会爆发出来——当然,此时您很可能已经不在公司了,没有人会知道这是您干的。
相反,把所有逻辑错误暴露出来,测试经理给出的bug级别才是准确的。

假设一个未知bug最终导致crash的几率是5%,那么:
不暴露错误的系统里,评估内部约有20个bug;实际是内部还有 200 个bug被隐藏;这220个bug至少有11个会导致crash且无法追踪。
暴露错误的系统里,评估内部约有20个bug,实际就是20个bug;这20个bug则只有1个会导致crash——由于没有隐藏错误,这一个crash会很快被捕获并修正。

于是,半年试运行后,不暴露错误的系统可能会crash 11次,修修补补后修复了8个,同时又引入3个可能导致crash的bug;
暴露错误的系统crash 1次,修正后永远不会crash。


看看Linux、orache他们是怎么做的;他们有没有重写c/c++库,去掉库中的assert(至少,release版的标准strcpy是不检查传入参数的)。



5、请注意讨论范围,不要任意发散

在下已经批评过某“坏人 ”把硬件不稳定这种八杆子打不着的问题扯进来了;有些朋友也不想想,既然我们连错误级别系统都搞出来了,系统可能没有日志吗?如此详细的错误级别,日志可能不细致、准确吗?这种精确错误级别界定机制下,我们的恢复机制粒度会不细吗?

日志不是糊涂账,不是火锅,不能什么都往里写——否则,这本糊涂账就不会有人去认真分析。


日志分很多种,比如访问日志、业务日志、调试日志等等,请注意它们的严格分类以及作用。


正式发布的正规系统都不会写调试日志,除非明确打开——比如Linux、Windows、oracle、ms sql server等等。

为什么?

原因就是: 调试日志中,低级别(TRACE)的记录相当于单步跟踪,这是为整体联调服务代码的无奈之举;高级别(WARNING及以上)记录则用于跟踪逻辑错误。

一个稳定的系统,肯定是不允许逻辑错误存在的。
测试一定要做到可以保证逻辑错误几乎不发生时才敢往出发代码——否则,谁知道会爆出什么炸弹。


为何c/c++标准库有大量 用assert、不检查strcpy参数指针等等“恶心” 行为?同样用着这些东西,为何别人就能写出真正稳定的代码?

原因很简单: 这种低级逻辑错误是必须在测试期处理掉的;assert就相当于调试日志,而且还是只写高危级错误的那种日志。


如果有些公司的测试连这种高危漏洞都不能排除——各位读者,你们觉得在下称它为“走过场”是否夸张?



为何Linux、Windows、oracle、ms sql server以及许许多多的公司敢用assert?
原因很简单——能被assert检查出来的都属于 第一梯队 bug,这类bug测不出来是近乎不可思议的事。

然后,assert查不出来的(如野指针等)才是 第二梯队 bug;这类bug除了良好设计的catch,谁都无能为力。

以上两类都属于 极端严重级别 的bug,它们是单元测试和第一阶段测试的针对目标。

然后,测试第二阶段开始,此阶段测的,才是业务逻辑是否正常、运算结果是否正确等“软”故障。



strcpy空参变arg_error式的行为,说白了就是把第一梯队的严重bug变成第二梯队的偶发故障和第三梯队的“软”故障。

这样一来,测试初期阶段就会形势一片大好,几乎没有几个会crash的bug——但是,一旦crash,就是那句话“有个逻辑错误,偶尔的发生, 影响到了3-5格模块的内容, 其他的逻辑错误都是返回错误。但是没有的异常错误, 最后在strcpy 里面爆发了。你想定位就慢慢花时间吧。”

好不容易把这种漏洞糊上,离deadline已经不远了;于是关于“软”故障的测试匆匆而过——该交货了。

长此以往,他们怎么不可能患上崩溃恐惧症?



——比较一下:
——这是在下曾呆过的一家小公司为某部/局级要害部门做的设备故障检测、处理及验证系统(人命关天的东西:如果我们有计算错误,导致设备状况恶化查不出来,一次死上万人都有可能)
——测试开始还不到一星期,测试组就已经发怒了:怎么到这时候了还有英文(指的是因assert爆出的错误对话框)?
——原因是:不到三天的密集爆发后,这个系统已经再也发现不了assert错了;所以他们已经进入追查“软”故障的流程,对再次遇到assert毫无心理准备!
——那个assert,是他们在持续4、5个月的测试中发现的最后3次“英文”之一。
——请注意,我们是遇到任何含糊之处全部抛异常或assert的。
——当然,我们当时用的不是c/c++,不需要处理指针。
(不过,虽然在下后来做的c/c++项目都没有经过如此严格的测试,但几乎每次都是前2、3次单元测试之后,内部就再也不会有任何种类的崩溃——于是后面的测试,在下就可以把精力集中在参数传递上了)

好吧,即使某些大佬们的系统不需要这样的安全性,得不到长达4、5个月的测试时间——1周不到的测试,仅余2个可能导致崩溃的bug: 诸位凭良心说下,你们做得到吗?

***************************************
说白了很简单:

我先前呆过的几家公司都是从一开始就没打算放过任何错误,不管大小。

这种心态下,你可以assert,程序可以崩溃,损失再大也得扛——自然,责任人当然也不可能逍遥法外。

同样,测试部门也必须担责任。


正规公司里,测试可不是伺候程序员大爷的苦工。

本质上说,程序员是公司任务的承包商;测试则是代表公司的采购部验货人。双方几乎是领导与被领导的关系——测试即使不是领导,也绝对不可能是面条公司的苦工


这里面就有个责任界定问题,即: 出了大事故谁来负责?

华为这种公司,一台9系列光交换机崩溃,一个小国的网络就彻底崩溃。
这责任,谁扛得起?


很简单,3级责任:
1、如果一个可以预料的故障,设计师居然预料不到,没有设计异常流程,他的饭碗甚至人身自由恐怕就很成问题了。
   对设计师来说,把不稳定的设备玩稳定是基本功。做不到,请另谋高就。

2、一个本该立刻暴露的故障,程序员稀里糊涂把它“容错”掉了,瞒得过测试,瞒得过数亿用户?
   一旦追究起来,此人饭碗难保;部门经理也要掉一级工资了。

3、作为测试,出现明显的漏测导致线上系统崩溃,不该自己反省下吗?下次卡紧点,不然你们经理又要掉一级工资了。



所以,某些公司里,如果程序员代码太垃圾,测试甚至会刻意保留一些bug不报,仅指出bug数目;要求对方彻底检查,直到保留的bug被自测出来,他们才可以放心。


形成习惯后,由于通过测试的部分可认为绝对可靠,后续开发的效率是极其惊人的。

比如,在下为那个要命系统做的2期改造,40多个修改点一星期搞定,测试找不出任何bug。
之后,客户单位根据自身情况提的任何需求,向来是上午的需求更改提交过来,下午就能出补丁包,半年未出现过bug。
以至于自在下03年底离职至今,竟没有人能接手这个项目——客户不信任,评价永远是“不如老系统”,压力太大。


总结:
    容错必须精确、理性。不然越容错越多。
    调试日志必须是空的。里面出现的任何WARNING及以上级别的信息都意味着逻辑错误,意味着不可挽回的损失。
    任何逻辑错误都不可容忍。它们总有爆的一天——不管你用什么方式美化它、容纳它。
    不要为自己准备太多退路。严格考察起来,这点小技俩瞒不过任何人。

只有你不给自己留退路,你才会真正关心代码质量,才能写出真正无措的代码——同样用着c/c++标准库,Linux、Windows、oracle他们为何就不担心崩溃?

************************************************

呵呵,在另外那个帖子中讨论时还针对神七飞船写了个案例,不过感觉前面的帖子已经说清楚了,就没有发。

大家在学校学的东西,往往都是一些局部的、分离的算法,很少有站在大局角度通盘考虑的;并且国内教授自己多数也没什么实践经验,教不好错误处理很正常。


案例:

神七飞船载重有限,因此减速火箭只能在着陆前5秒打开(因为燃料就这么多);实际上,这个数据考虑了最不利状态下姿态控制的需要并留有1秒的余量。

现在,要求是设计一个能保证安全着陆的程序。
这个程序要读取飞船当前的姿态、速率以及高度、风向、风速等信息,然后实时控制飞船着陆。


现在,考虑这样一种情形: 假如上级调度模块在宇宙空间的恶劣环境下,因射线击中存储器某单元造成状态翻转,致使栈写越界——于是着陆控制模块发现,指向飞船姿态数据区的指针被改写成0了。

当然,这种情况是无法事先预估到的。怎么办?


如果返回错误码,是可以避免因引用无效内存而崩溃;但被破坏的栈可不会自动恢复。
很可能,这个return指令会因为栈中返回地址也已经被破坏,而使得代码跳到一个未知的位置继续执行——也许飞船都撞到地面了,这段跑飞的代码还没回来;减速火箭自然也就不可能打开。

于是,飞船撞毁,宇航员必死无疑。



正确的做法只能是——发现异常就crash!

PS:介绍个新概念:watch-dog(搞嵌入式的应该很熟悉这个)。
——watch-dog的原理就是:要求程序必须安排合适逻辑定时清零计数器(报告状态正常);一旦计数器到时没有清零,就立即reset系统(相当于crash)。因为此时程序可能已经跑飞了(当然,也可能是时序没算对)。
——当然,watch-dog的周期不可能太短,否则对系统性能消耗过大;但太长又可能导致反应不够灵敏,这是必须根据实际折中考虑的。

实时系统的重启速度相当快。玩数码相机的应该有所体会——我的那个好像说是0.0x秒吧。

于是,由于设计上预留的1秒缓冲,若能立即crash的话,系统可以容忍连续10次失败仍能保证飞船安全着陆——连续2次或3次失败后自动切换备用系统等等设计也才有了发挥空间。

可见,crash不是洪水猛兽。它是构建真正稳定的系统的不可缺少的一环(当然,普通如word等软件,出错直接崩溃提交就行了,没必要设计机制保障7*24的绝对安全性)。


相反,如果仅仅返回个稀里糊涂的错误码,系统跑飞被watch-dog抓到还算好(但也已经耽误不少时间了),万一跑飞的那个地方刚好是刷新watch-dog的逻辑,岂不完蛋?

搞出这种飞机的,有几个脑袋够M249打眼?

——————————————————

说到底,这纯粹是个态度问题。

都知道捕获所有异常并忽略是完完全全的混蛋;但每个错误都要查清来龙去脉却也不是易事——尤其是在缺乏责任心的人那里。

所以,“聪明”人就有了发挥其“中国式智慧”的机会了:有错,我知道,我也报告了——别人不处理怪他。
别人?
别人也不是傻蛋。你报告我也报告,说不定问题绕了一圈又回你手里了。
——无所谓,反正程序还能跑。

最后,代码中充斥着各种各样稀奇古怪的错误,到处都是错误码,人人都在写日志——反正正常流程能走通,交得了差。

你说什么?空指针不准确确定原因和影响范围,稀里糊涂返回错误码可能导致程序跑飞?
跑飞就跑飞。又没崩溃。
反正到处都是检查,到处都是容错,说不定飞着飞着它就又飞回来了——像蟑螂一样,这系统命硬着呢。

你说这样说不定哪天就撞到格硬盘刷bios的那块代码了?
没事,那才多大点概率。我可以保证99%,至少也有90%的情况下跑不到那里。

总之,不崩溃才是王道——很明显,这种容错容的跑飞说不定还能跑回来的系统,你哪里知道崩溃时已经跑飞多少遍了!

一旦崩溃——苍天啊!这问题谁有本事定位!!

说不得,整个系统大返工,每个函数每个参数的——不是查故障啦,没法查——加容错代码。
——口号是:一定要保证程序跑飞后还能跑回来!
(当然,这时候逻辑是否正确、数据有无丢失就顾不得了)

显然,随着程序的崩溃,搞出这种垃圾的程序员怎么可能不恐惧到崩溃。
——我理解这种病人;但不认为他们值得同情。




一言以蔽之:一群没责任心的懒蛋想偷懒,结果越偷懒越是忙得不可开交;最终代码写了5000行,其中容错逻辑倒有3000多行——其中1800行是为了给 那2000行代码加容错逻辑;另外800行是给1800行容错代码加的容错;剩下的则是容错代码的容错的容错(的容错……)。

越容越复杂,错也就越多;错越多越无计可施,只能继续容错,于是系统就更复杂更不可调试……

——————————————
assert的思想本质就是: 出错了,一定要立刻崩溃(报告),千万别拖来拖去搞出先跑飞再崩溃的飞机。
只有这样,排错才会轻松惬意——跑一跑,根据assert报的行号直接过去修改问题就是,几乎不需要什么代价。
assert越多、越准确,错误就被限制得越死;一旦触发到,问题也就越好解决。

至于发行版assert自动失效(这已经是内建于c/c++标准库的机制了),原因仅仅是不希望为无用的东西付出运行时效率而已(无用二字正体现了对测试流程的自信,同时也显示出这种系统有多好测)。
——这种境界,显然不是没这样做过的人所能理解的。
阅读(3638) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~