深入探究一次页面分配失败导致的宕机问题

  1. 问题背景
  2. 背景知识
  3. 排查过程
    1. 初步判断
    2. vmcore分析
  4. 结论
  5. 解决方法
  6. 参考

问题背景

某台机器hardlockup导致宕机,需要排查宕机原因。
如果没有AI辅助,本次排查问题的学习成本+时间成本+排查难度都将翻倍,因此,Assisted-by: GPT-5.6。

背景知识

  • hardlockup
    内核态一直执行超过XXX秒,不能触发调度和响应中断,watchdog检测到后触发宕机。
  • console_sem的handover机制
    当CPU A持有console_sem时,它会先将自身标记为console_owner,然后开始打印。若此时CPU B也想打印并发现锁被占用,它会将console_waiter置为true并进入关中断自旋等待。A在每次写操作后都会检查console_waiter,一旦发现B在等待,A便立即清除console_waiter并直接退出(但不释放console_sem)。B检测到console_waiter被清除后随即停止自旋,并直接继承锁的控制权,成为新的console_owner,继续完成后续打印工作。
  • printk_cpu_sync
    用于在多 CPU 间串行化打印操作的专用同步机制,具有cpu可重入性并且可以在任意上下文中使用。最早叫printk_cpulock,可参见faebd693c593 (“printk: rename cpulock functions”)。
  • fb_flashcursor
    delayed_work周期性触发,管理控制台的光标闪烁等状态。

排查过程

初步判断

首先看一下宕机前的log,发现有创建很多pod的操作,创建pod就会创建veth,创建并初始化veth的时候需要向内核申请页面。
log中有多条页面分配失败的信息。

[10365589.875168] cptp: page allocation failure: order:7, mode:0x40dc0(GFP_KERNEL|__GFP_COMP|__GFP_ZERO), nodemask=(null),cpuset=containerd.service,mems_allowed=0-1
[10368797.966935] cptp: page allocation failure: order:7, mode:0x40dc0(GFP_KERNEL|__GFP_COMP|__GFP_ZERO), nodemask=(null),cpuset=containerd.service,mems_allowed=0-1
[10368813.289827] cptp: page allocation failure: order:7, mode:0x40dc0(GFP_KERNEL|__GFP_COMP|__GFP_ZERO), nodemask=(null),cpuset=containerd.service,mems_allowed=0-1
[yzwddsg@XXXXX 5.14.0-427.42.1.el9_4.x86_64]$ cat vmcore-dmesg.txt |grep 'age allocation failure' |wc -l
334

看一下当时确实没有多少高阶内存了.

[10368798.554198] Node 0 DMA: 0*4kB 0*8kB 0*16kB 0*32kB 0*64kB 0*128kB 0*256kB 0*512kB 1*1024kB (U) 1*2048kB (M) 2*4096
kB (M) = 11264kB
[10368798.568724] Node 0 DMA32: 12*4kB (UM) 5*8kB (M) 7*16kB (M) 7*32kB (M) 9*64kB (M) 4*128kB (M) 8*256kB (M) 7*512kB
(M) 6*1024kB (M) 6*2048kB (UM) 445*4096kB (M) = 1848296kB
[10368798.587239] Node 0 Normal: 50484*4kB (UME) 44379*8kB (UME) 21282*16kB (UMEH) 47195*32kB (UMEH) 5*64kB (MH) 0*128k
B 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 2408040kB
[10368798.605858] Node 1 Normal: 41969*4kB (UMEH) 72919*8kB (UMEH) 41149*16kB (UMEH) 30135*32kB (UMEH) 404*64kB (UME) 5
*128kB (U) 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 2400428kB

然后看一下宕机前的栈(NMI后面的就不贴了节省空间),这里可以大概看出,初始化veth的时候,申请页面,走了慢路径,然后warn_alloc打印一些警告信息,然后就宕机了。那么哪怕是分配不出页面也不至于造成hardlockup宕机吧?

