分类目录归档:crash分析

kdump简介

分析操作系统crash或hang的原因,需要用到kernel dump。Linux系统用来捕捉kernel dump的工具是kdump。

kdump的原理是启动一个特殊的dump-capture kernel把系统内存里的数据保存到文件里,为什么需要一个特殊的dump-capture kernel呢?因为原来的kernel已经出问题了,发生crash或hang了。

Dump-capture kernel 既可以是独立的,也可以与系统内核集成在一起–这需要硬件支持relocatable kernel才行。在X86_64系统上RHEL6/7和SLES11/12缺省都是与系统内核集成在一起的。

kdump工作的过程如下:

  1. 系统内核启动的时候,要给dump-capture kernel预留一块内存空间;
  2. 内核启动完成后,kdump service执行 kexec -p 命令把dump-capture kernel载入预留的内存里;
  3. 然后,如果系统发生crash,会自动reboot进入dump-capture kernel,dump-capture kernel只使用自己的预留内存,确保其余的内存数据不会被改动,它的任务是把系统内存里的数据写入到dump文件,比如/var/crash/vmcore,为了减小文件的大小,它会通过makedumpfile(8)命令对内存数据进行挑选和压缩;
  4. dump文件写完之后,dump-capture kernel自动reboot。
预留内存

配置kdump的一个关键环节是预留内存。预留的内存是有特殊要求的,它必须是连续的,在老的系统上比如SLES11还要求内存物理地址低于4GB(SLES12无此要求,因为新kernel允许使用高位内存)。可供预留的内存取决于硬件配置,BIOS/firmware占用的内存是不能预留的,EFI经常占用很多内存,I/O卡也要占用内存,在引导过程中都能看到:

为dump-capture kernel预留内存的方法是在kernel command line中加入如下参数:
crashkernel=size[@offset]

需要预留多少内存呢?
RHEL从6.2开始可以使用”crashkernel=auto”,让内核自行计算,如果有问题才手工指定。
SLES12则可以用”kdumptool calibrate”命令计算推荐值,具体方法参见SUSE DOC: Calculating crashkernel Allocation Size
更老的系统上需要根据系统总内存手工计算预留内存的大小,参考值请自行搜索。

不幸的是,确实有些系统存在无法预留足够内存的情况,如果dump-capture kernel需要的内存比较多,而硬件配置又比较复杂导致可用的连续内存不足,就有可能发生。这在老内核上更常见,而新内核允许使用高位内存就好多了。

通常RHEL 6.X最大建议768MB,实际上代码中最大的限制是896MB,但实际经验中800M以上就会有各种问题。

Redhat还提供了一个设置kdump的助手工具:
https://access.redhat.com/labs/kdumphelper/

kdump的性能

计算机的内存越来越大,kernel dump也越来越大,保存dump文件的时间也越来越长,为了提高速度,kdump的配置可以进行调整。

1,多CPU
缺省情况下dump-capture kernel只使用一个CPU,有时使用多个CPU会有帮助。这可以通过修改dump-capture kernel 的参数 nr_cpus=1 来实现,怎样修改dump-capture kernel的参数呢?不是在grub中,那里是普通kernel的参数,而是在 /etc/sysconfig/kdump 中,如下所示:
KDUMP_COMMANDLINE_APPEND=”irqpoll nr_cpus=4 …”

2,压缩算法
makedumpfile(8)默认的”-c”参数使用zlib,虽然压缩比很高但是速度很慢,通常低于30MB/s。可以选用速度更快的LZO,虽然压缩比稍微低一点但是速度可达800MB/s,把/etc/kdump.conf中的makedumpfile(8)的”-c”参数换成”-l”即可。

3,排除不需要的内存页
makedumpfile(8)的”-d”参数指定dump level,dump level是一个5-bit的编码,每个bit表示一种可以排除的内存页:
                    1 : Exclude the pages filled with zero.
                    2 : Exclude the non-private cache pages.
                    4 : Exclude all cache pages.
                    8 : Exclude the user process data pages.
                   16 : Exclude the free pages.
一般默认dump level是31,即排除以上所有类型的内存页。

参考资料:

