深入探索 Kdump,第 3 部分

Kdump 原理探秘

Comments
 
0

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 描述的信息,该结构体定义为 :

1
2
3
4
5
6
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 的代码就可以直接使用它了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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 进入等待:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_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 中完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
... ...
     /* 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() 传递给了生产内核的,下面是引导内核前的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
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”已经要被放弃了,代之以原来就有的如下语法:

1
2
3
4
5
6
7
8
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 使用的掌握。

相关主题
tcpdump [ -DenNqvX ] [ -c count ] [ -F file ] [ -i interface ] [ -r file ] [ -s snaplen ] [ -w file ] [ expression ] 抓包选项: -c:指定要抓取的包数量。注意,是最终要获取这么多个包。例如,指定"-c 10"将获取10个包,但可能已经处理了100个包,只不过只有10个包是满足条件的包。 -i interface:指定tcpdump需要监听的接口。若未指定该选项,将从系统接口列表中搜寻编号最小的已配置好的接口(不包括loopback接口,要抓取loopback接口使用tcpdump -i lo), :一旦找到第一个符合条件的接口,搜寻马上结束。可以使用'any'关键字表示所有网络接口。 -n:对地址以数字方式显式,否则显式为主机名,也就是说-n选项不做主机名解析。 -nn:除了-n的作用外,还把端口显示为数值,否则显示端口服务名。 -N:不打印出host的域名部分。例如tcpdump将会打印'nic'而不是'nic.ddn.mil'。 -P:指定要抓取的包是流入还是流出的包。可以给定的值为"in"、"out"和"inout",默认为"inout"。 -s len:设置tcpdump的数据包抓取长度为len,如果不设置默认将会是65535字节。对于要抓取的数据包较大时,长度设置不够可能会产生包截断,若出现包截断, :输出行中会出现"[|proto]"的标志(proto实际会显示为协议名)。但是抓取len越长,包的处理时间越长,并且会减少tcpdump可缓存的数据包的数量, :从而会导致数据包的丢失,所以在能抓取我们想要的包的前提下,抓取长度越小越好。 输出选项: -e:输出的每行中都将包括数据链路层头部信息,例如源MAC和目标MAC。 -q:快速打印输出。即打印很少的协议相关信息,从而输出行都比较简短。 -X:输出包的头部数据,会以16进制和ASCII两种方式同时输出。 -XX:输出包的头部数据,会以16进制和ASCII两种方式同时输出,更详细。 -v:当分析和打印的时候,产生详细的输出。 -vv:产生比-v更详细的输出。 -vvv:产生比-vv更详细的输出。 其他功能性选项: -D:列出可用于抓包的接口。将会列出接口的数值编号和接口名,它们都可以用于"-i"后。 -F:从文件中读取抓包的表达式。若使用该选项,则命令行中给定的其他表达式都将失效。 -w:将抓包数据输出到文件中而不是标准输出。可以同时配合"-G time"选项使得输出文件每time秒就自动切换到另一个文件。可通过"-r"选项载入这些文件以进行分析和打印。 -r:从给定的数据包文件中读取数据。使用"-"表示从标准输入中读取。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值