[10368956.210951]  <TASK>
[10368956.210951]  _printk+0x58/0x73
[10368956.210953]  dump_stack_print_info+0x7c/0xb0
[10368956.210954]  dump_stack_lvl+0x28/0x48
[10368956.210956]  warn_alloc+0x138/0x160
[10368956.210957]  ? __alloc_pages_direct_compact+0x1f1/0x280
[10368956.210958]  __alloc_pages_slowpath.constprop.0+0x8ac/0x960
[10368956.210959]  ? get_page_from_freelist+0x2ab/0x530
[10368956.210961]  ? veth_dev_init+0x78/0xf0 [veth]
[10368956.210963]  __alloc_pages+0x21d/0x250
[10368956.210964]  __kmalloc_large_node+0x79/0x100
[10368956.210965]  __kmalloc+0xca/0x140
[10368956.210966]  veth_dev_init+0x78/0xf0 [veth]
[10368956.210967]  register_netdevice+0x10d/0x620
[10368956.210969]  veth_newlink+0x169/0x3e0 [veth]
[10368956.210973]  __rtnl_newlink+0x722/0x9e0
[10368956.210975]  ? update_sg_lb_stats+0x7e/0x450
[10368956.210977]  ? xas_load+0x9/0xa0
[10368956.210977]  ? xas_load+0x9/0xa0
[10368956.210978]  ? xa_load+0x70/0xb0
[10368956.210981]  rtnl_newlink+0x44/0x70
[10368956.210982]  rtnetlink_rcv_msg+0x159/0x3d0
[10368956.210983]  ? rmqueue+0x7d3/0xd40
[10368956.210984]  ? pick_link+0x24b/0x430
[10368956.210985]  ? task_dump_owner+0x9e/0x100
[10368956.210986]  ? __pfx_rtnetlink_rcv_msg+0x10/0x10
[10368956.210987]  netlink_rcv_skb+0x54/0x100
[10368956.210990]  netlink_unicast+0x23b/0x360
[10368956.210991]  netlink_sendmsg+0x24c/0x4c0
[10368956.210993]  __sys_sendto+0x1dc/0x1f0
[10368956.210994]  ? do_anonymous_page+0x27a/0x4c0
[10368956.210996]  __x64_sys_sendto+0x20/0x30
[10368956.210997]  do_syscall_64+0x59/0x90
[10368956.210998]  ? do_user_addr_fault+0x1d6/0x6a0
[10368956.210999]  ? do_syscall_64+0x69/0x90
[10368956.211000]  ? exc_page_fault+0x62/0x150
[10368956.211001]  entry_SYSCALL_64_after_hwframe+0x77/0xe1
[10368956.211002] RIP: 0033:0x40328e
[10368956.211004] Code: 48 89 6c 24 38 48 8d 6c 24 38 e8 0d 00 00 00 48 8b 6c 24 38 48 83 c4 40 c3 cc cc cc 49 89 f2 48
 89 fa 48 89 ce 48 89 df 0f 05 <48> 3d 01 f0 ff ff 76 15 48 f7 d8 48 89 c1 48 c7 c0 ff ff ff ff 48
[10368956.211005] RSP: 002b:000000c00040ee40 EFLAGS: 00000206 ORIG_RAX: 000000000000002c
[10368956.211006] RAX: ffffffffffffffda RBX: 000000000000000a RCX: 000000000040328e
[10368956.211006] RDX: 000000000000008c RSI: 000000c000592000 RDI: 000000000000000a
[10368956.211007] RBP: 000000c00040ee80 R08: 000000c00058e01c R09: 000000000000000c
[10368956.211007] R10: 0000000000000000 R11: 0000000000000206 R12: 0000000000000000
[10368956.211008] R13: 0000000000000040 R14: 000000c000585d40 R15: 000000c000780000
[10368956.211009]  </TASK>

vmcore分析

首先看一下当前hardlockup导致宕机的栈,可以看到最后在执行kernel/printk/printk.c: 2046。

crash> bt
PID: 3365527  TASK: ff1634946440c680  CPU: 334  COMMAND: "cptp"
 #0 [fffffe122c54fa90] machine_kexec at ffffffff87a782e7
 #1 [fffffe122c54fae8] __crash_kexec at ffffffff87bef95a
 #2 [fffffe122c54fba8] panic at ffffffff8862b8b1
 #3 [fffffe122c54fc30] watchdog_overflow_callback at ffffffff87c32a6f
 #4 [fffffe122c54fc48] __perf_event_overflow at ffffffff87d11982
 #5 [fffffe122c54fc78] amd_pmu_v2_handle_irq at ffffffff87a09a6f
 #6 [fffffe122c54fe48] perf_event_nmi_handler at ffffffff87a04c98
 #7 [fffffe122c54fe68] nmi_handle at ffffffff87a3005b
 #8 [fffffe122c54feb0] default_do_nmi at ffffffff88682fc0
 #9 [fffffe122c54fed0] exc_nmi at ffffffff886831bf
