使用 kexec 快速重启 Linux

即使您的工作不要求您每天多次重新启动您的 Linux 机器,等待系统启动也实在是一件枯燥的事情。因而有了 kexec。本质上讲,kexec 是一个让您可以重新启动到一个新 Linux 内核的快速重新引导功能部件 — 不必通过引导装载程序。更快速的重新启动即使对于正常运行时间并不是至关重要时也是有益的 — 对那些每天都要多次重新启动机器的内核和系统软件开发者来说更可谓是救生者。kexec 当前只能用在 x86 32 位平台上。

随着计算机系统变得更快更好,系统重新启动时间也需要跟上发展。实际上,随着系统的处理器速度、存储容量和资源性能变得更加先进和复杂,重新启动的时间竟变长了。虽然更长的重新启动时间对每个人来说只是一种刺激,但它对生产系统的影响是至关重要的,因为更长的重新启动时间意味着正常运行时间的减少。除了影响系统对其用户的可用性之外,更长的重新启动时间对内核和系统软件开发者来说是一个主要的瓶颈,因为他们每天都要多次重新启动他们的机器。

当系统有很多松散分布的 SCSI 总线或者 ECC 校验的物理内存时,重新启动时间特别长。测试结果显示,重新启动过程中大部分时间消耗在固件(firmware)阶段,在此阶段,连接到系统的设备被识别出来并被初始化(要深入了解,请查看本文的 参考资料部分)。自然,大部分试图减少重新启动时间的努力都瞄准重新启动过程的这个阶段。其中一项努力带来了 kexec 的发展,kexec 是一个可用于 x86 平台上 Linux 内核的功能部件。使用 kexec,您可以直接重新启动到另一个内核,不再必须通过固件和引导装载程序阶段。跳过序列中最长的部分大大减少了重新启动时间。

Linux 中的引导概述



要理解 kexec,需要先具备 Linux 中引导过程的知识。Linux 中的引导过程有两个阶段:引导装载程序阶段和内核阶段。

引导装载程序阶段主要包括硬件阶段、固件阶段、第一级引导装载程序和第二级引导装载程序。引导过程从硬件加电启动开始。一些初始化工作完成后,控制转到固件。固件在一些体系结构中也称为“BIOS”,它去检测系统上的各种设备,包括内存控制器、存储设备、总线桥和其他硬件。固件基于设置将控制移交给一个最小化的引导装载程序,即大家所知的主引导记录(master boot record),这个主引导记录可能在磁盘驱动器上,或者在可移动媒体上,或者在网络上。将控制移交给操作系统的实际工作由第二阶段引导装载程序(通常被简单地认为是“引导装载器(boot loader)” )执行。这个引导装载程序让用户可以选择要装载的内核,将内核和相关参数装载到内存,初始化内核,设置需要的环境变量,并最终“运行”内核。

引导的下一个阶段是 内核阶段,此时内核已经获得控制权。它设置需要的数据结构,检测当前在系统上的设备,装载需要的设备驱动程序,并初始化设备。引导过程的最后阶段包括用户级初始化。在这个阶段,内核检查文件系统的完整性,挂载文件系统,设置交换分区(或者交换文件),启动系统服务,设置系统终端,并完成所有其他设置。

在系统重新启动时,引导装载阶段之前要先关闭先前正在运行的系统。这涉及到停止运行的进程,将高速缓冲存储器内容写回到磁盘,?载文件系统,然后执行硬件的重启。在本文的 参考资料部分,您可以找到对 Linux 中引导过程以及常见的引导相关概念的极好的描述。






回页首



kexec 概述



kexec 是 Linux 内核的一个补丁,让您可以从当前正在运行的内核直接引导到一个新内核。在上面描述的引导序列中,kexec 跳过了整个引导装载程序阶段(第一部分)并直接跳转到我们希望引导到的内核。不再有硬件的重启,不再有固件操作,不再涉及引导装载程序。完全避开了引导序列中最弱的一环 — 固件。这一功能部件带来的最大益处在于,系统现在可以极其快速地重新启动。对企业级系统而言,kexec 大大减少了重新启动引起的系统宕机时间。对内核和系统软件开发者而言,kexec 帮助您在开发和测试成果时可以迅速重新启动系统,而不必每次都要再经历耗时的固件阶段。

kexec 补丁是 Eric Biederman 的作品,这个项目仍处在积极的开发之中(查看 参考资料部分以深入了解此项目以及如何对它做出贡献)。

