分类目录归档:crash分析

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中断的类型–通常都是NMI。
  • 如果处于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的中断优先级

 

内核栈溢出

在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的地址,进而得到进程的指针。

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

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

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

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

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

 

怎样诊断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 Watchdog是怎样检测 lockup的?

所谓lockup,是指某段内核代码占着CPU不放。Lockup严重的情况下会导致整个系统失去响应。Linux kernel设计了一个检测lockup的机制,称为NMI Watchdog,是利用NMI中断实现的,用NMI是因为lockup有可能发生在中断被屏蔽的状态下,这时唯一能把CPU抢下来的方法就是通过NMI,因为NMI中断是不可屏蔽的。

Lockup有几个特点:首先只有内核代码才能引起lockup,因为用户代码是可以被抢占的,不可能形成lockup;其次内核代码必须处于禁止内核抢占的状态(preemption disabled),因为Linux是可抢占式的内核,只在某些特定的代码区才禁止抢占,在这些代码区才有可能形成lockup。

Lockup分为两种:soft lockup 和 hard lockup,它们的唯一区别是 hard lockup 发生在CPU屏蔽中断的情况下。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的,触发周期在2.6内核里是固定的60秒,不可手工调整,在3.10内核里可以手工调整,因为直接对应着内核参数kernel.watchdog_thresh,默认值10秒。这也是hard lockup threshold的值。

检测到 lockup 之后怎么办?可以自动panic,也可输出条信息就算完了,这是可以通过内核参数来定义的:
kernel.softlockup_panic: 决定了检测到softlock时是否自动panic,缺省值是0;
kernel.nmi_watchdog: 定义是否开启nmi watch、以及hardlock是否导致panic,该内核参数的格式是”=[panic,][nopanic,][num]”.(注:好像最新的kernel引入了新的内核参数kernel.hardlockup_panic,请自测。)

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

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