基於 Ubuntu 14.04 ,Linux Kernel 4.0 以上版本。

1. printk()

printk() 是內核提供的函數,用於將內核空間的信息打印到用戶空間緩沖區,打印的信息可以通過 demsg 命令查看,或者直接查看 /proc/kmsg 文件。緩沖區是一個環形隊列的結構,消息太多時,舊的消息就會被逐漸覆蓋,緩沖區大小是在 kernel/printk/printk.c 文件中的代碼設置的:

#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);

緩沖區大小是 CONFIG_LOG_BUF_SHIFT*2 個字節,CONFIG_LOG_BUF_SHIFT 是在 init/Kconfig 文件中設置的,我們可以在 menuconfig 的相關路徑中修改:

General setup -> Kernel log buffer size(16 => 64KB, 17 => 128kB)

還可以在加載內核時用啟動參數 log_buf_len=n[KMG] 設置,其中的 n 必須是 2 的整數倍。

在調用 printk() 函數時要設置消息級別,從 0 到 7 ,數值越小級別越高,相應的宏定義在 include/linux/kern_levels.h 文件中:

#define KERN_EMERG      KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT      KERN_SOH "1"    /* action must be taken immediately */
#define KERN_CRIT       KERN_SOH "2"    /* critical conditions */
#define KERN_ERR        KERN_SOH "3"    /* error conditions */
#define KERN_WARNING    KERN_SOH "4"    /* warning conditions */
#define KERN_NOTICE     KERN_SOH "5"    /* normal but significant condition */
#define KERN_INFO       KERN_SOH "6"    /* informational */
#define KERN_DEBUG      KERN_SOH "7"    /* debug-level messages */

#define KERN_DEFAULT    KERN_SOH "d"    /* the default kernel loglevel */

內核中還有一個默認日志級別,只有數值小於這個級別的消息才會被打印到控制台上,大於或者等於這個數值的消息不會顯示,它設置在 lib/Kconfig.debug 文件中,缺省情況下會設為 KERN_WARNING(4) ,我們可以在 menuconfig 的相關路徑中設置:

Kernel hacking -> printk and dmesg options -> Default message log level(1-7)

也可以用內核啟動參數 loglevel=n 設置,n 的取值是 0~7 。如果直接設置了啟動參數 debug ,那么日志級別就是 KERN_DEBUG(7) ,所有調試信息都會顯示在控制台上。還可以在系統啟動后,在 /proc/sys/kernel/printk 文件中調整 printk() 函數的輸出等級,該文件有四個數值,各自的含義:

  1. 控制台的日志級別:當前的打印級別,優先級高於該值(值越小,優先級越高)的消息將被打印至控制台
  2. 默認的消息日志級別: 將用該優先級來打印沒有優先級前綴的消息,也就是直接寫 printk("xxx") 而不帶打印級別的情況下,會使用該打印級別
  3. 最低的控制台日志級別: 控制台日志級別可被設置的最小值(一般是1)
  4. 默認的控制台日志級別: 控制台日志級別的默認值

修改方法:

root@sh-VirtualBox:/proc/sys/kernel# cat printk
4   4   1   7
root@sh-VirtualBox:/proc/sys/kernel# echo 5 > printk
root@sh-VirtualBox:/proc/sys/kernel# cat printk
5   4   1   7
root@sh-VirtualBox:/proc/sys/kernel# echo  "5 5" > printk
root@sh-VirtualBox:/proc/sys/kernel# cat printk
5   5   1   7

默認情況下,printk() 打印的消息是帶時間戳的,可以在 menuconfig 的相應路徑下關閉或者打開:

Kernel hacking -> printk and dmesg options -> Show timing information on printks

為了方便調用,內核提供很多封裝了 printk() 函數的宏,在 /include/linux/printk.h 頭文件中聲明的 pr_xxx() ,例如:

#define pr_fmt(fmt) fmt
#define pr_err(fmt, ...) printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...) printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

我們可用通過 pr_fmt(fmt) 添加一些自定義的消息格式,例如:

#define pr_fmt(fmt) "[driver] watchdog:" fmt

