引导Linux:历史和未来
译自http://www.almesberger.net/cv/papers/ols2k-9.ps.gz。
作者: Werner Almesberger
W. Almesberger是LILO和initrd的设计者。这篇文章是偶然在其主页上发现的。看看大牛对boot loader理解应该会很有趣,因此斗胆试译一二。由于技术、见识均极有限,过程中难免囫囵吞枣、词不达意,还望智者指正。
请留意本文成文于2000年,因此有很多新的技术,比如ramfs,initramfs等并未包含其中。但从讲解原理的角度来看,论述已经很充分了。
概述
引导一个操作系统意味着在以下几个方面取得妥协:
× 功能简单、通常不很稳定的系统环境(例如PC上的BIOS);
× 操作系统自身要求的功能;
× 用户想要创建的(有些情况下)相当复杂的系统设置。
自早先的从软驱引导以来,Linux的引导过程经历了蓬勃的发展,形成了几种具备多种功能的boot loader,如LILO、LOADLIN,GRUB等等,并籍此开发出了相应的镜像格式,越来越多的任务可以在系统启动之前就由boot loader完成,例如,可以在挂载根文件系统之前就加载驱动模块。
随着时间的推演,引导过程也变得越来越困难:新的设备越来越多,随之而来的是更多的问题;系统配置愈加复杂,而这些系统也要求被正确引导起来;新的功能不断被加入到内核,有一些功能,比如一个新的文件系统,有可能会影响到引导过程。
引导过程不得不应对的这些复杂性在系统安装时则变本加厉,因为要考虑理论上无限可能的系统配置,同时存储空间又是有限的。一般而言,要确保一张软盘的容量要足以容纳第一阶段的所有工作内容。
本文描述Linux的引导过程,它要完成的任务以及实现的方法。除了对现有技术的说明,还讲解了一些通用的设计理念,并对新近出现的一些技术做了探讨。
1. 一般性介绍
引导过程可以被划分为两个阶段:
(1). 将kernel载入内存并将系统的控制权转交给它;
(2). 初始化系统环境。
图1说明了执行这两个阶段的一种可能做法。
尽管本文着眼于i386架构,但很多概念是架构无关的。
1.1 加载内核
上述阶段(1)由boot loader完成。它要从某种介质上,如磁盘,或网络上的一个服务器,读取内核映像和一些额外的数据,并将之加载到正确的内存位置,或许还要改变CPU的模式,并最终运行内核。
除此之外,boot loader往往还需要执行其它的任务,比如将从固件或命令行中获取的参数传递给内核。有些boot loader还可以选择要引导的操作系统(多引导)。
我们将在第二章详细讨论boot loader的职责以及它们的设计。
1.2 启动内核
一旦内核开始运行,它首先初始化自身必需的数据结构,随后检测硬件并加载相应的设备驱动,直到它为运行第一个用户空间的进程做好准备。在运行用户进程之前,内核需要为进程提供一个文件系统,所以它必须首先挂载根文件系统(root file system)。
为了挂载根文件系统,内核需要:
(1) 知道根文件系统位于那种介质之上;
(2) 具有访问该介质的驱动。
对于最常见的系统配置 — 根文件系统是磁盘上的一个ext2分区 — 来说,再简单不过:将该磁盘作为参数传递给kernel,将ext2驱动编译到内核之中。
1.3 复杂性
如果内核没有包含该设备的驱动,情况就变得有些复杂了。这种情形对于那些“通用”的内核来说尤为常见,因为不可能把所有驱动都编译进内核。
这个问题可藉由initrd机制解决,该机制容许在挂载真正的根文件系统之前先使用RAM disk。RAM disk由boot loader加载。第三章将详细讨论initrd。
尽管initrd已被证明行之有效,但其在挂载initrd后再行挂载根文件系统的机制也从未让人完全满意。另外,内核中的其它变动也使得以一种“干净的”方式使用initrd愈加不可能。第四章讨论这些问题。
1.4 未来
引导过程面临着三项挑战:
(1) 那些和引导过程接口的固件和硬件的功能越来越强 — 历史教训,这意味着更多的bug。
(2) 容纳内核映像的那些文件系统越来越复杂,比如支持日志的文件系统或RAID,对于boot loader来说,正确解读之中的内容也相应地越来越难。
(3) 从外部引导,比如通过一个安全的网络链接。
图2:引导过程面临来自三个方面的挑战
与其让boot loader应对日益增长的复杂性, 不如转交给内核。第五章详细描述这个主题。
2. Boot loaders
一个boot loader要完成以下工作:
- 明确要加载什么
- 加载内核和其它必要的数据
- 为内核建立运行环境,比如将CPU设置为特权模式
- 运行内核
2.1 boot loader的分类
不同的boot loader,其大小和功能都不尽相同。 我们将讨论如下四种:
- 专用的boot loader, 如SYSLINUX, Netboot
- 运行在另一种操作系统上的通用型boot loader,如LOADLIN,ArLo
- 基于固件的支持文件系统的通用型boot loader, 如GRUB, SILO
- 基于固件的不支持文件系统的通用型boot loader, 如LILO
图3. 不同的boot loaders和不同的系统层接口,并利用其下层提供的服务
专用的boot loader往往只需要识别一种存储介质,比如flash或软盘,而且驻于其上的内核映像的格式也是已知的。
运行在其它操作系统上的boot loader通常利用宿主机提供的服务读取内核。这样一来,boot loader本身就不必再去了解存储介质的属性以及位于其上的文件系统。这种类型的缺点之一是当加载内核时要特别当心,以保证宿主系统直到新加载的linux系统运行起来时都要保持正常,比如,要留意不能写到为宿主系统保留的内存空间之中。与其它类型的boot loader相比,这类boot loader的另一个缺点在于引导过程会更久,原因很简单,宿主系统也需要时间被引导起来。
支持文件系统的boot loader自身就很像一个微缩版的操作系统了:它能驱动一种或几种文件系统,能通过固件提供的服务访问设备,有时,它们甚至能通过自有的驱动直接访问硬件。
不支持文件系统的boot loader依赖第三方服务将磁盘上的数据结构映射为更普通、更方便的表述方式。例如LILO, “map installer”(/sbin/lilo)利用已经包含在内核中的文件系统驱动来进行映射,其实就是简单地将一堆数据扇区所在的位置写入它的映射文件。(译注1:由于没有文件系统,无法通过路径名,而只能通过磁柱/扇区号这种物理方式访问数据)
2.2 支持文件系统
对LILO最多的抱怨就是它不支持文件系统;LILO的竞争对手们则将对文件系统的支持(不需要映射)当作自己的主要特色。因此,把这两类boot loader拿来做一番比较应该会很有趣。
图4展示了一个支持文件系统的boot loader在使用ext2时的情景:
首先,通过ext2驱动,文件被写到磁盘上。ext2文件系统在源文件上添加了一些元数据(meta information)。在引导时, boot loader通过分析这些元数据将相关的扇区载入内存。要达到这个目的,boot loader内显然至少要包括一个简化版的ext2文件系统驱动。
图4. 数据流:支持文件系统
不支持文件系统的boot loader在将文件写到磁盘上后需要一个额外的步骤,映射 - 通过这个步骤把一般化了的数据写到磁盘上(译注2:一般化 — 我的理解是去文件系统话,见译注1)。随后,boot loader才可以藉由这些数据来获取真正的数据。很显然,在这类boot loader中,由文件系统驱动所产生的那些文件系统相关的元数据是没有用的。
图5. 数据流:不支持文件系统
不支持文件系统的boot loader的主要短板在于map installer一定要在内核映像被添加之后,或者一个已经进行过映射的内核映像修改它在磁盘上的位置之后才能运行。
然而,不支持文件系统的boot loader有一个巨大的优势:如果有一个被Linux支持的文件系统满足一些基本的属性,那么它就能在不更改boot loader和map installer的前提下加载位于其上的内核。这也是LILO要如此设计的主要原因。
2.3 文件系统的历史和LILO
Linux早期,能用的boot loader只有从软盘引导的Shoelace。这是一个来自Minix的支持文件系统的boot loader。Shoelace只支持Minix文件系统。由于那时的Linux也只支持Minix文件系统,所以没有问题。但很快大家就发现Minix文件系统缺乏unix文件系统具有的一些功能,比如distinct creation/modification,文件访问时间等,Minix还限制文件名最长14个字符。所有这些都决定了它不可能成为Linux的首选文件系统。
为了方便地加入不同的文件系统,虚拟文件系统(VFS)被开发出来,又进一步催生了多种文件系统,有ext,Xiafs,包括增“大”版的Minix文件系统(文件名支持30个字符)。不同文件系统间的竞争非常激烈,无法预料谁能一统江湖。
混沌之中,至少有一件事是明确的:不管你更偏爱哪个文件系统,要从硬盘上启动Linux,根文件系统只能是Minix。因为Shoelace不支持任何其它的文件系统。发明LILO其实就是要改变这个局面。开发和维护多种(当时的Linux已收录了Minix、ext,Xiafs, BSD的FFS也已经移植到了Linux上,还有更多的人在为Linux开发更多的文件系统)文件系统看上去并不值得,而且boot loader不应该成为新文件系统的阻碍,所以在当时,不支持文件系统的boot loader类型才是潮流。
这种类型大获成功。今天,LILO可以从Linux支持的大多数文件系统上引导系统。然而,由于ext2如今已成为Linux事实上的“标准”文件系统,支持文件系统的boot loader设计又再获青睐,其中的一些得到了普遍的认可。
随着ext2日益普及, 开发人员转而开发下一代的文件系统,关键的更新在于支持日志。考虑到目前又有几种(图6)强势的文件系统相互竞争,看上去似乎能提供更大灵活性的不支持文件系统的boot loader又一次要占据优势了。
图6. linux“标准”文件系统的演进
2.4 还需要加载什么?
Linux boot loader除了加载内核映像之外,还需要向内核提供其它的数据,例如initial RAM disk,内核可以在其上建立用户空间而不必访问其它任何外设。这部分将在第三章讨论。
其它的附加数据包括给内核的参数块(parameter block)。典型的参数块包括具有root分区的设备的个数,系统控制台的模式,启动命令行(boot command line)等等。参数块的类型以及其中数据的布局都是架构相关的。同时,从多个数据源获得数据并组合成一个数据块的做法相当普遍,比如LILO就可以选择性的重新设置默认的VGA模式(使用其它数据源的数据)。
2.5 细节 - i386
一直在困扰boot loader开发人员的一个问题 — 尤其是在i386平台上 — 是由硬件、更贴切地说是由固件、带来的不同的磁盘尺寸限制。如果使用大于限制的磁盘,后果通常是超出限制之外的那部分磁盘空间只能在特定的情形下被访问。
有一个限制在Linux的世界里声名显赫,那就是在使用LILO时不可超越1024个磁柱(cylinder)的限制。这个限制来源于BIOS。BIOS中的函数最大支持1024个磁柱。这个限制对所有大于8GB的硬盘、或个别小于8GB的硬盘无效。LILO利用BIOS做磁盘访问,所以所有对磁盘的操作都局限于头1024个磁柱以内。1995年,Enhanced Disk Drive Specification(EDD)规范放宽了这个限制。1999年,LILO的开发版本开始支持EDD,稍后的通用发布版本也支持EDD(由John Coffman维护)。
图7. x86平台引导时的内存布局(经简化)
i386平台上的另一个限制是关于内存的。首先,在实模式下,CPU的地址线宽度为4+16,可以访问1MB的内存。由于启动扇区(译注3:原文如此 — when the boot sector starts)开始工作时CPU就处于实模式,因此,早期的boot loader只能引导不大于1MB的内核映像。
这个限制催生了压缩内核(zImage)的产生。zImage不得大于512kB(注1),但启动时它会自解压到更高的地址空间。压缩内核让最终的内核映像可以达到1MB左右。
#注1:除了存放内核映像,这1MB的空间内还要容纳BIOS,video memory,boot loader也要使用一部分空间。
但几年之后,1MB的限制也成为巨大的束缚,一种新的机制,bzImage,被开发出来以引导更大的内核。bzImage会被加载到1MB以上的内存区域,随后自解压,并将解压后的内核移动到1MB以下的区域。参数块保存在软盘的引导扇区中而实模式引导代码仍旧被加载到原先的低于1MB的内存中。本章结尾部分会对这种机制做详细描述。
由于zImage无论从哪个方面来讲都不及bzImage,很可能在未来几年内前者就会完全销声匿迹。
要把bzImage加载到1MB以上,boot loader要么把CPU切换到可以访问整个地址空间的模式;要么仍旧在实模式下工作,但要使用特殊的BIOS函数做数据拷贝。不幸的是,那些BIOS函数源自i286时代并且使用所谓i286的保护模式,该模式下有8+16位的地址线,因而能访问的最大范围是16MB。15MB(注2)对于容纳压缩内核来说绰绰有余,但这对initrd的尺寸又是一种限制,因为initrd和压缩内核共享这15MB空间。这个16MB的限制仅存在于boot loader,内核无此烦恼。有一些boot loader已经在使用那些可以避免此限制的拷贝机制。
#注2: 最低的1MB是为BIOS、boot loader、video memory保留的。
内核的尺寸还要受制于页表,这些页表是在内核初始化之前建立的。长期以来,只映射了4MB的内存。随着内核的不断增大,进来这项数据增加到了8MB。
要留意所有以上限制都局限于加载内核映像的阶段。之后由模块加载的代码就可以访问内核提供的所有内存。
加载bzImage的过程相当复杂,请参考图8.
首先,boot loader加载内核的setup sectors (1)和经压缩的内核映像(2),并跳转到setup code(3)。bzImage由压缩内核(text)和数据(data),和一小块解压缩代码组成。这个过程一旦结束,setup code就会跳转到解压缩代码(4)。接下来,内核会被分别解压缩到低于1MB的内存(5)和高于1MB的紧跟在bzImage之后的一块内存中(6)。通过利用低于1MB的内存区域,解压缩代码将其内存使用峰值减少了568kB。(译注4: 这个568kB是怎么得来的?)
图8. 加载bzImage
当内核被解压缩后,它需要被移动到1MB处,这项工作是由一个称作mover的移动程序完成的。该程序自身被拷贝到低地址处(7, 8)。把内核移动到目的地后(9,10),mover跳转到内核的入口点(11)。
2.6 加入新的功能
需要向引导过程中加入新的功能时,常被提及的问题是要在哪里实现该功能 - boot loader还是内核?图9描述了这个选择。
图9. 新的功能放到哪里?
Linux支持多种架构,而每种架构又可能有好几种boot loader。很明显,如果新功能需要对boot loader做大的改动,一定是要受到冷遇的。随着Linux支持更多的架构,甚至那些架构相关的修改也不容忽视。迄今最后一次影响到所有架构和boot loader的改动是initial RAM disk。很幸运,这项技术被绝大多数的开发人员认可并得到了良好的支持。
现在的趋势是将对引导过程的功能扩展都在内核中实现,例如,将Linux分为架构相关的部分(arch/)以及架构无关的一般性框架。近期对initrd的更新都是完全架构无关的。
第三章第五节会继续讨论这个主题。
3. 加载驱动
仅仅加载内核是不够的,因为访问根文件系统的驱动不一定包含在内核中。这一章描述造成这个悖论的原因以及解决办法。
3.1 驱动冲突
早期的很多Linux发行版本都遭遇过一个问题:那些用来访问附加存储介质(further storage medium),例如CD-ROM,的驱动和其它的驱动之间存在冲突。
在有ISA卡的系统上很容易发生这个问题,因为以前探测(probe)这种卡的唯一方法是写一个这种卡上公认地址上的寄存器,如果该卡的响应恰如预料,则认为该卡是存在的。如果两块ISA卡恰好有相同的这么一些地址并且对误操作的响应不是很优雅,比如,进入了一个只有进行一系列software reset才能离开的状态,或更极端地,只有进行硬件reset才能离开的状态,那么,系统不可能探测到这块卡并只能选择使用另一块。
为了避免这类冲突,发行版尝试采用一次发行多个预编译好的内核,每个内核均只包含数量有限的驱动。 这样的内核发行版要么用好多个软盘来装载所有这些内核,要么用户根据需要选择自己要用的那个内核,并不得不在安装前制作引导盘。这样的情形很难让人满意。
解决之道在于使用模块(modules)。模块可以在执行完一次比内核更仔细的硬件配置分析之后(译注5:利用模块工具 modprobe / modinfo -F ?)再加载,也可以简单地由用户决定是否加载。
3.2 动态构成内核
系统安装好后,也需要在加载根文件系统之前加载模块,这样的内核就个只包含所需要的部件。(译注6:这一段不理解,原文如下:Loading modules before the kernel mounts the root file system is also desirable after installation, when a customized kernel containing only the components required on the respective system should be used.
问题主要在于‘Loading modules before the kernel mounts the root file system’
(1)没有root file system,modules该放在哪里?
(2)模块设计本身就是为了让内核只包含必须的驱动,和根文件系统有什么关系?)
理想情况下,为达到这个目的,人们应该遍历所有的内核配置选项并从头编译内核。但有多少人可以忍受从上千个配置选项中遴选出自己需要的配置,尤其是微不足道的配置错误也可能让系统无法启动?再有,还可能有配置系统觉察不到的配置项之间的依赖关系,所以不恰当的配置项可能导致编译失败。另外,编译过程需要使用很多工具,编译器等等,不是每个用户都希望安装这些工具。最后,在较慢的机器上,编译内核费时颇多。
链接一个预编译的巨内核(monolithic)可能只能带来些许改观,因为这仍旧需要大多数的工具,同样地,任何冲突都可能导致链接失败。
再一次,最合理的选择是使用模块。模块框架被很多人日常使用,因此是可靠的技术。如果模块之间存在冲突, 例如缺失符号或同一个符号被多次定义,那么该模块和与之有依赖关系的那些模块将不会被加载,但比起整个系统编译失败,这个损失要小很多。
原则上可以在模块的基础上构建一个简化的链接器,这样可以享受模块机制带来的所有好处,同时又避免了由模块带来的额外开销。由于某些原因,这样的链接器从未实现。
3.3 鸡,蛋
使用模块的前提是支持文件系统(注3)。安装软盘可以容纳一个文件系统,但其它介质,如CD-ROM或第二章所描述的状况,就不一定了。时不时地,软盘驱动器可以被BIOS正确访问,但普通的软盘驱动则不行。
幸运的是,boot loader本来就被定义为要能从各种引导介质上读取数据。因此不难推论出让boot loader负责加载模块。为了尽可能的保持灵活性的同时又让boot loader尽可能的简单,boot loader加载一个单独的文件。而在内核看来,就是一块线性内存。内核将这块内存用作RAM disk。因此,这个机制就称作“initial RAM disk“,简称”initrd“。让人愉快的是,RAM disk驱动还能自动检测数据是否被压缩过。
图10. 加载initial RAM disk
要调试,或是将这个机制该做其它用途,可以使用命令行参数‘noinitrd’,该参数内核将这块内存当作RAM disk使用。这种情形下,可以通过块设备文件/dev/initrd访问该内存。
3.4 使用initrd
一旦加载了RAM disk,任何Linux程序都可以在其上运行。RAM disk有两种模式, 其一是当作根文件系统,这种模式下运行的应用是/sbin/init;另一种是系统将其当作生成真正的根文件系统的媒介。
在后一种情形下,将会调用一个叫做/linuxrc的程序以执行必要的初始化工作。当/linuxrc结束后,真正的根文件系统已被挂载并取代了initrd。在这之后,/sbin/init开始通常的启动流程。改变根文件系统的过程在第四章描述。
3.5 尺寸的考虑
对于initrd尺寸的主要限制在于要有足够的内存供内核、boot loader加载的initrd文件、解压缩该文件后产生的RAM disk以及内核必需的其它数据使用。一般而言,压缩的initrd文件的尺寸大约是处内核占用之外的其余内存的三分之一。
一个显而易见的改建措施是在RAM disk建立之后马上释放由initrd占用的内存。这一措施将很快实现。
顺便说一下,一个常见的误解是使用initrd意味着会浪费数以兆记的宝贵内存。这个误解来自于绝大多数的应用都链接C库,而大多数的C库都相当大 - 约4MB。即使是使用静态库(应用中只包含被调用的库函数),带来的尺寸上的减少也并不可观。比如,一个空程序(main(){})也依然有200KB以上。
这个问题的原因之一是C库内部有很多依赖关系,因而需要包含一些附加的部件。如果去掉这些依赖关系,生成的程序尺寸会更合理些,比如上面的空程序会变为约3KB。
另一个方法是根本就不用任何库。对于小型的程序来说这是可能的,micro-shell就是一个例子。
4. 改变根文件系统
改变根文件系统就像你要替换一块你正位于其上的地毯。大多数人会想到的是趁跳起来的功夫先把新地毯塞到脚下,然后再把它慢慢弄平整。第一个方案,change_root,就采用这个方法(4.2描述)。
另一个懒一些的方法是把新地毯在旧的旁边铺好,然后走过去就是了。这个优雅得多的方案,pivot_root, 在4.3描述。一个现在正在使用的类似的方案,是将新的根文件系统叠加到旧的之上。该方案在4.4描述。
4.1 哪些东西一直“很忙”
改变根文件系统需要一些技巧,因为Unix的设计是确保系统中无论何时都有一部分在访问它。尤其是,当有任何进程在运行时,至少以下的文件系统部件是“忙碌”的:
映射文件 进程以及共享库
终端 进程标准输入 / 输出 / 错误,一般使用/dev/console
目录 进程的当前目录和根目录。
此外,根文件系统还可能因为下列因素而“忙碌”
挂载点 挂载的文件系统,如/proc
demons Demon进程和内核线程。
4.2 跳跃吧,兄弟!
图11. 用change_root改变根文件系统
流程如下:
- 内核准备好initrd并运行/linuxrc
- /linuxrc做好挂载根文件系统的一切准备工作并将新的根文件系统设备的序号写入/proc/sys/kernel/real-root-dev
- 当/linuxrc退出后,内核尝试卸载旧的根文件系统并挂载由/proc/sys/kernel/real-root-dev所指定的设备上的文件系统
- 内核运行/sbin/init
change_root的设计目标之一是尽量方便shell脚本,以使过渡到initrd的过程尽可能简化。
下表显示这种方法如何处理那些“忙碌”的部件。
映射文件 在进程结束时消失
终端 在进程结束时关闭
目录 在进程结束后就不再访问
挂载点 不受影响
demons 不受影响
挂载点和demon仍旧是问题。挂载点的问题可以通过在/linuxrc结束前卸载所有文件系统来避免。Demon进程很难避免使用,而内核线程可能压根就拒绝被终止。
如果卸载旧根文件系统失败了(一直忙碌),将会打印一个警告信息并将其挂载到新的根文件系统的挂载点/initrd。之后,当其不再忙碌时,可以像其它任何文件系统一样被卸载掉。如果/initrd这个挂载点不存在,change_root则会放弃尝试,简单地就把旧的根文件系统留在那里,但不可访问。
4.3 更适用的方案
尽管change_root在绝大多数场合都工作的不错,但还是有一些让人不快的限制:
- 它只能挂载位于块设备之上的对象,这样一来,NFS, SMB等等文件系统就被排除在外了。
- 内核线程的使用日益流行,而这会让根文件系统保持忙碌。
- change_root只能使用一次,这将使调试初始化过程变得困难。
- 如果change_root未能成功挂载新的根文件系统,将使整个系统挂起。
另外,所有的设备魔数和chang_root所使用的硬编码的名字都未经任何处理。
就在change_root开始使用时,另一种松散地基于系统调用chroot的方法正在讨论之中。近期的关于VFS的更新使得新的方法的实现相对容易,因此它最终被实现出来。
图12 描述了新的机制,pivot_root的流程
图12. 用pivot_root改变根文件系统
- 新的根文件系统像其它任何文件系统一样被挂载。
- 其中的一个目录用来做为挂载旧的根文件系统的挂载点。
- 用该目录和新的根文件系统的挂载点为参数调用pivot_root。
- pivot_root将当前的根文件系统移到旧文件系统的挂载点并将新的根文件系统设置为当前的根。
与change_root的重要差异在于:
- 任何文件系统都可以作为根文件系统,比如NFS,SMB。
- pivot_root并不尝试卸载旧的根文件系统,其行为因此更可预期。
- pivot_root可被多次调用,并允许串联的根文件系统生成过程,因此有利于调试初始化脚本。
- pivot_root甚至支持逆操作,同样有利于调试。
很不幸,
4.4 联合挂载
Pivot_root仅存的问题是需要强制性的改变进程的根目录和当前目录。
ALexander Viro正在开发一种叫做‘联合挂载’(union mounts)的VFS扩展,允许将多个文件系统叠加到一个挂载点上。这些文件系统只有尝试在该挂载点上进行搜索时才会被访问。
回到地毯的例子,这有点像在换地毯时给了我们一块飞毯,以避免站到真正的地毯上。
尽管此时该工作尚未完成,但仍可预期它将让我们更干净地利用pivot_root引出的概念。
图13描述了其工作流程。文件系统要么直接在根上挂载/卸载,要么被移到其它的挂载点上。
图13. 用union mounts改变根文件系统
所以最终的情形就像这样:
映射文件 由exec改变
终端 关闭并重新打开
目录 目录的改变是透明的
挂载点 不受影响(除非把挂载点移到根上)
demons 目录的改变是透明的
本节描述的内容将很快加入到内核的主开发树。
5. Linux 引导 Linux
现在,我们已经可以把任何内核可以挂载的文件系统用作根文件系统。那么,如果我们把任何内核可以读取的文件当作kernel或initrd不是很美妙吗?
不支持文件系统的boot loader在文件存储于不连续的扇区上的时候会遇到麻烦。比如软件RAID,同一个数据块可能有多个实例,而且一个处于重建模式的RAID5需要通过计算数个数据块来还原一个损坏的卷(volum额)中的数据。更坏的情形是文件可能就不在本地存储中,而是在NFS或HTTP服务器上。
原则上,任意boot loader应该能访问任何内核能访问的资源。唯一的问题是这些功能要加入到boot loader当中。一旦boot loader包含了半打的文件系统,又支持RAID,TCP/IP协议栈,DHCP,HTTP等等,它看上去已经很像一个完整的操作系统了。。。
5.1 终极boot loader
。。。这样的boot loader带给我们一个非常便利的解决方案:我们手边就有一个可以访问任何内核可以访问的资源的程序 - 内核本身。所有其它可能用到的工具(比如DHCP之类的)也可以很方便的获得。
唯一缺少的是从Linux引导一个新的Linux的方法。理论上说,它和跑在其它宿主系统上的boot loader应该是一样的。然而,有些要求会稍许高些,因为我们要的解决方案是要能在任何Linux所支持的平台上使用,还因为这样的boot loader所面临的可能的系统配置更加多样,例如,大概没有人会指望LOADLIN运行在多CPU的系统上。
另一个要求是在内核间传递源自固件的数据,例如在i386平台上的video模式、内存布局、SMP配置等等,要么从BIOS直接获得,要么从BIOS初始化过的内存区域取得。由于这些内存区域有可能被内核改写,要么把它们保护起来,要么把这些数据传递给新的内核。
最后,初始化期间的有些操作,比如扫描SCSI或IDE总线,要花较长时间。如果将这些信息传递给新内核将会减少新内核的引导时间。
目前至少已经有三个从Linux引导Linux的实现: bootimg, LOBOS和Two Kernel Monte. 在5.3节会描述bootimg。
5.2资源浪费?
把一个完整的unix内核用作boot loader看上去是一种资源的浪费。本节会考量对时间、内存和磁盘空间的影响。
请留意以下的计算不适用于特殊的环境,比如嵌入式系统或电池驱动的设备。这类系统往往配置很少的内存或较慢的CPU。
先看时间:加载内核和initrd都需要时间。内核可能是压缩过的,还需要一些额外的时间解压缩。如果我们假设所有花费时间的总线扫描都只做一次,并且硬件也不是很慢或很旧,我们得到:
1 -2 秒 加载内核和initrd (1-2MB)
1 -2 秒 解压缩内核和initrd
1秒 其它开销
——————————————-
共 3 - 5 秒
考虑到一次正常的重启大概要20到60秒,增加3到5秒不算很夸张。带来的好处则显而易见:那些由于改变配置或内核更新导致的重启将会快很多,因为旧的内核可以直接引导新的内核,不必再经过BIOS或boot loader的环节。
内存用量的峰值出现在旧内核加载新内核和initrd的时候。尽量宽松地估计内核及initrd的大小,可以得到
1 -2 MB 引导内核(1-2MB)
2 -4 MB 内核数据
1 -2 MB initrd
0.5 -2 MB 压缩的内核
0.5 -2 MB 压缩的initrd
——————————————-
共 5 - 12 MB
对任何可用的Linux来说,5MB大概是对内存的最小需求了,只有在使用超大的内核或initrd时内存大小才会成为一个显著的问题,但这类系统很少在内存受限的情况下使用。
最后再来看看磁盘空间的状况:
1 压缩的内核
1 压缩的initrd
——————————————-
共 2 MB
根本就微不足道。那些经常更新内核的开发人员会维护一个独立的内核开发树,大概要100 - 120MB。
5.3 情景学习:i386平台
这一节大致浏览一下bootimg是如何加载Linux内核的。请留意bootimg还在开发当中,有可能发生大的改动。Bootimg由两部分组成:一个用户空间的程序用以加载必要的文件和准备一份加载地图;用以移动内存页面到正确地址及启动新内核的内核空间代码。
图14. Bootimg: set up from user space
如图14, 用户程序首先加载内核映像、initrd文件到它的地址空间(1)。然后把这些页帧的地址放到一个指针数组当中(2)。注意这些数据不一定在磁盘上,也有可能是通过网络得到,甚至有可能是bootimg在运行时生成的。接下来,bootimg从正在运行的内核当中拷贝参数块(3)并附加上新的启动命令行和initrd参数(4)。通过拷贝参数块,那些由BIOS设置的参数,比如内存的配置等,都保留了下来。除了刚才提到的指向源数据的指针数组,bootimg还维护了一个包含了所有目标内存页的指针数组(5)。以上工作完成后,bootimg建立一个包含了指向上述两个指针数组的指针及其它附加信息的描述符(6),最后调用bootimg系统调用。
图15. Bootimg: memory reorder in the kernel
如图15, bootimg系统调用首先将一些页面拷贝到内核空间(1)。这么做的主要目的是为了检查访问权限和确保这些页面被正确的对齐了,同时也会让将在下一节描述的崩溃转储工具更容易实现。拷贝的同时,bootimg还更新源指针(2)使其指向内核空间(注8)。这些可能位于任何地址的内存之前就已申请,所以要在启动内核之前将其移到正确的地方。这项工作由一个被拷贝到自己的页面的位置无关的函数完成(3)。这个函数将将所有页面移动到目的地址数组所指向的地址(4)。如果目的地址恰好和某个还在使用的页面重叠,该函数会先将这个页面的内容拷贝到一个空闲页面。留意这可能也触及该函数正在使用的页。一旦所有的页面都被拷贝到目的地址,新内核的启动代码就会被调用。
5.4 其它相关应用
除了从千奇百怪的介质引导内核,还有利用这个机制的其它应用也正在讨论之中。
LinuxBIOS将boot loader精简到了极致,它将一个内核放到通常防止PC的BIOS的Flash EPROM之中。这个内核的作用就是一个功能丰富的boot loader。
另一个有趣的应用是产生崩溃转储文件。很多传统的Unix系统可以在内核恐慌时将内存中的内容写入磁盘当中,从而有机会探索系统崩溃的原因。但内核恐慌进发生在内核检测到严重错误的时候,这时候不能保证有关内核转储的驱动都还能正常工作,即使能,这些驱动也可能改变系统的状态从而使从而使分析原因的工作变得不可能。
因此,想要用一个独立于正常内核的子系统还完成这项工作。利用类似于bootimg的机制来实现非常简单:实现内核转储的一个小内核会提前加载到initial RAM disk,当发生恐慌时,首先对这个小内核所占用的页面进行检测(它们或许也是该内核恐慌的受害者),随后启动这个小内核。由它创建一个干净的环境并完成转储。
6. 致谢
几年以来,很多人通过提交bug和建议为LILO做出贡献。从去年开始,针对LILO的开发工作已经停止。但John Coffman接过了火炬。
initial RAM disk和bzImage的架构设计是和Hans Lermen一起完成的。
pivot_root的设计深受内核邮件列表中的讨论的影响。尤其是H. Peter Anvin, Linus Torvalds和Mattew Wilcox帮助最终定型了该设计,Alexander Viro目前正在做进一步的改进工作。
bootimg的创意来自于Markus Wild在90年代早期在SVR4上的实现。其中使用的内存变序算法则深受由Otfried Cheong和Roger Gammans设计,并由后者实现的FiPaBoL的启发。
7. 总结
表1展示了Linux 引导过程的发展历程。仍在开发当中的项目用斜体字标识。同时,i386以外的架构被省略了。
第一个boot loader除了加载内核别无它用。第二代则挣脱了文件系统的束缚并增加了很多有用的功能,比如启动命令行和引导其它系统的能力。当今几乎所有的boot loader都属于第二代。
将任意文件系统用作根文件系统的功能从Linux早期就开始谋划,但进展缓慢。pivot_root的出现才使完全通用的解决方案成为现实。
最后,从软盘、磁盘以外的介质引导内核的想法相对较新。由于本文介绍的三种由Linux引导Linux的方法已经被普遍采用,我相信它们的融合会在不久的将来发生。
如同你已经看到的,引导Linux系统这个看上去简单的动作其实要克服很多问题。现代的Linux系统提供非常多的功能来解决这些问题,更多的激动人心的改进工作则正在开发之中。