显然,由于这个功能部件涉及到操作系统如此多的敏感部分,需要特别细心以使其始终正确工作。对 kexec 来说,最大的挑战在于,在 Linux 中,要重新引导到的新内核需要位于内存中与当前正在运行的内核相同的位置。仍然在当前内核的上下文中运行时,用新内核去替换内存中现有的内核,这是件困难的工作。另一个大问题是,系统中设备的状态。固件总是将设备初始化(或重启)到已知的“sane”状态。kexec 绕过固件阶段的事实,意味着设备的状态是不可靠的。

本文接下来的几节将向您展示如何征服这些挑战,并展示如何实现到新内核的直接引导。注意,当前 kexec 只能用于 x86 32 位平台。尽管将 kexec 移植到其他平台的工作正在进行,但是还没有可以用的代码版本。因此,后面几节中的所有技术细节都只特定于 x86 平台。






回页首



使用 kexec



Kexec 有两个组件。第一个是用户空间组件,叫做“kexec-tools”。第二个是真正的内核补丁。这两部分实现 kexec 的两个主要操作:将新内核装载到内存并重新启动到它。可以容易地获得一个启用 kexec 的内核。只需要下载 kexec-tools 包和特定内核的补丁(见 参考资料部分的链接),编译 kexec-tools 包以得到 kexec工具,并将特定内核的补丁加入到内核树中并重装启动到它。当然,您在编译内核时要确保选中 CONFIG_KEXEC 选项。

如上所述,kexec 的使用包括(1)将重新启动到的内核装载到内核中,然后(2)真正重新启动到它。装载内核的语法如下:

kexec -l <kernel-image> --append="<command-line-options>"

在这里, <kernel-image> 是您想要重新启动后的内核文件, <command-line-options> 容纳的是需要传递到新内核的命令行参数。由于错误的命令行选项可能会在重新启动时引发问题,所以,确保合法值传递到重新启动的内核的安全方法是传递 /proc/cmdline 的内容。

例如,如果您希望重新启动的内核映像是 /boot/bzImage, /proc/cmdline 的内容是 "root=/dev/hda1" ,那么装载内核的命令将是:

kexec -l /boot/bzImage -append="root=/dev/hda1"

然后,为了真正重新启动已装载的内核,只需要输入:

kexec -e

系统将立即重新启动。不同于正常的重新启动过程,在重新启动之前,kexec 不去执行彻底停止系统。需要您在尝试进行 kexec 重新启动之前去杀死所有应用程序并?载文件系统。






回页首



kexec

的魔力

在 kexec 的发展过程中,最大的挑战之一来自于这样一个事实:Linux 内核要从内存中固定的地址运行。也就是说,新内核需要安放于当前内核正在运行的位置。在 x86 系统上,内核位于物理地址 0x100000(虚拟地址为 0xc0000000,也叫做 PAGE_OFFSET )。用新内核覆盖旧内核的工作分三个阶段完成:

  1. 将新内核拷贝到内核中。
  2. 将这个内核映像移到动态内核内存中。
  3. 将这个映像拷贝到真正的目标位置(覆盖当前内核),然后启动新内核。

前两个阶段在内核的“装载”期间完成。第一个任务是解释内核映像文件的内容。Kexec-tools 已经被编译,因此,理论上您可以装载并引导任何(甚至是非-Linux 的)内核。当前,只能引导到 elf32 格式的内核映像。这个文件被解析,内核“段(segments)”被装载到缓存中。这些段根据代码的自然类型进行分类。例如,在使用常见的“bzImage”内核文件格式的情况下,典型的段是针对 16 位内核代码、32 位内核代码和初始 RAM 磁盘代码的段。用于追踪这些段的结构体被称为 kexec_segment ,是一个相当简单的结构体:

清单 1. kexec_segment 结构体

struct kexec_segment {
   void *buf;
   size_t bufsz;
   void *mem;
   size_t memsz;
};


结构体的前两个元素指向用户空间缓存和它的大小,接下来两个元素指明了段的最终目标位置和它的大小。

一旦特定内核文件格式的(kernel-file format-specific)模块将映像装载到用户内存,映像就会被 sys_kexec 系统调用转移到动态内核内存。这个系统调用给每个从用户空间传递而来的段分配动态内核页,并将段拷贝到这些内核页上。

kexec 还分配了一个用来存储汇编代码的小存根(small stub)的内核页,称为 reboot_code_buffer 。这个存根完成用将要重新启动到的内核来覆盖当前内核并跳转到它的实际工作。 reboot_code_buffer 是惟一的存留在其最终存放位置的缓存。换句话说,它从它最初装载到的位置开始执行。为此,在启用了 MMU 的系统上,驻留这些代码的页被 一致映射(identity mapped)。简单说,这需要用相同的物理和虚拟地址在 init_mm (内核的页表结构体)中创建一个页表条目。必须这样做才能在重新启动操作中访问这一代码段,如接下来将要论述的。

