Solaris 被认为是风格与 Linux™ 最为接近的一种 UNIX®,但是对于程序的迁移来说,它们在诸如内存映射、线程以及对自然语言的支持等领域还是有很大区别的。这个移植指南可以为您在计划将程序移植到 Linux/x86 上时提供一些建议,并且帮助您理解开发环境和体系架构之间的区别。
在各种风格的 UNIX 中,Solaris 被认为与 Linux 的风格最为接近。因此在开始将大型的基于 Unix 的应用程序移植到 Linux 上之前,首先要从 Solaris 中挑选出那些依赖于操作系统的代码。即便如此,在那些依赖于体系架构的领域、内存映射、线程或一些特殊的领域(例如系统管理和自然语言的支持),它们之间还是有差异的。
本文对这些差异进行了讨论,并加以对比,从而能够对您从运行在 32/64 位 SPARC 体系架构上的 Solaris 迁移到运行在 x86 体系架构上的 Linux 提供一些帮助。对于 Solaris 来说,这种讨论是基于版本 8 及更新的版本的。对于 Linux 来说,这种讨论着重于那些在基于 x86 处理器的服务器上可用的发行版本:SUSE LINUX Enterprise Server 9 和 Red Hat Enterprise Linux AS V3 或 V4。
本文内容包括:
- 移植规划
- 开发环境(编译器,make 工具,等等)
- 依赖于体系架构或系统的区别
移植规划
下面 6 个步骤为从 SPARC 平台上的 Solaris 成功迁移到 x86 平台上的 Linux 提供了一个完整的路线图。如果您曾经将应用程序从一个操作系统移植到另外一个操作系统上,那么这些步骤可能就会听起来非常熟悉:
- 准备
- 环境和 makefile 的变化
- 编译器修正
- 测试和调试
- 性能优化
- 打包并分发
步骤 1. 准备
正确准备的关键是了解某些领域之间的差异,例如:
- 系统调用
- 文件系统的支持
- 依赖于机器的代码
- 线程
- 内存映射
- 系统调用
- Endianness
在移植程序时,要确保有关的第三方包在目标平台上都是可用的。对于 32 位的应用程序来说,要考虑是否有必要迁移到 64 位的版本。还要确定在目标平台上使用哪种编译器。在基于 x86 的 Linux 平台上,可以使用 gcc 作为编译器。
步骤 2. 环境和 makefile 的变化
在这个步骤中,您将设置开发环境,这包括确定环境变量,修改 makefile,并对环境进行必要的修改。在这个步骤之后,您应该准备好开始编译自己的应用程序了。
在准备好进入下一个步骤之前,这个步骤可能会需要几次反复。
步骤 3. 编译
在这个步骤中,您将修正一些编译错误,链接错误,诸如此类。在能够得到一个干净的编译产品之前,这个步骤可能需要多次反复。
步骤 4. 测试和调试
在应用程序成功编译之后,要对其进行测试,看看是否存在运行时错误。在测试时,有些领域可能会涉及客户机/服务器的通信、数据交换格式、数据编码的转换(例如从单字节编码转换为多字节编码)以及数据的永久保存。
步骤 5. 性能优化
现在移植后的代码可以在目标平台上运行了。监视其性能,确保所移植的代码可以如我们所期望的一样工作;如果不能,就需要对性能进行优化。
有两个很好的性能分析工具:Performance Inspector 和 OProfile。Performance Inspector 提供了一组工具来判断 Linux 上的应用程序中的性能问题。从 2.6 版本的内核开始,OProfile 已经成为对代码进行优化分析的推荐工具。OProfile 在 RHEL4 中也可以使用。(有关这些工具的更多信息,请参阅 参考资料。)
步骤 6. 打包和分发
您要将所生成的代码分发给其他人吗?如果需要,就要确定打包的方法。
Linux 提供了几种方法对应用程序进行打包,例如 tarball、自行安装的 shell 脚本或 RPM。RPM 是 Linux 上广泛使用的包管理系统。Solaris使用 pkgadmin 作为自己的包管理器。
在 Solaris 中,pkgadmin 所使用的包规范模板文件与 RPM 所使用的规范文件不同 —— 将打包信息从模板文件转换成规范文件需要很多的努力。使用诸如 InstallShield for Multiplatforms(ISMP)之类的软件包可以在这两种平台上产生通用的打包软件,从而减少移植的努力。通过使用 ISMP,应用程序开发就可以使用一个跨平台的通用规范文件。
开发环境
现在让我们来看一下 Solaris 和 Linux 开发环境的一些差异,包括以下内容:
- Makefile
- 编译器和链接选项
- 32 位到 64 位的移植考虑
GNU Make 和 Solaris Make 的比较
如果您在原有的平台上使用的是 Solaris 的 Make,那么要在 Linux 上使用 GNU Make,就需要对 makefile 进行修改。AIX 5L Porting Guide (请参阅 参考资料 中的链接)的第 6 章详细介绍了二者之间的区别。
编译器和链接选项
正如前面介绍的一样,对 x86 上的 Linux 可用的编译器是 GNU GCC。下表是 SUN Studio C/C++ 编译器所广泛使用的一些编译选项,以及 GNU GCC 中的等价选项。
表 1. Solaris 到 Linux 的等价编译器选项
SUN Studio | GNU GCC | 说明 |
-# | -v | 通知编译器在编译的过程中显示信息。 |
-### | -### | 跟踪编译过程,而不会真正调用任何操作。 |
-Bstatic | -static | 让链接编辑器只查找那些名为 libx.a 的文件。 |
-Bdynamic | (缺省值) | 通知链接编辑器在给定 lx 选项时,首先查找名为 libx.so 的文件,然后查找名为 libx.a 的文件。 |
-G | -shared | 产生一个共享对象,而不是一个动态链接的可执行程序。 |
-xmemalign | -malign-natural, | 指定假定内存边界对齐的最大值,以及访问不对齐的数据时的行为。 |
-xO1, -xO2, -xO3, -xO4, -xO5 | -O, -O2, -O3 | 在编译过程中选择对代码进行优化的级别。有点类似于 O2。 |
-KPIC | -fPIC | 生成位置无关的代码,用于大型共享库。 |
-Kpic | -fpic | 生成位置无关的代码,用于小型共享库。 |
-mt | -pthread | 为多线程代码进行编译和链接。 |
-R dirlist | -Wl, -rpath, dirlist | 将动态链接库的搜索路径编译到可执行文件中。 |
-xarch | -mcpu, -march | 指定目标体系架构的指令集。 |
表 2 解释了不同风格的 Linux 上的不同级别的 C 编译器之间的区别。
表 2. 不同级别的 C 编译器之间的区别
操作系统 | 内核级 | GCC 级 | Glibc 级 | Gnu binutils 级 | JDK 级 |
SLES9 | 2.6 | 3.3.3 | 2.3.3 | 2.15.90 | 1.4.2* |
RHEL3 | 2.4 | 3.2.3 | 2.3.2 | 2.14.90 | 1.4.2 |
RHEL4 | 2.6 | 3.4.3 | 2.3.4-2 | 2.15.92.0.2-10.EL4 | 1.4.2* |
Solaris 10 | Solaris 10 | 3.3.2 | 2.2.3 | | 1.4.2 |
注意: * 在 64 位的 IBM JDK 1.4.2 for Linux on x86 中并不支持 LinuxThreads。Java 支持 NPTL 64 位线程库。
还有,如果您要在 SLES9 上编译 C++ 程序,并将其部署到 RHEL4 上,那么就需要使用 compat 库,它在包 compat-libstdc++-33-3.2.3-47.3.i386.rpm 中。之所以需要这个 compat 库,是因为在 SLES9 和 RHEL4 之间需要不同级别的 ABI 兼容性。SLES9 遵循 LSB 1.3 标准,而 RHEL4 遵循 LSB 2.0 标准。
32 位到 64 位移植的考虑
要将应用程序从 32 位环境移植到 64 位环境中,您有两种选择:
- 让应用程序可以容许 64 位操作
- 开拓目标平台上的 64 位操作能力
首先,将应用程序在一个 64 位的环境中使用,然后试图开拓目标平台的 64 位能力。
尽管 32 位的应用程序通常都可以在 64 位的目标平台上很好地运行,但是在 64 位环境中启用应用程序也是非常重要的,因为有些第三方的产品会以 64 位模式启动,而不提供 32 位版本的库对象。
在迁移到 64 位的环境中时,您需要决定是否继续支持该产品的 32 位版本。如果需要支持这两个版本,那么就需要适当的时间进行打包和测试。
开拓 64 位的能力应该是在实现容许 64 位操作之后的下一个步骤。在开始这种尝试之前,应该仔细衡量一下开拓 64 位能力的优点。
应用程序从 32 位到 64 位的迁移这个主题在很多出版物中已经进行了介绍(例如 参考资料 中列出的 AIX 5L Porting Guide 的第 3 章)。
体系架构和系统特有的区别
现在,让我们来看一下 Solaris 和 x86 上的 Linux 之间一些特定于体系架构和系统的区别,包括:基本的数据类型,内核参数的设置,体系架构特有的区别,endianness,系统调用,信号,数据类型,以及线程库。
参考资料 提供了在进行大型的移植尝试时,需要考虑的一个可行的检查清单。
基本的数据类型和对齐方式
在一个系统中有两种不同类型的数据类型:基本数据类型(basic data type)和派生数据类型(derived data types)。
基本数据类型 是由 C 和 C++ 语言规范所定义的所有数据类型。表 3 对 x86 上的 Linux 和 SPARC 上的 Solaris 这两者的基本数据类型进行了比较。
表 3. 比较 Linux 和 Solaris 的基本数据类型
| Linux(x86) | | Solaris(SPARC) |
基本类型 | ILP32 (大小) (单位为字节) | LP64 (用于基于 IA64 的系统) (大小) (单位为字节) | ILP32 (大小) (单位为字节) | LP64 (大小) (单位为字节) |
char | 1 | 1 | 1 | 1 |
short | 2 | 2 | 2 | 2 |
Int | 4 | 4 | 4 | 4 |
float | 4 | 4 | 4 | 4 |
long | 4 | 8 | 4 | 8 |
pointer | 4 | 8 | 4 | 8 |
long long | 8 | 8 | 8 | 8 |
double | 8 | 8 | 8 | 8 |
long double | 12 | 16 | 16 | 16 |
在两个平台之间或 32 位与 64 位模式之间移植应用程序时,您需要考虑不同环境中对齐设置之间的区别,从而避免出现性能降低和数据崩溃的危险。
表 4 介绍了 x86 上的 Linux 中以字节为单位的对齐值。
表 4. x86 上的 Linux 中的对齐值(以字节为单位)
数据类型 | Linux IA-32 (ILP32) | Linux IA-64 (LP64) |
Bool | 1 | 1 |
Char | 1 | 1 |
wchar_t | 4 | 4 |
int | 4 | 4 |
Short | 2 | 2 |
long | 4 | 8 |
long long | 8 | 8 |
Float | 4 | 4 |
Double | 4 | 8 |
long double | 4* | 16 |
注意: 对齐取决于所使用的编译器开关。这些开关控制着“long double”类型的大小。i386 应用程序的二进制接口规定这个大小是 96 位,因此 -m96bit-long-double
在 32 位模式中是默认值。现代的体系架构(Pentium 及更新的体系架构)更喜欢将“long double”按照 8 字节或 16 字节进行对齐。在符合 ABI 规范的数组或结果中,这是不可能的。因此指定一个 -m128bit-long-double
会将“long double” 按照 16 字节边界进行对齐,它会为“long double”填充 32 位的 0。
系统派生的数据类型
派生数据类型是已有的基本类型或其他派生类型的一个派生物或结构。系统派生数据类型 可以根据所采用的数据模型(32 位或 64 位)和硬件平台而具有不同的字节大小。
表 5 给出了 Linux 上与 Solaris 上不同的一些派生数据类型。
表 5. Solaris 和 Linux 中的派生数据类型
OS | gid_t | mode_t | pid_t | uid_t | wint_t |
Solaris ILP32 l | long | unsigned long | long | long | long |
Solaris LP64 | int | unsigned int | int | int | int |
Linux ILP32 | unsigned int | unsigned int | int | unsigned int | unsigned int |
Linux LP64 | unsigned int | unsigned int | int | unsigned int | unsigned int |
机器特有的代码的例子
在下面这些情况中需要机器特有的代码:
在 Linux-x86 平台上的堆栈指针可以实现为:
清单 1. 在 Linux-x86 上获取堆栈指针
int get_stack(void **StackPtr){ *StackPtr = 0; __asm__ __volatile__ ("movl %%esp, %0": "=m" (StackPtr) ); return(0);}
|
在 Solaris,下面这段示例代码让您可以获取堆栈指针:
清单 2. 在 Solaris 上获取堆栈指针
.section ".text" .align 8 .global my_stack .type my_stack,2my_stack: ! Save stack pointer through 1st parameter st %sp,[%o0] ! Compute size of frame in return result reg retl sub %fp,%sp,%o0 .size my_stack,(.-my_stack)
|
在 Linux x86 上,您可以使用一个 compare 和 swap 操作来实现原子锁。例如,下面是 Linux x86 上使用 compare 和 swap 操作的一个简单实现:
清单 3. Linux x86 上的 Compare 和 swap 操作
bool_t My_CompareAndSwap(IN int *ptr, IN int old, IN int new){ 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;}
|
在 Solaris SPARC 上,加锁的原子操作可以如下实现:
清单 4. Solaris 上的原子锁
.section ".text" .align 8 .global My_Ldstub .type My_Ldstub,2My_Ldstub: ldstub [%o0],%o0 ! Atomic load + set sll %o0,24,%o0 ! Zero fill ... retl ! ... result register srl %o0,24,%o0 ! ... and retrn .size My_Ldstub,(.-My_Ldstub)
|
字节顺序(endianness)
由于 SPARC 采用的是 big-endian,而 x86 采用的是 little-endian,因此您需要考虑 endianness 的问题。
Endianness 的问题在以下这些情况中都变得非常重要:移植客户机要在异构网络环境中进行通信的应用程序,将数据永久地保存到磁盘上,产品跟踪(在 SPARC 平台上所生成的跟踪信息必须能够在 x86 平台上正确地格式化),以及其他相关领域。IA-64 Linux 内核默认使用 little-endian,但是也可以使用 big-endian 的字节顺序。
系统调用
Solaris 系统调用使用了一种在 Linux 上是不可用的不同的命名和签名机制,这在“从 Solaris 向 Linux on POWER 迁移指南” 中已经介绍过了。以下这些系统调用现在在 RHEL4 中可以使用:
Waitid
Putmsg
putpmsg
Getmsg
getpmsg
Curses 库
Linux 用于 xurses 库的那些库函数比 Solaris 更加类似于 AIX。例如,诸如 addwch
之类的函数在 Linux 中不受支持。有些函数,例如 mvchgat
,在 Solaris 中不可用。当您将与 curses 有关的代码从 Solaris 移植到 Linux 上时,很多代码都需要重写。
Terminal IO
Solaris 中的 termio 结构与 Linux 中的这个结构是不同的。Linux 中的 termio 结构多了几个域,您可以使用它们来设置输入和输出速度。
Linux 中 termio 结构的定义位于 /usr/include/bits/termios.h 文件中,而 Solaris 中 termio 的定义则位于 /usr/include/sys/termios.h 文件中。
IOCTL
执行 ioctl 可以使用的选项在 Solaris 和 Linux 中是不同的。例如,要获得资源的利用情况,您可以向 ioctl 系统调用传递 PIOCUSAGE:
Return_code = ioctl("/proc/<pid>", PIOCUSAGE, &buff) ;
在 Linux 中,您必须直接从 /proc/<pid> 目录中读取相关的文件来获得相关信息,在 Linux 上不能使用 PIOCUSAGE
选项。在这两种情况中,pid
是正在执行命令的当前进程的进程 id。
另外一个例子是当您希望获取该进程的堆信息时。在 Solaris 中,要获取堆信息,您可以使用下面的方法:
Return_code = Ioctl("/proc/<pid>",PIOCPSINFO,&psinfo)
在这段代码中,psinfo
是一个 struct prpsinfo
结构;Prpsinfo.pr_size
就给出了该进程的堆大小。
在 Linux 中,正在使用的页数可以从 /proc/<pid>/mem 和 /proc/<pid>/stat 中获得。页面大小可以使用系统调用 getpagesize
获得。这两个值(页数和页面大小)的乘积就是堆的当前大小。
Getopt
在 Linux 中,只有在设置了 POSIXLY_CORRECT
环境变量之后,getopt
调用才遵循 POSIX 标准。
如果设置了环境变量 POSIXLY_CORRECT
,那么对参数的分析一碰到非选项参数(以“-”开始的参数)就立即停止。 其余的参数全部被解释为非选项参数。
调用 shell 脚本之间的区别
如果 shell 脚本使用了 su
命令,就会派生一个子 shell。这种行为与 Solaris 不同,在 Solaris 中, su
命令不会派生一个新 shell。
如果您在 Linux 上执行命令 su - root
,那么 ps
命令的输出就会如下所示:
30931 pts/4 00:00:00 su31004 pts/4 00:00:00 ksh
|
在这段代码中,30931 是进程 31004 的祖先进程。您可能要修改一些受到这种关系影响的脚本。
重启动时的设备处理
从 RHEL4 开始,Linux 就采用了热插拔子系统 udev 的概念。它提供了可配置的动态设备命名的支持。设备配置是在每次重新启动时动态构建的。
例如,如果您有一个新目录 /dev/dsk,那么系统可能并不知道它的存在,这个目录在重启之后可能就会丢失。为了避免 /dev/dsk 目录丢失的问题,可以建立一个 /etc/udev/devices/dsk 目录,这样系统在重新启动时就可以维护这种信息,因此即使系统重新启动之后,/dev/dsk 也依然会存在。
内核参数
在 Solaris 中,内核参数可以在 /etc/system 文件中进行设置。在 Linux 中,内核参数可以使用 sysctl
系统调用进行修改。可以修改的参数在 /proc/sys/kernel 中都可以看到,procfs
的支持对于 sysctl
正常工作来说是必需的。
信号
信号之间的区别在“Technical guide for porting applications from Solaris to Linux”中已经详细介绍过了。其他值得注意的一些区别是 Solaris 有 38 个信号,而 Linux 使用了 31 个信号,而且 sigaction
结构也不相同。例如,在 Linux 中,sigaction
接口如下所示:
清单 5. Linux 中的 sigaction 结构
struct sigaction { /* Signal handler. */#ifdef __USE_POSIX199309 union { /* Used if SA_SIGINFO is not set. */ __sighandler_t sa_handler; /* Used if SA_SIGINFO is set. */ void (*sa_sigaction) (int, siginfo_t *, void *); } __sigaction_handler;# define sa_handler __sigaction_handler.sa_handler# define sa_sigaction __sigaction_handler.sa_sigaction#else __sighandler_t sa_handler;#endif /* Additional set of signals to be blocked. */ __sigset_t sa_mask; /* Special flags. */ int sa_flags; /* Restore handler. */ void (*sa_restorer) (void); };
|
在 Solaris 中,sigaction 结构如下所示:
清单 6. Solaris 中的 sigaction 结构
struct sigaction { int sa_flags; union {#ifdef __cplusplus void (*_handler)(int);#else void (*_handler)();#endif#if defined(__EXTENSIONS__) || ((__STDC__ - 0 == 0) && \ !defined(_POSIX_C_SOURCE) && !defined(_XOPEN_SOURCE)) || \ (_POSIX_C_SOURCE > 2) || defined(_XPG4_2) void (*_sigaction)(int, siginfo_t *, void *);#endif } _funcptr; sigset_t sa_mask;#ifndef _LP64 int sa_resv[2];#endif};
|
由于所支持的信号个数不同,siginfo_t
结构也不相同。对于 Linux,这可以在 /usr/include/bits/signal.h 文件中找到;对于 Solaris,这可以在 /usr/include/sys/siginfo.h 文件中找到。
线程支持和 IPC
有关 Linux on POWER 与 Solaris 之间进行移植的出版物已经介绍了 Solaris 线程与 Linux 的 POSIX 线程(由 NPTL 库提供)之间的区别。
我的经验是,如果应用程序早已在 Solaris 上就使用了 POSIX 线程,那么它们移植到使用基于 NPTL 线程的 Linux 上就非常简单。在 Linux 上还可以利用一些新特性,例如进程共享互斥锁,因此 Solaris 中使用 pthread 互斥锁和条件变量的那些有关 IPC 机制的代码在 Linux 中都可以使用。
自然语言的支持
在 Solaris 中对于自然语言的支持的代码也与 Linux 中可能会有所不同,或者它们只是名字有所不同而已。在 Solaris 中大部分的 locales 和代码页在 Linux 中都有对应的内容。
结束语
从 Solaris 到 x86 上的 Linux 的移植工作在大部分情况中只是一个重新编译的过程,或者对编译器/链接器开关稍微进行一些修改。有时可能需要对某些平台特有的内容进行一些修改,例如加锁、内存映射、线程,等等。您应该了解一下这些差异,并对移植好好进行一下规划,从而减少移植所需要的时间。