這里要注意 pr_debug(),它與其他的宏不同,需要滿足如下兩個條件之一才會打印信息:

  1. 在源文件、或者編譯時定義了 DEBUG 宏,這個方式在開發內核模塊時很有用
  2. 開啟了 CONFIG_DYNAMIC_DEBUG ,也就是 menuconfig 中的 Kernel hacking -> printk and dmesg options

這里還有一個問題,內核啟動后,需要一段時間才能准備好控制台,這段時間內的內核信息是無法通過控制台顯示,內核為此提供了 early printk 機制,它會在內核啟動后就注冊一個 boot console ,讓后將內核信息顯示在這個控制台上。使能 early printk 的方法有兩步:

  1. 在 menuconfig 中打開 Early printk :Kernel hacking -> Early printk
  2. 在啟動參數中設置 earlyprintk=[vga|serial][,ttySn[,baudrate]][,keep]

如果用戶空間的 printf() 和內核空間的 printk() 同時執行,二者的輸出會互相干擾,內核為此提供了 /dev/ttyprintk 設備文件,可以將用戶空間的信息打印到這個設備中,這樣用戶信息與內核信息就會順序輸出,輸出的消息會自帶 [U] 前綴。對於沒有 /dev/ttyprintk 設備的系統,可以用 /dev/kmsg 代替,只是沒有了 [U] 標識,需要用戶自己添加前綴。

2. SysRq 鍵

標准鍵盤的右上角有一個 PrintScreen/SysRq 鍵,它的一個功能是截屏,另一個功能是當系統死機無法輸入命令時,用這個按鍵獲取內核信息。SysRq 鍵在確認內核運行、調查死機原因等情況時非常有效。關於它的詳細情況可以參考內核的 Documentation/sysrq.txt 文件。

要使用 SysRq 鍵,需要啟動內核配置 CONFIG_MAGIC_SYSRQ ,在 menuconfig 中的路徑是:

Kernel hacking -> Magic SysRq key

系統啟動后,就可以在 /proc/sys/kernel/sysrq 文件中設置 SysRq 按鍵的功能,該文件的默認值是內核選項 CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE 設置的,必須是十六進制,在 menuconfig 的路徑是:

Kernel hacking -> (0x01) Enable magic Sysrq key functions by default

注意,/proc/sys/kernel/sysrq 設置的各項功能,只對從鍵盤和串口控制台的輸入有效,對於遠程 ssh 等方式無效。直接向 /proc/sysrq-trigger 寫入命令鍵則不受限制:echo [command key] > /proc/sysrq-trigger 。

這個文件的值是位掩碼,取值如下,括號內是命令鍵:

  • 0 ,禁用 sysrq
  • 1 ,使能所有 sysrq 功能
  • 2 = 0x2 ,允許控制控制台日志級別(0~9)
  • 4 = 0x4 ,使能鍵盤控制 (kr)
  • 8 = 0x8 ,使能顯示進行等信息(lptwmcz)
  • 16 = 0x10 ,使能 sync 命令(s)
  • 32 = 0x20 ,使能只讀狀態下的重新掛在(u)
  • 64 = 0x40 ,使能進程信號,例如 term, kill(ei)
  • 128 = 0x80 ,使能重啟和關機(b)
  • 256 = 0x100 ,允許控制實時任務(q)

可以直接修改這個文件的值,比如使能 sync 和重新掛載:

# echo 48 > /proc/sys/kernel/sysrq

也可以在 /etc/sysctl.d/10-magic-sysrq.conf 文件中修改 kernel.sysrq 選項(也可能在 /etc/sysctl.conf 文件中)。配置好功能后,通過組合鍵 Alt-SysRq-<command key> 就可以使用 SysRq 鍵的各項功能,功能鍵如下:

