01 · 场景视图(+1):七个逼出 nbcon 的场景
结论:nbcon 要同时满足两条互相拉扯的要求:调用者绝不被串口拖住(RT 的要求),以及关键日志一定要出得去(调试与 panic 的要求)。老的 console_lock 模型只能二选一:要么调用者亲自刷(满足后者、牺牲前者),要么全部丢给线程(满足前者,但 panic 时线程早已停摆)。nbcon 的办法是按场景分优先级:普通 printk 走线程(S1),WARN/oops 在调用者上下文原子输出(S2),panic 最后允许强夺(S3),NMI 只写缓冲(S4),tty 与 printk 用同一套所有权互斥(S5),启动与关机走过渡路径(S6、S7)。下面七个场景就是后面四个视图的「验收用例」。
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 线程化失败的地方。
- 6.17
cf5543870186:第一个 nbcon 控制台注册时也立即建线程,否则会在注册时同步重放整个日志,慢串口上卡住启动。 - 7.3
5f339c46f153:printk_set_kthreads_ready()从early_initcall推迟到subsys_initcall(printk.c:3865),因为过早启动的打印线程会干扰某些平台的 SMP 启动(丢 IPI)。
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/ |
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 |