启动内核

作者:Alessandro Rubini

计算机系统是一台复杂的机器,而操作系统是一个精密的工具,它协调硬件的复杂性,为最终用户展示一个简单而标准化的环境。然而,当电源开启时,系统软件必须启动内核并在有限的操作环境中工作。我在这里描述了三个平台的启动过程:老式 PC 和功能更全的 Alpha 和 SPARC 平台。PC 被更详细地介绍,因为它仍然比其他平台更广泛地使用,而且因为它也是最棘手的平台。不会展示任何代码,因为汇编语言对于大多数读者来说是难以理解的,而且每个平台都有自己的汇编语言。

开机时的计算机

为了能够在电源开启时使用计算机,处理器从系统固件开始执行。固件是“不可移动的软件”,位于 ROM 中;一些制造商称之为基本输入输出系统 (BIOS) 以强调其软件角色,一些制造商称之为 PROM 或“闪存”以强调其硬件实现,而另一些制造商则称之为“控制台”以关注用户交互。

固件通常检查硬件的功能,从存储介质中检索部分(或全部)内核并执行它。内核的这第一部分必须加载其余部分并初始化整个系统。我在这里不处理固件问题,而是处理随 Linux 发行的内核代码。

PC

当 x86 处理器开启时,它是一个 16 位处理器,只能看到 1MB 的 RAM。这种环境被称为“实模式”,是由与同一系列的旧处理器的兼容性决定的。构成完整系统的一切都必须存在于可用的兆字节地址空间内,即固件、视频缓冲区、扩展板卡的空间和少量 RAM(臭名昭著的 640KB)都必须在那里。

更困难的是,PC 固件仅加载半千字节的代码,并在加载第一个扇区之前建立自己的内存布局。无论启动介质是什么,启动分区的第一个扇区都会被加载到地址 0x7c00 的内存中,执行从那里开始。0x7c00 处发生的事情取决于所使用的引导加载程序;我们在这里考察三种情况:没有引导加载程序、LILO、Loadlin。

启动 zImage 和 bzImage

即使不使用引导加载程序启动系统的情况很少见,但仍然可以通过将原始内核复制到软盘来实现。命令 cat zImage > /dev/fd0 在 Linux 上完美运行,尽管其他一些 Unix 系统只有通过使用 dd 命令才能可靠地完成此任务。在不深入细节的情况下,由 zImage 创建的原始软盘镜像随后可以使用 rdev 程序进行配置。

名为 zImage 的文件是压缩内核镜像,它在执行 make zImagemake boot 之后驻留在 arch/i386/boot 中——后一个调用是我更喜欢的,因为它在其他平台上也能保持不变地工作。如果您构建的是“大型 zImage”,则创建的内核文件称为 bzImage,并驻留在同一目录中。

由于可用内存量有限,启动 x86 内核是一项棘手的任务。Linux 内核尝试通过多次移动自身位置来最大化利用低 640 千字节的内存。让我们详细了解一下 zImage 内核执行的步骤;以下所有路径名都相对于 arch/i386/boot 目录。

  • 第一个扇区(在 0x7c00 执行)将自身移动到 0x90000,并在自身之后加载后续扇区,使用固件的功能从启动设备获取它们以访问磁盘。然后将内核的其余部分加载到地址 0x10000,允许最大半兆字节的数据大小——记住,这是压缩镜像。启动扇区代码位于 bootsect.S 中,这是一个实模式汇编文件。

  • 然后在 0x90200 处的代码(在 setup.S 中定义)负责一些硬件初始化,并允许更改默认文本模式 (video.S)。文本模式选择是从 2.1.9 版本开始的编译时选项。

  • 稍后,所有内核都从 0x10000 (64K) 移动到 0x1000 (4K)。此移动会覆盖存储在 RAM 中的 BIOS 数据,因此 BIOS 调用将不再能够执行。第一个物理页未被触及,因为它是所谓的“零页”,用于处理虚拟内存。

  • 此时,setup.S 进入保护模式并跳转到 0x1000,内核就位于那里。现在可以访问所有可用内存,系统可以开始运行。

