在某台计算机上模拟其他计算机的历史已经很长,通常是为了使用遗留应用程序,或者是为了使用更稳定、响应更快的OS的系统上运行流行的OS而编写的应用程序。随着Linux越来越流行,当开发人员规划将运行于非Linux系统上的二进制程序时,需要审视他们的选择。本文将研究模拟器的功能,并将详细地研究硬件模拟和软件模拟的问题。
在某台计算机上模拟其他计算机已有多年的历史。模拟老的计算机的一个常见原因是怀旧,不过不可否认,很多模拟器能够非常出色地运行多种计算机游戏。模拟其他计算机的另一个原因是为了使用只存在于某个特定平台上的应用程序软件。
通常,应用程序模拟以占据较大市场份额的平台为目标。例如,WINE 项目尝试去提供一个运行 Windows二进制程序的途径。
不过,近年来Linux已经被证明是一个稳定而且全能的操作系统;因而,它的市场份额也有所增长。市场份额的增长激起了人们对模拟Linux的兴趣。本文评论了在其他系统上进行Linux二进制程序模拟的现状,并着重指出为了让人们更方便地在模拟环境中运行他们的二进制程序,开发人员应该紧记的一些问题。
基本的模拟器 模拟器的思想很简单。计算机是充分可预测的。如果您想确切地了解如果给出一段特定的代码计算机会做什么,那么通过建立这台计算机的模型就可以得到结果。当然,这会涉及到相当多的开销,但是,如果要模拟的计算机比正在进行模拟的计算机老得多,那么模拟环境将比原来的机器更快。
有一些模拟层,比如NetBSD的Linux模拟层,只是提供某个环境的软件部分的模拟,从 Linux 程序库取得系统调用,并处理返回结果,使得看起来像是在使用 Linux内核。其他的模拟层,比如VirtualPC,可以模拟整台计算机,包括处理器。模拟处理器的速度会更慢,但是可以带来更好的兼容性。
发行版本格式的模拟器 尽管本文重点关注的是在其他平台上运行Linux二进制程序的方法,但是,经过编译的二进制程序发行版本同样占有一席之地。随着Linux模拟环境越来越普及,Linux二进制程序格式成为发行简单程序(不给出源代码)的一个可行方法。Linux二进制程序可以在多种系统上运行,无可否认,有时需要付出一些代价——以Linux二进制程序格式作为通用发行版本格式还会遇到一些挑战。
通常,模拟不足以让您在为另一个系统构建的程序中运行为某个系统构建的共享对象。如果您的产品大部分是以共享程序库对象的形式发行的,那么这些产品可能不会被加载到其他平台上。
有人认为,使用Linux二进制程序格式来向其他平台发行代码是疯狂的。也许这很疯狂,但它是可行的。近几年,我的主要的Web浏览器就一直在模拟环境中运行(更不用提字处理器、文档转换器,甚至信用卡处理软件)。
我们乐于使用的大部分软件应用程序都是商用的,并且,能够发行可以运行在很多平台之上的单一的二进制程序会使商业软件供应商大大受益。如果有多种多样的Linux模拟环境可用,那么Linux二进制程序格式会表现为第一个真正的软件发行版本选择。
噢,移植源代码是与发行有很大区别的任务;通常,移植任务更为简单。
完全硬件模拟器 完全硬件模拟器会模拟一台完整的机器;不只是处理器,还包括机器所有其余部分。例如,被模拟的计算机可能拥有自己的键盘控制器和视频卡。
完全硬件模拟常用于使用较老机器的程序。MAME街机游戏(arcade game)模拟器就是一个流行的示例,它模拟了多种老式街机游戏机的硬件。
就某些方面而言,完全硬件模拟器是进行模拟的最简单方式。很多工作都需要构建一个完全硬件模拟器,但是一旦您拥有这样一个模拟器,所有的事情就都可以迎刃而解。例如,用于 Macintosh 的 VirtualPC 版本 3 开始支持 Linux。
硬件模拟可以解决使用其他方式难以解决的问题。例如,我以前有一个BIOS闪存工具,仅以用于DOS的自解压缩的映像文件的格式发布。更糟糕的是,运行它的机器必须在传统的 ISA 软盘控制器上安装实际的软盘(我的 Windows 桌面机有一个 LS-120 驱动器)。通过模拟来解决这个问题吧!我在模拟器下运行该程序,将数据写入已经插入 Mac 的一个 USB 软盘驱动器。
硬件模拟也有其不利方面。为了让一切都能够运转,需要付出很大努力。如果需要网络,那么还需要很好地模拟网络芯片,以使得操作系统可以在这个芯片上运行。此外,模拟本身所没有的指令的代价可能非常高昂。通常,像这样一个系统可以近乎完美地运转,但是,与时限(timing)相关的功能可能会不可靠。
完全硬件模拟器已经使用了很长时间,最适合处理速度可能受模拟影响的遗留系统和代码。
虽然如此,想要在 Macintosh 或者任何其他非 x-86 机器上运行x86Linux二进制程序的用户,为了尝试运行程序,可能要完全依赖于某种当前可用的x86模拟器。在类似这样的系统上,大部分工具程序将运行得非常好(虽然可能较慢)。要担心的惟一一个主要顾虑是,为了提高性能,这种系统的用户可能安装较小的或者较老的 Linux 发行版本。使用 32 MB 内存来运行模拟机器的那些人不可能运行最新版本的 KDE。
部分硬件模拟器 部分硬件模拟器是一个中间解决方案:它们模拟一台计算机,但是这台计算机只能是与它们实际上所在的计算机类型相同的计算机。由于执行的速度与宿主机器相当,所以类似这样的程序可以降低模拟的成本。此类模拟器的示例包括 Serenity Virtual Station 和 VMWare。
当您拥有用于多种操作系统的应用程序,而且需要同时运行它们时,这些系统最为实用。类似于完全硬件模拟,这样的系统将运行一个完全的 Linux OS 环境,只要您的程序能够适当地跨Linux系统移植,那么就没什么问题。不过,再次声明,Linux的移植到较老版本的可移植性将有非常有用。使用虚拟机的人们可能愿意在这样的系统上运行一个较老的、占空间较小的 Linux 版本。
软件模拟器 在模拟世界中,软件模拟器是最基本的。软件模拟器不在某台虚拟机上运行您的应用程序——它不通过虚拟机,而是实时地去运行它。建立一个环境,在这个环境中,程序的代码可以正常运行,但是,程序访问操作系统的尝试会被通过某个模拟层来发送,这样,这些程序就可以使用了。WINE是一个极好的示例(虽然是用于Windows),尽管它并不是一个正式的模拟器。
有一些软件模拟器是由用户显式地调用,比如可用于 SCO 和 Solaris系统的lxrun程序。有些软件模拟器则构建成为UNIX内核对加载二进制映像的支持 —— 如果程序看起来不正确,那么,可以将它与一个可能模拟器表相对照,以查看它们是否可以运行它。
软件模拟器通常会带来最好的用户体验。不需要特殊的设置,不需要庞大的磁盘映像。程序只需要去运行即可(大部分情况下)。不过,访问系统调用、共享程序库以及文件系统结构会引发许多问题,所以,接下来我们将讨论它们。
系统调用 系统调用是模拟中最简单也是最困难的部分。系统调用具有明确定义的接口,而且,通常可以方便地检测并处理调用机制——这是简单的部分。困难之处在于可能难以或者不可能较好地实现系统调用。传统上,Linux模拟中最难以处理的是clone()系统调用。这个调用提供了获得简单线程的一个强制方法,即创建两个共享许多内容进程,共享的内容可以包括内存、文件描述符、信号处理——换句话说,可以包括任何内容和所有内容。不幸的是,如果您的操作系统不具备与此完全类似的功能,那么没有任何办法来实现这个系统调用。
更糟糕的是,由于当POSIX线程还没有完善或获得广泛支持之前,clone()就已经出现,并经常被用作POSIX线程的替代,所以,许多程序都以多种令人兴奋的、复杂的且(我必须要说)意想不到的方式来使用它。
如果您想让人们运行您的二进制程序,那么尝试让他们不要使用针对特定操作系统的系统调用;最好使用标准的POSIX系统调用。这是软件开发的一个良好的习惯做法。
基于内核的模拟器可以捕捉到到达它的系统调用。用户空间模拟器,比如 lxrun,会等待应用程序尝试进行系统调用。由于 Linux 系统调用功能与 Solaris 或SCOUNIX上的系统调用功能不同,所以结果是发生一个代码段错误。然后,lxrun程序像一个调试器那样纠正这个错误并使系统调用继续运行——但是,实际上,它已经截取了这个系统调用,并向底层操作系统进行相应的系统调用,而且解决了所有问题。聪明!
文件系统结构 文件系统的问题通常更为微妙。访问文件系统极其简单。不简单的是如何找到您想要的文件。
如果您的程序在模拟环境中运行,那么要访问的文件系统可能与您开发程序时使用的文件系统有本质上的不同。例如,如果您的程序使用了/proc文件系统(常用来获得内核状态和信息),那么在较新的内核中常见的特性在较老的系统中可能并不存在。
这里的开发人员比专有系统上的开发人员拥有巨大的优势,因为不同的Linux发行版本以不同方式安排文件,所以大部分程序员都非常清楚如何避免过分依赖于文件系统设计。但是 —— 有时 —— 不得不将文件名嵌入到程序之中。
许多模拟器解决这个难题所采取的一个方案是:建立一个针对文件系统调用的额外的解释层。例如,在 NetBSD 的 Linux 模拟环境代码中,首先根据 /emul/linux中的文件检查对文件的访问,之后才对系统真正的root目录中的文件进行检查。这就使得当Linux二进制程序不能使用标准文件时,系统可以提供“覆盖(override)”系统文件的文件。
实际上,这一方法的主要用途在于程序库和其他支持文件,不过也同样提供了许多系统二进制程序。例如,如果 Linux 二进制程序尝试调用 uname 来得到内核版本,却得到了NetBSD的版本号,这将非常令人迷惑。取而代之,它应得到所预期的 Linux 版本号。
共享程序库 如前所述,共享程序库是能够被模拟的二进制程序找到却不能够被系统二进制程序找到的一个非常好的例子。由于在不同的系统上共享程序库的格式和 ABI 细节可能各异,所以不能随意假定所有的系统都可以共享某个给定的程序库。名称可能冲突 —— 例如,当前NetBSD和SUSE7.3都拥有一个名为libncurses.so.5的文件。重要的是要使用其中正确的那一个。
共享程序库为开发人员指出了另外一个注意事项。了解不同的系统正在使用的程序库版本很重要。现在,NetBSD 的 Linux 模拟环境正在使用的是 SUSE 7.3 共享程序库。仍然有使用9.1共享程序库的代码,但是它们会获得警告,告之它们不能稳定地进行内核级模拟。
模拟环境软件包通常远远跟不上市场的步伐。即使您觉得大部分预期用户都应该拥有了相当新的Linux发行版本,但是大批模拟器还是几乎全都有些跟不上时代。
共享程序库还引发了另一个顾虑——不是每个系统都包含全部共享程序库。模拟环境软件包通常不会安装所有最新的共享程序库。而且,更麻烦的是,它们的用户也不太可能有能力轻松地安装所缺少的软件包。
在这些情况下,最大限度地减少对新特性和非核心共享程序库的依赖是一个好办法。模拟器用户可能会遇到这些问题。
不要误以为使用静态程序库就可以保证解决这些问题。静态程序库可能引入其自己的新的依赖,而且不容易检查到它们。如果静态地链接了一个使用某个不可移植的系统调用,那么通过重写算法来避免这个系统调用将没有什么用处。动态链接让您构建的程序能够在更大范围内的系统上运行。
调用其他程序的程序
有一种特别的情形比任何其他情形更令人们头疼,尤其与安装器相关。在很多系统上,调用 /bin/sh 所得到的 shell 不是bash。这就意味着使用 bash 扩展的脚本可能不能在其他系统上运行。
这就陷入了模拟器中的一个特别错综复杂的逻辑中。当执行二进制程序时,操作系统可能知道的足够多,可以核对相关的Linux二进制程序的Linux路径,而且它可能在那里安装 bash 的一个副本。但是,当您运行一个脚本时,内核不会将其看作是一个 Linux二进制程序;它发现脚本附带有一个解释程序路径,当尝试加载解释程序时,它将不再运行于模拟模式之下。
可移植shell脚本技术在这里得到了应用。当用户运行被模拟的应用程序时,这是要面对的最常见问题之一。安装器可能会因为不是可移植的 shell 脚本而不能运行。
类似于标准的开发,只是更为标准 为了方便那些可能要在模拟环境中运行您的程序的用户,开发软件时需要紧记以下事项,并且开发任何软件时都应该紧记这些事项:
尽可能遵循适当的标准。
避免“专门特性”。
不要挑战极限(push the envelope)。
而且,只要可以避免,就不要依赖于一个月前刚刚发布的某些东西来构建您的代码。因为那样做将缩小您的有效的目标市场。
本文地址:http://com.8s8s.com/it/it32631.htm