当前商务 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)
然后这将让我们得到一个类似如下的新的代码规划。
Mrlinux 于 2005-12-17 08:57:07发表:
表 1. 从 pthreads draft 4 到 10 调用所发生的变化
pthreads draft 4 pthreads draft 10
pthread_setcancel(CANCEL_ON) pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL)
pthread_setcancel(CANCEL_OFF) pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL)
pthread_setasynccancel(CANCEL_ON) pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL)
pthread_setasynccancel(CANCEL_OFF) pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL)
pthread_getspecific(key, value) *value = pthread_getspecific(&key)
可选的线程模型包括 native Linux threads 和 Native POSIX Thread Library(NPTL)实现,后者为 Linux 中的线程提供了与 POSIX 兼容的实现。Linux 内核(从 2.5 版本起)已经得到修改,具备了 POSIX 兼容支持。在 SLES9 中 NPTL 是可用的。Red Hat 已经为 RHEL3(它基于 Linux 2.4 内核)反向移植(backport)了 NPTL 支持。RHEL 3 既支持 NPTL 也支持 native Linux threads。可以通过设置环境变量 LD_ASSUME_KERNEL=2.4.1 切换到 native Linux threads,但是很多提供商都已经将他们的软件移植到了支持 NPTL 的 RHEL3 上。
使用 native Linux threads 的主要障碍在于以下方面:
子线程 SIGCHILD 会送到线程而不是进程(非 POSIX 行为)。
getpid() 被破坏 ---- 获得构成进程的一组线程的 pid 非常困难。
在某个线程中改变用户标识,不能将此变化应用于进程中的所有线程。
简言之,每个线程看起来都像是一个单独的进程(其行为的某些方面也是如此)。
内核方面也有问题:
拥有成百上千线程的进程会令 /proc 文件系统几乎不能使用。每一个线程都表现为一个独立的进程。
信号实现的问题主要在于缺少内核支持。SIGSTOP 等专用信号必须由内核来为所有线程处理。
实现同步原语时对信号的错误使用会带来更多问题。发送信号不是确保同步的明智方法。
另一方面,使用 NPTL:
在最新的发行版本中(Linux 2.6 以后),信号问题似乎已经被解决。现在,信号可以作为一个整体发送给进程。
已经实现了 futex(快速用户空间互斥体(fast userspace mutex) 是在 Linux 上实现锁定和构建 semaphores 和 POSIX mutexes 等高层次锁定抽象的基本工具),它能令调用者在内核中等待并可以被显式地唤醒。这样,PTHREAD_PROCESS_SHARED 和进程间 POSIX 同步原语可以被实现,而且现在可用。
它使用 1:1 模型(每个用户级线程有一个底层的内核线程),并且可以抢占(内核线程可以被抢占)。
它适用于极消耗 I/O 和 CPU 的应用程序。
其目标是实现百分之百 POSIX 兼容。
基于各个发行版本的改进,使用支持 NPTL 的 Linux 版本通常是明智的。
文件系统,使用参数,栈
在移植工作中,我的小组发现了许多变化多样的事情,由于它们相对较为简单,在此我将集中进行介绍。
对文件系统的支持
如果您的应用程序需要使用记录日志和写入数据文件等工具,那么,相对于原始的 I/O,基于文件系统的支持更便于安装、配置和管理。
系统使用参数
好像不存在收集参数信息(比如内存堆的使用)的直接的系统调用。要确定此类参数,需要利用 /proc 文件系统的支持。
Stackwalk
当前,只有在 Intel 体系结构上支持 pstack 等调用;在其他体系结构上的支持还在开发的过程中。要通过程序进行栈的追踪,程序员可能不得不使用 ABI 定义为尚未得到支持的体系结构去实现他们自己的版本。
另一个选择是使用基于 gdb 的脚本来获得栈的信息。产品的高可维护性通常要用到栈的信息。 gdb 更为标准化,可跨不同体系结构和发行版本使用。
内核映射和共享内存段的使用
如果应用程序使用共享内存段,那么必须要小心地设置共享内存段的起始位置,除非用户想依赖系统所提供的初始地址。另外,不同的体系结构具有不同的内存映射支持;共享内存可用的区域也可能不同。
例如,在 Intel 体系结构中,每个进程将底部四分之三的地址空间分配给用户区域;顶部部分分配给内核。这意味着任何 Linux 进程所占的全部内存最多可达 2 GB(390)或者 3 GB(Intel)。这个总数必须包括文本、数据和栈段,再加上所有的共享内存区域。在 Linux/390 上,用于共享内存的区域从 0x50000000 开始,并且必须在 0x7fffa000 之前结束。如果您想要在应用程序将要支持的所有体系结构上保持通用的起始地址,那么在确定那个地址之前必须考虑所有的体系结构。
信号
与其他 Unix 平台相比,信号 ---- 发送启动和停止某个传输或者其他操作的控制信号 ---- 并没有太大的不同,只是信号号码可能不一样,或者有些信号在某些发行版本(RHEL AS 3)上不可用,比如 SIGEMT。(要获得关于 Solaris 和 Linux 之间信号区别的详细资料,请参阅 参考资料 部分的参考文献。)
配置内核参数
程序员可能会被要求去调整内核的某些参数,以使得应用程序能够在运行期进行调整。如果是那样,则需要考虑的一些重要参数包括 threads-max(每个进程的最大线程数)、shmmax、 msgmax 等等。在 /proc/sys/kernel 中配置参数列表。可以使用 /sbin/sysctl 系统调用来配置这些参数。如果您正在移植某个大型的多线程应用程序,那么 threads-max 参数可能会尤其重要。
lex/yacc 等解析器工具
要做好准备,您在 AIX 或者 Solaris 上所编写的语法某些部分可能不能直接在 Linux 上使用。例如, yylineno(一个不正式的 lex 扫描器内部变量)等某些变量默认情况下可能不能直接在 Linux 上使用。下面的代码片断可以检查 yylineno 是否得到了直接的支持。打开一个名为 a.l 的文件(其内容如下):
%{
%}
%%
%%
然后输入 lex a.l。在 lex.yy.c 中搜索“yylineno”。如果那个变量不可用,有两种可能的支持 yylineno 的解决方案,即在 Linux 中为 lex 使用 -l 选项(换句话说,执行 lex -l a.l),或者将代码修改为如下所示:
%{
%}
%option yylineno
%%
%%
某些发行版本(比如 SLES 9)默认并没有附带 yacc,但附带 bison。如果需要 yacc,可能得去下载它。
全局化问题
在 Linux 上某些代码页的命名可能会不同。例如,AIX 上的 IBM-850 在 Linux 上可能另外命名为 ibm850, ISO8859-1 可能被另外命名为 ISO-8859-1。如果应用程序消息编录依赖于这些代码页中的某些,而且需要代码页转换,则可能必须修改脚本(使用 iconv 工具可以完成)。在 Linux 上,ja_Jp、en_US 等大部分常见的位置都可用。
安全性考虑
在新的发行版本中(RHEL AS 3),基于套接字的通信默认得到了保护,所以,如果您正在实现基于 IP 的服务器,其进程要监听某个特定的端口,那么您应该向 iptables 中添加新的服务。 iptables 用于建立、维护和检查 Linux 内核中的 IP 数据包过滤器规则表。
例如,首先您可能必须添加一个新的链,比如 /sbin/iptables -N RH-Firewall-1-INPUT,然后在那个链中添加新的服务,像这样:iptables -I RH-Firewall-1-INPUT -s 0/0 -i eth0 -m state --state NEW -p TCP --dport 60030 -j ACCEPT (新的目标端口 60030 被映射到 /etc/services 中的某个服务)。
定位安装的软件包和变量数据
遵循 Linux 社区所提出的打包(packaging)忠告是一个好主意 ---- 这些忠告有助于防止 /opt 中的代码和 /var 中的应用程序数据出现混乱。这些忠告建议将提供商与软件包名称包含在存放代码和数据的位置。作为说明,考虑下面的示例。
假设 IBM 开发了一个名为“abc”的应用程序。那个软件包理论上应该安装在 /opt/ibm/abc 中。相关的数据应该位于 /var/opt/ibm/abc 中,而不是简单的位于 /var 中。
测试
将一个新产品移植到新的平台,需要对那个产品进行详尽的测试。需要特别注意的方面包括进程间通信、打包、系统间通信(AIX 与 Linux,或者 Solaris 与 Linux 之间的客户机-服务器)、一致存储(出于字节次序的因素)、数据变换格式,等等。
外部文档可能会发生变化,所以应该进行彻底的文档复查。
移植到 Linux 测试用例需要依照开发工作适当地阶段化。在开始进行完全产品测试之前,应该先测试一些可交付使用的中间产品。这将帮助您在产品开发的早期阶段找出问题。(要深入了解高效通用测试方法,请参阅 参考资料 部分关于 XP 的参考文献。)
结束语
在本文中我简单涉及了移植的不同阶段,包括针对不同 OS 范围的设计选择、创建合适的目录结构、创建构建系统、完成代码修改,以及测试。我也强调了那些需要集中精力关注的地方,比如信号、共享内存、互斥体和条件变量、线程以及特定于体系结构的变化。本文基于在向 Linux 2.6-based 系统中移植大型的多线程应用程序时所获得的实际经验,我希望这个清单能帮助您节省时间和精力。
每次移植的细节都会发生变化,但是我所列举的基本概念(结合下面的参考文献中的材料)将使您更容易地完成移植的过程。
Mrlinux 于 2005-12-17 08:56:41发表:
图 2. 新的代码组织规划
。
中所提及的:
directly; include instead."
。
-I。 -fPIC 帮助生成位置无关代码,等价于 Solaris 上的 -fPIC 。-ansi 等价于 Solaris 上的 -Xa。
-L -l。 -shared 等价于 Solaris 上的 -G。
-e entry_point -L -l。
-I -DCMP_x86
-I -DCMP_PSERIES。
通常,应用程序的大部分代码通用于所有种类的 Unix,也可用于 Linux。经验表明,对于特定于 Linux 的代码,首先选择特定于 Solaris 的文件可以 最小化 向 Linux 移植特定于平台的代码所需的精力。
然后,修改 makefile 并引入特定于 Linux 的条目:
对将要使用的编译器的定义
程序库路径
线程库路径
编译器标记
包含文件路径
预处理程序标记
需要的所有其他内容
源文件中的很多改动与包含文件路径的修改有关系。例如,要使用变量 errno,需要明确地包含
在不直接包含特定于体系结构的包含文件(而是包含推荐文件)的所有地方,都必须要小心。例如,就像在
#ifndef _DLFCN_H
# error "Never use
#endif
您应该小心地使用指示符 -Dlinux 或者单词“linux”。Linux 上的预处理程序将单词“linux”翻译为数字 1。例如,如果文件中有一个 /home/linux 路径,而且使用 cpp 来对此文件进行预处理,则输出文件中的路径将是 /home/1。为了避免发生这种替换,预处理程序指示符可以是类似这样的: /lib/cpp -traditional -Ulinux
通用编译命令
程序员通常所使用的编译器是 gcc。典型的编译命令行可能类似这样: gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c
对于共享对象,典型的链接时间指示符应该是 gcc -fPIC -shared -o
对于拥有入口点的可重定位对象,典型的指示符可能是 gcc -fPIC -shared -o
在开始选择最佳操作环境之前,我将先分析在其他体系结构上编译代码所涉及的问题。
其他体系结构上的编译
另一个需要考虑的重要事项是,程序员应该能够让代码尽可能容易地在其他体系结构上编译。构建系统应该为涉及的每种体系结构准备单独的定义文件。例如,用于 x86 体系结构的编译器指示符应该有 -DCMP_x86 标记,用于某些特定于 pSeries 服务器上的 Linux 的代码应该有 -DCMP_PSERIES 指示符。对于在 x86 体系结构的系统上进行的编译,具体构建定义文件中的编译命令行类似如下:
gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c
而下面的编译命令行用于在 pSeries 体系结构上进行的编译:
gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c
-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 的哪个发行版本。您应该确保计划移植的程度所需要的所有软件都可用。例如,可能不能为 Linux 2.6 发行版本发布某个中间件产品,因为在大部分典型配置中所使用的一个关键的第三方数据库在那个发行版本上不能用。最初提供的产品或者应用程序可能不得不改为基于 Linux 2.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 ret;
#else
#error No code for this architecture in __FILE__
#endif
}
选择 IPC 机制
可选的 进程间通信(interprocess communication)(IPC)机制 ---- 用于应用程序间通信和数据共享的机制 ---- 通常包括使用 信号、编写可加载的内核扩展 或者 使用进程共享的互斥体(mutex)和条件变量。
信号是最容易实现的,但是在多线程环境中必须要小心,因为派生的所有线程都具有类似的信号掩码(signal mask)。在进程结构的建模中,通常应该只有一个线程来处理信号,否则信号就可能被发送到任意的线程,导致结果可能会不可预知。线程可能是由不受应用程序控制的其他参与实体在进程中派生的,这个应用程序可能不能控制它们的信号掩码。出于这个原因,信号可能并不是在大型多线程应用程序中进行 IPC 的流行方式。例如,在应用程序服务器中运行的应用程序可以派生它们自己的线程,可以捕获对应用程序服务器进程有实际意义的信号。
内核扩展不容易编写,可能不能方便地在支持 Linux 的多种体系结构间进行移植。
随着 POSIX draft 10 标准的出台,以及它在 Linux 2.6 上的实现的可用,进程共享互斥体(互斥对象:允许多个程序共享同一资源但不能同时使用的程序)和条件变量成为在多进程环境中实现 IPC 机制的合适选择。此机制要求建立共享内存,在其中存放互斥体和条件变量,所有进程都有对这些结构体的共同的引用。
选择线程模型
正要移植到 Linux 的某些老应用程序非常有可能是基于 pthreads draft 4。最新版本的 Linux 支持 pthreads draft 10,所以需要小心地对调用进行适当的映射。如果应用程序使用了某些基于第三方实现的异常处理机制(比如,DCE 提供的 TRY-CATCH 宏),那么程序员需要确保那些异常处理代码也与 pthreads draft 10 相兼容。