s1eep123's blog.

Kdump无法启动调试及其原理

Word count: 2.8kReading time: 12 min
2023/07/04

针对Tencent OS kdump服务无法运行及其原理分析

感谢同事及导师对此问题分析定位过程中的帮助。

客户提出母机crashkernel没有预留成功,导致kdump服务无法正常运行,这样会存在一个隐患,母机如果panic的话是无法成功生成crashcore的。

首先看看kdump的工作原理

内核运行过程中遇到致命错误会导致panic,此时kdump会将处于panic状态的内核数据现场进行保留,存储到硬盘中等外部设备,以便后续分析定位问题

kdump的设计是将物理内存的一部分划分出来专门留给kdump使用,如在redhat8中默认位168m,redhat9中默认位192m

kdump依赖于“双内核”设计,或者可以理解为只保留部分功能的微内核(个人猜测,为深入代码具体考证),在系统正常运行时,kdump会加载一个备用内核到kdump预留内存,系统崩溃时,通过kexec机制(一种通过硬件固件快速重启初始化系统机制)跳转至kdump内核进行数据dump

带来一个问题就是,系统正常运行过程中,kdump预留内存是完全空闲浪费的,应该越小越好。但随着内核功能增加以及kdump转储过程中需要相应工具驱动支持。kdump所需内存也越来越多。(这里存在一个优化kdump预留内存的点,后边有简单介绍)

一图流解释kdump运行机制

image-20230729115851956

其中最重要的一个环节就是kdump initramfs,即为kdump的用户态,也就是真正执行dump逻辑过程的部分,kdump内核启动后,会执行用户态init,通过对已崩溃的内存读写接口以及硬件操作接口来实现dump工作

而kdump用户态复用了普通内核的用户态initramfs生成工具:Dracut

在普通内核,Dracut生成的initramfs目的是给内核提供基本引导环境,挂载各种设备等。以便执行init进程

而在kdump内核中,则是挂在各种各样的crash dump存储设备,以便将数据存储到外部设备中,然后就是和普通内核一样启动init进程进行dump

针对kdump内核占用内存过大问题优化方法(从同事wiki中得知,具体实现以待考证)

  1. 对kdump initramfs进行裁剪
  2. 更改initramfs压缩算法,目前为了适配大多数采用gzip压缩
  3. 修改kdump内核参数,针对boot文件以及相关配置进行内存限制,cpu使用限制(个人感觉还是docker限制资源的那一套)
  4. 修改elf文件

真正的问题来了

在kernel中 kdump内存是怎么预留的呢,为什么客户的kdump会无法启动呢(首先猜测客户机器内存太小了,导致kdump无法找到对应大小内存无法预留成功)

要搞清楚是如何预留内存的,需要了解三个点(也就是kernel内存分配过程)

  1. e820表的解析
  2. memblock接管e820数据
  3. kdump从memblock寻找合适内存进行预留

从腾讯云官网购买32g的服务器,那么操作系统如何获取32g内存信息呢

查看系统开机日志,会发现这样log

image-20230729115913072

也就是说,系统刚刚开机初始化过程中,是通过e820表来进行物理内存管理的

e820(https://en.wikipedia.org/wiki/E820)大概意思就是通过int15h来进行探测内存,看一看哪些内存是保留给BIOS使用的,探测结果就是上图dmesg显示的

e820这还有一个坑就是探测返回结果是无序的,至于为什么会这样设计,需深入kernel代码进行考证,主要是三个过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
main.c → detect_memory → detect_memory_e820

static int detect_memory_e820(void)
{
int count = 0;
struct biosregs ireg, oreg;
struct e820entry *desc = boot_params.e820_map;
static struct e820entry buf; /* static so it is zeroed */

initregs(&ireg);
ireg.ax = 0xe820;
ireg.cx = sizeof buf;
ireg.edx = SMAP;
ireg.di = (size_t)&buf;

do {
intcall(0x15, &ireg, &oreg);
ireg.ebx = oreg.ebx; /* for next iteration... */

/* BIOSes which terminate the chain with CF = 1 as opposed
to %ebx = 0 don't always report the SMAP signature on
the final, failing, probe. */
if (oreg.eflags & X86_EFLAGS_CF)
break;

/* Some BIOSes stop returning SMAP in the middle of
the search loop. We don't know exactly how the BIOS
screwed up the map at that point, we might have a
partial map, the full map, or complete garbage, so
just return failure. */
if (oreg.eax != SMAP) {
count = 0;
break;
}

*desc++ = buf;
count++;
} while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));

