今天我们看一看内核锁机制引起的一类稳定性问题:soft lockup。
soft lockup 案例
用 crash tool 解析出的信息如下:
KERNEL: examples/softlockup/vmlinux [TAINTED]
DUMPFILE: examples/softlockup/dump-softlockup.bin
CPUS: 2 [OFFLINE: 1]
DATE: Tue Nov 21 15:43:05 CST 2023
UPTIME: 00:00:36
LOAD AVERAGE: 0.68, 0.16, 0.05
TASKS: 48
NODENAME: (none)
RELEASE: 4.19.298
VERSION: #23 SMP PREEMPT Tue Nov 21 15:41:46 CST 2023
MACHINE: aarch64 (unknown Mhz)
MEMORY: 128 MB
PANIC: "Kernel panic - not syncing: softlockup: hung tasks"
PID: 43
COMMAND: "kworker/1:1"
TASK: ffff8000049f0d80 [THREAD_INFO: ffff8000049f0d80]
CPU: 1
STATE: TASK_RUNNING (PANIC)
这一句提示了 kernel panic 的原因:发生了 soft lockup。
PANIC: "Kernel panic - not syncing: softlockup: hung tasks"
那 soft lockup 究竟表示什么呢?内核 log 中的提示可能更加直白:
[ 36.081682] watchdog: BUG: soft lockup - CPU#1 stuck for 22s! [kworker/1:1:43]
CPU#1 stuck for 22s! 提示 CPU#1 卡住了 22秒。也就是说,CPU#1 上已经长时间(22秒)没有发生调度切换。22秒这个时间并不是非常准确的,但是鉴于这个时间相对于CPU运行速度来说,已经是很长的时间了,所以精确与否已经无关紧要。
既然 CPU#1 长时间未被调度切换,那么 CPU#1 上当前运行的进程,就是引起 CPU#1 长时间不调度的元凶。crash tool 解析出的信息中,已经指明了当前进程的 PID 是 43。
PANIC: "Kernel panic - not syncing: softlockup: hung tasks"
PID: 43
我们尝试用 bt 命令看一下进程 43 在做什么:
crash8.0.0> bt 43
PID: 43 TASK: ffff8000049f0d80 CPU: 1 COMMAND: "kworker/1:1"
#0 [ffff00000800bce0] __delay at ffff000008ad9ed4
#1 [ffff00000800bd10] __const_udelay at ffff000008ad9e3c
#2 [ffff00000800bd20] panic at ffff000008aef020
#3 [ffff00000800be00] watchdog_timer_fn at ffff00000818c10c
#4 [ffff00000800be60] __hrtimer_run_queues at ffff00000814d50c
#5 [ffff00000800bed0] hrtimer_interrupt at ffff00000814d8c8
#6 [ffff00000800bf30] arch_timer_handler_virt at ffff00000894c984
#7 [ffff00000800bf40] handle_percpu_devid_irq at ffff000008136b9c
#8 [ffff00000800bf70] generic_handle_irq at ffff000008130dd8
#9 [ffff00000800bf80] __handle_domain_irq at ffff00000813153c
#10 [ffff00000800bfc0] gic_handle_irq at ffff0000080819b4
--- ---
#11 [ffff0000093abd20] el1_irq at ffff00000808342c
PC: ffff000008ad9ed8 [__delay+144]
LR: ffff000008ad9ed8 [__delay+144]
SP: ffff0000093abd30 PSTATE: 80000005
X29: ffff0000093abd30 X28: ffff000009127000 X27: ffff00000936bcd8
X26: ffff8000049ce838 X25: 0000000000000000 X24: ffff0000092ccab0
X23: 0000000000418958 X22: ffffffff79617703 X21: 00000000869ea167
X20: 000000000000f424 X19: ffff00000924bd10 X18: ffff000008f952e0
X17: 0000000000000000 X16: 0000000000000000 X15: 0000000000000010
X14: 0720072007200720 X13: 0720072007200720 X12: 0720072007200720
X11: 0720072007200720 X10: 0720072007200720 X9: 0720072007200720
X8: 0720072007200720 X7: 0720072007200720 X6: 00000000000000cc
X5: 0000000000000000 X4: 0000000000000000 X3: ffff0000092ccaa0
X2: ffff0000092cd998 X1: ffff0000093abd30 X0: 00000000869f7953
#12 [ffff0000093abd30] __delay at ffff000008ad9ed4
#13 [ffff0000093abd60] __const_udelay at ffff000008ad9e3c
#14 [ffff0000093abd70] test_work_func at ffff000008af42d0
#15 [ffff0000093abdb0] process_one_work at ffff0000080f4650
#16 [ffff0000093abe00] worker_thread at ffff0000080f47cc
#17 [ffff0000093abe60] kthread at ffff0000080fa8d4
bt 命令同时输出了 IRQ 和 进程 43 的调用栈,我们先把关注点放在尾部的进程调用栈上:
#12 [ffff0000093abd30] __delay at ffff000008ad9ed4
#13 [ffff0000093abd60] __const_udelay at ffff000008ad9e3c
#14 [ffff0000093abd70] test_work_func at ffff000008af42d0
#15 [ffff0000093abdb0] process_one_work at ffff0000080f4650
#16 [ffff0000093abe00] worker_thread at ffff0000080f47cc
#17 [ffff0000093abe60] kthread at ffff0000080fa8d4
__delay 和 __const_udelay 是内核的公共函数,意义不大,我们看一下 test_work_func 函数的内容:
crash8.0.0> dis -s test_work_func
FILE: ../drivers/input/keyboard/gpio_keys.c
LINE: 403
398
399 static void test_work_func(struct work_struct *work)
400 {
401 int i;
402
* 403 pr_info("%s enter\n", func);
404 spin_lock_init(&test_lock);
405 spin_lock(&test_lock);
406 for (i = 0; i < 30; i++)
407 mdelay(1000);
408 spin_unlock(&test_lock);
409 pr_info("%s exit\n", func);
410 }
可以看到,test_work_func 函数做了一个 delay 30 秒的动作,那么是这个长时间的 delay 造成了 CPU 没有发生调度切换吗?
我们知道,一个进程即使长时间 delay,它仍然是可以被其他进程抢占的。更关键的原因,或许你已经注意到了,这个长时间 delay 发生在自旋锁拿锁期间。各类内核书籍都有教导我们,自旋锁属于忙等待的方式,所以适用于临界区耗时很短的情况。
如果我们看一看自旋锁的实现,就能发现自选锁在 lock 期间关闭了抢占。这就是导致 CPU 不发生调度切换的原因。
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
* preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
soft lockup 侦测原理
soft lockup 侦测的原理,我们能够从 bt 命令输出的 IRQ 调用栈中看出端倪:
#2 [ffff00000800bd20] panic at ffff000008aef020
#3 [ffff00000800be00] watchdog_timer_fn at ffff00000818c10c
#4 [ffff00000800be60] __hrtimer_run_queues at ffff00000814d50c
#5 [ffff00000800bed0] hrtimer_interrupt at ffff00000814d8c8
#6 [ffff00000800bf30] arch_timer_handler_virt at ffff00000894c984
#7 [ffff00000800bf40] handle_percpu_devid_irq at ffff000008136b9c
#8 [ffff00000800bf70] generic_handle_irq at ffff000008130dd8
#9 [ffff00000800bf80] __handle_domain_irq at ffff00000813153c
#10 [ffff00000800bfc0] gic_handle_irq at ffff0000080819b4
#11 [ffff0000093abd20] el1_irq at ffff00000808342c
gic_handle_irq 是 ARM 的中断处理函数,arch_timer_handler_virt 提示该中断是一个 timer 中断。
内核会为每个 CPU 启动一个 hrtimer(hr = high resolution,高精度),由于 timer 是通过中断触发的,所以不受内核是否关闭抢占的影响。
在 timer 的到期回调函数中,有两个主要动作:
- 调度内核线程喂狗,喂狗的动作就是更新狗的时间戳。具体实现与内核的版本相关,老版本是唤醒喂狗线程,新版本(比如 4.19)是通过stop_cpu机制。但无论新老机制,正常喂狗的前提是该 CPU 能正常调度。
- 对比当前时间戳和喂狗的时间戳,如果当前时间戳减去喂狗的时间戳大于一定阈值(默认阈值20秒),那说明该 CPU 已经长时间没有成功喂狗, CPU 已经“卡住”、发生了 soft lockup。
static void watchdog_enable(unsigned int cpu)
{
。。。
/* 启动 hrtimer */
hrtimer_init(hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
hrtimer->function = watchdog_timer_fn;
hrtimer_start(hrtimer, ns_to_ktime(sample_period),
HRTIMER_MODE_REL_PINNED);
。。。
}
/* hrtimer 的回调函数 */
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
。。。
/* 发起喂狗动作,softlockup_fn */
if (completion_done(this_cpu_ptr(&softlockup_completion))) {
reinit_completion(this_cpu_ptr(&softlockup_completion));
stop_one_cpu_nowait(smp_processor_id(),
softlockup_fn, NULL,
this_cpu_ptr(&softlockup_stop_work));
}
。。。
duration = is_softlockup(touch_ts);
if (unlikely(duration)) {
//发生了 soft lockup,做出处理
}
}
从上述原理可知,soft lockup 的侦测需要依赖 timer 中断。如果 CPU 的中断被禁止了,那 soft lockup 机制就不能正常工作;针对这种异常场景,内核引入了另外一种 lockup 侦测机制:hard lockup。后续我们会谈一谈这个机制。