'b' - Will immediately reboot the system without syncing or unmounting your disks.
'c' - Will perform a system crash by a NULL pointer dereference. A crashdump will be taken if configured.
'd' - Shows all locks that are held.
'e' - Send a SIGTERM to all processes, except for init.
'f' - Will call the oom killer to kill a memory hog process, but do not panic if nothing can be killed.
'g' - Used by kgdb (kernel debugger)
'h' - Will display help (actually any other key than those listed here will display help. but 'h' is easy to remember :-)
'i' - Send a SIGKILL to all processes, except for init.
'j' - Forcibly "Just thaw it" - filesystems frozen by the FIFREEZE ioctl.
'k' - Secure Access Key (SAK) Kills all programs on the current virtual console. NOTE: See important comments below in SAK section.
'l' - Shows a stack backtrace for all active CPUs.
'm' - Will dump current memory info to your console.
'n' - Used to make RT tasks nice-able
'o' - Will shut your system off (if configured and supported).
'p' - Will dump the current registers and flags to your console.
'q' - Will dump per CPU lists of all armed hrtimers (but NOT regular timer_list timers) and detailed information about all clockevent devices.
'r' - Turns off keyboard raw mode and sets it to XLATE.
's' - Will attempt to sync all mounted filesystems.
't' - Will dump a list of current tasks and their information to your console.
'u' - Will attempt to remount all mounted filesystems read-only.
'v' - Forcefully restores framebuffer console
'v' - Causes ETM buffer dump [ARM-specific]
'w' - Dumps tasks that are in uninterruptable (blocked) state.
'x' - Used by xmon interface on ppc/powerpc platforms. Show global PMU Registers on sparc64. Dump all TLB entries on MIPS.
'y' - Show global CPU Registers [SPARC-64 specific]
'z' - Dump the ftrace buffer
'0'-'9' - Sets the console log level, controlling which kernel messages will be printed to your console. ('0', for example would make it so that only emergency messages like PANICs or OOPSes would make it to your console.)

如果系統疑似死機,可以一次執行 s-u-b 命令重啟內核,如果不需要重啟,可以執行 c 命令提取崩潰轉儲,獲取內核信息(內核崩潰轉儲是指將系統內存的內容輸出到文件)。還可以嘗試用 i 命令向進程發送 SIGKILL 信號,使系統恢復。

3. Kdump

Kdump 是內核提供的崩潰轉儲功能,工作原理是在系統內核崩潰時啟動一個特殊的 dump-capture kernel 把系統內存里的數據保存到磁盤文件中,由內核機制和用戶空間工具共同完成。Dump-capture kernel 可以是獨立的,也可以和系統內核集成在一起(這需要硬件支持)。Kdump 的工作過程如下:

  1. 系統內核啟動的時候,要給 dump-capture kernel 預留一塊內存空間;
  2. 內核啟動完成后,用戶空間的 kdump service 執行 kexec -p 命令把 dump-capture kernel 載入預留的內存里(/sys/kernel/kexec_crash_loaded 的值為 1 表示已經加載);
  3. 如果系統發生 crash,生產內核會自動 reboot 進入 dump-capture kernel,dump-capture kernel 只使用自己的預留內存,確保其余的內存數據不會被改動,它的任務是把系統內存里的數據寫入到 dump 文件,比如 /var/crash/vmcore,為了減小文件的大小,它會通過 makedumpfile(8) 命令對內存數據進行挑選和壓縮;
  4. dump 文件寫完之后,dump-capture kernel 自動 reboot 。

預留內存的方法是用內核啟動參數 crashkernel=size[@offset] 實現的,某些內核支持 crashkernel=auto 自動分配大小,如果不支持,或者系統沒有足夠內存,就需要手動設置。通常 offset 可以設置為 16MB(0x1000000) ,size 根據系統內存的大小設置,而且要與 64MB 對齊:

  1. 如果系統內存小於 512MB ,則不要保留內存
  2. 如果系統內存介於 512MB 到 2GB 之間,可以保留 64MB 內存
  3. 如果系統內存大於 2GB ,可以保留 128MB 內存

可能導致內核崩潰的事件包括:

  • Kernel Panic
  • Non Maskable Interrupts (NMI)
  • Machine Check Exceptions (MCE)
  • Hardware failure
  • Manual intervention

對於某些崩潰事件(例如 panic、NMI),內核會自動做出反應,並通過 kexec 觸發崩潰轉儲,其他情況下需要手動捕獲內存信息。

