引言
Section outline
-
定义可移植性
根据 Robert A. Edmunds 编著的《普伦蒂斯-霍尔计算机术语标准词典》,可移植性定义如下:“可移植性:一个与兼容性相关的术语。可移植性决定了一个程序或其他软件可以从一个计算机系统移动到另一个计算机系统的程度。” 这里的关键短语是“一个程序可以被移动的程度”。
来自维基百科:“在软件工程中,移植是将软件适配到与给定程序(为执行此目的而设计)最初设计的计算环境不同的计算环境中执行的过程(例如,不同的 CPU、操作系统或第三方库)。当更改软件/硬件以使其可在不同的环境中使用时,也使用该术语。当将软件移植到新平台的成本远低于从头开始编写的成本时,该软件是可移植的。相对于其实现成本而言,移植软件的成本越低,就认为它越可移植。”
我们可以从两个角度讨论可移植性:通用和特定。从广义上讲,可移植性仅仅意味着在一个或多个与设计环境不同的环境中运行程序。由于生产和维护软件的成本远远超过生产硬件的成本,因此我们有巨大的动力来延长软件的寿命,使其超出当前硬件的形态。这样做在经济上是完全合理的。
特定的可移植性涉及识别给定程序必须执行的各个目标环境,并清楚地说明这些环境之间的差异。移植场景的示例包括:
- 在同一机器上从一个操作系统移动到另一个操作系统。
- 在一台机器上的某个操作系统版本移动到另一台具有不同架构的机器上的相同操作系统版本。
- 在不同机器上相同的操作系统的不同变体(例如各种 UNIX 和 Linux)之间移动。
- 在两个完全不同的硬件和操作系统环境之间移动。
- 在使用不同浮点硬件或仿真软件的系统之间移动。
- 在同一系统上使用不同的编译器之间移动。
- 在符合标准 C 的实现和不符合标准 C 的实现之间移动,反之亦然。
- 在同一编译器上重新编译代码,但使用不同的编译器选项。
- 在同一系统上从一个编译器版本移动到同一编译器的另一个版本。
最后两种情况可能不太明显。然而,当采用已经编译无误、可以运行并完成工作的现有代码,并通过同一编译器的新版本或仅仅使用不同的编译时选项运行时,可能会遇到问题。潜在意外行为的一个原因是实现定义的行为发生变化(例如,普通
char
的有符号性)。另一个原因可能是之前依赖于未定义行为,而该行为恰好实现了程序员期望的结果(例如,某些表达式的求值顺序)。请注意,在不符合标准 C 的系统之间移植代码是可以的!例如,早期的数字信号处理 (DSP) 芯片仅支持 32 位浮点数据和运算,在这种情况下,类型
float
、double
和long double
(如果后两者甚至被编译器支持)都映射到 32 位。在这种情况下,有意义的应用程序仍然可以在 DSP 芯片系列的成员之间移植。移植不仅仅是将一段软件在多个目标上运行起来。它还涉及以合理(且经济实惠)的资源量、及时地完成这项工作,并确保生成的代码能够充分地执行。将一个系统移植到一个目标,结果移植完成后,它运行缓慢或占用大量系统资源以至于无法使用,是没有意义的。
需要问自己的重要问题是:
- 我正在移植到还是从标准 C 实现移植?如果是,支持哪些标准版本?
- 我正在移植的代码在设计和编写时是否考虑了可移植性?
- 我是否预先知道所有环境是什么样的,以及我实际有多少环境可用于测试?
- 我对速度、内存和磁盘效率的性能要求是什么?
还有另一个重要的移植场景,即使用 C++ 编译器进行编译。即使这样的移植代码没有利用 C++ 的特性,也会进行某些额外的检查。例如,C++ 要求 C 使用原型风格的函数声明和定义。并且,随着时间的推移,可以添加使用 C++ 特性的新代码,或者现有的 C++ 函数可以调用 C 代码。请注意,C++ 标准不止一个;到目前为止,我们已经有了 C++99、C++03、C++11、C++14、C++17 和 C++20。
可移植性并非新鲜事物
随着 20 世纪 80 年代初良好且廉价的 C 编译器和开发工具的广泛普及,软件可移植性的概念开始流行起来。以至于,听有些人说,可移植性是因为 C 才成为可能的。
可移植性的概念比 C 古老得多,早在 Dennis Ritchie 想到 C 之前,软件就已经成功地被移植了。1959 年,一个小团体定义了一种名为 COBOL 的标准商业语言,1960 年,两家供应商(Remington Rand 和 RCA)实现了该语言的编译器。同年 12 月,他们进行了一项实验,交换了 COBOL 程序,根据 COBOL 设计团队成员 Jean E. Sammet 的说法,“……仅由于实现上的差异而进行了最少量的修改,这些程序就在两台机器上运行了。” 关于 COBOL 开发的逻辑上独立于机器的数据描述,Sammet 在 1969 年写道:“[COBOL] 并不能同时在不同机器上保持效率和兼容性。”
Fortran 也是可移植性领域的早期参与者。根据维基百科,“……FORTRAN 日益普及促使竞争的计算机制造商为其机器提供 FORTRAN 编译器,到 1963 年,已经存在 40 多个 FORTRAN 编译器。由于这些原因,FORTRAN 被认为是第一种广泛使用的跨平台编程语言。”
仅仅因为一个程序是用 C 语言编写的,并不能说明移植它需要多少工作。这项任务可能是微不足道的、困难的、不可能的或不经济的。如果一个程序在编写时没有考虑将其移植到某些不同环境的可能性,那么它实际移植到该环境的难易程度可能很大程度上取决于其作者的严谨性和个人特点,以及语言本身。
为一系列环境(其中一些可能尚未定义)设计可移植的程序可能很困难,但并非不可能。这只需要相当大的严谨性和计划。它需要理解和控制(并在合理的情况下消除)可能在您预期的不同环境中产生不可接受的不同结果的特性的使用。这种理解有助于您避免有意(或更可能是无意地)依赖于您正在编写的程序的不可移植的特性或特征。此外,此类项目的一个关键目标通常不是编写一个无需修改即可在任何系统上运行的程序,而是隔离特定于环境的功能,以便可以为新系统重写这些功能。对于任何语言,主要的可移植性考虑因素都大致相同。只有具体的实现细节由所使用的语言决定。
可移植性的经济性
成功移植的两个主要要求是:拥有必要的技术专长和工具,以及获得管理层的支持和批准。也就是说,必须承认许多项目是由个人或没有管理层的小团体实施的,但仍然需要可移植性。
显然,需要拥有或能够获得并留住优秀的 C 程序员。“优秀”这个词并不意味着仅指或完全指资深专家,因为这类员工通常会有难以管理的自负。也许成功移植项目所需的最重要的特质是纪律,包括个人层面和团队层面。
管理层支持的问题通常更重要,但开发者和管理层本身都很大程度上忽略了这个问题。考虑以下场景。为所有(或具有代表性的子集)指定的目标环境提供了足够的硬件和软件,并且开发团队每周至少虔诚地在所有目标上运行所有代码。通常,他们每天晚上都会以批处理作业的形式提交测试流。
项目进行六个月后,管理层审查进展情况,发现项目占用的资源超出预期(难道不是一直这样吗?),并决定至少暂时缩小目标范围。也就是说,“我们必须在展会上展示一些切实的东西,因为我们已经宣布了该产品”或者“风险投资家希望在下次董事会会议上看到原型”。无论出于何种原因,某些目标的测试和专门针对这些目标的开发都被暂停了,通常是永久性的。
从那时起,开发团队必须忽略被放弃的机器的特性,因为它们不再是项目的一部分,而且公司无法承担额外的资源来认真考虑它们。当然,管理层的建议通常是:“虽然我们不希望您特意去支持被放弃的环境,但如果您不做任何使我们以后无法或低效地重新启用它们的事情,那就太好了。”
当然,随着项目进一步延迟,竞争对手宣布和/或发布替代产品,或者公司陷入经济困境,其他目标也可能被放弃,可能只剩下一个,因为这是开发和营销部门唯一能支持的。每次放弃一个目标,开发团队就开始偷工减料,因为它不再需要担心其他硬件和/或操作系统目标。最终,这降低了以后重新启动被放弃目标的可能性,因为所有在放弃对这些目标的支持后设计和编写的代码都需要检查(当然,前提是这些代码甚至可以被识别出来),以确定所需的工作量以及恢复支持该目标的影响。您很可能会发现,某些设计决策要么禁止要么负面影响重新激活被放弃的项目。
最终结果通常是产品最初只针对一个目标交付,并且永远不会在任何其他环境中提供。另一种情况是针对一个目标交付,然后回头为其他一个或多个目标挽救“尽可能多的”代码。在这种情况下,这项任务可能与移植从未考虑过可移植性的代码的任务没有什么不同。
衡量可移植性
如何知道一个系统何时或是否已成功移植?是当代码编译和链接没有错误时吗?结果必须完全相同吗?如果不是,那么什么程度的接近才算足够?哪些测试用例足以证明成功?除了最简单的情况外,您将无法详尽地/完全地测试每种可能的输入/情况。
当然,代码必须编译和链接没有错误,但是由于实现定义的行为,从不同的目标获得不同的结果是完全可能的。合法的差异甚至可能大到使其毫无用处。例如,浮点数的范围和精度在一个目标到另一个目标之间可能差异很大,以至于最有限的浮点环境产生的结果不够精确。当然,这是一个设计问题,应该在移植系统之前充分考虑。
一个普遍的误解是,必须在所有目标上使用完全相同的源代码文件,并且这些文件充满了条件编译的行。完全没有必要这样做。当然,您可能需要一些目标特定的自定义头文件。您可能还需要用 C 编写的系统特定代码,甚至可能是汇编语言或其他语言。只要这些代码被隔离在单独的模块中,并且这些模块的内容和接口都有详细的文档记录,这种方法就不成问题。
如果您在多个目标上使用相同的数据文件,则需要确保数据被正确移植,尤其是在数据以二进制而不是文本格式存储并且涉及字节序差异的情况下。否则,您可能会浪费大量资源来寻找不存在的代码错误。
除非您充分定义了您的特定可移植性场景和要求,否则您无法知道何时实现了它。并且根据定义,如果您实现了它,您必须感到满意。如果您不满意,要么是您的要求发生了变化,要么是您的设计存在缺陷。最重要的是,成功地将一个程序移植到某些环境并不能可靠地表明将其移植到另一个目标所需的工作量。
环境问题
正如其他章节所指出的,一些可移植性问题与实现语言几乎或完全无关。相反,这些问题与程序必须在其上执行的硬件和操作系统环境相关。本书主体部分已经暗示了其中一些问题;它们在此总结如下:
- 混合语言环境。 对将要调用或被其他语言处理器调用的 C 代码可能存在某些要求。
- 命令行处理。 不同的命令行处理器不仅行为差异很大,而且某些目标甚至可能不存在等效的命令行处理器。
- 数据表示。 这当然完全是实现定义的,并且可能差异很大。不仅
int
类型的大小在您的目标之间可能不同,而且您甚至不能保证分配给对象的所有位都用于表示该对象的值。另一个重要的问题是字内字节和长字内字的顺序。这种编码方案被称为大端或小端。 - CPU 速度。 通常的做法是假设在给定的环境中执行一个空循环
n
次会导致暂停 5 秒,例如。然而,在更快或更慢的机器上运行相同的程序将使这种方法失效。(在具有不同时钟速度的同一处理器版本上运行也是如此。)或者,当同一系统上运行更多(或更少)程序时,计时可能略有不同。相关的问题包括硬件和软件定时器中断的处理频率和效率。 - 操作系统。 即使存在(独立 C 不需要操作系统),主要问题是单任务与多任务以及固定内存组织与虚拟内存组织。其他问题包括处理同步和异步中断的能力、可重入代码的存在以及共享内存。在某些系统上,获取系统日期和时间等看似简单的任务可能是不可能的。当然,系统时间测量的粒度差异很大。
- 文件系统。 同一个文件的多个版本是否可以共存,或者是否存储了创建或上次修改的日期和时间,这是实现相关的。文件名中允许的字符集、名称长度以及名称是否区分大小写也是如此。至于设备和目录命名约定,其差异之大,如同发明者的想象力一样广阔。因此,C 标准对文件系统没有任何规定,除了单个用户访问的顺序文件。
- 开发支持工具。 这些工具可能会对您为给定系统编写代码的方式产生重大影响,或者对编写代码的要求产生重大影响。它们包括 C 编译器、链接器、目标文件和源文件库管理器、汇编器、源代码管理系统、宏预处理器和实用程序库。限制的示例包括外部标识符的大小写、重要性和数量,甚至每个目标模块的大小或源模块的数量和大小。也许覆盖链接器对覆盖方案的复杂性有重大限制。
- 交叉编译。 在目标不是开发软件的系统的环境中,字符集、算术表示和字节序的差异变得重要。
- 屏幕和键盘设备。 这些设备使用的协议差异很大。虽然许多设备实现了各种 ANSI 标准的部分或全部,但也有许多设备没有实现,或者包含不兼容的扩展。可能无法普遍实现从标准输入获取字符而不回显,或者不需要同时按下回车或输入键。直接光标寻址、图形显示和输入设备(如光笔、轨迹球和鼠标)也是如此。
- 其他外围接口。 您的设计可能需要与打印机、绘图仪、扫描仪和调制解调器等设备进行交互。虽然每种设备可能存在一些事实上的标准,但由于某种原因,您可能不得不采用“略微”不兼容的设备。
程序员可移植性
在所有关于可移植性的讨论中,我们不断提到将代码从一个环境移动到另一个环境的方面。虽然这是一个重要的考虑因素,但更有可能的是,C 程序员比他们编写的软件更频繁地移动到不同的环境。出于这个原因,作者创造了术语“程序员可移植性”。
程序员可移植性可以定义为 C 程序员从一个环境移动到另一个环境的难易程度。这对于任何 C 项目都很重要,而不仅仅是涉及代码可移植性的项目。如果您采用某些编程策略和风格,您可以使新团队成员更容易和更快地融入项目。但请注意,虽然您可能已经制定了一种强大的方法,但如果它与主流 C 实践相差太远,那么教授或说服其他 C 程序员其优点将是困难和/或昂贵的。