http://lse.sourceforge.net/kdump/documentation/ols2oo5-kdump-paper.pdf
https://www.kernel.org/doc/Documentation/kdump/kdump.txt
http://people.redhat.com/nhorman/papers/ols-slides.pdf
http://events.linuxfoundation.org/sites/events/files/slides/slide_final_0.pdf

内核栈溢出

在Linux系统上,进程运行分为用户态与内核态,进入内核态之后使用的是内核栈,作为基本的安全机制,用户程序不能直接访问内核栈,所以尽管内核栈属于进程的地址空间,但与用户栈是分开的。Linux的内核栈大小是固定的,从2.6.32-520开始缺省大小是16KB,之前的kernel版本缺省大小是8KB。内核栈的大小可以修改,但要通过重新编译内核实现。以下文件定义了它的大小:

arch/x86/include/asm/page_64_types.h
8KB:
#define THREAD_ORDER 1
16KB:
#define THREAD_ORDER 2

由于内核栈的大小是有限的,就会有发生溢出的可能,比如调用嵌套太多、参数太多都会导致内核栈的使用超出设定的大小。内核栈溢出的结果往往是系统崩溃,因为溢出会覆盖掉本不该触碰的数据,首当其冲的就是thread_info — 它就在内核栈的底部,内核栈是从高地址往低地址生长的,一旦溢出首先就破坏了thread_info,thread_info里存放着指向进程的指针等关键数据,迟早会被访问到,那时系统崩溃就是必然的事。kstack-smash

【小知识】:把thread_info放在内核栈的底部是一个精巧的设计,在高端CPU中比如PowerPC、Itanium往往都保留了一个专门的寄存器来存放当前进程的指针,因为这个指针的使用率极高,然而x86的寄存器太少了,专门分配一个寄存器实在太奢侈,所以Linux巧妙地利用了栈寄存器,把thread_info放在内核栈的底部,这样通过栈寄存器里的指针可以很方便地算出thread_info的地址,而thread_info的第一个字段就是进程的指针。

内核栈溢出导致的系统崩溃有时会被直接报出来,比如你可能会看到:

但更多的情况是不直接报错,而是各种奇怪的panic。在分析vmcore的时候,它们的共同点是thread_info被破坏了。以下是一个实例,注意在task_struct中stack字段直接指向内核栈底部也就是thread_info的位置,我们看到thread_info显然被破坏了:cpu的值大得离谱,而且指向task的指针与task_struct的实际地址不匹配:

作为一种分析故障的手段,可以监控内核栈的大小和深度,方法如下:

然后检查下列数值,可以看到迄今为止内核栈使用的峰值和对应的backtrace:

你可以写个脚本定时收集上述数据,有助于找到导致溢出的代码。下面是一个输出结果的实例:

 

内核如何检测soft lockup与hard lockup?

所谓lockup,是指某段内核代码占着CPU不放。Lockup严重的情况下会导致整个系统失去响应。Lockup有几个特点:

  • 首先只有内核代码才能引起lockup,因为用户代码是可以被抢占的,不可能形成lockup(只有一种情况例外,就是SCHED_FIFO优先级为99的实时进程即使在用户态也可能使[watchdog/x]内核线程抢不到CPU而形成soft lock,参见《Real-Time进程会导致系统Lockup吗?》)
  • 其次内核代码必须处于禁止内核抢占的状态(preemption disabled),因为Linux是可抢占式的内核,只在某些特定的代码区才禁止抢占,在这些代码区才有可能形成lockup。

Lockup分为两种:soft lockup 和 hard lockup,它们的区别是 hard lockup 发生在CPU屏蔽中断的情况下。

  • Soft lockup是指CPU被内核代码占据,以至于无法执行其它进程。检测soft lockup的原理是给每个CPU分配一个定时执行的内核线程[watchdog/x],如果该线程在设定的期限内没有得到执行的话就意味着发生了soft lockup,[watchdog/x]是SCHED_FIFO实时进程,优先级为最高的99,拥有优先运行的特权。
  • Hard lockup比soft lockup更加严重,CPU不仅无法执行其它进程,而且不再响应中断。检测hard lockup的原理利用了PMU的NMI perf event,因为NMI中断是不可屏蔽的,在CPU不再响应中断的情况下仍然可以得到执行,它再去检查时钟中断的计数器hrtimer_interrupts是否在保持递增,如果停滞就意味着时钟中断未得到响应,也就是发生了hard lockup。