当内核足够小,可以容纳在半兆字节的内存中时——地址范围在 0x10000 到 0x90000 之间——刚刚描述的步骤曾经是启动的全部过程。随着系统添加了功能,内核变得大于半兆字节,不再能够移动到 0x1000。因此,0x1000 处的代码不再是 Linux 内核,而是 gzip 程序的“gunzip”部分驻留在该地址。现在需要以下额外步骤来解压内核并执行它

  • compressed 目录中的 head.S 位于 0x1000,负责 “gunzip” 内核;它调用函数 decompress_kernel,该函数在 compressed/misc.c 中定义,它反过来调用 inflate,后者将其输出写入到从地址 0x100000 (1MB) 开始的位置。现在可以访问高端内存,因为处理器肯定已经脱离了其有限的启动环境——“实模式”。

  • 解压后,head.S 跳转到内核的实际起始位置。相关代码在 boot 目录之外的 ../kernel/head.S 中。

启动过程现在结束了,head.S(即在引入压缩启动之前位于 0x1000 的 0x100000 代码)可以完成处理器初始化并调用 start_kernel()。此步骤之后所有函数的代码都用 C 语言编写。

系统启动时执行的各种数据移动如图 1 所示。

Booting the Kernel

图 1. 系统启动数据图

上面显示的启动步骤依赖于压缩内核可以容纳在半兆字节空间中的假设。虽然大多数时候是真的,但塞满设备驱动程序的系统可能无法容纳到这个空间中。例如,安装盘中使用的内核很容易超出可用空间。需要一种新方法来解决这个问题——这种新方法称为 bzImage,并在内核版本 1.3.73 中引入。

bzImage 通过从顶层 Linux 源代码目录发出 make bzImage 命令生成。这种内核镜像的启动方式与 zImage 类似,但有一些变化

  • 当系统加载到地址 0x10000 时,在加载每个 64K 数据块后,会调用一个小助手例程。助手例程使用特殊的 BIOS 调用将数据块移动到高端内存。只有较新的 BIOS 版本实现了此功能,因此,make boot 仍然构建传统的 zImage,尽管这在不久的将来可能会改变。

  • setup.S 不会将系统移回 0x1000 (4K),而是在进入保护模式后,而是直接跳转到地址 0x100000 (1MB),数据已在之前的步骤中被 BIOS 移动到该地址。

  • 在 1MB 处找到的解压器将解压后的内核镜像写入低端内存,直到耗尽,然后再写入压缩镜像之后的高端内存。然后将两部分重新组装到地址 0x100000 (1MB)。需要多次内存移动才能正确执行任务。

构建大型压缩镜像的规则可以从 Makefile 中读取;它影响 arch/i386/boot 中的几个文件。bzImage 的一个优点是,当调用 kernel/head.S 时,它没有注意到额外的工作,一切照常进行。

使用 LILO

大多数 Linux-x86 用户不从软盘启动原始内核镜像;而是从硬盘启动 LILO。LILO 替换了上面概述的过程的一部分,以便它可以加载散布在磁盘上的 Linux 内核。此功能允许用户从文件系统分区启动内核文件,而无需使用软盘。

实际上,LILO 使用 BIOS 服务从磁盘加载单个扇区,然后跳转到 setup.S。换句话说,它以与 bootsect.S 相同的方式安排内存布局;因此,通常的启动机制可以顺利完成。LILO 也能够处理内核命令行,这本身就是一个避免启动原始内核镜像的充分理由。

如果您想使用 LILO 启动 bzImage,则必须使用 LILO 18 或更高版本。早期版本的 LILO 无法将段加载到高端内存中,这是在加载大型镜像时需要的,以便 setup.S 找到预期的内存布局。

LILO 的主要缺点是它使用 BIOS 加载系统。这强制内核和其他相关文件进入磁盘的前 1024 个柱面,以便可以被 BIOS 访问。当使用 PC 固件时,您会发现架构实际上有多么老式。

即使您不运行 LILO,您也可以欣赏随 LILO 源代码分发的文档文件。它们记录了 PC 上的启动过程,并解释了如何处理(几乎)每种可能的情况。

使用 Loadlin

如果您想从另一个操作系统启动您的操作系统,Loadlin 是您的工具。此程序类似于 LILO,因为它从磁盘分区加载内核,然后跳转到 setup.S。它与 LILO 的不同之处在于,它不仅面临 BIOS 限制,还必须在不损害系统稳定性的情况下处理已建立的内存布局。另一方面,Loadlin 不受半千字节长度的限制,因为它是一个完整的程序文件,而不是启动扇区。Loadlin 1.6 及更高版本能够加载大型镜像。

Loadlin 可以将命令行传递给内核,因此它像 LILO 一样灵活。大多数时候,您会编写一个 linux.bat 文件,以便在调用 linux 命令时将功能齐全的命令行传递给 Loadlin。

