第三章 可移植性
3.1 可移植性的需要
概念:编写的软件可以尽可能地被各种不同的计算机所使用,而不需要在这一过程中对软件进行大量修改。 软件具备可移植性的正式解释:如果将一个软件移植并使其适应新环境所需要付出的努力小于从头开始重建同一个软件所需要付出的努力,我们就说这个软件具备可移植性。
UNIX是一个可移植的操作系统,因为它使用C编程语言进行了重写。因为可以对不同的计算机编写C编译器,所以对UNIX操作系统的移植是完全可行的。
POSIX标准,可移植操作系统接口,使得软件开发人员可以更容易地编写可移植软件,使得软件可以运行于不同的UNIX系统上。POSIX最后又被SUS所补充(单一UNIX规范)。
20世纪90年代,SUN公司的绿色计划,java虚拟几,一次编译就可以在移植了虚拟几的任意平台上运行。JVM+JAVA.
3.2 Linux的可移植性
3.2.1 抽象层
Linux系统的核心是Linux内核,内核的标准版本身就支持24中不同的硬件平台架构。从某种程度上来说,Linux内核的主要目的就是为了提供在这些不同的机器硬件之上的一个可移植软件抽象。
标准系统函数库,负责提供常用的软件例程。而内核则负责处理繁琐的硬件细节。这样,就不需要在编写普通应用程序的时候关注于机器的底层硬件了。
现在的程序员,不需要关注机器中的硬件,而只需要关注内核之上提供给你的软件环境。不同的Linux发行版以及单个系统软件和函数库的版本将给你带来很多麻烦。
在资源受限系统里,可以使用伎俩以提高性能,但是程序是不可移植的。
3.2.2 Linux发行版
用户关心的是发行版的可移植性,而不是软件将运行在哪一个硬件平台上。不同的Linux发行版的根本区别,是在于发行商提供的(或支持的)软件包的集合。
1. 软件包
不同的应用程序和工具会以软件包的形式提供给用户,在软件包里还会提供文档之类的描述性数据和最重要的依赖信息。通过发行商提供的汇总已知软件包状态的数据库和软件包中的依赖信息,Linux发行版的软件包管理软件可以自动解决软件包之间的依赖关系。
依赖关系的解决需要Linux发行版的软件包管理程序确定在安装一个特定的软件包之前必须先安装哪些其他的软件包。而现代化的包管理软件则可以自动处理必须的依赖软件包的获取和安装,而不需要用户手动做这些事情。例如RPM和DPKG两种管理系统。
2. Linux标准化规范
事实上的标准用于确定普通文件系统的位置、函数库的有效性、配置文件和应用程序以及其他一些注意事项等,包括了LSB(Linux标准化规范) www.freestandards.org. LSB使用RPM作为其可移植性软件包格式概念的基础,并对LSB兼容的软件包的命名和内部内容做了各种约束。
但LSB不是灵丹妙药,只是一个使可移植性变得简单的机会。
3. Linux厂商的角色
在考虑当前发展趋势和利益变化的前提下,不断跟踪上游社区软件的版本,并决定在发行版中包含什么软件包。
作为一名开发人员,选用什么样的发行版和发行版的版本,都是由你自己决定。
4. 基于RPM的发行版&派生自Debian的发行版
Red Hat Package Manager yum的使用 Redhat, Suse等
debian dpkg apt synaptic www.debian.org debian, ubuntu等
5. 源代码发行版
Gentoo, Slackware(直接操作二进制tarball和其他档案文件)
6. 自己动手
嵌入式应用场合经常使用/大型金融机构为了审计和大规模部署偶尔也会创建自己的精简版Linux。优化也是一个问题。
www.linuxfromscratch.org
3.2.3 建立软件包
厂商提供的补丁的作用:修改软件包中所包含软件的功能(编译时),以使它们符合特定发行版的标准。
1. 建立RPM软件包
a. 编写spec文件
所有的RPM软件包都是根据在spec(规格)文件中包含的一系列指令建立的。spec文件描述了RPM的建立过程:首先获取程序的源代码,然后根据发行版版本情况给软件打补丁,针对特定的编译环境进行编译,最后生成一个单独的软件包文件。该软件包能在一个目标系统上被安装和删除。spec文件的段落描述:
通用头信息:包含了包名称、用途摘要、版本、许可证、源代码最初来源。这里可以执行宏代换,例如:
Name: hello
Summary: A simple hello world program
Version: 1.0 Release: 1
License: GPL
Group: Applications/Productivity
URL: http://p2p.wrox.com/
Source0: %{name}-%{version}.tar.gz
Patch0: hello-1.0-hack.patch
Buildroot: %{_tmppath}/%{name}-buildroot
%description
This is a simple hello world program which prints
the words "Hello, World!" except we've modified it
to print the words "Goodbye, World!" via a patch.
Prep 指明为了准备要编译的源代码所必须采用的行动
Build 告诉RPM如何编译源代码。
Clean: RPM完成编译后的清理工作的执行
Post: 软件包安装之后被执行,包括启动新添加系统服务的命令、执行安装后更新的命令、提醒用户软件需要额外的配置等。
Files: 告诉RPM哪些目录和文件将被安装
Changelog: 软件改变的历史和日志
Extras: 可选择的可选段落。
b. 建立一个示范的RPM
c. 为源代码打上补丁
d. 配置构建环境
vim ~/.rpmmacros
%home %(echo $HOME)
%_topdir %{home}/rpm
mkdir -p $HOME/rpm/{BUILD RPMS SOURCES SPECS SRPMS}
然后将对应的文件拷贝到相关位置。
e. 启动构建过程
rpmbuild -ba /home/***/rpm/SPEC/hello-1.0-1.spec
结果是:
[root@develop rpm]# ls RPMS/i386/
hello-1.0-1.i386.rpm hello-debuginfo-1.0-1.i386.rpm
然后我们安装它并运行它
[root@develop i386]# rpm -ivh hello-1.0-1.i386.rpm
Preparing... ########################################### [100%]
1:hello ########################################### [100%]
The files are now installed, here we do anything else we need to.
[root@develop i386]# hello Goodbye, World!
2. Debian软件包
Debian软件包由dpkg软件包管理工具制作。与RPM一样,dpkg遵循由一个单独的文本文件所指定的一系列指令。这个文件名字是control。
解压缩开文件后使用下列命令可以创建出一个debian软件包:
dh_make -e dash@eb.com -f ../hello-1.0.tar.gz
选择s类型的包,接着会在当前目录下出现一个debian目录,control文件的内容是:
Source: hello
Section: devel
Priority: extra
Maintainer: dash <dash@eb.com>
Build-Depends: debhelper (>= 7)
Standards-Version: 3.7.3
Homepage: <insert the upstream URL, if relevant>
Package: hello
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description:A simple Hello World Example program.
Source: 软件包自身的名字
Package: 应用程序自身的名字
Section: RPM软件包的GROUP
构建Debian软件包,Makefile的写法:
all: hello
install:
mkdir -p ${DESTDIR}
cp hello ${DESTDIR}
clean:
然后运行:
dpkg-buildpackage -rfakeroot
生成的包有和文件有·:
hello-1.0 hello_1.0-1_amd64.changes hello_1.0-1_amd64.deb
hello_1.0.orig.tar.gz hello-1.0.tar.gz
可能问题:在构建软件包的时候,用户没有安装GP,正确分发的Debian软件包应该包括GPG(PGP)签名
Debian开发者: DD: Debian Developer. Debian New Maintainer's Guide可以获得更多信息。
3.2.4 可移植的源代码
首先看一下目录结构:
dash@Rachel:~/code/myarticle$ tree
.
|-- include
| `-- hello.h
`-- src
|-- hello.c
`-- hello_msg.c
2 directories, 3 files
然后我们执行autoscan,得到configure.scan的内容为:
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.61)
AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS)
AC_CONFIG_SRCDIR([src/hello_msg.c])
AC_CONFIG_HEADER([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
AC_HEADER_STDC
AC_CHECK_HEADERS([stdlib.h unistd.h])
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_OUTPUT
修改几行即可:
FULL-PACKAGE-NAME 为程序名称,VERSION为当前版本, BUG-REPORT-ADDRESS为bug汇报地址
修改
AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS)
为
AC_INIT(Hello, 1.0, BUG@sina.com)
修改
AC_OUTPUT
为
AC_OUTPUT(Makefile src/Makefile)
代表我们需要两个地方的Makefile
增加:
AM_INIT_AUTOMAKE([1.9])
AC_PROG_LIBTOOL
修改完后,改名为configure.in,内容如下:
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.61)
#AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS)
AC_INIT(Hello, 1.0, BUG@sina.com)
AC_CONFIG_SRCDIR([src/hello_msg.c])
AC_CONFIG_HEADER([include/config.h])
AM_INIT_AUTOMAKE([1.9])
AC_PROG_LIBTOOL
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
AC_HEADER_STDC
AC_CHECK_HEADERS([stdlib.h unistd.h stdio.h])
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_OUTPUT(Makefile src/Makefile)
分别建立根目录下和src目录下的 Makefile.am
内容为:
dash@Rachel:~/code/myarticle$ cat Makefile.am
SUBDIRS = src
dash@Rachel:~/code/myarticle$ cat src/Makefile.am
bin_PROGRAMS = hello
hello_SOURCES = hello.c
hello_LDADD = libhello.la
lib_LTLIBRARIES = libhello.la
libhello_la_SOURCES = hello_msg.c
一切就绪后,可以敲命令开始autotools这套东西了:
然后执行
aclocal
autoconf
autoheader
libtoolize
automake --add-missing
最后,就可以通过./configure, make , make install 来使用autotools后生成的结果了。
国际化:
Linux国际化的推动:开放国际化倡议Open Internationalisation Initiative (OpenI18N),可以在www.openI18N.org找到更多的关于OpenI18N规范的内容
Canonical公司(ubuntu发行版的制作者)发布了一个Rosetta的项目(www.launchpad.net),它允许各种能力的人通过Web界面快速低发布针对流行Linux应用软件的语言翻译。
dash@Rachel:~/code/myarticle$ locale -a
C
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZW.utf8
POSIX
字符集的使用:
软件厂商联合起来,共同合作以产生了一个通用的字符编码系统解决方案,改方案可以普遍适用于处理未来软件不断增长的对语言本地化的需求,这一工作的主要结果就是Unicode标准。
宽字符的使用: wide character 用wchar_t来表示,多字节数据类型,用于在内部表示一个Unicode字符串中的每一个字符。需要包含头文件wchar.h,一个wchar_t有4字节宽。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <wchar.h> int main(int argc, char **argv) { char *sc = "short characters"; wchar_t *wc = L"wider characters"; printf("%ls are more universally useful than %s, ", wc, sc); printf("\n"); printf("but they do use more space (%d as opposed to %d bytes).\n", wcslen(wc)*sizeof(wchar_t), strlen(sc)); exit(0); }
wcslen用于计算宽字符串中包含的字符数,这里对应于strlen返回的量。printf中需要用l前缀来表示打印出的是宽字符。而声明的时候也需要用L来注明。printf有一个对应宽字符的版本叫wprintf,可以对字符宽度的控制有很好的灵活性。
单一UNIX规范中有包含了支持宽字符显示和转换的API函数的完整列表。如果要实现真正的可移植Unicode,需要更一步约束自己,使用只由ISO C90标准定义的函数和表示方法。
man -k wide characters icov: 在不同字符编码之间转换的简易方式。
本地语言化
gettext可以帮忙解决翻译的问题,可以把翻译外包给那些有语言天赋的人以获得额外的好处。
gettext可以简单地利用一个普通的ASCII文本字符串并使用它作为某一软件对应翻译表格的索引。每一个语言翻译被存储在.po(可移植对象)文件中,可以与软件一起分发。
/* A sample gettext program */ #include <stdio.h> #include <locale.h> /* LC_ environment variables */ #include <libintl.h> /* Internationalized library */ #include <stdlib.h> #define _(String) gettext (String) #define gettext_noop(String) String #define N_(String) gettext_noop (String) int main(int argc, char **argv) { setlocale(LC_ALL, ""); bindtextdomain("gettext_example", "."); textdomain("gettext_example"); printf("%s\n", gettext("Hello, World!")); return 0; }
定义的那些宏的定义可以在自由软件基金会的网站上找到。setlocale调用使得其后的gettext调用将它的翻译基于用户可以设置的或者还没有设置的各种环境变量。
|-- en_GB | `-- LC_MESSAGES | `-- gettext_example.mo |-- en_US | `-- LC_MESSAGES | `-- gettext_example.mo |-- gettext_example |-- gettext_example.c |-- gettext_example.po |-- gettext_example_en_GB.po |-- gettext_example_en_US.po |-- gettext_example_zh_CN.po `-- zh_CN `-- LC_MESSAGES `-- gettext_example.mo
# gettext_example_en_US.po内容 # gettext_example # Jon Masters <jcm@jonmasters.org>, 2006. #: gettext_example.c:18 msgid "Hello, World!" msgstr "cup of coffee?"
执行下列命令:
xgettext -ao gettext_example.po gettext_example.c msgfmt -o zh_CN/LC_MESSAGES/gettext_example.mo gettext_example_zh_CN.po
对应于不同的LANG设置,我们可以得到针对"Hello,World!"字符串的不同的翻译。
在非C/C++的编程语言中,例如更高级的语言Perl和Python实现了它们自己的内部Unicode处理机制。在图形化界面设置中,也有对应的国际化问题的解决方案。
3.3 硬件可移植性
3.3.1 64位兼容
int i = (int)some_pointer;
转换为:
void *p = some_pointer;
3.3.2 字节序中立
1234的存储方式: big-endian: 1234 little-endian: 4321
对于字节序的判断,还有两种不同的判断方法。在<深入理解计算机系统>里有提到过。
/* * Test the endianess of this machine */ #include <stdio.h> #include <stdlib.h> static inline int little_endian() { int endian = 1; return (0 == (*(char *)&endian)); } void broken_endian_example() { union { int i; char j[sizeof(int)]; } test = {0xdeadbeef}; int i = 0; for (i=0;i<sizeof(int);i++) { printf("test.j[%d] = 0x%x\n", i, test.j[i]); } } int main(int argc, char **argv) { printf("This machine is "); little_endian() ? printf("little") : printf("big"); printf(" endian\n"); printf("This program was build on a machine that is: "); #if BYTE_ORDER == BIG_ENDIAN printf("big endian\n"); #else #if BYTE_ORDER == LITTLE_ENDIAN printf("little endian\n"); #else printf("something weird\n"); #endif #endif printf("and here's a silly example...\n"); broken_endian_example(); exit(0); }