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

Kdump作为高可靠的linux内核转储机制,有着一些鲜为人知的技术细节。本文试图以powerpc架构为重点,深入介绍这些细节使读者更好地理解Kdump

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/vmcoreELF 格式

kexec 在加载捕获内核时,会计算并生成一个 ELF 文件头。这个 ELF 头含有生产内核的内存位置等等一系列信息。这个 ELF 头连同其他信息一起保存在由 reserve_crashkernel() 保留的那段内存中。当崩溃发生时,此 ELF 头的位置会被传给捕获内核,由它生成 /proc/vmcore 以供保存。

/dev/oldmemraw 格式

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() 加载了捕获内核,它会先让没有发生崩溃的 cpuslave 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 physmem 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 kernelsecondary 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 从而调用 purgatorypurgatory 则利用 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 kerneladdress

        LOADADDR(6,run_at_load) # the load flag

        lwz    7,0(6)          # possibly patchedby kexec-elf-ppc64

        stw    7,0x5c(4)       # and patch itinto the kernel

        li     5,0             # r5 will be 0 forkernel

        mtctr  4               # prepare branchtoo

        mr     3,16            # restore dtaddress

 

                                # skip cacheflush, 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 的系统,则要设为 256Mrhel6 引入的“auto”已经要被放弃了,代之以原来就有的如下语法:

 crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]

                range=start-[end]

 

                  'start' is inclusive and 'end' isexclusive.

 

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

参考资料

学习

·        参考 kexec-tools 的开发站点

·        参考 linux kernel 的开发站点

·        参考 developerWorks 上文章: PowerPC 汇编简介

·        参考 Kexec Hibernation Progress

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值