#10 [fffffe122c54fef0] end_repeat_nmi at ffffffff888016e9
    [exception RIP: vprintk_emit+436]
    RIP: ffffffff87b93174  RSP: ff49b5b4e83870b8  RFLAGS: 00000002
    RAX: 0000000000000001  RBX: 0000000000000046  RCX: 000000000001c5a2
    RDX: ff1634946440c680  RSI: 0000000000000002  RDI: ffffffff8a4a8f58
    RBP: 00000000ffffffff   R8: 8000000100009736   R9: ff16353f79902f27
    R10: ffffffffffffffff  R11: 000000000000007a  R12: 0000000000000000
    R13: 0000000000000000  R14: ffffffff88f87460  R15: 0000000000000078
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
--- <NMI exception stack> ---
#11 [ff49b5b4e83870b8] vprintk_emit at ffffffff87b93174
#12 [ff49b5b4e83870f8] _printk at ffffffff88631e18
#13 [ff49b5b4e8387158] dump_stack_print_info at ffffffff880594ac
#14 [ff49b5b4e83871a0] dump_stack_lvl at ffffffff8864a0b9
#15 [ff49b5b4e83871b8] warn_alloc at ffffffff87daadb8
#16 [ff49b5b4e8387260] __alloc_pages_slowpath.constprop.0 at ffffffff87dab69c
#17 [ff49b5b4e8387330] __alloc_pages at ffffffff87dab97d
#18 [ff49b5b4e8387390] __kmalloc_large_node at ffffffff87d69499
#19 [ff49b5b4e83873c8] __kmalloc at ffffffff87d6997a
#20 [ff49b5b4e83873f8] veth_dev_init at ffffffffc16d9e48 [veth]
#21 [ff49b5b4e8387410] register_netdevice at ffffffff8840653d
#22 [ff49b5b4e8387448] veth_newlink at ffffffffc16daa89 [veth]
#23 [ff49b5b4e83876b8] __rtnl_newlink at ffffffff88417352
#24 [ff49b5b4e8387b20] rtnl_newlink at ffffffff88417664
#25 [ff49b5b4e8387b48] rtnetlink_rcv_msg at ffffffff884142a9
#26 [ff49b5b4e8387bd8] netlink_rcv_skb at ffffffff8847e454
#27 [ff49b5b4e8387cb8] netlink_unicast at ffffffff8847dbab
#28 [ff49b5b4e8387d08] netlink_sendmsg at ffffffff8847df2c
#29 [ff49b5b4e8387d80] __sys_sendto at ffffffff883d274c
#30 [ff49b5b4e8387ec0] __x64_sys_sendto at ffffffff883d2790
#31 [ff49b5b4e8387ec8] do_syscall_64 at ffffffff88681459
#32 [ff49b5b4e8387f50] entry_SYSCALL_64_after_hwframe at ffffffff8880012f
    RIP: 000000000040328e  RSP: 000000c00040ee40  RFLAGS: 00000206
    RAX: ffffffffffffffda  RBX: 000000000000000a  RCX: 000000000040328e
    RDX: 000000000000008c  RSI: 000000c000592000  RDI: 000000000000000a
    RBP: 000000c00040ee80   R8: 000000c00058e01c   R9: 000000000000000c
    R10: 0000000000000000  R11: 0000000000000206  R12: 0000000000000000
    R13: 0000000000000040  R14: 000000c000585d40  R15: 000000c000780000
    ORIG_RAX: 000000000000002c  CS: 0033  SS: 002b



crash> dis -lr ffffffff87b93174 |tail
0xffffffff87b9315b <vprintk_emit+411>:	call   0xffffffff88697d80 <_raw_spin_unlock>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/kernel/printk/printk.c: 2046
0xffffffff87b93160 <vprintk_emit+416>:	movzbl 0x2915de8(%rip),%eax        # 0xffffffff8a4a8f4f <console_waiter>
0xffffffff87b93167 <vprintk_emit+423>:	test   %al,%al
0xffffffff87b93169 <vprintk_emit+425>:	je     0xffffffff87b93178 <vprintk_emit+440>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/./arch/x86/include/asm/vdso/processor.h: 13
0xffffffff87b9316b <vprintk_emit+427>:	pause
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/kernel/printk/printk.c: 2046
0xffffffff87b9316d <vprintk_emit+429>:	movzbl 0x2915ddb(%rip),%eax        # 0xffffffff8a4a8f4f <console_waiter>
0xffffffff87b93174 <vprintk_emit+436>:	test   %al,%al