return boot_params.e820_entries = count;
}

探测信息放到boot_params.e820_map这个全局变量中

接着内核会调用setup_memory_map函数将探测到的内存信息打印出来

1
2
3
4
5
6
7
8
9
void __init setup_memory_map(void)
{
char *who;

who = x86_init.resources.memory_setup();
memcpy(&e820_saved, &e820, sizeof(struct e820map));
printk(KERN_INFO "e820: BIOS-provided physical RAM map:\n");
e820_print_map(who);
}

再之后会有一个排序重新拷贝的过程,因为是结果是无序的

这部分代码过于抽象,水平有限,留置后续分析

最终调用到函数e820_print_map将其打印到log中

1
2
3
4
5
6
7
8
9
10
11
12
13
void __init e820_print_map(char *who)
{
int i;

for (i = 0; i < e820.nr_map; i++) {
printk(KERN_INFO "%s: [mem %#018Lx-%#018Lx] ", who,
(unsigned long long) e820.map[i].addr,
(unsigned long long)
(e820.map[i].addr + e820.map[i].size - 1));
e820_print_type(e820.map[i].type);
printk(KERN_CONT "\n");
}
}

对应的结果就是:

image-20230729120217000

其中usable就是代表是内核可以使用的物理内存大小,将这三块usable加起来的大小就是32G物理内存。

当然了从这里探测物理内存就算结束了

memblock是如何接管e820数据

在e820表探测完毕系统的物理内存之后,会将物理内存交接给memblock管理。memblock主要作用是在buddy系统没启动之前管理系统的物理内存。

memblock使用的数据结构

1
2
3
4
5
6
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};

bootom_up是一个bool变量,True代表底部(bottom)到顶部(up)分配内存,flase就是反方向分配内存。默认是flase的值。

memory和reserved是代表的struct memblock_type的结构,memory代表的是e820表中used这部分,reserved就代表的是e820表reserved这部分内存

1
2
3
4
5
6
struct memblock_type {
unsigned long cnt; /* number of regions */
unsigned long max; /* size of the allocated array */
phys_addr_t total_size; /* size of all regions */
struct memblock_region *regions;
};

系统在启动过程中会将memblock_x86_fill函数中填充e820表的数据到memblock中,在高版本的内核中函数名为:e820__memblock_setup();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void __init memblock_x86_fill(void)
{
int i;
u64 end;

/*
* EFI may have more than 128 entries
* We are safe to enable resizing, beause memblock_x86_fill()
* is rather later for x86
*/
memblock_allow_resize();

for (i = 0; i < e820.nr_map; i++) {
struct e820entry *ei = &e820.map[i];

end = ei->addr + ei->size;
if (end != (resource_size_t)end)
continue;

if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN)
continue;

memblock_add(ei->addr, ei->size);
}

/* throw away partial pages */
memblock_trim_memory(PAGE_SIZE);

memblock_dump_all();
}

通过crash可以dump出memblock数据,因为是个全局变量,或者修改启动参数也可以

image-20230729120233011

memory中的total_size就是我们虚拟机的物理内存大小了。

image-20230729120248154

那么kdump是如何预留内存的呢

