当前UNIX上的企业级软件大部分都是为了迎合大公司的商务需要。因而它必须支持新出现的技术,并能顺应迅速发展的市场潮流,比如强大而灵活的Linux操作系统的大量使用。由于这种软件大部分是大型的、多线程的而且是多进程的,所以将其移植到Linux面临着挑战。通过本文,可以获得在把某个企业级软件真正移植到Linux的过程中得到的清单和建议。
当前商务IT行为的一个实际情形是,很多组织正在将IT转移到Linux,因为它具备了作为系统平台的灵活性与稳定性。另一个实际情形是,舍弃现有企业级软件的代价过于高昂。这两种情形经常同时出现,但关键是要解决它们。
将企业级软件移植到Linux可能会面临很多有趣的挑战。每一个步骤都必须要小心--从做出设计选择,到获得可用的构建系统,再到最终得到要在Linux上执行的针对特定系统的代码。
本文基于我在RHEL和SLES发行版本中(在Intel和IBM eServer zSeries体系结构上运行C应用程序)所获得的经验,但是这些经验同样适用于其他发行版本。我将讨论一些在Linux上运行您的应用程序的计划和需要考虑的技术问题,包括以下内容:
* 获得可用的构建系统。
* 确定可行的操作环境。
* 尽量减少在多种体系结构上构建产品所投入的精力(Linux需要得到那些体系结构的支持)。
* 确定特定体系结构的变化,比如互斥锁定(mutex locking)。
* 使用新的编译器,尽可能为多种体系结构维持一个详尽的通用代码基(code base)。
* 确定IPC机制。
* 选择合适的线程模型。
* 按Linux特定的指导方针改变安装和包装方式。
* 确定信号选项。
* 选择解析器工具,比如lex/yacc。
* 做出全局化选择。
获得可用的构建系统
支持多个平台的产品通常要求指定将要运行产品的具体操作系统。这种通用代码通常保存在源目录结构的独立代码组成部分中。
例如,特定于操作系统的代码规划可能是类似这样:
src/operating_system_specific_code_component/aix(用于AIX)。
src/operating_system_specific_code_component/solaris(用于Solaris)。
src/operating_system_specific_code_component/UNIX(用于其他种类的Unix)。
下图从更为“图形化”的角度展示了特定于操作系统的代码规划。
图1 代码组织规划
获得Linux构建系统
首先,您应该为特定于Linux的代码创建一个目录,并将来自某个平台的文件置于其中。当您为Linux引入了一个新目录后,规划可能类似这样:
src/operating_system_specific_code_component/linux(用于Linux)
然后这将让我们得到一个类似如下的新的代码规划。
图2 新的代码组织规划
通常,应用程序的大部分代码通用于所有种类的Unix,也可用于Linux。经验表明,对于特定于Linux的代码,首先选择特定于Solaris的文件可以最小化向Linux移植特定于平台的代码所需的精力。
然后,修改makefile并引入特定于Linux的条目:
对将要使用的编译器的定义
* 程序库路径
* 线程库路径
* 编译器标记
* 包含文件路径
* 预处理程序标记
* 需要的所有其他内容
源文件中的很多改动与包含文件路径的修改有关系。例如,要使用变量errno,需要明确地包含。
在不直接包含特定于体系结构的包含文件(而是包含推荐文件)的所有地方,都必须要小心。例如,就像在中所提及的:
#ifndef
_DLFCN_H
# error "Never use
directly; include
instead."
#endif
您应该小心地使用指示符-Dlinux 或者单词“linux”。Linux上的预处理程序将单词“linux”翻译为数字1。例如,如果文件中有一个/home/linux路径,而且使用cpp来对此文件进行预处理,则输出文件中的路径将是/home/1。为了避免发生这种替换,预处理程序指示符可以是类似这样的:/lib/cpp -traditional -Ulinux。
通用编译命令
程序员通常所使用的编译器是gcc。典型的编译命令行可能类似这样:gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c -I。-fPIC帮助生成位置无关代码,等价于Solaris上的-fPIC。-ansi等价于Solaris上的-Xa。
对于共享对象,典型的链接时间指示符应该是gcc -fPIC -shared -o -L-l。-shared等价于Solaris上的-G。
对于拥有入口点的可重定位对象,典型的指示符可能是gcc -fPIC -shared -o -e entry_point -L-l。
在开始选择最佳操作环境之前,我将先分析在其他体系结构上编译代码所涉及的问题。
他体系结构上的编译
另一个需要考虑的重要事项是,程序员应该能够让代码尽可能容易地在其他体系结构上编译。构建系统应该为涉及的每种体系结构准备单独的定义文件。例如,用于x86体系结构的编译器指示符应该有-DCMP_x86标记,用于某些特定于pSeries服务器上的Linux的代码应该有-DCMP_PSERIES指示符。对于在x86体系结构的系统上进行的编译,具体构建定义文件中的编译命令行类似如下:
gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c -I -DCMP_x86
而下面的编译命令行用于在pSeries体系结构上进行的编译:
gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c -I-DCMP_PSERIES。
-CMP_x86和-CMP_PSERIES都是用户定义标记,当程序的特定于Linux的代码将要使用特定于体系结构的代码时都要使用它们。我的经验是,大部分用于Linux的应用程序代码都是与体系结构无关的,特定于体系结构的代码出现在需要编写汇编代码的地方。例如,如果您要使用比较(compare)和交换(swap)指令的实现来开发锁,那么您将要使用特定于体系结构的代码。
代码的安排应该使得在代码规划中特定于Linux的目录内不存在特定于体系结构的子目录。为什么?因为Linux已经为屏蔽体系结构细节做出了很多工作,应用程序的程序员通常不应该关心应用程序将要在哪种体系结构之上去编译。目标应该是,以最少的精力,对代码、代码规划和makefile文件进行最少的修改,就可以令为特定体系结构所编写的程序在其他体系结构上被编译。通过避免在linux目录中出现特定于体系结构的子目录,可以大大简化makefile文件。
linux子目录中的源文件中可能会有带有预处理程序指示符的代码形式,如下:
#ifdef CMP_x86
#elif CMP_PSERIES
#else
#error No code for this architecture in __FILE__
#endif
确定可行的操作环境
计划步骤的关键是确定应用程序要移植到Linux的哪个发行版本。您应该确保计划移植的程度所需要的所有软件都可用。例如,可能不能为Linux2.6发行版本发布某个中间件产品,因为在大部分典型配置中所使用的一个关键的第三方数据库在那个发行版本上不能用。最初提供的产品或者应用程序可能不得不改为基于Linux2.4发行版本。
应用程序交互所需要的某些软件,也可能并不是在应用程序所面向的所有发行版本和体系结构上都可用。对所选操作环境的可行性进行仔细研究。
需要考虑的另一个问题是,应用程序是32位的还是64位的,它是否要与其他也以32位或64位模式运行的第三方软件共存。
特定于体系结构的变化
应用程序中特定于体系结构的代码通常局限于少数地方。在本节我将考虑一些示例。
确定字节次序(endian-ness)
程序员不必担心是为何种体系结构编写代码。Linux在/usr/include/endian.h中给出了确定字节次序的途径。您可以使用下面的典型代码片断来确定操作环境是big-endian还是little-endian;您可以方便地设置具体的标记。
/* Are we big-endian? */
#include
#if
__BYTE_ORDER == __LITTLE_ENDIAN
#define MY_BIG_ENDIAN
#elif __BYTE_ORDER == __BIG_ENDIAN
#undef MY_BIG_ENDIAN
#endif
确定栈指针
可以编写内联程序集(inline assembly)来确定栈指针。
int get_stack(void **StackPtr)
{
*StackPtr = 0;
#ifdef CMP_x86
__asm__ __volatile__ ("movl %%esp, %0": "=m" (StackPtr) );
#else
#error No code for this architecture in __FILE__
#endif
return(0);
}
实现比较与交换
这里是为Intel体系结构实现比较与交换的一个示例。
bool_t My_CompareAndSwap(IN int *ptr, IN int old, IN int new)
{
#ifdef CMP_x86
unsigned char ret;
/* Note that sete sets a 'byte' not the word */
__asm__ __volatile__ (
"
lock\n"
"
cmpxchgl %2,%1\n"
"
sete %0\n"
: "=q" (ret), "=m" (*ptr)
: "r" (new), "m" (*ptr), "a" (old)
: "memory");
return re