当前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 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相兼容。 下表中是从draft 4到10所发生的变化的示例。 表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(它基于Linux2.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进程所占的全部内存最多可达2GB(390)或者3GB(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 系统中移植大型的多线程应用程序时所获得的实际经验,我希望这个清单能帮助您节省时间和精力。 每次移植的细节都会发生变化,但是我所列举的基本概念(结合下面的参考文献中的材料)将使您更容易地完成移植的过程。
[1] [2] 下一页
来源:赛迪网
(出处:http://www.sheup.com)
(出处:http://www.sheup.com/)