Loadlin 可用于将任何联网的 PC 变成 Linux 盒子。所需的只是配备通过 NFS 挂载根分区的内核镜像、Loadlin 和一个包含正确 IP 号码的 linux.bat。您还需要一个正确配置的 NFS 服务器,但任何 Linux 机器都可以胜任这项工作。例如,以下命令行将 PC (alfred.unipv.it) 变成工作站

loadlin c:\zimage rw nfsroot=/usr/root/alfred \
nfsaddrs=193.204.35.117:193.204.35.110:193.204.35.254:255.255.255.0:alfred.unipv.it
更多内容

代码不像我描述的那么容易——它必须处理很多细节,例如围绕内核命令行,密切关注正在使用的启动技术等等。好奇的读者可以查看源文件以了解更多信息并阅读作者的注释。注释中有很多信息,而且阅读起来也很有趣。

我个人觉得大多数用户永远不需要接触启动代码,因为当系统启动并运行时,事情会变得更加有趣。在那些时候,您可以利用处理器和所有可用 RAM 的所有功能,而不会因处理器级问题而发狂。

启动 Alpha

Alpha 平台比 PC 成熟得多,其固件反映了这种成熟度。我对 Alpha 的经验仅限于 ARC 固件,它是最广泛使用的。

在执行通常的设备检测后,固件会显示一个启动菜单,让您可以选择要启动的文件。固件可以读取磁盘分区(尽管只能读取 FAT 分区),因此您可以实际启动“文件”,而无需破解启动扇区和构建磁盘块映射。

启动的文件通常是 linload.exe,它反过来加载 MILO(“迷你加载器”)。为了通过 ARC 固件启动 Linux,您必须在硬盘上有一个小的 FAT 分区来存储 linload.exe 和 milo 文件。除非您升级 MILO,否则 Linux 内核不需要访问该分区,因此可以安全地将 FAT 支持从您的 Alpha 内核中排除。

实际上,用户可以利用不同的选项。ARC 启动菜单可以配置为默认启动 Linux,MILO 可以烧录到闪存中,以便摆脱 FAT 分区。但是,无论你做什么,最终都会运行 MILO。

MILO 程序是 Linux 内核的精简版本。它拥有所有 Linux 设备驱动程序和一个文件系统解码器;与内核不同,它没有进程控制,但包含 Alpha 初始化代码。此工具可以设置并启用虚拟内存,并可以从 ext2 分区或 iso9660 设备加载文件。所讨论的“文件”被加载到虚拟地址 0xfffffc0000300000,然后执行。此虚拟地址也是 Linux 内核运行的地址;但是,你不太可能加载除 Linux 之外的任何东西。一个例外是用于将 MILO 烧录到闪存 ROM 中的 fmu(“闪存管理实用程序”)程序——fmu 被编译为从内核运行的同一虚拟地址执行,并且随 MILO 分发。

有趣的是,MILO 还包括一个小型 386 模拟器和一些 PC BIOS 功能。这是为了执行在许多 ISA/PCI 外围板卡上找到的自初始化代码所必需的(PCI 板卡,虽然声称与处理器无关,但在其 ROM 镜像中使用 Intel 机器代码)。

由于 MILO 完成了所有这些,Linux 内核还剩下什么?——实际上很少。在 Linux-Alpha 中执行的第一个内核代码是 arch/alpha/kernel/head.S,它所做的只是设置一些指针并跳转到 start_kernel()。实际上,Alpha 的 kernel/head.S 比等效的 x86 源文件短得多。

如果出于某种原因,您不希望运行 MILO,还有另一种选择,尽管不实用。在 arch/alpha/boot 中,您会找到“原始”加载程序的源代码,该加载程序通过从顶层 Linux 源代码目录发出 make rawboot 命令编译。此实用程序可以使用固件的“回调”从设备的顺序区域(软盘或硬盘)加载文件。

实际上,原始加载程序完成的任务类似于 bootsect.S 为 PC 平台执行的任务——它强制将内核副本复制到原始软盘或原始硬盘分区。没有真正的原因使用这种技术——它非常复杂,并且缺乏 MILO 提供的灵活性。我个人不知道它是否仍然有效;Linux 使用的“PALcode”由 MILO 导出,并且与 ARC 固件导出的 PALcode 不同。PALcode 是 Alpha 处理器用于实现分页等低级硬件管理的低级函数库;如果当前的 PALcode 实现的操作与软件预期的不同,则系统将无法工作。

启动 SPARC 工作站

启动 SPARC 计算机在用户方面类似于启动 Alpha,在软件方面类似于启动 PC。