Linux kernel设计了一个检测lockup的机制,称为NMI Watchdog,是利用NMI中断实现的,用NMI是因为lockup有可能发生在中断被屏蔽的状态下,这时唯一能把CPU抢下来的方法就是通过NMI,因为NMI中断是不可屏蔽的。NMI Watchdog 中包含 soft lockup detector 和 hard lockup detector,2.6之后的内核的实现方法如下。

NMI Watchdog 的触发机制包括两部分:

  1. 一个高精度计时器(hrtimer),对应的中断处理例程是kernel/watchdog.c: watchdog_timer_fn(),在该例程中:
    • 要递增计数器hrtimer_interrupts,这个计数器供hard lockup detector用于判断CPU是否响应中断;
    • 还要唤醒[watchdog/x]内核线程,该线程的任务是更新一个时间戳;
    • soft lock detector检查时间戳,如果超过soft lockup threshold一直未更新,说明[watchdog/x]未得到运行机会,意味着CPU被霸占,也就是发生了soft lockup。
  2. 基于PMU的NMI perf event,当PMU的计数器溢出时会触发NMI中断,对应的中断处理例程是 kernel/watchdog.c: watchdog_overflow_callback(),hard lockup detector就在其中,它会检查上述hrtimer的中断次数(hrtimer_interrupts)是否在保持递增,如果停滞则表明hrtimer中断未得到响应,也就是发生了hard lockup。

hrtimer的周期是:softlockup_thresh/5。
注:

  • 在2.6内核中:
    softlockup_thresh的值等于内核参数kernel.watchdog_thresh,默认60秒;
  • 而到3.10内核中:
    内核参数kernel.watchdog_thresh名称未变,但含义变成了hard lockup threshold,默认10秒;
    soft lockup threshold则等于(2*kernel.watchdog_thresh),即默认20秒。

NMI perf event是基于PMU的,触发周期(hard lockup threshold)在2.6内核里是固定的60秒,不可手工调整;在3.10内核里可以手工调整,因为直接对应着内核参数kernel.watchdog_thresh,默认值10秒。

检测到 lockup 之后怎么办?可以自动panic,也可输出条信息就算完了,这是可以通过内核参数来定义的:

  • kernel.softlockup_panic: 决定了检测到soft lockup时是否自动panic,缺省值是0;
  • kernel.nmi_watchdog: 定义是否开启nmi watchdog、以及hard lockup是否导致panic,该内核参数的格式是”=[panic,][nopanic,][num]”.
    (注:最新的kernel引入了新的内核参数kernel.hardlockup_panic,可以通过检查是否存在 /proc/sys/kernel/hardlockup_panic来判断你的内核是否支持。)

参考资料:

Softlockup detector and hardlockup detector (aka nmi_watchdog)

kernel/watchdog.c:
设置PMU NMI perf event的代码 wachdog_nmi_enable()
响应NMI perf overflow中断的代码 watchdog_overflow_callback()
[watchdog/x]内核线程 watchdog()
响应hrtimer中断的代码 watchdog_timer_fn()

x86发生无法恢复的硬件故障之后…

x86发生无法恢复的硬件故障之后,会触发中断通知操作系统,相关的中断类型主要有两个:

另外还有两个必须提到:

  • PCIe AER – Advanced Error Reporting,这是PCIe的新特性,发生故障时可以通过中断报告故障的详细信息,需要硬件、BIOS和driver的支持才行,但现在仍有许多硬件不支持。
  • SMI – System Management Interrupt,这是给BIOS/firmware使用的一个特殊中断,这个中断不是直接给OS用的。触发SMI中断以后,系统进入SMM模式-System Management Mode,BIOS/firmware在这种特殊模式下进行电源管理、硬件故障处理等工作。在新的APEI(ACPI Platform Error Interface)标准下,硬件故障触发的中断可以不直接通知OS,而先通过SMI进入BIOS/firmware,让BIOS/firmware可以先行处理硬件故障,视情况再决定是否通知OS继续处理。这种模式称为Firmware First Model,在新的kernel上默认是打开的,可以这样检查:
    $ dmesg | grep -i apei
    [ 1.020287] GHES: APEI firmware first mode is enabled by WHEA _OSC.