在 Ubuntu 上首先要安裝內核崩潰轉儲工具:

$ sudo apt-get install linux-crashdump

如果是 Fedora 操作系統,通常是安裝 crash 和 kexec-tools 軟件包。

linux-crashdump 包安裝了三個工具,分別是:crash,kexec-tools 和 makedumpfile。安裝過程中會出現如下對話框,選擇 Yes ,表示默認使能 kdump :

|------------------------| Configuring kdump-tools |------------------------|
|                                                                           |
|                                                                           |
| If you choose this option, the kdump-tools mechanism will be enabled. A   |
| reboot is still required in order to enable the crashkernel kernel        |
| parameter.                                                                |
|                                                                           |
| Should kdump-tools be enabled by default?                                 |
|                                                                           |
|                    <Yes>                       <No>                       |
|                                                                           |
|---------------------------------------------------------------------------|

然后編輯 /etc/default/kdump-tools 文件,修改選項 USE_KDUMP=1 ,使能內核加載 kdump ,然后重啟系統,內核自動激活 crashkernel= 啟動參數 ,kdump-tools 默認啟動,用 kdump-config show 命令和 /sys/kernel/kexec_crash_loaded 文件查看 kdump 的配置和狀態,在 /proc/cmdline 文件中查看 crashkernel 的設置:

$ kdump-config show
DUMP_MODE:        kdump
USE_KDUMP:        1
KDUMP_SYSCTL:     kernel.panic_on_oops=1
KDUMP_COREDIR:    /var/crash
crashkernel addr: 0x2d000000
current state:    ready to kdump

kexec command:
/sbin/kexec -p --command-line="BOOT_IMAGE=/boot/vmlinuz-4.4.0-31-generic root=UUID=2744d8e0-18c2-493f-b61c-d887647494a0 ro quiet splash vt.handoff=7 irqpoll maxcpus=1 nousb" --initrd=/boot/initrd.img-4.4.0-31-generic /boot/vmlinuz-4.4.0-31-generic
$ cat /sys/kernel/kexec_crash_loaded
1
$ cat /proc/cmdline 
BOOT_IMAGE=/boot/vmlinuz-4.4.0-31-generic root=UUID=2744d8e0-18c2-493f-b61c-d887647494a0 ro quiet splash crashkernel=384M-:128M vt.handoff=7

系統啟動后,可以通過向 /sys/kernel/kexec_crash_size 寫入一個比原來小的數值來縮小甚至完全釋放 crashkernel 。然后執行 sudo kdump-config load 加載 kdump ,也可以把 /etc/init.d/kdump-tool 服務設為默認啟動,這樣系統會自動加載。准備工作完成后,嘗試提取崩潰轉儲,先確保 sysrq=1 ,然后手動觸發一次崩潰:

# echo c > /proc/sysrq-tirgger

稍等片刻,如果轉儲成功,內核會自動重啟,並且在 /var/crash/ 目錄下生成轉儲文件:

$ ls -l /var/crash/*
total 28
drwxr-sr-x 2 root whoopsie  4096  7月  6 11:45 201807061145
-rw-r----- 1 root whoopsie 18095  7月  6 11:45 linux-image-4.4.0-31-generic-201807061145.crash
$ ls -l /var/crash/201807061145/
total 55300
-rw------- 1 root whoopsie    41223  7月  6 11:45 dmesg.201807061145
-rw------- 1 root whoopsie 56578589  7月  6 11:45 dump.201807061145

轉儲需要時間,如果手動關機重啟會導致轉儲不完整,數據無法解讀。

如果是 RedHat 系統,生成的轉儲文件是 vmcore ,可以直接用 crash 命令分析。而 Ubuntu 提供了叫做 Apport 的工具,將系統內其他有用的信息一起打包生成了 linux-image-4.4.0-31-generic-201807061145.crash 文件,而以時間戳命名的文件夾 201807061145 包含了 dmesg 信息文件和 kdump 轉儲文件,對於某些版本,這兩個文件也包含在 crash 文件中。對 crash 文件解壓后可以得到幾個與系統信息有關的純文本文件:

$ sudo apport-unpack /var/crash/linux-image-4.4.0-31-generic-201807061145.crash ~/201807061145.crash
$ ls ~/201807061145.crash
Architecture  Date  DistroRelease  Package  ProblemType  Uname  VmCoreDmesg

4. 崩潰測試

內核有一個 lkdtm 模塊,Linux Kernel Dump Test Module ,通過各種方式使內核崩潰,用於測試崩潰轉儲的功能。通常發行版的內核不會使能這個模塊,需要啟用內核 CONFIG_LKDTM 選項,在 menuconfig 的路徑是:

Kernel hacking -> RunTime Testing -> Linux Kernel Dump Test Tool Module

最好編譯成模塊,然后加載模塊時,通過模塊參數指定崩潰位置和崩潰原因,即可造成所需的內核崩潰。

5. crash 命令

crash 是一個強大的交互式工具,基於 gdb ,用於分析內核映像,比如內核崩潰轉儲信息。有些系統中,安裝 linux-crashdump 時會包含 crash ,如果沒有,需要手動安裝:

sudo apt-get install crash

分析之前需要安裝系帶有 debug-info 的內核,叫做 kernel-debuginfo ,這是 redhat 的叫法, ubuntu 下叫 debug symbols,簡稱 dbgsym 。 ubuntu 默認安裝時不會安裝 dbgsym ,默認倉庫上也沒有 dbgsym 包。 dbgsym 包存在於獨立的倉庫上,官方倉庫地址為 http://ddebs.ubuntu.com/ ,安裝方法參考:https://oolap.com/2015-11-07-ubuntu-install-dbgsym 。kernel-debuginfo 的版本應該和系統運行的內核版本完全一致,如果是自行編譯的內核,可能無法在官方倉庫中找到對應版本的 kernel-debuginfo ,這時可以自行編譯安裝 kernel-debuginfo ,參考下一節。安裝完成后,會在 /usr/lib/debug/boot/ 目錄下生成帶有調試信息的 vmlinux ,然后用 crash 工具分析 kdump 生成崩潰轉儲信息:

$ sudo crash  /usr/lib/debug/boot/vmlinux-4.4.0-31-generic /var/crash/201807061145/dump.201807061145

下面以一個 Fedora14(kernel 2.6.37) 下產生的轉儲文件 vmcore 為例說明 crash 的用法,crash 成功啟動后先打印一段轉儲文件的分析報告,包括崩潰時間、崩潰類型、CPU、內存等,然后進入一個交互環境:

KERNEL: /boot/vmlinux
DUMPFILE: vmcore
CPUS: 1
DATE: Fri Jul 27 13:59:13 2018
UPTIME: 00:05:23
LOAD AVERAGE: 0.01, 0.11, 0.07
TASKS: 56
NODENAME: localhost.localdomain
RELEASE: 2.6.37.6
VERSION: #11 SMP Thu Jul 26 15:42:06 CST 2018
MACHINE: i686 (1500 Mhz)
MEMORY: 1 GB
PANIC: "[  323.903003] Oops: 0002 [#1] SMP " (check log for details)
PID: 4437
COMMAND: "bash"
TASK: f6ec0c90 [THREAD_INFO: f6d2e000]
CPU: 0
STATE: TASK_RUNNING (PANIC)
crash >

可以看到引起崩潰的進程是 PID: 4437 , crash 提供了 ps 命令顯示所有進程的狀態,用 ps | grep 4437 可以篩選出引起崩潰的進程:

crash > ps | grep 4437
  PID    PPID    CPU      TASK      ST   %MEM    VSZ    RSS   COMM 
  4437   4426    0    f6ec0c90       RU    0.2   8064   1780   bash

bt 命令用於輸出某個進程的內核棧的遍歷,沒有指定 PID 時默認輸出引起崩潰的進程的內核棧信息:

crash> bt
PID: 4437   TASK: f6ec0c90  CPU: 0   COMMAND: "bash"
#0 [f6d2fdec] crash_kexec at c0466264
#1 [f6d2fe2c] __bad_area_nosemaphore at c04225b5
#2 [f6d2fe48] bad_area at c042260c
#3 [f6d2fe60] do_page_fault at c079c8c9
#4 [f6d2fed8] error_code (via page_fault) at c079a685
EAX: 00000063  EBX: 00000063  ECX: ffffffd6  EDX: 00000000  EBP: f6d2ff18 
DS:  007b      ESI: c095dfe0  ES:  007b      EDI: 00000004  GS:  00e0
CS:  0060      EIP: c061e0b9  ERR: ffffffff  EFLAGS: 00010046
#5 [f6d2ff0c] sysrq_handle_crash at c061e0b9 
#6 [f6d2ff1c] __handle_sysrq at c061e63d 
#7 [f6d2ff40] write_sysrq_trigger at c061e6e2
#8 [f6d2ff50] proc_reg_write at c0507c84    
#9 [f6d2ff74] vfs_write at c04cdf4c
#10 [f6d2ff90] sys_write at c04ce11d
#11 [f6d2ffb0] ia32_sysenter_target at c0403298 
EAX: 00000004  EBX: 00000001  ECX: b77f8000  EDX: 00000002
DS:  007b      ESI: b77f8000  ES:  007b      EDI: 00000002 
SS:  007b      ESP: bfc32fd0  EBP: bfc33008  GS:  0033 
CS:  0073      EIP: b77fc424  ERR: 00000004  EFLAGS: 00000246

可以看到系統崩潰前最后一條調用是 #5 [f6d2ff0c] sysrq_handle_crash at c061e0b9 ,我們用 dis 命令看一下這個地址的反匯編結果:

crash> dis -l c061e0b9 
/usr/src/linux-2.6.37/drivers/tty/sysrq.c: 134
0xc061e0b9 <sysrq_handle_crash+23>:     movb   $0x1,0x0   

出錯的代碼位於 /usr/src/linux-2.6.37/drivers/tty/sysrq.c 文件的 134 行:

129 static void sysrq_handle_crash(int key) 
130 {  
131     char *killer = NULL;
132     panic_on_oops = 1;      /* force panic */ 
133     wmb(); 
134     *killer = 1; 
135 }  