假设kdump cmdline配置的参数是crashkernel=768M,通过代码看看crashkernel是如何预留内存的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
static void __init reserve_crashkernel(void)
{
unsigned long long crash_size, crash_base, total_mem;
bool high = false;
int ret;

total_mem = memblock_phys_mem_size();

/* crashkernel=XM */
ret = parse_crashkernel(boot_command_line, total_mem, &crash_size, &crash_base);
if (ret != 0 || crash_size <= 0) {
/* crashkernel=X,high */
ret = parse_crashkernel_high(boot_command_line, total_mem,
&crash_size, &crash_base);
if (ret != 0 || crash_size <= 0)
return;
high = true;
}

/* 0 means: find the address automatically */
if (crash_base <= 0) {
/*
* kexec want bzImage is below CRASH_KERNEL_ADDR_MAX
*/
crash_base = memblock_find_in_range(CRASH_ALIGN,
high ? CRASH_ADDR_HIGH_MAX
: CRASH_ADDR_LOW_MAX,
crash_size, CRASH_ALIGN);
#ifdef CONFIG_X86_64
/*
* crashkernel=X reserve below 896M fails? Try below 4G
*/
if (!high && !crash_base)
crash_base = memblock_find_in_range(CRASH_ALIGN,
(1ULL << 32),
crash_size, CRASH_ALIGN);
/*
* crashkernel=X reserve below 4G fails? Try MAXMEM
*/
if (!high && !crash_base)
crash_base = memblock_find_in_range(CRASH_ALIGN,
CRASH_ADDR_HIGH_MAX,
crash_size, CRASH_ALIGN);
#endif
if (!crash_base) {
pr_info("crashkernel reservation failed - No suitable area found.\n");
return;
}

} else {
unsigned long long start;

start = memblock_find_in_range(crash_base,
crash_base + crash_size,
crash_size, 1 << 20);
if (start != crash_base) {
pr_info("crashkernel reservation failed - memory is in use.\n");
return;
}
}
ret = memblock_reserve(crash_base, crash_size);
if (ret) {
pr_err("%s: Error reserving crashkernel memblock.\n", __func__);
return;
}

if (crash_base >= (1ULL << 32) && reserve_crashkernel_low()) {
memblock_free(crash_base, crash_size);
return;
}

pr_info("Reserving %ldMB of memory at %ldMB for crashkernel (System RAM: %ldMB)\n",
(unsigned long)(crash_size >> 20),
(unsigned long)(crash_base >> 20),
(unsigned long)(total_mem >> 20));

crashk_res.start = crash_base;
crashk_res.end = crash_base + crash_size - 1;
insert_resource(&iomem_resource, &crashk_res);
}

memblock_phys_mem_size函数是获取memblock flag等于memory的物理内存大小总和,这个totalmem就等于33822329856

通过parse_crashkernel会解析出crash_size=768M, crash_base=0。

当crash_base等于0时,调用这个函数memblock_find_in_range, 因为high是等于0的,所有参数分别是CRASH_ALIGN, CRASH_ADDR_LOW_MAX, crash_size, CRASH_ALIGN

其中CRASH_ALIGN=16<<20,代表的16M对齐,CRASH_ADDR_LOW_MAX在x86_64下是896M。 翻译过来就从MEMBLOCK分配器中查找开始地址是16M,结束地址是896M区间中,寻找大小为768M的区域。

通过一个例子来分析。这个例子是现网的母机无法成功保留crashkernel大小

image-20230729120304353

可以看到是没有保留256M大小的区间的。接着从从896M下面寻找空闲区间

空闲区间:0x2a661000 - 0x2a65c057 = 0x4FA9
空闲区间:0x2a654018 - 0x2a653057 = 0xFC1
空闲区间:0x2a642018 - 0x2a641057 = 0xFC1
空闲区间:0x2a630018 - 0x2a40eFFF = 0x221019
空闲区间:0x2a30F000 - 0x1e2c6FFF = 0xC048001
空闲区间:0x1d208000 - 0x244FFFF = 0x1 ADB8 001
空闲区间:0x1000000 - 0xFFFFF = 0xF00001

