深入探索 Kdump,第 3 部分: Kdump 原理探秘

时间:2022-05-01 16:51:11

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 使用的掌握。