這里為指針賦值 *killer = 1 ,而 131 行定義的是一個空指針,比如出錯。

crash 還有很多命令:

  • log :打印系統消息緩沖區,從而可能找到系統崩潰的線索。
  • sys :顯示系統概況。
  • kmem :顯示內存使用信息。
  • irq :顯示中斷的信息。
  • mod :顯示模塊信息。
  • runq :顯示處於運行隊列的進程。
  • struct :顯示結構的定義、地址和數據。

6. kernel-debuginfo

kernel-debuginfo 是指帶有 Debug information 的內核,就是在編譯內核是指定 CONFIG_DEBUG_INFO 等相關選項,在 menuconfig 的路徑是:

Kernel hacking -> Kernel debugging -> Compile the kernel with debug info

與 Kdump 分析相關的選項還有:

  • kexec system call :CONFIG_KEXEC=y
  • sysfs file system support : CONFIG_SYSFS=y
  • Compile the kernel with debug info : CONFIG_DEBUG_INFO=Y
  • kernel crash dumps : CONFIG_CRASH_DUMP=y
  • /proc/vmcore support : CONFIG_PROC_VMCORE=y

編譯成功后,就會在源碼目錄下生成帶有 debuginfo 的內核鏡像 vmlinux ,kdump 、crash 等內核調試方法都會用到它。vmlinux 是一個包含 Linux kernel 的靜態鏈接的可執行文件,ELF 格式。通常 /boot 目錄下啟動的內核是 vmlinuz ,它是 vmlinux 經過 gzip 和 objcopy 制作出來的壓縮文件。vmlinuz 是一種統稱,有兩種具體的表現形式 zImage 和 bzImage 。bzimage 和 zImage 的區別在於本身的大小,以及加載到內存時的地址不同,zImage在 0~640KB,而bzImage 則在 1M 以上的位置。

不同的程序查找這個內核的路徑是不一樣的,通常需要在如下路徑建立這個內核的符號鏈接:

/boot/vmlinux-`uname -r`
/usr/lib/debug/lib/modules/`uname -r`/vmlinux
/lib/modules/`uname -r`/vmlinux