找到对应的源码看一下,就是卡在了这个console_trylock_spinning函数的的while循环中。
根据背景知识中的内容我们可以知道这是一直在尝试死循环读console_waiter,直到handover把锁转移过来把console_waiter置为false,而在这之前的printk_safe_enter_irqsave中正好关闭了中断。
所以可以合理推断是之前持有sem锁的owner一直没有handover写console_waiter,所以这里就关了中断之后一直循环等待最终导致hardlockup。

static int console_trylock_spinning(void)
{
......

        printk_safe_enter_irqsave(flags);

        raw_spin_lock(&console_owner_lock);
        owner = READ_ONCE(console_owner);
        waiter = READ_ONCE(console_waiter);
        if (!waiter && owner && owner != current) {
        WRITE_ONCE(console_waiter, true);
                spin = true;
        }
        raw_spin_unlock(&console_owner_lock);

        if (!spin) {
                printk_safe_exit_irqrestore(flags);
                return 0;
        }

        while (READ_ONCE(console_waiter))
                cpu_relax();
        spin_release(&console_owner_dep_map, _THIS_IP_);
        printk_safe_exit_irqrestore(flags);
        /*
         * The owner passed the console lock to us.
         * Since we did not spin on console lock, annotate
         * this as a trylock. Otherwise lockdep will
         * complain.
         */
        mutex_acquire(&console_lock_dep_map, 0, 1, _THIS_IP_);

        return 1;
}

可以通过以下操作验证我们上述的猜测:
console_waiter一直是true,然后console_sem中的counter是0说明现在sem确实被别人持有,然后wait_list指向自己,说明没有别的waiter,符合使用handover的特征。

crash> p console_waiter
console_waiter = $1 = true

crash> p console_sem
console_sem = $2 = {
  lock = {
    raw_lock = {
      {
        val = {
          counter = 0
        },
        {
          locked = 0 '\000',
          pending = 0 '\000'
        },
        {
          locked_pending = 0,
          tail = 0
        }
      }
    }
  },
  count = 0,
  wait_list = {
    next = 0xffffffff899e7ae8 <console_sem+8>,
    prev = 0xffffffff899e7ae8 <console_sem+8>
  }
}

然后我们查看一下这个sem的owner,是一个kwoker。

crash> p console_owner
console_owner = $3 = (struct task_struct *) 0xff16347e4de04680
crash> struct task_struct.tgid,pid,comm 0xff16347e4de04680
  tgid = 1463052,
  pid = 1463052,
  comm = "kworker/290:1\000\000",

crash> bt 1463052
PID: 1463052  TASK: ff16347e4de04680  CPU: 290  COMMAND: "kworker/290:1"
 #0 [ff49b5b50ea37e90] process_one_work at ffffffff87b2d292
 #1 [ff49b5b50ea37ed8] worker_thread at ffffffff87b2d860
 #2 [ff49b5b50ea37f18] kthread at ffffffff87b35a2d
 #3 [ff49b5b50ea37f50] ret_from_fork at ffffffff87a02c69

但是这个kworker执行了什么任务呢?看不到了。
这里我想了几种别的方式来尝试找一下蛛丝马迹来着,比如既然是查console_sem被谁持有,那么持有console_sem的函数会不会大概率打印一些log呢,从dmesg中可不可以找到一些信息?或者bf -f中有没有可能还存有一些信息呢?很遗憾,没找到什么。大模型还是厉害,给找到了方法:通过kwoker的task_struct找到kthread结构体,然后通过kthread结构体找到woker结构体,进而找到执行的func,可以看到是在执行fb_flashcursor函数。

crash> struct task_struct.worker_private ff16347e4de04680
  worker_private = 0xff163480e89abc00,
crash> struct kthread.data 0xff163480e89abc00
  data = 0xff16347f36eba6c0,
crash> struct worker.current_work,current_func,current_pwq,current_start,desc 0xff16347f36eba6c0
  current_work = 0xff1633bf5a41dcd8,
  current_func = 0xffffffff880ea1a0 <fb_flashcursor>,
  current_pwq = 0xff16353e7b4b8d00,
