比间隔3 小时的10次操作要高许多,因为持续的精力集中能减少思考时间。在这样的冲刺
之后,提出下一个时间块要求之前,小组通常需要一到两天的时间来从事书面文档工作。并
且,通常3人左右的小组能卓有成效地安排和共享时间块。在调试新操作系统时,这似乎是
一种使用目标机器的最好方法。
上述方法尽管没有在任何理论中被提及,在实际情况中却一直如此。另外,同天文工
作者一样,系统调试总是夜班性质的工作。二十年前,当所有机房负责人在家中安睡时,我
正工作在701上。三代机器过去了,技术完全改变了,操作系统出现了,然而大家喜好的工
作方式没有改变。这种工作方式得以延续,是因为它的生产率最高。现在,人们已开始认识
到它的生产力,并且敞开地接受这种富有成效的实践。
辅助机器和数据服务
仿真装置。如果目标机器是新产品,则需要一个目标机器的逻辑仿真装置。这样,在
生产出新机器之前,就有辅助的调试平台可供使用。同样重要的是——即使在新机器出现之
- 73 -
----------------------- Page 86-----------------------
后,仿真装置仍然可以提供可靠的调试平台。
可靠并不等于精确。在某些方面,仿真机器肯定无法精确地达到与新型机器一致的实
现。但是至少在一段时间内,它的实现是稳定的,新硬件就不会。
现在,我们已经习惯于计算机硬件自始至终能正常工作。除非程序开发人员发现相同
运算在运行时会产生不一致的结果,否则出错时,他都会被建议去检查自己代码中的错误,
而不是去怀疑他的运行平台。
这样的经验,对于支持新型机器的编程工作来说,是不好的。实验室研制和试制的模
型产品和早期硬件不会像定义的那样运行,不会稳定工作,甚至每天都不会一样。当一些缺
陷被发现时,所有的机器拷贝,包括软件编程小组所使用的,都会发生修改。这种飘忽不定
的开发基础实在是够糟的。而硬件失败,通常是间歇性的,导致情况更加恶劣。不确定性是
所有情况中最糟糕的,因为它剥夺了开发人员查找bug 的动力——可能根本就没有问题。所
以,一套运行在稳定平台上的可靠仿真装置,提供了远大于我们所期望的功用。
编译器和汇编平台。出于同样的原因,编译器和汇编软件需要运行在可靠的辅助平台
上,为目标机器编译目标代码。接着,可以在仿真器上立刻开始后续的调试。
高级语言的编程开发中,在目标机器上开始全面测试目标代码之前,编译器可以在辅
助机器上完成很多目标代码的调试和测试工作。这为直接运行提供了支持,而不仅仅是稳定
机器上的仿真结果。
程序库和管理。在OS/360 开发中,一个非常成功的重要辅助机器应用是维护程序库。
该系统由W. R.Crowley 带领开发,连接两台7010机器,共享一个很大的磁盘数据库。7010
同时还提供System/360 汇编程序。所有经过测试或者正在测试的代码都保存在该库中,包
括源代码和汇编装载模块。这个库实际上划分成不同访问规则下的子库。
首先,每个组或者编程人员分配了一个区域,用来存放他的程序拷贝、测试用例以及
单元测试需要的测试辅助例程和数据。在这个开发库 (playpen)中,不存在任何限制开发
人员的规定。他可以自由处置自己的程序,他是它们的拥有者。
当开发人员准备将软件单元集成到更大的部分时,他向集成经理提交一份拷贝,后者
将拷贝放置在系统集成子库中。此时,原作者不可以再改变代码,除非得到了集成经理的批
准。当系统合并在一起时,集成经理开始进行所有的系统测试工作,识别和修补bug。
- 74 -
----------------------- Page 87-----------------------
有时,系统的一个版本可能会被广泛应用,它被提升到当前版本子库。此时,这个拷
贝是不可更改的,除非有重大缺陷。该版本可以用于所有新模块的集成和测试。7010 上的
一个程序目录对每个模块的每个版本进行跟踪,包括它的状态、用途和变更。
这有两个重要的理念。首先是受控,即程序的拷贝属于经理,他可以独立地授权程序
的变更。其次是使发布的进展变得正式,以及开发库(playpen)与集成、发布的正式分离。
在我看来,这是OS/360 工作中最优秀的成果之一。它实际上是管理技术的一部分,很
多大型的项目都独立地发展了这些技术2,包括Bell试验室、ICL、剑桥大学等。它同样适
用于文档,是一种不可缺少的技术。
编程工具。随着调试技术的出现,旧方法的使用减少了,但并没有消失。因此,还是
需要内存转储、源文件编辑、快照转储、甚至跟踪等工具。
与之类似,一整套实用程序同样是必要的,用来实现磁带走带、拷贝磁盘、打印文件、
更改目录等工作。如果一开始就任命了项目的工具操作和维护人员,那么这些工作可以一次
完成,并且随时处在待命状态。
文档系统。在所有的工具中,最能节省劳动力的,可能是运行在可靠平台上的、计算
机化的文本编辑系统。我们有一套使用非常方便的系统,由J. W.Franklin 发明。没有它,
OS/360手册的进度可能会远远落后,而且更加晦涩难懂。另外,对于6 英尺的OS/360手册,
很多人认为它表达的是一大堆口头垃圾,巨大容量带来了新的不理解问题——这种观点有一
些道理。
对此,我通过两种途径作出了反应。首先,OS/360 的文档规模是不可避免的,需要制
订仔细的阅读计划。如果选择性地阅读,则可以忽略大部分内容和省下大量时间。人们必须
把OS/360 的文档看成是图书馆或者百科全书,而不是一系列强制阅读的文章。
第二,它比那些刻画了大多数编程系统特性的短篇文档更加可取。不过,我也承认,
“概
手册仍有某些需要大量改进的地方,经改进后文档篇幅会大大减少。事实上,某些部分(
念和设施”)已经被很好地改写了。
性能仿真装置。最好有一个。正如我们将在下章讨论到的,彻底地开发一个。使用相
同的自顶向下设计方法,来实现性能仿真器、逻辑仿真装置和产品。尽可能早地开始这项工
作,仔细地听取“它们表达的意见”。
- 75 -
----------------------- Page 88-----------------------
高级语言和交互式编程
在十年前的OS/360 开发中,并没有使用现在最重要的两种系统编程工具。目前,它们
也没有得到广泛应用,但是所有证据都证明它们的功效和适用。他们是(1)高级语言和(2)
交互式编程。我确信只有懒散和惰性会妨碍它们的广泛应用,技术上的困难很快就不再成为
借口。
高级语言。使用高级语言的主要原因是生产率和调试速度。我们在前面已讨论过生产
率的问题(第8 章)。其中,并没有提到大量的数字论据,但是所体现出来的是整体提升,
而不仅仅是部分增加。
调试上的改进来自下列事实——存在更少的bug,而且更容易查找。bug更少的原因,
是因为它避免在错误面前暴露所有级别的工作,这样不但会造成语法上的错误,还会产生语
义上的问题,如不当使用寄存器等。编译器的诊断机制可以帮助找出这些类似的错误,更重
要的是,它非常容易插入调试的快照。
就我而言,这些生产率和调试方面的优势是势不可挡的。我无法想象使用汇编语言能
方便地开发出系统软件。
那么,上述工具的传统反对意见有哪些呢?这里有三点:它无法完成我想做的事情;
目标代码过于庞大;目标代码运行速度过慢。
就功能而言,我相信反对不再存在。所有证据都显示了人们可以完成想做的事情,只
3,4
是需要花费时间和精力找出如何做而已,这可能需要一些讨人嫌的技巧 。
就空间而言,新的优化编译器已非常令人满意,并且将持续地改进。
就速度而言,经优化编译器生成的代码,比绝大多数程序员手写代码的效率要高。而
且,在前者被全面测试之后,可以将其中的百分之一至五替换成手写的代码,这往往能解决
5
速度方面的问题 。
系统编程需要什么样的高级语言呢?现在可供合理选择的语言是PL/I6。它提供完整的
功能集;它与操作系统环境相吻合;它有各种各样的编译器,一些是交互式的,一些速度很
快,一些诊断性很好,另一些能产生优化程度很高的代码。我自己觉得使用APL 来解决算法
更快一些,然后,将它们翻译成某个系统环境下的PL/I 语言。
- 76 -
----------------------- Page 89-----------------------
交互式编程。MIT 的Multics项目的成果之一,是它对软件编程系统开发的贡献。在那
些系统编程所关注的方面,Multics (以及后续系统,IBM 的TSS)和其他交互式计算机系统
在概念上有很大的不同:多个级别上数据和程序的共享和保护,可延伸的库管理,以及协助
终端用户共同开发的设施。我确信在某些应用上,批处理系统决不会被交互式系统所取代。
但是,我认为Multics 小组是交互式系统开发上最具有说服力的成功案例。
然而,目前还没有非常明显的证据来证明这些功能强大的工具的效力。正如人们所普
遍认识的那样,调试是系统编程中很慢和较困难的部分,而漫长的调试周转时间是调试的祸
7
根。就这一点而言,交互式编程的逻辑合理性是勿庸置疑的 。
另外,从很多采用这种方式了开发小型系统和系统某个部分的人那里,我们听到了很
多好的证据。我唯一见到的关于大型编程系统开发方面的数字,来自Bell实验室John Harr
的论文。它们如图12.2所示。这些数字分别反映了代码编写、汇编装配和程序调试的情况。
第一个大部分是控制程序;其他三个则是语言解释、编辑等程序。Harr 的数据表明了系统
8
软件开发中,交互式编程的生产率至少是原来的两倍 。
批处理(B )或交互
程序 规模 指令/人年
式(C)
ESS 代码 800,000 B 500-1000
7094 ESS 支持 120,000 B 2100-3400
360 ESS 支持 32,000 C 8000
360 ESS 支持 8,300 B 4000
图12.2:批处理和交互式编程生产率的对比
由于远程键盘终端无法用于内存转储的调试,大多数交互式工具的有效使用需要采用
高级语言来进行开发。有了高级语言,可以很容易地修改代码和选择性地打印结果。实际上,
它们组成了一对强大的工具。
- 77 -
----------------------- Page 90-----------------------
整体部分(The Whole and the Parts)
我能召唤遥远的精灵。
那又怎么样,我也可以,谁都可以,问题是你真的召唤的时候,它们会来吗?
- 莎士比亚,《亨利四世》,第一部分
I can call spirits from the vasty deep.
Why, so can I, or so can any man; but will they come when you do call for them?
- SHAKESPEARE, KING HENRY IV, Part I
和古老的神话里一样,现代神话里也总有一些爱吹嘘的人:“我可以编写控制航空货运、
拦截弹道导弹、管理银行账户、控制生产线的系统。”对这些人,回答很简单,“我也可以,
任何人都可以,但是其他人成功了吗?”
如何开发一个可以运行的系统?如何测试系统?如何将经过测试的一系列构件集成到
已测试过、可以依赖的系统?对这些问题,我们以前或多或少地提到了一些方法,现在就来
更加系统地考虑一下。
剔除bug 的设计
防范bug 的定义。系统各个组成部分的开发者都会做出一些假设,而这些假设之间的
不匹配,是大多数致命和难以察觉的bug 的主要来源。第4、5、6 章所讨论的获取概念完整
性的途径,就是直接面对这些问题。简言之,产品的概念完整性在使它易于使用的同时,也
使开发更容易进行以及bug 更不容易产生。
上述方法所意味的详尽体系结构设计正是出于这个目的。Bell 实验室安全监控系统项
目的V.A.Vyssotsky 提出,“关键的工作是产品定义。许许多多的失败完全源于那些产品未
精确定义的地方。1”细致的功能定义、详细的规格说明、规范化的功能描述说明以及这些
- 78 -
----------------------- Page 91-----------------------
方法的实施,大大减少了系统中必须查找的bug 数量。
测试规格说明。在编写任何代码之前,规格说明必须提交给测试小组,以详细地检查
说明的完整性和明确性。如同Vyssotsky 所述,开发人员自己不会完成这项工作:“他们不
会告诉你他们不懂。相反,他们乐于自己摸索出解决问题和澄清疑惑的办法。”
自顶向下的设计。在1971 年的一篇论文中,Niklaus Wirth 把一种被很多最优秀的编
程人员所使用的设计流程2 形式化。尽管他的理念是为了程序设计,同样也完全适用于复杂
系统的软件开发设计。他将程序开发划分成体系结构设计、设计实现和物理编码实现,每个
步骤可以使用自顶向下的方法很好地实现。
简言之,Wirth 的流程将设计看成一系列精化步骤。开始是勾画出能得到主要结果的,
但比较粗略的任务定义和大概的解决方案。然后,对该定义和方案进行细致的检查,以判断
结果与期望之间的差距。同时,将上述步骤的解决方案,在更细的步骤中进行分解,每一项
任务定义的精化变成了解决方案中算法的精化,后者还可能伴随着数据表达方式的精化。
在这个过程中,当识别出解决方案或者数据的模块时,对这些模块的进一步细化可以
和其他的工作独立,而模块的大小程度决定了程序的适用性和可变化的程度。
Wirth 主张在每个步骤中,尽可能使用级别较高的表达方法来表现概念和隐藏细节,除
非有必要进行进一步的细化。
好的自顶向下设计从几个方面避免了bug。首先,清晰的结构和表达方式更容易对需求
和模块功能进行精确的描述。其次,模块分割和模块独立性避免了系统级的bug。另外,细
节的隐藏使结构上的缺陷更加容易识别。第四,设计在每个精化步骤的层次上是可以测试的,
所以测试可以尽早开始,并且每个步骤的重点可以放在合适的级别上。
当遇到一些意想不到的问题时,按部就班的流程并不意味着步骤不能反过来,直到推
翻顶层设计,重新开始整个过程。实际上,这种情况经常发生。至少,它让我们更加清楚在
什么时候和为什么抛弃了某个臃肿的设计,并重新开始。一些糟糕的系统往往就是试图挽救
一个基础很差的设计,而对它添加了很多表面装饰般的补丁。自顶向下的方法减少了这样的
企图。
我确信在十年内,自顶向下进行设计将会是最重要的新型形式化软件开发方法。
结构化编程。另外一系列减少bug 数量的新方法很大程度上来自Dijkstra3。Bohm 和
- 79 -
----------------------- Page 92-----------------------
4
Jacopini 的为其提供了理论证明 。
基本上,该方法所设计程序的控制结构,仅包含语句形式的循环结构,例如DOWHILE,
以及IF...THEN...ELSE 的条件判断结构,而具体的条件部分在IF...THEN...ELSE 后的花括
号中描述。Bohm 和Jacopini 展示了这些结构在理论上是可以证明的。而Dijkstra 认为另
外一种方法,即通过GO TO 不加限制的分支跳转,会产生导致自身逻辑错误的结构。
这种方法的基本理念非常优秀,但仍有人提出了一些反面的意见。一些附加的控制结
构非常有效,例如,在多个条件下的多路分支 (CASE、SWITCH 语句),异常跳转等 (GO TO
ABNORMAL END)。此外,关于完全避免GO TO 语句的说法显得有些教条主义,而且似乎有些
吹毛求疵。
关键的地方和构建无bug 程序的核心,是把系统的结构作为控制结构来考虑,而不是
独立的跳转语句。这种思考方法是我们在程序设计发展史上向前迈出的一大步。
构件单元调试
程序调试过程在过去的二十年中有过很多反复,甚至在某些方面,它们又回到了出发
的起点。整个调试过程有四个步骤,跟随这个过程来检验每个步骤各自的动机是一件很有趣
的事情。
本机调试。早期的机器的输入和输出设备很差,延迟也很长。典型的情况是,机器采
用纸带或者磁带的方式来读写,采用离线设备来完成磁带的准备和打印工作。这使得磁带输
入/输出对于调试是不可忍受的。因此,在一次机器交互会话中会尽可能多地包含试验性操
作。
在那种情况下,程序员仔细地设计他的调试过程——计划停止的地点,检验内存的位
置,需要检查的东西以及如果没有预期结果时的对策。花费在编写调试程序上的时间,可能