发生无法恢复的硬件故障之后,硬件直接触发中断,而不是kernel或driver通过指令触发的。那么触发的是哪一种类型的中断呢?

  • 如果处于Firmware First Model(见上文SMI段落中的解释)
    先通过SMI中断进入BIOS/firmware,BIOS/firmware对故障进行处理之后再决定是否以中断的方式通知OS,并确定中断的类型。
  • 如果处于OS Native Model(即Firmware First Model关闭的情况)
    • CPU/memory/chipset的无法恢复的故障通常触发MCE中断;
    • I/O故障可能触发PCIe AER中断,前提是软硬件都支持;
    • 其它硬件故障统统触发NMI中断。

MCE的中断优先级最高,是专为硬件故障诊断而设计的,包含了关于故障的详细信息,有助于定位故障点,它并不能涵盖所有的故障类型,限于 memory, cpu cache 和 system bus。其它常见的故障比如I/O卡通常不触发MCE。

如果软硬件都支持的话,PCIe的故障会触发AER中断,它是专为PCIe硬件故障诊断而设计的,包含了故障的详细信息,方便诊断。但许多硬件尚不支持AER。

除此之外的各种无法恢复的硬件故障统统触发NMI中断。使用NMI的缺点是NMI Status and Control Register仅有的单字节状态无法精确指示故障位置,信息量很小,很难定位故障点,毕竟NMI不是专为处理硬件故障设计的。

与小型机相比,x86的硬件故障处理机制还存在很大的不足:
小型机上发生无法恢复的硬件故障会触发一个单一的中断,只要触发这个中断就可以断定是硬件故障,并且伴随中断提供了充分的故障信息,便于定位故障。不像x86这样有多种中断类型的可能,MCE和AER倒也罢了,最后所有其它类型的硬件故障都归入NMI,而NMI是一个过度使用的公用中断,软件也用、硬件也用,perf profiling也用,watchdog也用,而且NMI本不是专为故障诊断设计的,提供的故障信息量非常少,使得故障定位很难;即使是专门为方便故障诊断而设计的MCE和AER,在实践中定位故障也不容易,除非BIOS遵照规范提供了详尽的硬件槽位信息,这在多数通用机上没有做到,有些专用设备比如磁盘阵列什么的,为了方便售后服务在BIOS/firmware上作了很多工作,这才使得MCE和AER的故障定位容易了些。总的来说,x86的硬件故障处理机制像是打补丁拼凑出来的,缺乏整体性,还有漏洞,而且标准又不是强制性的,各厂商的产品都有差异,使得本来就漏洞百出的机制又打了折扣。Intel x86的RAS只是看上去很美而已。

注:SMI还可以被服务器用于电源管理、监测等功能,比如HPE Proliant Server的BIOS就把SMI用于CPU耗电监测(Processor Power and Utilization Monitoring)和内存故障预警(Memory Pre-Failure Notification)等。

注:我们在讨论“x86发生无法恢复的硬件故障”这个题目的时候,请注意触发中断是在硬件层面上发生的,某个硬件故障直接通过硬件针脚触发中断,并不是kernel或driver通过指令触发中断。虽然kernel和driver也可以通过指令触发NMI或SMI,但那是另一个话题–即“kernel或driver认为有故障而故意触发NMI或SMI”–我们在此不予讨论。

x86 CPU的中断优先级
x86 CPU的中断优先级

 

怎样诊断Machine-check Exception

Machine Check Exception (MCE) 是CPU发现硬件错误时触发的异常(exception),中断号是18,异常的类型是abort:

MCE18

导致MCE的原因主要有:总线故障、内存ECC校验错、cache错误、TLB错误、内部时钟错误,等等。不仅硬件故障会引起MCE,不恰当的BIOS配置、firmware bug、软件bug也有可能引起MCE。