struct: invalid data structure reference: worker.current_start

看一下fb_flashcursor函数,console_trylock尝试加锁,然后执行完一些逻辑之后,执行console_unlock解锁。
这里,其实bt -r打印栈上所有内容后可以看到是执行到console_unlock了,所以其实是console_unlock内部执行出现了问题?

static void fb_flashcursor(struct work_struct *work)
{
        struct fbcon_ops *ops = container_of(work, struct fbcon_ops, cursor_work.work);
        struct fb_info *info;
        struct vc_data *vc = NULL;
        int c;
        int mode;
        int ret;

        /* FIXME: we should sort out the unbind locking instead */
        /* instead we just fail to flash the cursor if we can't get
         * the lock instead of blocking fbcon deinit */
        ret = console_trylock();
        if (ret == 0)
                return;

        /* protected by console_lock */
        info = ops->info;

        if (ops->currcon != -1)
                vc = vc_cons[ops->currcon].d;

        if (!vc || !con_is_visible(vc) ||
            fbcon_info_from_console(vc->vc_num) != info ||
            vc->vc_deccm != 1) {
                console_unlock();
                return;
        }

        c = scr_readw((u16 *) vc->vc_pos);
        mode = (!ops->cursor_flash || ops->cursor_state.enable) ?
                CM_ERASE : CM_DRAW;
        ops->cursor(vc, info, mode, get_color(vc, info, c, 1),
                    get_color(vc, info, c, 0));
        console_unlock();

        queue_delayed_work(system_power_efficient_wq, &ops->cursor_work,
                           ops->cur_blink_jiffies);
}

然后看一下console_unlock函数,重点是如果如果buffer里面有内容,则先flush,如果没有handover机制,则unlock,如果有handover,则在console_flush_all函数里面处理。

void console_unlock(void)
{
        bool do_cond_resched;
        bool handover;
        bool flushed;
        u64 next_seq;

        if (console_suspended) {
                up_console_sem();
                return;
        }

        do_cond_resched = console_may_schedule;

        do {
                console_may_schedule = 0;

                flushed = console_flush_all(do_cond_resched, &next_seq, &handover);
                if (!handover)
                        __console_unlock();

                if (!flushed)
                        break;
                        
        } while (prb_read_valid(prb, next_seq, NULL) && console_trylock());
}

如果有handover的话,则在console_flush_all的流程里调用到console_lock_spinning_disable_and_check里对handover进行处理,包括sem的移交和console_waiter的写入等。
所以很明显,还没有执行到console_lock_spinning_disable_and_check的逻辑。

console_flush_all
  ->console_emit_next_record_transferable
    ->console_emit_next_record
      ->__console_emit_next_record
        ->console_lock_spinning_disable_and_check

通过bt -r看一下栈上还有哪些内容,发现有个serial8250_console_write+865。看一下__console_emit_next_record函数的片段。
所以其实就是执行到call_console_driver里面了。call_console_driver里面是遍历调用各个console的write函数。

       if (handover) {
                printk_safe_enter_irqsave(flags);
                console_lock_spinning_enable();

                /* don't trace irqsoff print latency */
                stop_critical_timings();
        }

        call_console_driver(con, write_text, len, dropped_text, atomic_printing);

        write_console_seq(con, seq + 1, atomic_printing);

        if (handover) {
                start_critical_timings();
                *handover = console_lock_spinning_disable_and_check();
                printk_safe_exit_irqrestore(flags);
        }

然后看一下serial8250_console_write+865附近。