有些程序還需要在 /lib/modules/ 目錄下建立內核源碼樹和構建目錄的符號鏈接:

/lib/modules/`uname -r`/source
/lib/modules/`uname -r`/build

7. NMI

NMI(non-maskable interrupt) 就是不可屏蔽的中斷,當 x86 發生了無法恢復的硬件故障后,會觸發這個中斷通知操作系統,如果操作系統配置了 kdump,還會觸發崩潰轉儲。根據 Intel 的軟件開發者手冊第三卷 6.7 的描述,NMI 的來源有兩個:

  • 外部引腳 NMI pin,外部設備可以通過這個引腳觸發 NMI ,有些服務器甚至提供了 NMI 觸發按鈕
  • 處理器系統總線或者 APIC 串行總線產生的 NMI 消息(包括芯片錯誤、內存校驗錯誤、總線數據損壞等)

x86 在 IO 端口寄存器 0x70 的 bit7 提供了 NMI_Enable 位,可以如下代碼使能、或者禁用 NMI :

void NMI_enable(void)
{
    outb(0x70, inb(0x70)&0x7F);
}
void NMI_disable(void)
{
    outb(0x70, inb(0x70)|0x80);
}

Linux 內核提供了名為 NMI watchdog 的機制,用於檢測系統是否失去響應(也稱為 lockup,包括 soft lockup 和 hard lockup),原理是周期性的產生 NMI ,由 NMI handler 響應中斷並刷新 hrtimer 定時器,如果一段時間內沒有刷新,就表示系統失去了相應,於是調用 panic,超時時間在內核配置里設置,默認是 5 秒。相關代碼在內核的 kernel/watchdog.c 文件中。

NMI watchdog 依賴 APIC ,所有要將 APIC 編譯進內核,啟動參數中也不要關閉 APIC 。傳統的 x86 架構采用 8259A 芯片處理中斷,現在的 x86 架構都引入了 APIC 。可以執行 cat /proc/interrupts ,如果輸出結果中列出了 IO-APIC-* ,說明系統正在使用 APIC ,如果看到 XT-PIC ,說明系統正在使用 8259A 芯片。

NMI watchdog 的開關是內核啟動參數 nmi_watchdog=[panic,]N ,也可以在 /etc/sysctl.conf 、/etc/sysctl.d/* 等配置文件中添加內核參數 kernel.nmi_watchdog=[panic,]N 。其中 panic 可選,表示 NMI watchdog 超時時產生 panic ,進而可以觸發 kdump 。N 可以取值 0~2 ,0 表示禁用 NMI watchdog ,如果要啟用 NMI watchdog ,在具有 IO-ACPI 的系統中設為 1 ,在沒有 IO-ACPI 的系統中設為 2 。設置成功后,可以看到如下內核信息:

$ dmesg | grep NMI
ACPI: LAPIC_NMI (acpi_id[0xff] high edge lint[0x1])
NMI watchdog: enabled on all CPUs, permanently consumes one hw-PMU counter.

然后可以看到 NMI 中斷計數:

$ cat /proc/interrupts  | grep NMI
NMI:        449        207        197        179   Non-maskable interrupts

因為 NMI 是硬件產生的,所以在虛擬機上測試很可能會失敗,內核會報錯誤信息 : NMI watchdog: disable(cpu0): hardware events not enabled

我們可以編寫一個模塊驗證 NMI watchdog 能否正常工作,它的原理是在加載模塊時禁用所有中斷,這樣 NMI handler 就不會響應,也不會刷新定時器,直到超時:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>

static int __init nmitest_init(void)
{
    printk("nmitest init\n");
    local_irq_disable();
    while(1);
    return 0;
}

static void __exit nmitest_exit(void)
{
    printk("nmitest exit\n");
}

module_init(nmitest_init);
module_exit(nmitest_exit);

MODULE_LICENSE("GPL");

系統運行過程中要禁用 NMI watchdog ,可以將 /proc/sys/kernel/nmi_watchdog 設為 0 。

8. Soft lockup 和 Hard lockup