在 Linux 系统上,如果发生的MCE错误属于可以自动纠正的类型,那么系统保持继续运行,MCE错误日志会记录在一个ring buffer中(这个ring buffer通过设备文件/dev/mcelog来访问),用 mcelog(8) 命令可以读取MCE日志,系统通常会通过cron任务或者mcelog.service把ring buffer中的MCE日志写入/var/log/mcelog文件中。如果发生的MCE错误属于无法恢复的类型,那么系统会panic,错误信息会输出在终端上和message buffer里。

分析MCE需要参考Intel手册第3卷,15章Machine-Check Architecture和16章Interpreting Machine-Check Error Codes。由于MCE在不同型号的CPU上有差异,解读的方法也有不同,第16章是专门解释在不同的CPU型号上如何解读MCE错误码。

每个CPU上有一组寄存器称为 Machine-Check MSR (Model-Specific Register),用于Machine-Check的控制与记录,分为全局寄存器和若干Bank寄存器(CPU的硬件单元分成若干组,每一组称为一个Bank)。当发生MCE时,错误信息记录在全局状态寄存器 MCG_STATUS MSR 和Bank寄存器 MCi_STATUS MSR 中,如下图黄色框所示:

MCE_MSR

分析MCE的方法,就是根据Intel手册解读上述寄存器中记录的错误信息。Linux内核把MCE的信息保存在下面的结构体中:

 

下面的MCE错误信息截取自一台因MCE而crash的机器,我们以此为例来解读一下MCE信息。
注:产生以下信息的内核函数是:
static void print_mce(struct mce *m)
源程序:arch/x86/kernel/cpu/mcheck/mce.c
如果需要的话,阅读源程序可以理解输出的信息与原始数据的对应关系。

其中CPU和Bank是MCE的接收者:

  • CPU 3 – 表示检测到MCE错误的是3号CPU,对应struct mce的extcpu字段;
  • Bank 5 – 一组硬件单元称为一个bank,每个bank对应一组machine-check寄存器;

MCE的错误代码包括两部分:

  • Machine Check Exception: 4 – 表示 IA32_MCG_STATUS MSR寄存器的状态码是4(含义见后文),对应 mcgstatus字段;
  • be00000000800400 – 表示 IA32_MCi_STATUS MSR寄存器中的错误码(含义见后文),对应status字段。

Machine Check Excheption: 4 的含义

它来自全局状态寄存器 IA32_MCG_STATUS MSR,(对应struct mce的 mcgstatus字段),只用到三个bit,如下所示。4表示machine-check in progress。

MCG_status

  • Bit 0: Restart IP Valid. 表示程序的执行是否可以在被异常中断的指令处重新开始。
  • Bit 1: Error IP Valid. 表示被中断的指令是否与MCE错误直接相关。
  • Bit 2: Machine Check In Progress. 表示 machine check 正在进行中。

be00000000800400 的含义

它来自bank寄存器IA32_MCi_STATUS MSR,(对应struct mce的status字段)。

MCi_status

be00000000800400 的二进制位如下:

Bit 63: VAL. 表示本寄存器中包含有效的错误码
Bit 61: UC. 表示是无法纠正的MCE
Bit 60: EN. 表示处于允许报告错误的状态
Bit 59: MISCV. 表示MCi_MISC寄存器中含有对该错误的补充信息
Bit 58: ADDRV. 表示MCi_ADDR寄存器含有发生错误的内存地址
Bit 57: PCC. 表示该CPU的上下文状态已被该错误破坏,无法恢复软件代码的运行
Bits [16:31] 包含特定CPU型号相关的扩展错误码. 本例中是0x0080.
Bits [0:15] 包含MCE错误码,该错误码是所有CPU型号通用的,分为两类:simple error codes(简单错误码) 和 compound error codes(复合错误码),本例中0x0400表示Internal timer error:

– Simple Error Codes:
0000 0000 0000 0000 – 没有错误.
0000 0000 0000 0001 – Unclassified. 未分类的错误类型.
0000 0000 0000 0010 – ROM微码校验错
0000 0000 0000 0011 – MCE是由于别的CPU的BINT# 引起的.
0000 0000 0000 0100 – Functional redundancy check (FRC) master/slave error.
0000 0000 0000 0101 – Internal parity error.
0000 0100 0000 0000 – Internal timer error.
0000 01xx xxxx xxxx – Internal unclassified error. 至少有一个x等于1

