今天我们看一看内核锁机制引起的一类稳定性问题: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 的到期回调函数中,有两个主要动作:

  1. 调度内核线程喂狗,喂狗的动作就是更新狗的时间戳。具体实现与内核的版本相关,老版本是唤醒喂狗线程,新版本(比如 4.19)是通过stop_cpu机制。但无论新老机制,正常喂狗的前提是该 CPU 能正常调度。
  2. 对比当前时间戳和喂狗的时间戳,如果当前时间戳减去喂狗的时间戳大于一定阈值(默认阈值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。后续我们会谈一谈这个机制。