nbcon 专题Linux v7.3 · 6.12.113

10 · E5 收敛(6.19 → 7.3):让打印节奏不再伤人

结论:驱动大面积转换后,问题从「能不能输出」变成「输出的方式会不会伤到系统」。6.19 → 7.3 的修复可以归为五条原则。紧急时少交接:emergency / panic 期间打印线程直接休眠,不再被反复抢、反复重印。原子刷出小步走:每输出一条记录就释放所有权、恢复中断,不再让其他 CPU hard lockup,也不再让 ARM64 的 SMMU 命令超时。普通路径更懒:唤醒线程的 irq_work 改为 LAZY,不为每次 printk 发 IPI,顺带消除了 Tegra20/30 进 idle 时的挂死。启动更晚:打印线程推迟到 subsys_initcall,避免干扰 SMP 启动。驱动不碰会睡眠的东西:pl011 的时钟改为常开,8250 的 modem control 延后到 LAZY irq_work;写不出安全原子路径的驱动用 CON_NBCON_ATOMIC_UNSAFE,只在 panic 末尾尝试。同期 8250(第二次)和 Qualcomm GENI 完成转换,netconsole(7.0)成为第一个「线程 + 不安全原子」的控制台。

要解决的问题(症状来自修复提交里的现场日志)

# 问题 症状 修复
P1 emergency 反复接管线程 每行前都是 ** replaying previous printk message **,输出被撕碎 c41c0ebfa1e0(6.19)
P2 原子刷出整段持有所有权 其他 CPU 在 nbcon_reacquire_nobuf() / __uart_port_nbcon_acquire() 自旋,触发 Watchdog detected hard LOCKUP d5d399efff65(6.19)
P3 原子刷出整段关中断 ARM64:kexec 重启时 pl011_console_write_atomic() 长时间关中断,arm-smmu-v3 CMD_SYNC timeout、CSD 锁无响应 9bd18e1262c0(6.19)
P4 pr/legacy 整段持 console_lock PocketBeagle 2、9600 波特:console_on_rootfs 的 filp_open 在 5.6 s 开始,146 s 才返回 741ea7aa95dd、1bc9a28f076f(6.19)
P5 只能线程打印的驱动 panic 时完全无输出,于是有人写不安全的 write_atomic "developers may be tempted to implement unsafe ->write_atomic() callbacks" 187de7c212e5(6.19)
P6 8250 nbcon 的 irq_work 在 cpuidle 入口触发 IPI Tegra20 / Tegra30 无法启动 560f4deda327(7.3-rc3)、37aba16e7943(7.3-rc6)
P7 打印线程过早在启动期开关中断 某些 SMP 平台启动挂死(丢 IPI),撤掉 nbcon 即恢复 5f339c46f153(7.3)
P8 pl011 write_atomic() 里 clk_enable() RT 上 "BUG: sleeping function called from invalid context … rt_spin_lock from clk_enable_lock" c0e8cfef7546(7.3-rc1)

方案与提交

版本 提交 做了什么
6.19 c41c0ebfa1e0 Block printk kthreads when any CPU is in an emergency context 全局 nbcon_cpu_emergency_cnt;线程的唤醒条件与循环里都检查它
6.19 4c3ba0d59256 Allow printk kthread to sleep when the system is in panic panic_in_progress() 时线程不再尝试获取
6.19 d5d399efff65 + 9bd18e1262c0 __nbcon_atomic_flush_pending_con() 改为循环内 scoped_guard(irqsave):每条 acquire → emit → release,并恢复中断
6.19 741ea7aa95dd + ba00f7c4d005 + 1bc9a28f076f console_flush_one_record();pr/legacy 每条记录释放一次 console_lock
6.19 187de7c212e5 Allow unsafe write_atomic() for panic CON_NBCON_ATOMIC_UNSAFE:console_is_usable() 只在 nbcon_allow_unsafe_takeover() 为真时放行
6.19 49f7d3054e84、286b113d7000、62627bf0cadf 等 KDB 能在 nbcon 控制台上输出
6.19 26873e3e7f0c、66e7c1e0ee08、d01ff281bd9b suspend 时不排 irq_work;printk_trigger_flush() 能刷所有类型
7.0 bdfcca65e7a6 device_lock / device_unlock 成为必选回调
7.0 7eab73b18630 netconsole: convert to NBCON 线程打印;CON_NBCON_ATOMIC_UNSAFE
7.0 60325c27d3cf printk 记录携带执行上下文(task / CPU),供 netconsole 使用
7.3 d3539347022a serial: 8250: Switch to nbcon console, take 2 第二次转换;modem control 用 irq_work,suspend / resume 期间不用
7.3 74658868da2a serial: qcom-geni: Convert console to nbcon 线程按 32 字节分批;atomic 可以取消正在进行的 TX
7.3 7f93da9d78d4 + c0e8cfef7546 pl011:注销时 unprepare;控制台存续期间时钟常开
7.3 560f4deda327、0433632bbe8d nbcon irq_work 改 LAZY;nbcon_free() 前先刷掉它
7.3 37aba16e7943 8250 console_msr_work 改 LAZY
7.3 5f339c46f153 Postpone kthread startup early_initcall → subsys_initcall
7.3 36630cafbeed、3219f9b61d45、72dd0ec09e7c 控制台 use-after-free、预启用控制台注册、kthread_run 失败不再 WARN
7.3 c83e3d291243 braille 用 nbcon API 的串口也能作 braille 控制台

架构图:之前 / 之后

