Linux Kernel BUG:soft lockup CPU#1 stuck分析
1.線上內核bug日志
kernel: Deltaway too big! 18428729675200069867 ts=18446743954022816244 write stamp =18014278822746377
kernel:------------[ cut here ]------------
kernel:WARNING: at kernel/trace/ring_buffer.c:1988 rb_reserve_next_event+0x2ce/0x370()(Not tainted)
kernel:Hardware name: ProLiant DL360 G7
kernel:Modules linked in: fuse ipv6 power_meter bnx2 sg microcode serio_raw iTCO_wdtiTCO_vendor_support hpilo hpwdt i7core_edac edac_core shpchp ext4 mbcache jbd2sd_mod crc_t10dif hpsa radeon ttm drm_kms_helper drm i2c_algo_bit i2c_coredm_mirror dm_region_hash dm_log dm_mod [last unloaded: scsi_wait_scan]
kernel: Pid:5483, comm: master Not tainted 2.6.32-220.el6.x86_64 #1
kernel: CallTrace:
kernel:[<ffffffff81069b77>] ? warn_slowpath_common+0x87/0xc0
kernel:[<ffffffff81069bca>] ? warn_slowpath_null+0x1a/0x20
kernel:[<ffffffff810ea8ae>] ? rb_reserve_next_event+0x2ce/0x370
kernel:[<ffffffff810eab02>] ? ring_buffer_lock_reserve+0xa2/0x160
kernel:[<ffffffff810ec97c>] ? trace_buffer_lock_reserve+0x2c/0x70
kernel:[<ffffffff810ecb16>] ? trace_current_buffer_lock_reserve+0x16/0x20
kernel:[<ffffffff8107ae1e>] ? ftrace_raw_event_hrtimer_cancel+0x4e/0xb0
kernel:[<ffffffff81095e7a>] ? hrtimer_try_to_cancel+0xba/0xd0
kernel:[<ffffffff8106f634>] ? do_setitimer+0xd4/0x220
kernel:[<ffffffff8106f88a>] ? alarm_setitimer+0x3a/0x60
kernel:[<ffffffff8107c27e>] ? sys_alarm+0xe/0x20
kernel:[<ffffffff8100b308>] ? tracesys+0xd9/0xde
kernel: ---[end trace 4d0a1ef2e62cb1a2 ]---
abrt-dump-oops: Reported 1 kernel oopses to Abrt
kernel: BUG: softlockup - CPU#11 stuck for 4278190091s! [qmgr:5492]
kernel:Modules linked in: fuse ipv6 power_meter bnx2 sg microcode serio_raw iTCO_wdtiTCO_vendor_support hpilo hpwdt i7core_edac edac_core shpchp ext4 mbcache jbd2sd_mod crc_t10dif hpsa radeon ttm drm_kms_helper drm i2c_algo_bit i2c_coredm_mirror dm_region_hash dm_log dm_mod [last unloaded: scsi_wait_scan]
kernel: CPU 11
kernel:Modules linked in: fuse ipv6 power_meter bnx2 sg microcode serio_raw iTCO_wdtiTCO_vendor_support hpilo hpwdt i7core_edac edac_core shpchp ext4 mbcache jbd2sd_mod crc_t10dif hpsa radeon ttm drm_kms_helper drm i2c_algo_bit i2c_coredm_mirror dm_region_hash dm_log dm_mod [last unloaded: scsi_wait_scan]
kernel:
kernel: Pid:5492, comm: qmgr Tainted: G W ---------------- 2.6.32-220.el6.x86_64 #1 HPProLiant DL360 G7
kernel: RIP:0010:[<ffffffff8106f730>] [<ffffffff8106f730>]do_setitimer+0x1d0/0x220
kernel: RSP:0018:ffff88080a661ef8 EFLAGS: 00000286
kernel: RAX:ffff88080b175a08 RBX: ffff88080a661f18 RCX: 0000000000000000
kernel: RDX:0000000000000000 RSI: 0000000000000082 RDI: ffff88080c8c4c40
kernel: RBP:ffffffff8100bc0e R08: 0000000000000000 R09: 0099d7270e01c3f1
kernel: R10:0000000000000000 R11: 0000000000000246 R12: ffffffff810ef9a3
kernel: R13:ffff88080a661e88 R14: 0000000000000000 R15: ffff88080a65a544
kernel: FS:00007f10b245f7c0(0000) GS:ffff88083c4a0000(0000) knlGS:0000000000000000
kernel: CS:0010 DS: 0000 ES: 0000 CR0: 000000008005003b
kernel: CR2:00007ff955977380 CR3: 000000100a80b000 CR4: 00000000000006e0
kernel: DR0:0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
kernel: DR3:0000000000000000 DR6: 00000000ffff0ff0 DR7: 0000000000000400
kernel:Process qmgr (pid: 5492, threadinfo ffff88080a660000, task ffff880809577500)
kernel: Stack:
kernel:00007f10b323def0 00007f10b248ead0 00007f10b26d0f78 00007f10b248ede0
kernel:<0> ffff88080a661f68 ffffffff8106f88a 0000000000000000 0000000000000000
kernel:<0> 000000000000014c 00000000000f423d 0000000000000000 0000000000000000
kernel: CallTrace:
kernel:[<ffffffff8106f88a>] ? alarm_setitimer+0x3a/0x60
kernel:[<ffffffff8107c27e>] ? sys_alarm+0xe/0x20
kernel:[<ffffffff8100b308>] ? tracesys+0xd9/0xde
kernel: Code:89 ef e8 74 66 02 00 83 3d 15 69 b5 00 00 75 37 49 8b 84 24 70 07 00 00 48 0508 08 00 00 66 ff 00 66 66 90 fb 66 0f 1f 44 00 00 <31> c0 e9 64 fe ff ff49 8b 84 24 68 07 00 00 48 c7 80 d0 00 00
kernel: CallTrace:
kernel:[<ffffffff8106f769>] ? do_setitimer+0x209/0x220
kernel:[<ffffffff8106f88a>] ? alarm_setitimer+0x3a/0x60
kernel:[<ffffffff8107c27e>] ? sys_alarm+0xe/0x20
kernel:[<ffffffff8100b308>] ? tracesys+0xd9/0xde
abrt-dump-oops: Reported 1 kernel oopses to Abrt
2.內核軟死鎖(soft lockup)bug原因分析
Soft lockup名稱解釋:所謂,soft lockup就是說,這個bug沒有讓系統徹底死機,但是若干個進程(或者kernel thread)被鎖死在了某個狀態(一般在內核區域),很多情況下這個是由於內核鎖的使用的問題。
Linux內核對於每一個cpu都有一個監控進程,在技術界這個叫做watchdog(看門狗)。通過ps –ef | grep watchdog能夠看見,進程名稱大概是watchdog/X(數字:cpu邏輯編號1/2/3/4之類的)。這個進程或者線程每一秒鍾運行一次,否則會睡眠和待機。這個進程運行會收集每一個cpu運行時使用數據的時間並且存放到屬於每個cpu自己的內核數據結構。在內核中有很多特定的中斷函數。這些中斷函數會調用soft lockup計數,他會使用當前的時間戳與特定(對應的)cpu的內核數據結構中保存的時間對比,如果發現當前的時間戳比對應cpu保存的時間大於設定的閥值,他就假設監測進程或看門狗線程在一個相當可觀的時間還沒有執。Cpu軟鎖為什么會產生,是怎么產生的?如果linux內核是經過精心設計安排的CPU調度訪問,那么怎么會產生cpu軟死鎖?那么只能說由於用戶開發的或者第三方軟件引入,看我們服務器內核panic的原因就是qmgr進程引起。因為每一個無限的循環都會一直有一個cpu的執行流程(qmgr進程示一個后台郵件的消息隊列服務進程),並且擁有一定的優先級。Cpu調度器調度一個驅動程序來運行,如果這個驅動程序有問題並且沒有被檢測到,那么這個驅動程序將會暫用cpu的很長時間。根據前面的描述,看門狗進程會抓住(catch)這一點並且拋出一個軟死鎖(soft lockup)錯誤。軟死鎖會掛起cpu使你的系統不可用。
如果是用戶空間的進程或線程引起的問題backtrace是不會有內容的,如果內核線程那么在soft lockup消息中會顯示出backtrace信息。
3.根據linux內核源碼分析錯誤
根據我們第一部分內核拋出的錯誤信息和call trace(linux內核的跟蹤子系統)來分析產生的具體原因。
首先根據我們的centos版本安裝相應的linux內核源碼,具體步驟如下:
(1)下載源碼的rpm包kernel-2.6.32-220.17.1.el6.src.rpm
(2)安裝相應的依賴庫,命令:yuminstall rpm-build redhat-rpm-config asciidoc newt-devel
(3)安裝源碼包:rpm -ikernel-2.6.32-220.17.1.el6.src.rpm
(4)進入建立源碼的目錄:cd~/rpmbuild/SPECS
(5)建立生成源碼目錄:rpmbuild-bp --target=`uname -m` kernel.spec
下面開始真正的根據內核bug日志分析源碼:
(1)第一階段內核錯誤日志分析(時間在Dec 4 14:03:34這個階段的日志輸出代碼分析,其實這部分代碼不會導致cpu軟死鎖,主要是第二階段錯誤日志顯示導致cpu軟死鎖)
我們首先通過日志定位到相關源代碼:看下面日志:Dec 4 14:03:34 BP-YZH-1-xxxx kernel: WARNING: atkernel/trace/ring_buffer.c:1988 rb_reserve_next_event+0x2ce/0x370() (Not tainted)
根據日志內容我們可以很容易的定位到kernel/trace/ring_buffer.c這個文件的1988行代碼如下:WARN_ON(1)。
先簡單解釋一下WARN_ON的作用:WARN_ON只是打印出當前棧信息,不會panic。所以會看到后面有一大堆的棧信息。這個宏定義如下:
#ifndef WARN_ON
#defineWARN_ON(condition) ({ \
int __ret_warn_on = !!(condition); \
if (unlikely(__ret_warn_on)) \
__WARN(); \
unlikely(__ret_warn_on); \
})
#endif
這個宏很簡單保證傳遞進來的條件值為0或者1(兩次邏輯非操作的結果),然后使用分支預測技術(保證執行概率大的分支緊鄰上面的指令)判斷是否需要調用__WARN()宏定義。如果滿足條件執行了__WARN()宏定義也接着執行一條空指令;。上面調用WARN_ON宏是傳遞的1,所以會執行__WARN()。下面繼續看一下__WARN()宏定義如下:
#define __WARN() warn_slowpath_null(__FILE__,__LINE__)
從接下來的call trace信息中我們也確實發現調用了warn_slowpath_null這個函數。通過在linux內核源代碼中搜索這個函數的實現,發現在panic.c(內核恐慌時的相關功能實現)中實現如下:
voidwarn_slowpath_null(const char *file, int line)
{
warn_slowpath_common(file, line,__builtin_return_address(0),
TAINT_WARN, NULL);
}
EXPORT_SYMBOL(warn_slowpath_null);//都出這個符號,讓其他模塊可以使用這個函數
同樣的我們看到了warn_slowpath_common這個函數,而在call trace當中這個函數在warn_slowpath_null函數之前打印出來,再次印證了這個流程是正確的。同樣在panic.c這個文件中我發現了warn_slowpath_common這個函數的實現如下:
static voidwarn_slowpath_common(const char *file, int line, void *caller,
unsigned taint, struct slowpath_args *args)
{
const char *board;
printk(KERN_WARNING "------------[ cut here]------------\n");
printk(KERN_WARNING "WARNING: at %s:%d %pS()(%s)\n",
file, line, caller, print_tainted());
board = dmi_get_system_info(DMI_PRODUCT_NAME);//得到dmi系統信息
if (board)
printk(KERN_WARNING "Hardware name:%s\n", board);//通過我們的日志信息可以發現我們硬件名稱是ProLiant DL360 G7
if (args)
vprintk(args->fmt, args->args);
print_modules();//打印系統模塊信息
dump_stack();//dump信息輸出(call trace開始)
print_oops_end_marker();//打印oops結束
add_taint(taint);
}
分析這個函數的實現不難發現我們的很多日志信息從這里開始輸出,包括打印一些系統信息,就不繼續深入分析了(請看代碼注釋,里面調用相關函數打印對應信息,通過我分析這些函數的實現和我們的日志信息完全能夠對應,其中dump_stack是與cpu體系結構相關的,我們的服務器應該是屬於x86體系)。這里在繼續分析一下dump_stack函數的實現,因為這個是與cpu體系結構相關的,而且這個函數直接反應出導致內核panic的相關進程。這個函數實現如下:
/*
* The architecture-independent dump_stackgenerator
*/
void dump_stack(void)
{
unsigned long stack;
printk("Pid: %d, comm: %.20s %s %s %.*s\n",
current->pid, current->comm,print_tainted(),
init_utsname()->release,
(int)strcspn(init_utsname()->version, ""),
init_utsname()->version);
show_trace(NULL, NULL, &stack);
}
EXPORT_SYMBOL(dump_stack);
通過我們的日志信息(Dec 4 14:03:34 BP-YZH-1-xxxx kernel: Pid: 5483, comm: master Nottainted 2.6.32-220.el6.x86_64 #1)很容易看出來,這個進程首先是由於master進程導致的,進程id是5483.這個函數的最后一行代碼調用show_trace函數打印call trace信息,這個函數實現如下:
void show_trace(structtask_struct *task, struct pt_regs *regs,
unsigned long *stack)
{
show_trace_log_lvl(task, regs, stack, "");
}
這個函數繼續調用show_trace_log_lvl函數,如下:
void show_trace_log_lvl(structtask_struct *task, struct pt_regs *regs,
unsigned long *stack, char *log_lvl)
{
printk("%sCall Trace:\n", log_lvl);
dump_trace(task, regs, stack, &print_trace_ops,log_lvl);
}
這個函數打印出我們日志中的Call Trace信息,然后繼續調用dump_trace函數(x86-64相關的體系結構,也就是64位的,還有一個32位的相應實現),如下:
/*
* x86-64 can have up to three kernel stacks:
* process stack
* interrupt stack
* severe exception (double fault, nmi, stackfault, debug, mce) hardware stack
*/
void dump_trace(structtask_struct *task, struct pt_regs *regs,
unsigned long *stack,
const struct stacktrace_ops *ops, void *data)
{
const unsigned cpu = get_cpu();
unsigned long *irq_stack_end =
(unsigned long *)per_cpu(irq_stack_ptr, cpu);
unsigned used = 0;
struct thread_info *tinfo;
int graph = 0;
unsigned long bp;
if (!task)
task = current;
if (!stack) {
unsigned long dummy;
stack = &dummy;
if (task && task != current)
stack = (unsigned long*)task->thread.sp;
}
bp = stack_frame(task, regs);
/*
* Print function callentries in all stacks, starting at the
* current stackaddress. If the stacks consist of nested
* exceptions
*/
tinfo = task_thread_info(task);
for (;;) {
char *id;
unsigned long *estack_end;
estack_end = in_exception_stack(cpu, (unsignedlong)stack,
&used,&id);
if (estack_end) {
if (ops->stack(data, id) < 0)
break;
bp = ops->walk_stack(tinfo, stack, bp,ops,
data, estack_end, &graph);
ops->stack(data,"<EOE>");
/*
*We link to the next stack via the
*second-to-last pointer (index -2 to end) in the
*exception stack:
*/
stack = (unsigned long *) estack_end[-2];
continue;
}
if (irq_stack_end) {
unsigned long *irq_stack;
irq_stack = irq_stack_end -
(IRQ_STACK_SIZE - 64) /sizeof(*irq_stack);
if (in_irq_stack(stack, irq_stack,irq_stack_end)) {
if (ops->stack(data,"IRQ") < 0)
break;
bp = ops->walk_stack(tinfo,stack, bp,
ops, data,irq_stack_end, &graph);
/*
* We link to the next stack (which would be
* the process stack normally) the last
* pointer (index -1 to end) in the IRQ stack:
*/
stack = (unsigned long *)(irq_stack_end[-1]);
bp = fixup_bp_irq_link(bp,stack, irq_stack,
irq_stack_end);
irq_stack_end = NULL;
ops->stack(data,"EOI");
continue;
}
}
break;
}
/*
* This handles theprocess stack:
*/
bp = ops->walk_stack(tinfo, stack, bp, ops, data, NULL,&graph);
put_cpu();
}
EXPORT_SYMBOL(dump_trace);
通過這個函數的注釋我們可以清楚的知道這個函數打印很多信息,包括進程堆棧信息、中斷堆棧信息以及一些服務異常的硬件堆棧信息(如雙重故障,NMI,堆棧錯誤,調試,MCE)。
通過上面這些分析只想說明一點,整個流程是沒有問題的。到目前為止只有第一行日志信息還沒有找到輸出的地址,其實看源碼很容易就看出來了,就在WARN_ON宏的上一行代碼就是了,代碼如下:
printk(KERN_WARNING "Delta way toobig! %llu"
" ts=%llu write stamp =%llu\n%s",
(unsigned long long)*delta,
(unsigned long long)*ts,
(unsigned long long)cpu_buffer->write_stamp,
local_clock_stable ? "" :
"If you just came from asuspend/resume,\n"
"please switch to the trace globalclock:\n"
" echo global > /sys/kernel/debug/tracing/trace_clock\n");
Printk函數的作用相當於printf,只是這個是專門用於內核的(有一些特殊實現)。通過一句代碼我們也可以發現local_clock_stable是為0,因為沒有打印最后那一點信息出來。到這里為止基本把第一階段出現內核錯誤和bug的代碼全部理解清楚了(日志時間為:Dec 4 14:03:34這個時間輸出的),后面附件具體分析導致這個原因代碼的原因。
(2)第二階段(時間在Dec 4 14:03:41,與第一階段只相差了幾秒鍾,應該還是存在一定的關聯)內核錯誤日志輸出代碼分析,這部分日志輸出顯示了導致了cpu軟死鎖。
根據第二部分關於可能產生軟死鎖原因的分析,我們能夠知道,這個軟死鎖最終是由watchdog程序拋出的。對第二階段的第一條輸出日志,我通過搜索找到在文件kernel/watchdog.c中的296行找到。這行代碼所在的函數是watchdog_timer_fn,這個函數定義如下:
/* watchdog kickerfunctions */
static enumhrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
unsigned long touch_ts = __get_cpu_var(watchdog_touch_ts);
struct pt_regs *regs = get_irq_regs();
int duration;
/* kick the hardlockup detector */
watchdog_interrupt_count();
/* kick the softlockup detector */
wake_up_process(__get_cpu_var(softlockup_watchdog));
/* .. and repeat */
hrtimer_forward_now(hrtimer,ns_to_ktime(get_sample_period()));
if (touch_ts == 0) {
if(unlikely(__get_cpu_var(softlockup_touch_sync))) {
/* If the time stamp was touchedatomically
*make sure the scheduler tick is up to date. */
__get_cpu_var(softlockup_touch_sync) =false;
sched_clock_tick();
}
__touch_watchdog();
return HRTIMER_RESTART;
}
/* check for a softlockup
* This is done bymaking sure a high priority task is
* being scheduled. The task touches the watchdog to
* indicate it isgetting cpu time. If it hasn't then
* this is a goodindication some task is hogging the cpu
*/
duration = is_softlockup(touch_ts);
if (unlikely(duration)) {
/* only warn once */
if (__get_cpu_var(soft_watchdog_warn) == true)
return HRTIMER_RESTART;
printk(KERN_ERR "BUG: soft lockup - CPU#%dstuck for %us! [%s:%d]\n",
smp_processor_id(), duration,
current->comm, task_pid_nr(current));
print_modules();
print_irqtrace_events(current);
if (regs)
show_regs(regs);
else
dump_stack();
if (softlockup_panic)
panic("softlockup: hungtasks");
__get_cpu_var(soft_watchdog_warn) = true;
} else
__get_cpu_var(soft_watchdog_warn) = false;
return HRTIMER_RESTART;
}
下面首先需要分析一下什么情況下會調用這個函數並且導致cpu軟死鎖,根據第二部分cpu軟死鎖產生原因的分析,每一個cpu都會啟動一個對應的watchdog線程來監控cpu的執行狀態,創建watchdog線程是在使能watchdog功能的時候,在函數watchdog_enable里面實現,代碼如下:
static intwatchdog_enable(int cpu)
{
struct task_struct *p = per_cpu(softlockup_watchdog, cpu);//保存每一個cpu的私有數據
int err = 0;
/* enable the perf event */
err = watchdog_nmi_enable(cpu);
/* Regardless of err above, fall through and startsoftlockup */
/* create the watchdog thread */
if (!p) {
p = kthread_create(watchdog, (void *)(unsignedlong)cpu, "watchdog/%d", cpu);
if (IS_ERR(p)) {
printk(KERN_ERR "softlockup watchdogfor %i failed\n", cpu);
if (!err)
/* if hardlockup hasn't alreadyset this */
err = PTR_ERR(p);
goto out;
}
kthread_bind(p, cpu);
per_cpu(watchdog_touch_ts, cpu) = 0;
per_cpu(softlockup_watchdog, cpu) = p;
wake_up_process(p);
}
out:
return err;
}
從上面這個watchdog使能函數可以看出,每一個cpu的watchdog監控線程的入口函數是watchdog函數,還注冊了一個softlockup軟死鎖響應函數。下面開始從watchdog函數繼續開始分析,代碼如下:
/* The watchdog thread -touches the timestamp. */
static int watchdog(void*unused)
{
struct sched_param param = { .sched_priority = MAX_RT_PRIO-1};
struct hrtimer *hrtimer = &__raw_get_cpu_var(watchdog_hrtimer);
sched_setscheduler(current, SCHED_FIFO, ¶m);
/* initialize timestamp */
__touch_watchdog();
/* kick off the timer for the hardlockup detector */
/* done here because hrtimer_start can only pin tosmp_processor_id() */
hrtimer_start(hrtimer, ns_to_ktime(get_sample_period()),
HRTIMER_MODE_REL_PINNED);
set_current_state(TASK_INTERRUPTIBLE);
/* Run briefly once per second to reset the softlockuptimestamp.
* If this getsdelayed for more than 60 seconds then the
* debug-printouttriggers in watchdog_timer_fn().*/
while (!kthread_should_stop()) {
__touch_watchdog();
schedule();
if (kthread_should_stop())
break;
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}
這個函數首先設置watchdog線程的cpu的調度策略為FIFO(先來先服務),並且優先級最低,然后初始化一個時間戳,設置當前的cpu狀態為可中斷的狀態。然后就進入while循環了,通過while循環上面的注釋我們可以清楚的知道每一秒鍾都會重新設置softlockup的時間戳,如果超過60秒鍾那么就會觸發debug信息被打印,打印信息是在函數watchdog_timer_fn里面,這個函數是在watchdog模塊初始化的時候注冊的,每一個模塊加載最先執行的都是模塊初始化函數,當然卸載模塊的時候也有相應的資源清理函數。
到這里為止我們知道了如果要打印出我們服務器內核的錯誤信息就必須得滿足一個條件,就是已經超過60秒鍾沒有重新設置softlockup的時間戳了(每一,秒鍾重新設置softlockup的時間戳,有一個比較通俗的說法叫做喂狗,因為上面的watchdog程序也俗稱看門狗程序,cpu就是通過這種方式來檢測cpu是否運行正常的,已經超過60秒鍾沒有喂狗了,那么狗肯定餓了,也就是cpu處於飢餓狀態,浪費了cpu的時間了,最大的可能就是cpu產生了軟死鎖)。這里就不詳細分析cpu調度相關的功能了,還是繼續分析產生這種bug的原因。下面最主要的就是分析為什么超過60秒鍾沒有喂狗(重新設置softlockup時間戳)了。分析這個應該需要結合第一階段的錯誤日志分析,第一階段也是由於計算時間差值大於了一個設定的時間。
我們還是先接着上面,分析一下日志的輸出是否和我們分析的代碼能夠對應上吧。最后就是通過call trace分析原因。
上面我們已經分析到了函數watchdog_timer_fn打印debug信息的地方了,這個函數的在前面已經粘貼出來了,我們結合我們的錯誤日志分析一下對應的輸出內核,通過函數的分析下面這句代碼就是打印我們第二階段錯誤日志信息的第一條:
printk(KERN_ERR"BUG: soft lockup - CPU#%d stuck for %us! [%s:%d]\n",
smp_processor_id(), duration,
current->comm,task_pid_nr(current));
打印的對應日志信息:Dec 4 14:03:41BP-YZH-1-xxxx kernel: BUG: soft lockup - CPU#11 stuck for 4278190091s![qmgr:5492]
而輸出這一條的條件就是linux內核檢測出了已經存在軟死鎖(通過前面if條件判斷的代碼以及前面的代碼注釋都可以說明這一點),通過這條日志信息我們可以看到cpu的邏輯編號、進程名稱和進程id(通過我們的日志信息可以看出分別是11、qmgr進程、進程id是5492)。我們在看看一個更重要的值,這個值就是決定了是否產生了軟死鎖,這個值就是duration,這里顯示是4278190091秒。下面詳細分析一下這個值是怎么被計算出來的,通過代碼發現是通過下面這個函數計算出來的:
staticint is_softlockup(unsigned long touch_ts)
{
unsignedlong now = get_timestamp(smp_processor_id());
/*Warn about unreasonable delays: */
if(time_after(now, touch_ts + softlockup_thresh))
returnnow - touch_ts;
return0;
}
首先這個函數得到一個對應cpu現在的時間戳,具體怎么詳細計算出來的就不分析了,反正與cpu的平率有關系,通過ns換算成為秒鍾(2^30ns=1.074s,然后通過cpu_clock(this_cpu) >> 30LL;這句代碼計算出來的)。接着通過調用time_after函數判斷現在的時間戳now與上一次喂狗的時間戳加上softlockup_thresh(60)的先后關系,也就是判斷是不是有超過60秒鍾沒有喂狗了,如果是就會返回一個現在的時間戳減去上一次喂狗的時間戳,如果沒有就會返回0。根據我們的日志信息,很明顯已經超過了,所以返回值不為0,就導致了軟死鎖被檢測到。
后面接着打印了linux內核加載了的相關模塊信息,然后打印中斷trace的事件信息,一直跟蹤下去,發現和我們打印的內核bug信息完全符合。
下面開始分析一下call trace,不管是第一階段的日志還是第二階段的日志堆棧最上層的函數都是tracesys,說明這些信息都是在trace子系統中發生的,至少說明當時都處於trace子系統的運行。其實tracesys是用匯編實現的,主要用於跟蹤系統調用的功能。第二階段的call trace基本是沒有什么信息,分析不出什么效果。有一點需要說明一下,第一階段的日志打印的信息顯示是master進程,第二階段顯示是qmgr進程。在centos上master進程是qmgr進程的父進程。下面看第一階段日志的call trace信息,如下(去掉了公共前綴信息):
Call Trace:
[<ffffffff81069b77>]? warn_slowpath_common+0x87/0xc0
[<ffffffff81069bca>]? warn_slowpath_null+0x1a/0x20
[<ffffffff810ea8ae>]? rb_reserve_next_event+0x2ce/0x370
[<ffffffff810eab02>]? ring_buffer_lock_reserve+0xa2/0x160
[<ffffffff810ec97c>]? trace_buffer_lock_reserve+0x2c/0x70
[<ffffffff810ecb16>]? trace_current_buffer_lock_reserve+0x16/0x20
[<ffffffff8107ae1e>]? ftrace_raw_event_hrtimer_cancel+0x4e/0xb0
[<ffffffff81095e7a>]? hrtimer_try_to_cancel+0xba/0xd0
[<ffffffff8106f634>]? do_setitimer+0xd4/0x220
[<ffffffff8106f88a>]? alarm_setitimer+0x3a/0x60
[<ffffffff8107c27e>]? sys_alarm+0xe/0x20
[<ffffffff8100b308>]? tracesys+0xd9/0xde
我們直接從trace_current_buffer_lock_reserve這個函數看是吧,前面有兩個函數分別是嘗試停用計時器(hrtimer_try_to_cancel)和取消跟蹤調試的事件計時器(ftrace_raw_event_hrtimer_cancel),在前面幾個函數在trace子系統觸發的時候都會被調用(跟蹤一個系統調用)。trace_current_buffer_lock_reserve這個函數最終根據call trace會調用rb_reserve_next_event函數。這個函數實現如下:
static struct ring_buffer_event * rb_reserve_next_event(structring_buffer *buffer,
struct ring_buffer_per_cpu *cpu_buffer, unsignedlong length)
{
structring_buffer_event *event;
u64 ts, delta =0;
int commit = 0;
int nr_loops =0;
rb_start_commit(cpu_buffer);
#ifdef CONFIG_RING_BUFFER_ALLOW_SWAP
/*
* Due to the ability to swap a cpu buffer froma buffer
* it is possible it was swapped before wecommitted.
* (committing stops a swap). We check for ithere and
* if it happened, we have to fail the write.
*/
barrier();
if(unlikely(ACCESS_ONCE(cpu_buffer->buffer) != buffer)) {
local_dec(&cpu_buffer->committing);
local_dec(&cpu_buffer->commits);
returnNULL;
}
#endif
length =rb_calculate_event_length(length);
again:
/*
* We allow for interrupts to reenter here anddo a trace.
* If one does, it will cause this originalcode to loop
* back here. Even with heavy interruptshappening, this
* should only happen a few times in a row. Ifthis happens
* 1000 times in a row, there must be either aninterrupt
* storm or we have something buggy.
* Bail!
*/
if(RB_WARN_ON(cpu_buffer, ++nr_loops > 1000))
gotoout_fail;
ts =rb_time_stamp(cpu_buffer->buffer);
/*
* Only the first commit can update thetimestamp.
* Yes there is a race here. If an interruptcomes in
* just after the conditional and it tracestoo, then it
* will also check the deltas. More than onetimestamp may
* also be made. But only the entry that didthe actual
* commit will be something other than zero.
*/
if(likely(cpu_buffer->tail_page == cpu_buffer->commit_page &&
rb_page_write(cpu_buffer->tail_page) ==
rb_commit_index(cpu_buffer))) {
u64diff;
diff= ts - cpu_buffer->write_stamp;
/*make sure this diff is calculated here */
barrier();
/*Did the write stamp get updated already? */
if(unlikely(ts < cpu_buffer->write_stamp))
gotoget_event;
delta= diff;
if(unlikely(test_time_stamp(delta))) {
commit= rb_add_time_stamp(cpu_buffer, &ts, &delta);
if(commit == -EBUSY)
gotoout_fail;
if(commit == -EAGAIN)
gotoagain;
RB_WARN_ON(cpu_buffer,commit < 0);
}
}
get_event:
event =__rb_reserve_next(cpu_buffer, 0, length, &ts);
if(unlikely(PTR_ERR(event) == -EAGAIN))
gotoagain;
if (!event)
gotoout_fail;
if(!rb_event_is_commit(cpu_buffer, event))
delta = 0;
event->time_delta= delta;
return event;
out_fail:
rb_end_commit(cpu_buffer);
return NULL;
}
我們首先看一下這個函數的參數:其中有兩個比較復雜和重要的結構體ring_buffer_per_cpu和ring_buffer,分析這兩個結構體主要是為了后面的函數內部分析做一個准備。我們先看看ring_buffer結構體(簡稱為RB結構體)的定義,如下:
struct ring_buffer {
unsigned pages; /*RB中的頁面數*/
unsigned flags; /*RB的標志,目前只有RB_FL_OVERWRITE可用*/
int cpus; /*ring buffer中包含的cpu個數*/
atomic_t record_disabled; /*整個ring buffer的禁用標志,用原子操作了防止競爭*/
cpumask_var_t cpumask; /* cpu位圖*/
struct lock_class_key *reader_lock_key;/*讀鎖關鍵字*/
struct mutex mutex; /*RB訪問鎖*/
structring_buffer_per_cpu **buffers; /*CPU的緩存區頁面,每個CPU對應一項*/
#ifdef CONFIG_HOTPLUG_CPU
structnotifier_block cpu_notify; /*多CPU情況下的cpu hotplug 通知鏈表*/
#endif
u64 (*clock)(void); /*RB所用的時間,用來計數時間戳*/
}
在RB的操作中,我們可以禁止全局的RB操作,例如,完全禁用掉Trace功能后,整個RB都是不允許再操做的,這時,就可以將原子變量record_disabled 加1。相反的,如果啟用的話,將其減1即可。只有當record_disabled的值等於0時,才允許操作RB。
同時,有些時候,要對RB的一些數據進行更新,比如,我要重新設置一下RB的緩存區大小,這都需要串行操作,因此,在ring_buffer結構中有mutex成員,用來避免這些更改RB的操作的競爭。
ring_buffer_per_cpu定義如下:
struct ring_buffer_per_cpu {
int cpu; /*該cpu buffer所在的CPU*/
structring_buffer *buffer; /*cpu buffer所屬的RB*/
/*讀鎖,用了避免讀者的操行操作,有時在
*寫者切換頁面的時候,也需要持有此鎖*/
spinlock_t reader_lock; /* serialize readers */
raw_spinlock_t lock;
structlock_class_key lock_key;
structlist_head pages; /*cpu buffer的頁面鏈表*/
structbuffer_page *head_page; /* read fromhead *//*起始讀位置*/
structbuffer_page *tail_page; /* write totail *//*寫位置*/
/*提交位置,只有當被寫的頁面提交過后才允許被讀*/
structbuffer_page *commit_page; /* committed pages */
structbuffer_page *reader_page; /*reader頁面, 用來交換讀頁面*/
unsigned long nmi_dropped;
unsigned long commit_overrun;
unsigned long overrun;
unsigned long read;
local_t entries;
u64 write_stamp; /*最新的頁面commit時間*/
u64 read_stamp; /*最新的頁面read時間*/
atomic_t record_disabled; /*cpu buffer的禁用啟用標志*/
}
首先,對每一個cpu的操作限制是由ring_buffer_per_cpu->record_disabled來實現的。同ring_buffer一樣,禁用加1,啟用減1。
我們也可以看到,每個cpu都有一系列的頁面,這樣頁面都鏈入在pages中。
該頁面的結構如下:
struct buffer_page {
struct list_headlist; /* list of buffer pages *//*用來形成鏈表*/
local_t write; /* index for next write *//*寫的位置*/
unsigned read; /* index for next read *//*讀的位置*/
local_t entries; /* entries on this page *//*頁面中有多少項數據*/
structbuffer_data_page *page; /* Actual datapage */
};
具體的緩存區是由structbuffer_data_page指向的,實際上,它是具體頁面的管理頭部,結構如下:
struct buffer_data_page {
u64 time_stamp; /* page time stamp *//*頁面第一次被寫時的時間戳*/
local_t commit; /* write committed index */ /*提交的位置*/
unsigned char data[]; /* data of buffer page *//*用來存放數據的緩存區*/
};
把所有基本的結構體解析清楚以后我們就可以正在分析這個函數了,代碼已經在前面寫出來了。函數里面首先定義了一個ring_buffer_event結構體的變量,先看一下這個結構體的定義如下:
/* Don't refer to this struct directly, use functions below.*/
struct ring_buffer_event {
kmemcheck_bitfield_begin(bitfield);//具體作用參考Documentation / kmemcheck.txt
u32 type_len:5, time_delta:27;
kmemcheck_bitfield_end(bitfield);
u32 array[];
};
這個結構體注釋提醒不要直接使用,只能在函數體里面使用,這里是在函數體里面定義和使用的。里面加入了動態內存檢測的字段kmemcheck,這個特性類似用戶態動態內存檢測工具valgrind,只是實現上有很大的區別,而且這個主要是用於內核動態內存檢測技術。繼續上面函數的分析,然后定義了一些需要用到的變量。繼續就開始調用函數rb_start_commit,函數代碼如下:
static void rb_start_commit(struct ring_buffer_per_cpu*cpu_buffer)
{
local_inc(&cpu_buffer->committing);
local_inc(&cpu_buffer->commits);
}
分別以cpu_buffer的committing和commits字段為參數調用local_inc函數,一直跟蹤下去最終會調用如下函數:
/**
* atomic_add - addinteger to atomic variable
* @i: integer value toadd
* @v: pointer of typeatomic_t
* Atomically adds @ito @v.
*/
static inline void atomic_add(int i, atomic_t *v)
{
asmvolatile(LOCK_PREFIX "addl %1,%0"
: "=m" (v->counter)
: "ir" (i), "m"(v->counter));
}
其實最終就是使用匯編實現一個長整型變量的原子加(addl)1操作。接下來是一段需要使能了RB交換swap功能(和虛擬內存交換,linux系統稱為交換區,有硬盤充當)。接下來調用函數rb_calculate_event_length計算事件長度。繼續我們就會看到一大段英文注釋,大體上是說,在調用這個函數之前禁止了搶占,中斷和NMI在這里存在着競爭,因此在下面的運行中,隨時都會被中斷/NMI所搶占。由於在從struct ring_buffer_per_cpu中取頁面的時候,會有當前頁面空間不足,需要前進一個頁面的情況.每次前進一個頁面都會跳轉到again,此時nr_loops都會增加1,如果在一次請求中,這樣的情況出現了1000次,說明中斷搶占的次數太多了,很可能是由於中斷風暴(interrupte storm)或者是bug造成的。下面就是調用宏函數RB_WARN_ON,如果超過1000次就打印相關call trace信息,根據我們的服務器日志這里沒有打印。繼續就是調用函數ring_buffer_time_stamp取當前的時間戳並賦值給ts臨時變量。下面又是一大段英文注釋,意思是只有第一次處於提交狀態的請求才能夠更新cpu_buffer->write_stamp(cpu的寫時間戳),這里存在着競爭。下面詳細分析這里代碼執行的情況,因為涉及到delta計算的問題,這也是導致我們服務器bug的最基本和開始的原因。
從這里的if判斷可以看到,只有在fistcommit的時候才會計算delta,其它的情況下,會獲取下一個event。我們來思考一下,為什么在確認了是fist commit,進入到了if,還需要進行:
if (unlikely(ts < cpu_buffer->write_stamp))
gotoget_event;
的判斷呢? 這個delta到底是用來做什么的呢?它為什么要用這樣的判斷方式呢?我們在之前說過,在ring_buffer_per_cpu中的每一塊數據都帶有一個event的頭部,即:
struct ring_buffer_event {
u32 type:2, len:3, time_delta:27;
u32 array[];
};
它里面有一個time_delta的成員占27位。在每一個頁面的頭部,即Structbuffer_data_page里面也有一個時間戳,即:
struct buffer_data_page {
u64 time_stamp; /* page time stamp */
local_t commit; /* write commited index */
unsigned char data[]; /* data of buffer page */
}
那這幾個時間戳有什么樣的關聯呢?在ring_buffer_per_cpu中有一個timestamp,它表示最近commit時的時間戳。每次切換進一個新頁面時,該頁面對應的:
buffer_data_page->time_stamp會記錄當前的時間戳。即buffer_date_page->time_stamp記錄頁面被切換成寫頁面時的時間戳。而ring_buffer_event->time_delta表示當前時間和上一次commit時間即ring_buffer_per_cpu->time_stamp的差值。
綜上所述,存在以下關系:
頁面中的第一個event,event1在進行寫操作時的時間戳為:
buffer_data_page->time_stamp+ ring_buffer_event1->time_delta.
第二個event,event2在進行寫操作時的時間戳為:
buffer_data_page->time_stamp+ring_buffer_event1->time_delta+
ring_buffer_event2->time_delta.
依次類推,不過有種情況是特別的,即RINGBUF_TYPE_TIME_EXTEND類型的EVENT,它是為了有時delta時間超過27位時,擴展了一個32位用來存放的時間戳。這也就是上面代碼中的if (test_time_stamp(delta))。另外需要注意,這里的返回值commit,只有在fistcommit的時候才會為1。
分析到這里我們發現計算出來的delta值超過了設定的閥值,所以打印處理debug信息,我們分析一下打印出來的幾個時間值(應該是系統節拍值),日志如下:
Delta way too big! 18428729675200069867ts=18446743954022816244 write stamp = 18014278822746377
通過上面這條日志我們可以確定此時的delta=18428729675200069867,ts=18446743954022816244write stamp = 18014278822746377(cpu里面記錄的上次寫入的值)。這三個值有如下關系:diff = ts - cpu_buffer->write_stamp;通過計算器計算確實滿足。那下面在看看ts是怎么計算出來的,他是計算linux服務器沖啟動到現在的節拍數,和設置的HZ有關系,最終還是調用sched_clock函數計算所得,這個函數代碼實現如下:
unsigned long long __attribute__((weak)) sched_clock(void)
{
//因為內核在啟動的時候就將jiffies的值初始化為INITIAL_JIFFIES(-300*HZ)
return(unsigned long long)(jiffies - INITIAL_JIFFIES)
*(NSEC_PER_SEC / HZ);
}
以上函數是cpu調度的節拍數計算方式,全局變量jiffies用來記錄從系統啟動以來產生的節拍的總數,啟動時,內核將該變量初始化為INITIAL_JIFFIES,網上有的說法又是初始化為0,為了驗證到底初始化為多少我們使用一個內核模塊在啟動的時候就把這個值打印出來看一看就清楚,通過測試初始化值確實是INITIAL_JIFFIES。此后,每次時鍾中斷處理程序都會增加該變量的值。一秒內時鍾中斷的次數等於HZ,所以jiffies一秒內增加的值也就是HZ。系統運行時間以秒為單位,等於jiffies/HZ。注意,jiffies類型為無符號長整型(unsigned long),其他任何類型存放它都不正確。將以秒為單位的時間轉化為jiffies:seconds * Hz。將jiffies轉化為以秒為單位的時間:jiffies / HZ。HZ的值在param.h中設置為100,那么通過日志中打印出來的ts值(單位是納秒)計算服務器已經啟動了多少天:18446743954022816244/1000000000/24/60/60=213504(天),這個值明顯不對,那我們在計算一下上一次cpu記錄的寫入值(write stamp = 18014278822746377)是否正確?同樣的計算方式如下:18014278822746377/1000000000/24/60/60=208(天)這個值還算比較正確,就是上一次寫入對應的值已經是208天以前的時候。
INITIAL_JIFFIES的宏定義如下:
#defineINITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))
SEC_PER_SEC是每一秒鍾的納秒數(1000000000)。
於是整個表達式計算為:系統當前運行的節拍值4294937296(INITIAL_JIFFIES計算所得),然后再除以1000000000/100=10000000。從我們的日志中可以看出當前計算出來的值是ts:18446743954022816244。根據這個值我們可以反推出jiffies-INITIAL_JIFFIES=1844674395402。這個值明顯不對,在看cpu軟死鎖產生的一條日志信息如下:
BUG: soft lockup- CPU#11 stuck for 4278190091s! [qmgr:5492]
通過這條日子好也可以看出這個時間4278190091s完全不正常。造成這種不正常的原因可能是內存破壞造成的。
新發現:在使用內核模塊修改jiffies值的時候,直接導致了centos產生了cpu softlockup錯誤,而且從內核打印出的錯誤信息可以看出最后加載的模塊就是我寫的那個測試模塊,對比線上服務器崩潰的內核日志查看最后加載的內核模塊是scsi_scan_wait。由此可以推斷可能是由於scsi_scan_wait這個內核模塊導致了qmgr進程產生了cpu軟死鎖,也就是導致了qmgr已經超過了60秒沒有得到中斷響應了。
4.結論
由於一直不能重現這個錯誤,所以一直沒有排查出問題的正在原因,目前還是繼續排查,想辦法還原錯誤。如果有哪位以前遇到過同樣的問題並且得到了解決麻煩告知一聲,非常感謝!