0xffffffff881b9b80 <serial8250_console_write+816>:      call   *%rax
0xffffffff881b9b82 <serial8250_console_write+818>:      nopl   (%rax)
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250.h: 286
0xffffffff881b9b85 <serial8250_console_write+821>:      mov    0x240(%r15),%rdi
0xffffffff881b9b8c <serial8250_console_write+828>:      test   %rdi,%rdi
0xffffffff881b9b8f <serial8250_console_write+831>:      je     0xffffffff881b9b9b <serial8250_console_write+843>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250.h: 287
0xffffffff881b9b91 <serial8250_console_write+833>:      mov    $0x6,%esi
0xffffffff881b9b96 <serial8250_console_write+838>:      call   0xffffffff881c05d0 <mctrl_gpio_set>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250_port.c: 3452
0xffffffff881b9b9b <serial8250_console_write+843>:      movb   $0x0,0x238(%r15)
0xffffffff881b9ba3 <serial8250_console_write+851>:      jmp    0xffffffff881b9916 <serial8250_console_write+198>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250.h: 182
0xffffffff881b9ba8 <serial8250_console_write+856>:      mov    0x220(%r15),%ebp
0xffffffff881b9baf <serial8250_console_write+863>:      jmp    0xffffffff881b9bbc <serial8250_console_write+876>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/./arch/x86/include/asm/irqflags.h: 122
0xffffffff881b9bb1 <serial8250_console_write+865>:      and    $0x2,%bh
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/./arch/x86/include/asm/irqflags.h: 134
0xffffffff881b9bb4 <serial8250_console_write+868>:      je     0xffffffff881b9bb7 <serial8250_console_write+871>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/./arch/x86/include/asm/irqflags.h: 42
0xffffffff881b9bb6 <serial8250_console_write+870>:      sti
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250.h: 186
0xffffffff881b9bb7 <serial8250_console_write+871>:      call   0xffffffff87b8ed30 <__printk_cpu_sync_wait>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/./arch/x86/include/asm/irqflags.h: 26
0xffffffff881b9bbc <serial8250_console_write+876>:      pushf
0xffffffff881b9bbd <serial8250_console_write+877>:      pop    %rbx
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/./arch/x86/include/asm/irqflags.h: 37
0xffffffff881b9bbe <serial8250_console_write+878>:      cli
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250.h: 186
0xffffffff881b9bbf <serial8250_console_write+879>:      call   0xffffffff87b8fd60 <__printk_cpu_sync_try_get>
0xffffffff881b9bc4 <serial8250_console_write+884>:      test   %eax,%eax
0xffffffff881b9bc6 <serial8250_console_write+886>:      je     0xffffffff881b9bb1 <serial8250_console_write+865>
/usr/src/debug/kernel-5.14.0-427.42.1.el9_4/linux-5.14.0-427.42.1.el9_4.x86_64/drivers/tty/serial/8250/8250.h: 115

这里,附近看到有调用__printk_cpu_sync_try_get,难道是在等printk_cpu_sync?
看一下printk_cpu_sync_owner,正好是334号cpu。看一下最开始hardlockup宕机的栈,正好就发生在334号cpu。恰好形成了ABBA死锁。

crash> p printk_cpu_sync_owner
printk_cpu_sync_owner = $1 = {
  counter = 334
}

结论

如图,fbcon周期性触发,获取console_sem然后在console_unlock路径中尝试获取printk_cpu_sync。而创建veth初始化的时候需要分配页面,快速分配失败走慢路径warn_alloc打印警告日志,它会先持有printk_cpu_sync,然后又尝试获取console_sem,因此两者互相等待形成死锁。

解决方法

上游中这个patch可以缓解这个问题,是在创建veth的时候使用kvcalloc来代替kcalloc,也就是不再强制去分配很大的连续的物理页面。

commit 1ce7d306ea63f3e379557c79abd88052e0483813
Author: Jakub Kicinski <kuba@kernel.org>
Date: Fri Feb 23 15:59:08 2024 -0800
 veth: try harder when allocating queue memory

而这个patch应该可以彻底解决这个问题,也就是在设计上,持有printk_cpu_sync的owner不应该等待其他锁,所以持有printk_cpu_sync的代码会把log延迟打印,这样就不会形成死锁了。

commit 0161e2d6950fe66cf6ac1c10d945bae971f33667
Author: John Ogness <john.ogness@linutronix.de>
Date: Mon Dec 9 12:23:46 2024 +0106
 printk: Defer legacy printing when holding printk_cpu_sync

参考

[1] https://mp.weixin.qq.com/s?__biz=Mzk3NTMzMTA4MA==&mid=2247483657&idx=1&sn=6c522cf0fdb47f5431d36acbba35ce0a&chksm=c5c628c78f56451565678de9ee5da15477d373780da295fc661f46ed40c0da2b568b51a5aab1&sessionid=1784368699&scene=305&subscene=10000&clicktime=1784368709&enterid=1784368709&ascene=3&fasttmpl_type=0&fasttmpl_fullversion=8349583-zh_CN-zip&fasttmpl_flag=0&realreporttime=1784368709794&poc_token=HIZOW2qj4lttowoSkaG1wmu5vsRzyFH6GvRzth93


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com