还是没能找到合适空间,尝试到4G下面去找,如果4G下面也没有就尝试最大的空间去找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef CONFIG_X86_64
/*
* crashkernel=X reserve below 896M fails? Try below 4G
*/
if (!high && !crash_base)
crash_base = memblock_find_in_range(CRASH_ALIGN,
(1ULL << 32),
crash_size, CRASH_ALIGN);
/*
* crashkernel=X reserve below 4G fails? Try MAXMEM
*/
if (!high && !crash_base)
crash_base = memblock_find_in_range(CRASH_ALIGN,
CRASH_ADDR_HIGH_MAX,
crash_size, CRASH_ALIGN);
#endif

从4G下面寻找空闲 区间

0x4a15f000 - 0x4758eFFF = 0x2BD0 001
0x427e7000 - 0x427e3FFF = 0x3001
0x4249e000 - 0x3f38c797 = 0x3 111 869
0x3f38c018 - 0x3f38bFFF = 0x19

从4G下面也是没有找到空闲区间,只能去MAXMEM下面寻找

从MAXMEM下面寻找空闲区间

空闲区间:0x603fffe000 - 0x603fffd003 = 0xxFFD
空闲区间:0x603fffd000 - 0x603FFFCFFF = 0x1
空闲区间:0x603ffd6000 - 0x3040000000 = 0x2FFFFD6000

看起来这个区间0x3040000000 - 0x603ffd6000区间是符合要求的,但是因为16M对齐,所以是0x603f000000 - 768M = 0x600F000000

所以最终寻找的区间是:0x600F000000 到 0x0000603effffff, 从demsg中也是可以看到的

image-20230729120317050

当crash_base的大小大于4G的时候,还需要去lowmemory去reserve下

1
2
3
4
if (crash_base >= (1ULL << 32) && reserve_crashkernel_low()) {
memblock_free(crash_base, crash_size);
return;
}

这里在这台机器上就出现了失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int __init reserve_crashkernel_low(void)
{
#ifdef CONFIG_X86_64
unsigned long long base, low_base = 0, low_size = 0;
unsigned long total_low_mem;
int ret;

total_low_mem = memblock_mem_size(1UL << (32 - PAGE_SHIFT));

/* crashkernel=Y,low */
ret = parse_crashkernel_low(boot_command_line, total_low_mem, &low_size, &base);
if (ret) {
/*
* two parts from lib/swiotlb.c:
* -swiotlb size: user-specified with swiotlb= or default.
*
* -swiotlb overflow buffer: now hardcoded to 32k. We round it
* to 8M for other buffers that may need to stay low too. Also
* make sure we allocate enough extra low memory so that we
* don't run out of DMA buffers for 32-bit devices.
*/
low_size = max(swiotlb_size_or_default() + (8UL << 20), 256UL << 20);
} else {
/* passed with crashkernel=0,low ? */
if (!low_size)
return 0;
}

low_base = memblock_find_in_range(low_size, 1ULL << 32, low_size, CRASH_ALIGN);
if (!low_base) {
pr_err("Cannot reserve %ldMB crashkernel low memory, please try smaller size.\n",
(unsigned long)(low_size >> 20));
return -ENOMEM;
}

这里会记着从Low_size=256M开始的地址,结束地址是4G,大小是256M的区域去寻找。结果失败了,导致crash无法预留成功

最后通过升级BIOS来进行解决该问题

升级前

image-20230729120328230

升级后

image-20230729120343039

可以看到升级了BIOS之后连续的低端地址变大了,从而可以有效的提升crashkernel的预留成功机会。

CATALOG
  1. 1. 真正的问题来了
  2. 2. 从4G下面寻找空闲 区间
  3. 3. 从MAXMEM下面寻找空闲区间