为多个 UNIX 平台编写软件 |
级别: 中级 Martin Brown, 自由作家, Consultant 2006 年 8 月 28 日 如果为多个 UNIX® 平台编写软件,您会注意到在两个平台上编译软件非常困难。本教程将讨论一些工具和技巧,可以使在代码级支持不同的 UNIX 平台的过程变得相当容易。其原因不是缺乏工具或二进制兼容性问题,而是设置标准 UNIX 环境的头文件和函数的问题。 开始之前 在本教程中,您将了解为了在各种 UNIX® 平台上实现兼容而对应用程序进行构建和迁移操作时的相关问题。我们的重点不是特定的平台差异,而是将为您提供相关知识和工具,让您自己进行决策,以便实现您的 UNIX 应用程序的广泛兼容性。 开发在多个平台上编译和构建的软件可能是一项非常复杂的任务。不同的 UNIX 变体间存在的各种大大小小的差异可能会导致问题。这既包括缺少工具和库的情况,又包括用于构建代码的头文件中存在差异的情况。考虑到这些差异后,您的代码的可移植性将更强,而这正是本教程中要讨论的主题。您还将了解如何使用 GNU 自动工具,以消除迁移和开发过程中的复杂和麻烦。
您将需要访问您系统上的 C 编译器,以对一些示例进行试验。为了对示例自动工具会话进行试验,您需要能够访问通过 GNU 网站提供的自动配置/自动工具包。
|
UNIX 不兼容性 之所以在构建应用程序时 UNIX 会出现问题,是因为所有 UNIX 分发并非完全相同(虽然很多都是基于标准组件和理念)。理解这些差异的影响是在不同环境中构建应用程序的第一步。 有许多不同的 UNIX 变体,包括各种免费版本,如各种 BSD (Berkeley Systems Division) 变体(FreeBSD、OpenBSD、NetBSD)和 Linux®。这些差异最初源自 UNIX 变体所使用的源 UNIX 分发。有两个源变体,来自 AT&T 的版本(最终发展为了目前的 SVR4)和加州大学伯克利分校(University of California Berkeley,UCB)所开发的版本(目前称为 BSD 版本)。 不同的公司的 UNIX 操作系统均以这其中的一个基本版本为基础,然后添加了供应商特定的扩展和增强功能。一些公司(特别是 Sun)更改了其 UNIX 支持的源。例如,Sun 就从 SunOS 中的 BSD 核心变成了在 Solaris 中使用 SVR4 核心。 System V Release 4 是现在商业分发中最常见的版本,包括基于原始 AT&T 核心且带有一些添加的 BSD 元素的核心。 结果是,尽管从技术而言,不同的 UNIX 版本(AIX®、HP-UX、Solaris 等)都是 UNIX,但系统之间存在的差异意味着采用了不同的库、头文件(甚至用于进行构建的工具也不同),从而意味着无法将 C 源代码方便地从一个平台迁移到另一个平台。 实际函数可能没有更改;而不同的仅是函数的位置和定义。
POSIX 组织进行了大量的工作,以对各种不同系统进行标准化,包括操作系统、实用工具和编程语言。这些标准涵盖了许多方面,既包括“标准”函数及其应返回的内容,也包括这些函数所依赖的 OS 的功能和特性。 对于 UNIX,有关的主要 POSIX 标准是 1003.1,该标准定义了应用程序和操作系统间的接口。 多家不同的公司均已采用了 POSIX 标准,包括 Sun Microsystems、IBM、Digital 和 Hewlett Packard。甚至 Microsoft 也在 Microsoft Windows 内提供了 POSIX 兼容层。 POSIX 1003.1 标准(也称为 POSIX.1)定义用于在 OS 内执行特定操作的函数名,包括参数、格式和调用顺序。该标准还指定预期的返回值和其他错误(包括错误代码相关的标准) 总之,任何符合 POSIX 的函数调用应在各种操作系统上都能正常工作,而无需进行任何修改。例如, 如果所构建的应用程序基于特定 UNIX 变体,则将您的系统迁移到 POSIX 标准是实现更好兼容性的第一步,但这并不能消除所有问题。
第一个任务是确定将要移植的应用程序的复杂性。典型的应用程序包含很多不同的元素。需要确定哪些元素需要进行重新开发,哪些需要加以调整,以兼容更多平台。 需要调查研究的元素包括:
当然,大部分 C 代码都具有平台兼容性。现在的大部分 C 编译器都基于相关标准,因此对代码进行编译不存在问题。相反,进行移植时出现的大部分问题都与库、头文件和构建环境中的限制相关。
|
常见的冲突区域 在这一部分,我们将讨论一些与处理头文件、库、构建工具和环境相关的重要问题。 移植过程的核心元素是可用的构建工具和环境。如果使用标准 Makefile,则这个过程要容易得多,但仍然需要考虑一些关键的差异。 例如,需要对提供的 C 编译器、C 预处理程序和工具( 例如,大部分 UNIX 变体都提供 可以通过使用免费软件工具来简化环境和相关过程,如 例如,标准 UNIX 另外,也要注意位于不同平台的不同位置上的不同工具。例如,Solaris 包括了 make 作为标准编译器(但不是 C 编译器),它位于 /usr/ccs/bin/make,而不是在很多其他平台上所使用的缺省路径,如 /usr/bin/make。 处理 Makefile 级别的这些差异的最简单方式(不使用 GNU 自动工具)是为每个环境创建独立的 Makefile,根据平台调用每个 Makefile。然后就能在使用
您仍然需要手动管理文件间的个体差异,但相应的过程应该会简单得多。
通常,通过使用 C 预处理程序指令来选择不同的代码片段,可以处理头文件和大部分体系结构和功能差异。 系统能够正常工作的原因是由于 C 代码在使用 C 预处理程序 ( 您事实上可能已经使用了指令来允许调试代码。例如,您可能会在 Solaris x86 环境内构建应用程序时指定以下定义:
通过头文件或在编译器的命令行上包含定义,可以指定此定义:
在源代码内,将随后通过检查此定义来标识要使用的正确源代码。例如,在清单 1 中就给出了定义的一个特例。 清单 1. 选择异常
还可以在 可以在整个源代码中使用相同的系统。例如,可以使用它包含不同的头文件(清单 2)和在 清单 2. 选择头文件
需要在整个代码中都采用相同的原则(假定知道需要什么内容)。
在不同的 UNIX 变体之间进行迁移所遇到的最常见问题是操作系统中提供的用于定义结构、变量类型和函数的头文件。 一些头文件具有不同位置。例如,limits.h 的内容或多或少算得上是 UNIX 平台上的标准头文件,但位于不同的目录中。例如,在 AIX 内,将使用以下代码导入该头文件:
但在 Solaris 内要使用以下代码:
在某些情况下,所需的信息或许不位于相同的文件中,或者与在原始平台中使用的定义不同,或者直接不可用。 在前一种情况,您需要找到所需的函数定义、结构或变量的位置。最后的方法是使用
所查找的定义与源平台上的定义匹配时,则可以根据情况插入备选定义。不过,请注意,不要更改系统或库调用的含义;更改前台函数定义并不会改变基础库函数。 如果定义不存在,则可能表明其所依赖的库或接口不存在。
不同的 UNIX 变体将组件放入不同的库中,可能要求包括一系列其他平台不需要的库。此类情况的一个典型例子就是网络方面的库。在很多 UNIX 平台上,构建网络应用程序所必需的库将在链接时自动包含到应用程序中。 不过,在 Solaris 内,必须具体地添加这些库,才能将其与您的应用程序链接:
没有办法列出很多不同 UNIX 变体和函数的所有不同选项和可能的差异。解决此问题的唯一办法是按应用程序进行处理。尝试编译应用程序,等待缺少符号警告。 例如,如果未包含数学库,可能得到与清单 3 中所示的类似的错误。 清单 3. 缺少符号错误
获得了缺少符号的列表后,可以查找目标平台上的手册页,并标识所需的库。 对于完全缺少的函数,可能需要自己构建这些函数,或从第三方库提供。例如,GNU glibc 包含很多您可能依赖的本机 UNIX C 库中不提供的函数。 自己构建函数时,可以创建包含不同平台的函数定义的单个 C 源文件,然后使用前面显示的直接方法来选择是否在不同平台上构建相应的函数。
除了头文件和库的基本知识之外,下面将开始了解非常特定于平台的差异,而此类差异通常更难于解决。 这些差异中的最基本的就是主机 CPU 的字节顺序。字节顺序会影响如何引用和访问多字节数据的方式。这是由于 CPU 操作方式造成的。 Big Endian CPU(包括 SPARC、PA-RISC 和 PowerPC)采用首先引用最高有效位的方式引用信息,而 Little Endian CPU(主要是 Intel)则采用首先引用最低有效位的方式引用信息。这样可能会导致将地址完全反向,如会从 ABCD 反向为 DCBA。 这并不会影响字符串,因为字符串基于单个字节,但这会影响多字节值(如 32 位地址),因为其中的字节顺序会导致在 Big endian CPU 和 Little endian CPU 上以不同的方式对待值。 此问题并不能简单地得到解决,但可以采用很多方法来引用和存储信息,而不依赖对多字节数据的直接访问,从而避免此问题。
不同 UNIX 变体内的不同 不同 UNIX 变体支持一系列不同的信号。很多核心信号都保持了一致性;例如, 还要注意,进程间通信(Interprocess Communication,IPC)方法并未标准化。尽管大部分 UNIX 变体支持 SVR4 IPC,仍然注意并没有这方面的全面支持。通常应采用更为开放的 IPC 标准,如命名管道。
|
使用 GNU 自动工具 目前所讨论的方法的唯一问题在于,这些方法均要求进行大量的管理工作,以引入和组织不同的元素。它们要求能够访问需要支持的所有不同的平台;为了囊括所有的不同选项和备选方法,可能会大幅度增加开发时间。 GNU 自动工具包是一个可产生配置规则和文件的框架集的系统。在新目标主机上运行配置脚本时,它会检查操作系统和基础代码的要求,并产生合适的头文件配置和构建环境(基于标准 Makefile),以用于在该主机上构建应用程序。 如果在计算机上构建过开放源代码应用程序,则可能会注意到以下序列:
由于自动工具系统依赖于特定平台的已预先知道的值的组合(例如,它知道目标类型、库和头文件可用性),并结合用于确定特定函数和头文件的可用性的运行时驱动信息,因此整个流程可以正常工作。 对于很多应用程序,您并不需要为了提高兼容性而对代码进行手动更改。另外,甚至不需要知道 显然,为了获得系统提供的各种好处,必须首先告知自动工具环境通常将如何构建应用程序,并使用自动工具系统扫描您的源代码,从而确定为了让您的应用程序正常工作所需的函数、库和其他组件。
使用自动工具的第一步是为应用程序创建合适的结构。对于此示例,将使用简单的计算器应用程序(请参见下载),该应用程序依赖于一个词法分析组件(要求使用 lex)和一个语法组件(要求使用 yacc)以及数个支持特定操作的其他文件。可以在清单 4 中看到相关的文件列表。 清单 4. 计算器的源文件
清单 5 中显示了手动编写的用于构建计算器的 Makefile。 清单 5. 一个手动编写的 Makefile
为了将应用程序设置为与自动工具一起使用,请删除 Makefile,创建一个新目录(可以将其命名为 calc),然后创建一个子目录 (src),以便将所有源文件复制到其中。清单 6 可以看到此布局——已经可以使用自动工具。 清单 6. 已经可以与自动工具一起构建的源文件
现在已经可以开始使用自动工具进行配置了。
需要在源目录的每个目录(包括项目的根)中创建一个 Makefile.am 文件。根中的 Makefile.am 用于引用其他目录的内容;每个源目录中的 Makefile.am 仅用于定义该目录中信息的构建过程和要求。 因此,对于您的示例,calc 中的 Makefile.am 将与以下所示的文本类似:
这直接指定了包含将使用配置系统配置(和构建)的环境的子目录列表。 您的 src 目录中的 Makefile.am 文件应与清单 7 中所示类似。 清单 7. 用于构建缺省目标的 src/Makefile.am 框架
各行的前缀非常重要:前缀用于标识选项应用到的目标。 清单 7 中的第一行指定执行 第二行指定将生成的应用程序的名称。第三行列出构建目标所需的源文件。请注意,尽管您的应用程序依赖于独立工具(即 lex 或 flex 和 yacc 或 bison)生成的 C 文件,但并不需要显式地指定此阶段。这是因为自动工具包已经基于其扩展名知道了如何从这些源文件构建 C 源代码。 第四行设置了一个常规选项,以将 清单 4 中的最后一行指定构建此目标所需的其他库。请注意,只需要按照使用 cc 时一样的方法指定库名称; 配置了基本构建过程后,需要让自动工具扫描源代码,并确定源代码中可能在不同环境中具有不同源和定义的函数和其他元素。
configure.ac 文件定义您源代码中特定于配置的元素。此文件的内容用于生成必要的配置脚本,以最终在目标主机上的构建环境进行配置。 您不必自己创建此文件;可以使用
不要担心错误——这些错误仅指示并没有可作为扫描基础的 configure.ac 文件。第一次运行此工具时,会创建名为 configure.scan 的文件。可以在清单 8 中看到此文件包含的内容。 清单 8. 缺省自动扫描配置
FIXME 元素可给出需要编辑的良好指示。首先,将该文件从 configure.scan 重命名为 configure.ac。 需要在此处重新配置一系列元素。从 然后添加代码行来配置系统进行
还需要更新 AC_CHECK_LIB 行,以在所使用的库内检查特定函数。对于此应用程序,只需要确保数学库 ( 结果文件应与清单 9 中所示类似。 清单 9. 最终的 configure.ac
该文件将由
自动工具系统通过生成配置文件和一个 config.h 头文件进行工作;相比之下,后者要重要得多,将用于包含构建脚本所需的所有必要定义信息。 需要将 config.h 头文件添加到 C 源文件,以便在编译期间加载配置信息。 可能要需要对代码进行其他一些次要的更改。例如,配置脚本假定 cal.c 之类的 yacc 文件将产生两个文件,calc.h 和 calc.c。请确保这不会覆盖任何现有内容或直接更改
现在已经有了一个 Makefile 框架和项目的配置框架。您需要将此转换为四个不同的组件:
此过程包含四个步骤。首先,生成
可以忽略此命令生成的任何警告,但应对错误加以注意;这些错误可能指示 configure.ac 中存在键入错误。然后,创建模板头文件:
现在可以生成模板 Makefile 了。在进行此操作前,应该创建属于标准工具配置的一系列文件。这些文件包括:
暂时可以只创建空文件:
现在可以创建 Makefile 模板了:
最后,需要生成配置脚本,以在目标主机上实际执行已确定的配置:
清单 10 所示为整个序列(及示例输出)。 清单 10. 配置构建序列
如果检查根目录,应该可以标识各个主要组件,如 清单 11. 示例目录结构
现在可以尝试对应用程序进行配置了。
要试验配置,直接运行 清单 12. 示例配置脚本执行
然后可以运行 make 来构建应用程序(请参阅清单 13)。 清单 13. 运行 make
可以通过使用
通过 NFS 连接共享文件并使用此方法在每次构建后进行“复位”,可以方便地在多个平台上进行测试,而不必将分发复制到每个主机上。
自动工具系统可帮助处理构建多平台兼容应用程序的大量复杂性,但并不是最终的解决方案。 自动工具解决方案无法考虑不属于分发文件一部分的特殊库、第三方库或您自己的库(虽然没有任何事情会阻止您添加它们,或者创建特定自动工具配置)。 自动工具也不能说明所使用的任何特定于 OS 的元素。如果所依赖的库或函数是 Solaris 特有的(例如,它是 Solaris 内核接口所特有的),则自动工具将无法解决此问题。 自动工具系统只能解决主要的 UNIX 问题及 C 库差异、其头文件关系和编译链接元素所需的构建环境。它无法为软件移植目的而模拟其他库和内核环境。 这并不意味着不应该使用自动工具;它可能是最有效的构建环境移植工具——但不要期望它能带来奇迹。为了最有效地使用自动工具,必须随时准备将其与本教程前面讨论的其他技术一起使用,以确保广泛的兼容性。 最后,无论进行多少工作,都不能替代使用目标平台进行实际测试。自动工具可以会简化构建过程,但不要期望它能消除进行调试和测试的需求。
|
总结 开发在多个 UNIX 平台下工作的应用程序必须了解影响编译和连接过程的基本问题。几乎在所有情况下,出现的问题与两个截然不同的领域相关:支持标准系统和内核功能的头文件,以及用于支持特定扩展和功能的库。隐藏在这些差异背后的是特定操作系统限制的复杂问题。例如,不同 UNIX 变体支持的信号的差异可能会带来问题。 其中的一些差异可以通过使用相关技巧绕过差异和问题来减少。为了简化头文件、库和构建环境问题,可以使用 GNU 提供的自动工具/自动配置包。这将通过结合使用已知差异和发现脚本来在构建过程中确定目标上的环境,然后构建一个合适的构建脚本和头文件来处理特定于平台的问题。 自动工具可以简化构建过程,但它并不是设计用来处理平台间的所有差异的。例如,它无法处理缺少的函数、库和核心 OS 差异,但它将在差异处理中扮演很重要的角色。
|
|
||||||||||||||||||||||||||||||
讨论
|
|
|||||||||||||||||