– Compound Error Codes:
000F 0000 0000 11LL – Generic cache hierarchy errors.
000F 0000 0001 TTLL – TLB errors.
000F 0000 1MMM CCCC – Memory controller errors (Intel-only).
000F 0001 RRRR TTLL – Memory errors in the cache hierarchy.
000F 1PPT RRRR IILL – Bus and interconnect errors.

下一步,由于Bits [16:31] 是非零值0x0080,包含的是特定CPU型号相关的扩展错误码,我们要参考Intel手册第三卷第16章。首先确定CPU型号,我们需要的是CPU faimily和model,从/proc/cpuinfo中可以找到:

根据cpu family/model,即06_1AH,找到Intel手册中对应的章节,但是没找到匹配Internal timer error和0x0080的条目。所以只能到此为止了。

总结我们的发现:
CPU 3 发现了无法纠正的MCE,是Internal timer error,被中断的指令与MCE不相关,被中断的程序指令不能恢复运行,CPU的上下文已被MCE破坏。

 

怎样禁用MCE

可以在 /boot/grub/grub.conf 中加入以下内容:mce=off。

还有其它的MCE选项,比如禁用CMCI(Corrected Machine Check Interrupt),或者禁用MCE日志,等等,例如:
mce=off  — Disable machine check
mce=no_cmci  — Disable CMCI(Corrected Machine Check Interrupt)
参见文档:Documentation/x86/x86_64/boot-options.txt

 

每个 CPU 都有一个sysfs接口:
/sys/devices/system/machinecheck/machinecheckN
注:(N = CPU number)

其中包含的可调参数参见:Documentation/x86/x86_64/machinecheck

参考资料:
Intel – Intel 64 and IA-32 Architectures Software Developer’s Manual . Chapters 15 and 16.
AMD – AMD64 Architecture Programmer’s Manual Volume 2: System Programming . Chapter 9.

 

NMI是什么

NMI(non-maskable interrupt),就是不可屏蔽的中断。根据Intel的Software Developer手册Volume 3,NMI的来源有两个:
– NMI pin
– delivery mode NMI messages through system bus or local APIC serial bus

NMI通常用于通知操作系统发生了无法恢复的硬件错误,也可以用于系统调试与采样,大多数服务器还提供了人工触发NMI的接口,比如NMI按钮或者iLO命令等。

  1. 无法恢复的硬件错误通常包括:芯片错误、内存ECC校验错、总线数据损坏等等。
  2. 当系统挂起,失去响应的时候,可以人工触发NMI,使系统重置,如果早已配置好了kdump,那么会保存crash dump以供分析。有的服务器提供了NMI按钮,而刀片服务器通常不提供按钮,但可以用iLO命令触发。
  3. Linux还提供一种称为”NMI watchdog“的机制,用于检测系统是否失去响应(也称为lockup),可以配置为在发生lockup时自动触发panic。原理是周期性地生成NMI,由NMI handler检查hrtimer中断的发生次数,如果一定时间内这个数字停顿了,表示系统失去了响应,于是调用panic例程。NMI watchdog的开关是通过内核参数 kernel.nmi_watchdog 或者在boot parameter中加入”nmi_watchdog=1″参数实现,比如:
    在RHEL上编辑 /boot/grub/menu.lst:

    kernel /vmlinuz-2.6.18-128.el5 ro root=/dev/sda nmi_watchdog=1

    然后你会看到:
    # grep NMI /proc/interrupts
    NMI: 0 0 0 0

Linux kernel笼统地把NMI分为三大类:内存校验错 mem_parity_error(),总线数据损坏 io_check_error(),其他的全部归入 unknown_nmi_error()。kernel对NMI是不能精确定位的,对故障诊断很不利,硬件驱动程序可以注册自己的NMI处理例程,kernel会在发生NMI之后通过notify_die()调用这些第三方注册的处理例程。


参考资料:
Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3
http://wiki.osdev.org/Non_Maskable_Interrupt