关于 reboot_code_buffer 、各种段以及其他细节的信息通过使用 kimage 结构来保持:

清单 2. kimage 结构体

struct kimage {
        kimage_entry_t head;
        kimage_entry_t *entry;
        kimage_entry_t *last_entry;
        unsigned long destination;
        unsigned long offset;
        unsigned long start;
        struct page *reboot_code_pages;
        unsigned long nr_segments;
        struct kexec_segment segment[KEXEC_SEGMENT_MAX+1];
        struct list_head dest_pages;
        struct list_head unuseable_pages;
};


当前,这个结构体中最重要的部分是指向容纳映像的内核内存中缓存的 segment[KEXEC_SEGMENT_MAX+1] 元素,和指向重新启动过程中使用的汇编存根的 reboot_code_pages

一旦内核映像被装载,系统就可以重新启动到它。使用 kexec -e 命令来开始真正重新启动到新内核。这个命令实际上是用 sys_reboot 系统调用来通知内核执行重新启动,但是使用了一个特别的 - LINUX_REBOOT_CMD_KEXEC 标志。

重新启动的系统调用遇到这个特别的标志后,将控制权移交给 machine_kexec() 函数。 machine_kexec() 执行的动作完全针对特定体系结构。在当前的 x86 实现中,动作的序列如下:

  1. 从当前进程的 mm struct 切换到使用内核的 init_mm 结构体,以访问一致映射的 reboot_code_buffer
  2. 停止 apics 并禁用中断。
  3. 将汇编存根代码拷贝到您在装载内核映像时分配的 reboot_code_buffer 中。此汇编代码可以在 relocate_new_kernel 例程中找到。
  4. 将内核数据段( __KERNEL_DS )值装载到段寄存器,并使用 GDT 和 IDT 无效。
  5. 跳转到 reboot_code_buffer 中的代码,将一些重要的信息以参数的形式传递给新内核,比如容纳有内核映像的源/目的地址的间接页,新内核的起始地址, reboot_code_buffer 页的地址,以及一个标明系统是否启用了物理地址扩展(physical address extension,PAE)的标志。

汇编存根代码执行下面的操作:

  • 自栈中读取参数,并将它们存储到寄存器中,然后禁用中断。
  • 使用以参数形式传递给自己的页地址,在页的末端设置一个栈。
  • 将新内核映像的起始地址存储到栈中,以使得存根代码的返回自动将系统引导到新的内核映像。
  • 设置 cr0 寄存器的适当位来禁用内存分页。
  • 将页目录基址寄存器 cr4 重设为 0。
  • 清空快表(Translation Lookaside Buffers,TLB)。
  • 将所有内核映像页拷贝到最终目标页。
  • 再次清空 TLB。
  • 将除了栈指针寄存器 esp(因为它指向容纳新内核起始地址的栈)以外的所有寄存器重设为 0。
  • 自存根代码“返回”。自动将系统引导到新内核。

这一系列工作完成后,新内核获得控制权,然后系统正常引导起来。






回页首



kexec 的益处



要求高可用性的系统,以及需要不断重新启动系统的内核开发人员,都将受益于 kexec。因为 kexec 跳过了系统重新启动过程中最耗时的部分(也就是固件阶段),所以重新启动变得非常快,可用性得到了提高。

kexec 在宕机转储(crash dump)工具中也得到了令人关注的应用。Linux Kernel Crash Dumps(LKCD)项目(查看 参考资料中的链接)使用 kexec 开发了一种不同的转储机制。在系统出错或者用户转储开始时,系统内存映像被压缩并转储到可用的空闲内存页中。接下来,系统使用 kexec 重新启动到另一个内核。新内核会被告知转储存储在何处,并防止任何进程使用那些内存区域。随后,内存转储可以写出到磁盘分区或者通过网络写到另一台机器。

这一设计的关键在于这样一个事实,通过避开重新启动过程中的固件阶段,LKCD 可以防止物理内存内容被固件清除掉。在宕机的时候,LKCD 也不需要依赖于一个不可靠的磁盘或者网络设备驱动器来将内存映像写出到目标位置。当重新启动执行,系统处于可靠状态后,转储通过正常的系统设备驱动器写出到目标位置。






回页首



kexec 的发展趋势



Kexec 当前只能用于 x86 32 位平台。使其可以应用于 PPC 64 和 AMD 64 等其他体系结构的平台将会有所帮助。而且,更好地与关机(shutdown)接口集成,以方便地终止进程、停止设备和?载文件系统,将使它更便于普通用户使用。

您可以为 kexec 的开发做出贡献。开始时请在一个测试用的系统上尝试 kexec。您还可以加入到“fastboot”邮件列表中,所有关于项目的技术讨论都在那里进行(查看 参考资料中的链接)。