nbcon 专题Linux v7.3 · 6.12.113

01 · 场景视图(+1):七个逼出 nbcon 的场景

结论:nbcon 要同时满足两条互相拉扯的要求:调用者绝不被串口拖住(RT 的要求),以及关键日志一定要出得去(调试与 panic 的要求)。老的 console_lock 模型只能二选一:要么调用者亲自刷(满足后者、牺牲前者),要么全部丢给线程(满足前者,但 panic 时线程早已停摆)。nbcon 的办法是按场景分优先级:普通 printk 走线程(S1),WARN/oops 在调用者上下文原子输出(S2),panic 最后允许强夺(S3),NMI 只写缓冲(S4),tty 与 printk 用同一套所有权互斥(S5),启动与关机走过渡路径(S6、S7)。下面七个场景就是后面四个视图的「验收用例」。

场景视图:七个场景、7.3 的处理路径与满足版本
场景视图:七个场景、7.3 的处理路径与满足版本

S1 · 普通 printk(RT 线程或线程化中断里)

诉求:RT 线程里调用一次 pr_info(),开销应接近一次内存拷贝,不能因为串口在 115200 波特率下每字符约 87 µs 而被拖住。

7.3 路径(以 pl011 控制台、PREEMPT_RT、打印线程已就绪为例):

[CPU4 · RT 线程]  pr_info()
  _printk → vprintk → vprintk_default → vprintk_emit()               printk.c:2423
    printk_get_console_flush_type(&ft)                               printk.c:2444 → internal.h:194
        prio = NORMAL,have_nbcon_console && printk_kthreads_running → ft.nbcon_offload
        RT 上 is_printk_legacy_deferred() 恒真(printk_safe.c:59)  → ft.legacy_offload
    vprintk_store()                                                  printk.c:2455(无锁写 prb)
    nbcon_kthreads_wake()                                            printk.c:2461 → nbcon.c:1313
        rcuwait_has_sleeper(&con->rcuwait) ? irq_work_queue(&con->irq_work)   IRQ_WORK_LAZY
    defer_console_output()                                           printk.c:2484 → 4681(legacy 控制台)
  返回 —— 调用者到此结束

[CPU4 · irq_work/4(RT 上 SCHED_FIFO 低优先级,下一个 tick 唤醒)]
  run_irq_workd → nbcon_irq_work()                                   nbcon.c:1286
    nbcon_kthread_wake() → rcuwait_wake_up()                         internal.h

[CPU0 · pr/ttyAMA0(SCHED_OTHER nice -20)]
  nbcon_kthread_func()                                               nbcon.c:1220
    nbcon_emit_one(&wctxt, use_atomic=false)                         nbcon.c:1129
      con->device_lock() = pl011_console_device_lock()               amba-pl011.c:2713
      cant_migrate()
      nbcon_context_try_acquire(ctxt, false)                         nbcon.c:598(NORMAL)
      nbcon_emit_next_record(wctxt, false)                           nbcon.c:980
        con->write_thread() = pl011_console_write_thread()           amba-pl011.c:2673
      nbcon_context_release()                                        nbcon.c:677
      con->device_unlock()

RT 部署要点:irq_work 在调用 printk 的那颗 CPU 上排队。在 RT 上,它由本核的 irq_work/N 线程执行(kernel/irq_work.c:313-322,sched_set_fifo_low)。如果本核 tick 已停(nohz_full),LAZY 也会立刻自发一次 IPI(irq_work.c 的 __irq_work_queue_local)。所以隔离核上的 RT 线程不要在热路径里 printk,代价不在串口,而在本核多一次线程切换。

S2 · WARN / oops / lockdep / RCU stall

诉求:系统出了问题但还在跑。这时的回溯必须尽快出去,不能排在打印线程后面(线程可能根本调度不上)。

7.3 路径:

[CPUx] WARN_ON(cond)
  __warn()                                                           panic.c:1074
    nbcon_cpu_emergency_enter()                                      panic.c:1077 → nbcon.c:1717
        preempt_disable(); atomic_inc(&nbcon_cpu_emergency_cnt); nesting++
    printk(...) × N
      vprintk_emit() → flush_type:prio = EMERGENCY → ft.nbcon_atomic
        nbcon_atomic_flush_pending()                                 nbcon.c:1689
          __nbcon_atomic_flush_pending_con()                         nbcon.c:1564
            scoped_guard(irqsave) { try_acquire(EMERGENCY) → emit(write_atomic) → release }  (逐条)
    nbcon_cpu_emergency_exit()                                       panic.c:1117 → nbcon.c:1734
        计数归零 → nbcon_kthreads_wake()

同样进入 emergency 段的还有:oops_enter()(panic.c:1040)、lockdep 的二十多处报告点(6.12.113 kernel/locking/lockdep.c:577 等)、RCU stall(kernel/rcu/tree_stall.h:612, 688)、expedited stall(tree_exp.h:654)。如果打印线程正在输出,就要走进程视图里的 handover 时序。

S3 · panic

诉求:最后的现场一定要出去,哪怕要冒风险强行打断另一个 CPU 的写入。

7.3 路径:

[panic CPU] panic() → vpanic()                                       panic.c:783 → 577
  panic_other_cpus_shutdown()                                        panic.c:673(smp_send_stop / crash_smp_send_stop)
  printk_legacy_allow_panic_sync()                                   panic.c:675 → printk.c:2394
      此前 legacy 控制台保持沉默,让安全的 nbcon 先输出(e35a8884270b)
  ...通知链 / kmsg_dump...
  console_flush_on_panic(CONSOLE_FLUSH_PENDING)                      panic.c:710 → printk.c:3534
      nbcon_atomic_flush_pending()        // 先 nbcon
      console_flush_all()                 // 再 legacy,不拿 console_lock
  ...
  console_flush_on_panic(CONSOLE_FLUSH_PENDING)                      panic.c:767
  nbcon_atomic_flush_unsafe()                                        panic.c:768 → nbcon.c:1701
      panic_nbcon_allow_unsafe_takeover = true → 允许 hostile;ATOMIC_UNSAFE 控制台此时才被调用

S4 · NMI 回溯(arm64 pseudo-NMI)

诉求:NMI 里不能拿任何锁,但回溯又最重要。

路径:NMI 中 vprintk_emit() 只做 vprintk_store():prb 是无锁多写者结构,5.15 起不再需要 safe 缓冲。is_printk_legacy_deferred() 遇 in_nmi() 返回真,legacy 刷出延后到 irq_work。nbcon 的 NORMAL 优先级走线程;如果此时已在 panic,则由 panic CPU 原子刷出。arm64 上真正的 NMI 需要开 ARM64_PSEUDO_NMI 并在命令行加 irqchip.gicv3_pseudo_nmi=1,见物理视图。

S5 · 用户经 tty 写串口,与 printk 同时发生

诉求:getty/登录 shell 正在通过同一个 UART 收发,printk 的 write_atomic() 不能把寄存器写乱。

路径:

[任务] write(/dev/ttyAMA0) → uart_write()                          serial_core.c:607
  uart_port_ref_lock() → uart_port_lock_irqsave(port, &flags)        serial_core.c:79 → serial_core.h:721
    __uart_port_lock_irqsave()                                       serial_core.h:611(spin_lock)
    __uart_port_nbcon_acquire()                                      serial_core.h:678
      __uart_port_using_nbcon()?                                     serial_core.h:644(CON_NBCON 且 write_atomic)
      while (!nbcon_device_try_acquire(up->cons)) cpu_relax();       nbcon.c:1882(NORMAL + 进入 unsafe)
  kfifo_in() → __uart_start()(填 TX FIFO、开 TX 中断)
  uart_port_unlock_deref() → uart_port_unlock_irqrestore()           serial_core.h:791
    __uart_port_nbcon_release() → nbcon_device_release()             nbcon.c:1907
      exit_unsafe → release;若打印线程不可用,就地补刷(atomic / legacy)

持锁期间,EMERGENCY 的 write_atomic() 只能请求交接;只有 panic 末尾才会强夺。

S6 · 启动早期:earlycon / boot console

诉求:内核刚起来时打印线程还不存在,early 控制台与正式控制台又可能驱动同一块硬件。

路径:printk_kthreads_check_locked()(printk.c:3795)只要还有 boot console 就不启动线程(注释写明:boot console 与正式控制台之间无法同步硬件寄存器),这时 NORMAL 也走 nbcon_atomic 或 legacy 循环。正式 nbcon 控制台注册、boot console 注销后才建 pr/*。这正是 5.19 线程化失败的地方。

S7 · 关机、kexec 与 suspend

诉求:重启前最后几行要能出去;suspend 期间不能被 irq_work 唤醒。

路径:printk_kthreads_shutdown()(printk.c:3759,syscore shutdown)先清 printk_kthreads_running,再停掉所有 nbcon 线程,最后用 nbcon_atomic_flush_pending() 把剩余记录原子刷出,之后的 NORMAL 消息都走原子路径。6.19 起,suspend 期间不再排 irq_work(26873e3e7f0c、66e7c1e0ee08),控制台挂起时线程也不再空转。ARM64 上的一个实测问题(kexec 重启时 pl011 原子刷出导致 SMMU 超时)见 E5。

场景 × 视图对照

场景 逻辑视图 进程视图 开发视图 物理视图
S1 普通 NORMAL 所有权 pr/、irq_work/N nbcon_kthread_func 线程放 housekeeping 核
S2 emergency EMERGENCY、handover 交接时序 panic.c / lockdep / rcu 调用点 任意核亲自写 UART
S3 panic PANIC、hostile、unsafe_takeover panic CPU 次序 vpanic 停核方式(IPI / pseudo-NMI)
S4 NMI 只写 prb 延后刷出 vprintk_store pseudo-NMI、NMI 安全 cmpxchg
S5 tty device ctxt 持锁者与 atomic 互斥 serial_core.h 契约 UART 寄存器状态
S6 启动 boot console 约束 线程何时启动 printk_kthreads_check_locked earlycon=pl011,…
S7 关机 — 线程停止 → 原子 syscore kexec + SMMU