用户看到固件加载并执行一个程序,该程序反过来能够检索和解压缩在磁盘分区上找到的文件。所讨论的“程序”称为 SILO,它可以从 ext2ufs 分区读取文件。与 MILO(像 LILO)不同,SILO 能够启动另一个操作系统。Alpha 上不需要此功能,因为固件可以启动多个系统;一旦你运行 MILO,你已经做出了选择(正确的选择——Linux)。

当 SPARC 计算机启动时,固件在执行所有硬件检查和设备初始化后加载启动扇区。有趣的是,Sbus 设备 平台独立的,它们的初始化代码是可移植的 Forth 代码,而不是绑定到特定处理器的机器语言。

加载的启动扇区是您在 Linux-SPARC 系统中的 /boot/first.b 中找到的,并且是裸露的 512 字节。它被加载到地址 0x4000,其作用是从磁盘检索 /boot/second.b 并将其写入地址 0x280000 (2.5 MB);选择此地址是因为 SPARC 规范声明在启动时必须映射至少 3MB 的 RAM。

第二阶段引导加载程序然后完成所有其他操作。它与 libext2.a 链接以访问系统分区,因此可以从你的 Linux 文件系统加载内核镜像。它还可以解压缩镜像,因为它包含来自 gzip 程序的 inflate.c 例程。

second.b 访问一个名为 /etc/silo.conf 的配置文件,其形状类似于 lilo.conf。由于文件在启动时读取,当新内核添加到启动选项时,无需重新安装内核映射。当 SILO 显示其提示符时,您可以选择 silo.conf 文件中指定的任何内核镜像(或其他操作系统),或者您可以指定完整的设备/路径名称对,以加载不同的内核镜像,而无需编辑配置文件。

SILO 将磁盘文件加载到地址 0x4000。这意味着内核必须小于 2.5MB;如果它更大,SILO 将拒绝覆盖其自身的镜像。目前,没有可以想象到的 Linux-SPARC 内核超过该大小,除非它是使用 -g 编译的以获得可用的调试信息。在这种情况下,内核镜像必须在交给 SILO 之前被剥离。

最后,SILO 执行内核解压缩和/或重映射,以将镜像放置在虚拟地址 0xf0004000。在 SILO 完成后接管的代码是 arch/sparc/kernel/head.S。源代码包括处理器的所有陷阱表以及设置机器并调用 start_kernel() 的实际代码。SPARC 版本的 head.S 相当大。

start_kernel 及之后

在特定于体系结构的初始化完成后,init/main.c 程序将控制您正在使用的任何处理器。

start_kernel() 函数首先调用 setup_arch(),这是最后一个特定于体系结构的功能。然而,与其他代码不同,setup_arch() 可以利用处理器的所有功能,并且是一个比之前描述的那些更容易处理的源文件。此函数在每个体系结构源代码树下的 kernel/setup.c 代码中定义。

start_kernel() 函数然后初始化所有内核的子系统——IPC、网络、缓冲区缓存等等。完成所有初始化后,这两行代码完成代码

kernel_thread(init, NULL, 0);
cpu_idle(NULL);

init 线程是进程号 1:它挂载根分区,如果在编译时选择了 CONFIG_INITRD,则执行 /linuxrc,然后执行 init 程序。如果找不到 init,则执行 /etc/rc。通常不鼓励使用 rc,因为在处理系统配置方面,init 比 shell 脚本灵活得多。事实上,内核的 2.1.21 版本移除了 /etc/rc{/} 选项,使其过时。如果 init/etc/rc 都无法运行或退出,则会重复执行 /bin/sh(但 2.1.21 及更高版本的内核将仅执行一次)。此功能仅作为一种保障措施存在,以防 init 文件被错误删除或损坏。如果您从内核中删除 a.out 支持而不重新编译旧的 init,您将很高兴在重启后至少有一个 shell 运行。在生成进程号 1 后,内核不再有其他任务要做,所有其他功能都由用户空间的 init/etc/rc/bin/sh 处理。进程 0 呢?所谓的“空闲”任务执行 cpu_idle(),这是一个在无限循环中调用 idle() 的函数。idle() 反过来是一个依赖于体系结构的函数,它通常负责关闭处理器以节省电力并延长处理器的寿命。

Alessandro 是一位 Linux 爱好者,他编写文档是因为他不够聪明,无法编写软件。他的 486 擅长在源代码中 grep,并谦虚地将真正的工作留给 Alpha 和 SPARC。可以通过电子邮件 rubini@ipvvis.unipv.it 与他联系。

加载 Disqus 评论