Kdump 实现的基本原理
Kdump 的实现可以分为两个部分:内核和用户工具。内核提供机制,用户工具在这些机制上实现各种转储策略。内核机制对用户工具的接口是一个系统调用:kexec_load(),它被用于加载捕获内核和传递一些相关信息。捕获内核启动后,会像一般内核一样,去运行为它创建的 ramdisk 上的 init 程序。而各种转储机制都可以事先在 init 中实现。为了在生产内核崩溃时能顺利启动捕获内核,捕获内核(以及它的 ramdisk)是事先放到生产内核的内存中的。而捕获内核启动后需要使用的一小部分内存是通过 crashkernel=Y@X 这一内核参数在生产内核中保存的。为了生产内核的内存不被捕获内核启动时破坏,同时省去额外编译一个内核用作捕获内核的麻烦,kerenl 又实现了可重定位内核(relocatable kernel)技术。
生产内核的内存是通过 /proc/vmcore 这个文件交给捕获内核的。为了生成它,用户工具先在生产内核中分析出内存的使用和分布等情况,然后把这些信息综合起来 生成一个 ELF 文件头保存起来。捕获内核被引导时会被同时传递这个 ELF 文件头的地址,通过分析它,捕获内核就可以生成出 /proc/vmcore。有了 /proc/vmcore 这个文件,捕获内核的 ramdisk 中的脚本就可以通过通常的文件读写和网络来实现 各种策略了。同时 kdump 的用户工具还提供了缩减内存镜像尺寸的工具。这就是 Kdump 的基本设计。
Kexec 详解
用户空间工具
kdump 的很大一部分工作都是在用户空间内完成的。与 kexec 相关的集中在一个叫“kexec-tools”的工具中的“kexec”程序中。该程序主要是为调用 kexec_load() 收集各种信息,然后调用之。这些信息主要包括 purgatory 的入口地址,还有一组由 struct kexec_segment 描述的信息,该结构体定义为 :
struct kexec_segment { const void *buf; size_t bufsz; const void *mem; size_t memsz; };
kernel 系统调用
kexec 在 kernel 里以一个系统调用 kexec_load() 的形式提供给用户。这个系统调用主要用来把另一个内核和其 ramdisk 加载到当前内核中。在 kdump 中,捕获内核只能使用事先预留的一小段内存。生产内核的内存镜像会被以 /proc/vmcore 的形式提供给用户。这是一个 ELF 格式的方件,它的头是由用户空间工具 kexec 生成并传递来的。在系统崩溃时,系统最后会调用 machine_kexec()。这通常是一个硬件相关的函数。它会引导捕获内核,从而完成 kdump 的过程。
kdump 内存处理
用于捕获内核的内存
生产内核分析 cmdline 中的 crashkernel 参数后,调用 reserve_crashkernel() 来为捕获内核保存一段内存。这是一个 arch-dependent 的 function。保存之后,在 powerpc 上可以从 /proc/device-trees/chosen/linux,crashkernel-size 和 /proc/device-trees/chose/linux,crashkernel-base 中得到大小及位置信息。
捕获内核如何为 dump 工具提供生产内核的内存镜像
/proc/vmcore ELF 格式
kexec 在加载捕获内核时,会计算并生成一个 ELF 文件头。这个 ELF 头含有生产内核的内存位置等等一系列信息。这个 ELF 头连同其他信息一起保存在由 reserve_crashkernel() 保留的那段内存中。当崩溃发生时,此 ELF 头的位置会被传给捕获内核,由它生成 /proc/vmcore 以供 保存。
/dev/oldmem raw 格式
captured kernel 启动后,还会用 raw 格式通过 /dev/oldmem 来提供生产内核的内存。用户态的工具可能要自己提取其中的 ELF header 以便得到 vmcore。但它的好处是可以只提取 vmcore 的一部分而不用 dump 出全部 vmcore。
可重定位内核(relocatable kernel)
可重定位内核的意义
在 kdump 出现之前,内核只能从一个固定的物理地址上启动。这对 kdump 来说是一种限制。因为为了收集生产内核的内存镜像,捕获内核不能从生产内核使用的启动地址上启动。因此就需要另编译一个从一个不同的地址启动的内核来作捕获内核。这就是为什么 RHEL5 中有一个包叫 kernel-kdump 的原因。技术的创新往往来自对方便的追求。开发人员为了不用费心多编译一个内核,为 kernel 实现了“可重定向”这个特性。
实现原理
x86_64: 运行时修改 text 段及 data 段的眏射
kernel 在启动以后,会检测自己被加载到了什么位置。然后根据这个来更新自己的内存页表以反映 kernel 的 text 段和 data 段中虚拟地址与物理地址之间正确的映射关系。
i386: 使用预先生成的重定位信息
i386 中的 text 和 data 段是已经写死的线性映射区的一部分,要想使用修改页表的办法支持重定向是比较困难的。于是在编译内核时,另生成一份所有需要重定位的 symbol 的位置信息,放进 bzimage 格式的内核中。内核启动解压缩后,根据加载的地址和这份表来时行重定位。
powerpc: 将 vmlinuz 链接为“position-independent executable”形式
与 x86 体系不同,在 powerpc 体系中,/boot/vmlinuz 并不是一个 bzimage 格式的文件,它就是一个 ELF 格式的文件,而且启动机理也不尽相同。因此,在 powerpc 上主要是利用了“位置无关可执行”格式这一成熟技术来实现可重定位。
makedumpfile 简介
有些服务器有着超大的内存,可能比它的硬盘的容量还大。为了转储这样的内存镜像,就有了 makedumpfile,它的最主要的用途就是减少转储的内存镜像的体积。它有两个手段达到这个目的:页面过滤和页面压缩。
页面过滤
makedumpfile 在处理 /proc/vmcore 时,能够过滤掉这样一些内存页:
- 全是 0 的页
- 缓存页
- 用户进程页
- 空闲页
这些类型的页往往是无关紧要的。通过“-d”这个选项指定一个过滤等级来去掉相应的页,如果要去掉所有这些类型,需要指定“31”。
页面压缩
makedumpfile 可以逐页地压缩 vmcore 中的内存页,在事后的分析中,crash 可以在分析到某页时才将其解压缩。
神秘的 purgatory
细心的 kdump 用户可能注意过在 kdump 刚开始运行时 console 上会出现这样一句话:“I'm in purgatory.”。这就是进入了 purgatory 时的提示。简单说,purgatory 就是一个 bootloader,一个为 kdump 定作的 boot loader。它被赋予了这样一个古怪的名字应该只是一种调侃。实事上,与其说是内核可以引导另一个内核,不如说是 purgatory 可以引导一个内核。 下面的分析完全基于 ppc64。
purgatory 和 kexec
在特定体系架构上编译 kexec 时,purgatory 会从相应特定体系的源码生成。它是一个 ELF 格式的 relocatable 文件。为了使用上的方 便,它被一个工具,“bin-to-hex”,翻译成一个数组并放在 kexec/purgatory.c 里。这样 kexec 的代码就可以直接使用它了:
// kexec/purgatory.c #iclude <stddef.h> const char purgatory[] = { 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x15, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xba, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x20, 0x00, 0x1d, ... ... 0x79, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, }; size_t purgatory_size = sizeof(purgatory); // kexec/arch/ppc64/kexec-elf-ppc64.c 中分读入 purgatory: /* Add v2wrap to the current image */ elf_rel_build_load(info, &info->rhdr, purgatory, purgatory_size, 0, max_addr, 1, 0);
在通过 kexec_load() 将 purgatory 传给 kernel 之前,kexec 还会对已经读入的 purgatory 进行一些改造,存进一些在引导 内核时必需的信息,如新内核在内存中的地址、device tree blob 的地址等等。值得注意的是,kexec 用捕获内核的前 256 个字节覆盖从 purgatory 入口点开始的 256 个字节(在 ppc64 上刚好是 64 条汇编指令,覆盖完成后,kexec 会把第一个指令恢复成 purgatory 自己原来的)。这一部分有两个功能:标明 kenrel 是否是 relocatable 的和让 slave cpu 等待 primary cpu。
purgatory 和 kernel
kernel 崩溃了,如果 kexec_load() 加载了捕获内核,它会先让没有发生崩溃的 cpu(slave cpu)通过调用 kexec_wait 进入等待:
_GLOBAL(kexec_wait) bl 1f 1: mflr r5 addi r5,r5,kexec_flag-1b 99: HMT_LOW #ifdef CONFIG_KEXEC /* use no memory without kexec */ lwz r4,0(r5) cmpwi 0,r4,0 bnea 0x60 #endif b 99b /* this can be in text because we won't change it until we are * running in real anyways */ kexec_flag: .long 0
这里 kexec_wait 去检查“kexec_flag”的值(初始值是 0),如果是 0 则回到“99:”继续检查;如果不是 0 了,就跳到 0x60 处。这样发生崩溃的 cpu 可以从容地完成一些工作,例如把在 kexec 中得到的捕获内核的前 15 条指令和自己的第一条指令拷到内存的起点等等,再让 slave cpu 跳到 0x60 去等待 primary cpu 完成启动,这是在 kexec_sequence 中完成的。
... ... /* copy dest pages, flush whole dest image */ mr r3,r29 bl .kexec_copy_flush /* (image) */ /* turn off mmu */ bl real_mode /* copy 0x100 bytes starting at start to 0 */ li r3,0 mr r4,r30 /* start, aka phys mem offset */ li r5,0x100 li r6,0 bl .copy_and_flush /* (dest, src, copy limit, start offset) */ 1: /* assume normal blr return */ /* release other cpus to the new kernel secondary start at 0x60 */ mflr r5 li r6,1 stw r6,kexec_flag-1b(5) ... ...
bl 会把“1:”的地址存入 LR,因此 r5 中被 mflr 存入的是这个地址,而“kexec_flag-1b(5)”就是 kexec_flag 的地址了。stw 向这个地址存入了“1”。这都是在前面的拷贝和关闭 MMU 等完成之后了才做的。在 kexec_sequence 最后,将 purgatory 的入口地址存入 CTR,然后调用 bctrl 从而调用 purgatory。purgatory 则利用 kexec 中存入的一系列信息,最终启动了捕获内核。
作为 bootloader 的 purgatory
虽然 kexec 用捕获内核开头的 256 个字节覆盖了 purgatory 入口处的 256 个字节,但保留了入口的第一条指令,即 b master。于是 purgatory 会跳转到“master”,作一些校验工作;加载一系列自己正常运行和引导内核必不可少的信息,如内核(捕获内核)在内存中的位 置、device tree blob 的位置等。这些都是在 kexec 中生成、存放好了,然后写到 purgatory 的 ELF 格式中,同时也由 kexec_load() 传递给了生产内核的,下面是引导内核前的代码片段:
80: LOADADDR(6,kernel) ld 4,0(6) # load the kernel address LOADADDR(6,run_at_load) # the load flag lwz 7,0(6) # possibly patched by kexec-elf-ppc64 stw 7,0x5c(4) # and patch it into the kernel li 5,0 # r5 will be 0 for kernel mtctr 4 # prepare branch too mr 3,16 # restore dt address # skip cache flush, do we care? bctr # start kernel
此处,LOADADDR 是一个宏,它是一组 5 个的 ppc64 向寄存器加载 64 位即时数的专用指令。这里先把内核的保存位置载入寄存器 4,然后把 purgatory 中 run_at_load 处的值写入从该位置往后位移 0x5c 的地方。这个 run_at_load 是在被 kexec 用捕获内核覆盖的那部分中,kexec 在覆盖这部分时还作了一件事,就是检查捕获内核是否是可重定位的。如果是,就在这个位置上写入 1。因此,如果捕获内核是可重定位的内核,那么在 purgatory 引导它之前,距它开头 0x5c 处的值是 1。这个值告诉捕获内核它应该从它被加载的位置启动。接下来,将捕获内核的地址写入 CTR,用 bctr 来启动它。
kdump 实用小技巧
如何设定 crashkernel 参数
在 kdump 的配置中,往往困惑于 crashkernel 的设置。“crashkernel=X@Y”,X 应该多大? Y 又应该设在哪里呢?实际我们 可以完全省略“@Y”这一部分,这样,kernel 会为我们自动选择一个起始地址。而对于 X 的大小,般对 i386/x86_64 的系统, 设为 128M 即可;对于 powerpc 的系统,则要设为 256M。rhel6 引入的“auto”已经要被放弃了,代之以原来就有的如下语法:
crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset] range=start-[end] 'start' is inclusive and 'end' is exclusive. For example: crashkernel=512M-2G:64M,2G-:128M
如何判断捕获内核是否加载
可通过查看 /sys/kernel/kexec_crash_loaded 的值。“1”为已经加载,“0”为还未加载。
缩小 crashkernel
可以通过向 /sys/kernel/kexec_crash_size 中输入一个比其原值小的数来缩小甚至完全释放 crashkernel。
kdump 相关新技术
kexec 原来只是用于内核的快速启动,但很快被用来实现内存转储,成为了企业级的的重要应用。但是创新的步伐 并未就此停止。
系统休眠
使用 kexec/kdump 来实现系统休眠(hibernation)已经进行了几年了。目前的状态不得而知,但这种思路上的 创新很让人眼前一亮。目前只支持 x86 体系。相关内容参见最后的链接。
小结
kdump 是目前最有效的 linux™ 内存镜像收集机制,广泛应用于各大 linux™ 厂商的各种产品中,在 debug 内核方面起着不可替换的重要作用。本文着重于深入探索 kdump 的实现机制,希望能让读者通过了解细节从而促进对 kdump 使用的掌握。