1. 程式人生 > 其它 >Linux 核心除錯方法【轉】

Linux 核心除錯方法【轉】

轉自:https://shaocheng.li/posts/2018/07/05/

Table of Contents

基於 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