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 控制台 |
架构图:之前 / 之后
调用栈
原子刷出 · 之前(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)
- 同步模式:John Ogness 在 2026-07 发出
console=…,sync系列(让 nbcon 控制台在 printk 调用者上下文同步输出,仅用于调试),Petr Mladek 仍在评审,未合入 7.3。 - WARN on unsafe reentrance(2026-07):检测驱动在 unsafe 区内重入。
- ARM64 未转换的驱动:
samsung_tty、meson_uart、msm_serial、fsl_lpuart、sh-sci、tegra-tcu(v7.3 中均为CON_PRINTBUFFER等,无CON_NBCON)。 - 8250 在 Tegra 上的收尾:7.3-rc3 已经把 printk 侧的 irq_work 改成 LAZY(
printk-for-7.3-rc3说明:"allows to keep the converted uart 8250 console driver in 7.3")。