之前(6.15–6.18):六类问题与现场症状
之前(6.15–6.18):六类问题与现场症状
之后(6.19–7.3):线程侧、原子侧、驱动侧的新节奏
之后(6.19–7.3):线程侧、原子侧、驱动侧的新节奏

调用栈

原子刷出 · 之前(v6.18):

nbcon_atomic_flush_pending_con(con, stop_seq, allow_unsafe)            nbcon.c:1544
  local_irq_save(flags)                                                :1558   ← 整段关中断
  __nbcon_atomic_flush_pending_con()                                   :1496
    nbcon_context_try_acquire(ctxt)                                    ← 拿一次
    while (nbcon_seq_read(con) < stop_seq)
        nbcon_emit_next_record(&wctxt, true) → write_atomic()          ← 115200 下每条约数 ms
    nbcon_context_release(ctxt)                                        ← 全部刷完才放
  local_irq_restore(flags)

原子刷出 · 之后(v7.3):

nbcon_atomic_flush_pending_con(con, stop_seq)                          nbcon.c:1620
  __nbcon_atomic_flush_pending_con()                                   :1564
    ctxt->allow_unsafe_takeover = nbcon_allow_unsafe_takeover()        :1471(只有 panic 末尾为真)
    while (nbcon_seq_read(con) < stop_seq) {
      scoped_guard(irqsave) {                                          :1583   ← 每条关一次中断
        nbcon_context_try_acquire(ctxt, false)
        nbcon_emit_next_record(&wctxt, true)
        nbcon_context_release(ctxt)                                    ← 每条都放
      }
    }

打印线程 · 之后(v7.3):

nbcon_kthread_func()                                                   nbcon.c:1220
  rcuwait_wait_event(&con->rcuwait, nbcon_kthread_should_wakeup())
    nbcon_kthread_should_wakeup()                                      :1180
      if (atomic_read(&nbcon_cpu_emergency_cnt) || panic_in_progress())   :1196-1198
          return false;                                                ← emergency / panic 期间不醒
  do {
      if (nbcon_cpu_emergency_cnt || panic_in_progress())             :1255-1256
          goto wait_for_event;
      cookie = console_srcu_read_lock()
      backlog = nbcon_emit_one(&wctxt, false)
      console_srcu_read_unlock(cookie); cond_resched()
  } while (backlog)

nbcon_cpu_emergency_exit()                                             nbcon.c:1734
  if (atomic_dec_return(&nbcon_cpu_emergency_cnt) == 0)
      printk_get_console_flush_type(&ft); if (ft.nbcon_offload) nbcon_kthreads_wake()

pr/legacy · 之前(v6.18)/ 之后(v7.3):

v6.18  legacy_kthread_func()                                           printk.c:3602
         console_lock(); __console_flush_and_unlock()                  :3611   ← 一次刷完全部积压
v7.3   legacy_kthread_func()                                           printk.c:3704
         do {
           console_lock()
           console_flush_one_record(true, &next_seq, &handover, &try_again)   :3719 → :3234
           if (!handover) __console_unlock()                           ← 每条都放 console_lock
         } while (try_again)

线程启动时机:

v6.18  early_initcall(printk_set_kthreads_ready)                       printk.c:3749
v7.3   subsys_initcall(printk_set_kthreads_ready)                      printk.c:3865
         → 所有 arch_initcall 之后,SMP 启动期的 IPI 不再被打印线程开关中断干扰

pl011 时钟 · 之前(v6.15–7.2)/ 之后(v7.3):

之前  pl011_console_write_atomic()                                     v6.15 amba-pl011.c:2519
        clk_enable(uap->clk)                                           :2527
          clk_enable_lock() → spin_trylock_irqsave 失败 → spin_lock_irqsave
            → RT:rt_spin_lock() → might_sleep 报错
之后  pl011_console_setup()   → clk_prepare_enable(uap->clk)            v7.3 :2546
      pl011_console_write_atomic()                                     :2645   不再碰时钟
      pl011_console_exit()    → clk_disable_unprepare(uap->clk)         :2575

7.3 的 8250 与 qcom-geni:

univ8250_console_write_atomic()                                        8250_core.c:393
  serial8250_console_write(up, wctxt, true)                            8250_port.c:3441
    nbcon_enter_unsafe() …… 逐字节 / FIFO 写                           :3375 / :3402
    modem 状态变化 → irq_work_queue(&up->console_msr_work)             :3549-3550
                      (IRQ_WORK_INIT_LAZY,:3603;suspend 期间不用)
univ8250_console_device_lock()                                         8250_core.c:409

qcom_geni_serial_console_write_thread()                                qcom_geni_serial.c:660
  按 32 字节分批提交 M-side TX 命令;每批之间开安全窗口
qcom_geni_serial_console_write_atomic()                                :706
  可取消正在进行的普通 TX;之后用 irq_work 恢复排队的 TTY 输出

4+1 增量

视图 E5 带来的变化
场景 S2 不再撕碎输出;S3 有了 ATOMIC_UNSAFE 兜底;S6 启动更稳;S7 suspend / kexec 不再卡死
逻辑 全局 emergency 计数;CON_NBCON_ATOMIC_UNSAFE;nbcon_allow_unsafe_takeover();执行上下文字段
进程 原子刷出粒度从「全部积压」变为「一条记录」;线程在 emergency / panic 期间休眠;LAZY 唤醒
开发 console_flush_one_record();KDB / braille API;device_lock 必选;console_is_usable() 进 console.h
物理 ARM64 pl011 时钟常开(DT 平台功耗代价);8250 系、骁龙进入 nbcon;Tegra 系 idle 问题解决

还在路上(截至 2026-10-20)