nbcon 专题Linux v7.3 · 6.12.113

08 · E3 落地与 RT 进主线(6.11 被拒 → 6.12)

结论:6.12 一次性合入了 nbcon 的「活」的部分:选路分派(printk_get_console_flush_type())、打印线程(每个 nbcon 控制台一个 pr/<con>,加 write_thread() 与 device_lock())、emergency 段(WARN / oops / lockdep / RCU stall 在调用者上下文原子输出)、panic 次序(先 nbcon、再 legacy、最后 hostile),以及 RT 专用的 pr/legacy:legacy 控制台在 RT 上一律交给线程,驱动不用改。同一个合并窗口里,sched-rt-2024-09-17 合入,arm64 / x86 / riscv 可以选 PREEMPT_RT。在这之前,6.11 的 pull 被 Linus 拒绝,原因是 emergency 段到结束时才统一刷出,他回复 "The 'flush at end' is not even remotely sane"。6.12 改成 emergency 段里每次 printk 就地刷出。代价是:6.12 时 ARM64 上没有任何串口驱动是 nbcon,RT 的收益只体现在「调用者不再被拖住」,panic 时的可靠性要等 E4。

要解决的问题

# 问题 出处
P1 printk 调用者亲自刷串口,RT 延迟无上界 6.11 vprintk_emit() → console_trylock_spinning() → console_unlock()
P2 RT 上 legacy 驱动的 write() 拿 spinlock_t,在原子上下文会睡眠 5f53ca3ff83b:"The write() callback of legacy consoles usually makes use of spinlocks. This is not permitted with PREEMPT_RT in atomic contexts."
P3 紧急信息(回溯)必须马上出去,不能等线程 ecb5e1aa82c8:"This information should be pushed out to the consoles ASAP."
P4 打印线程与驱动自身的 tty 访问如何同步 7a16a7718907:增加 device_lock() / device_unlock()
P5 write_atomic() 与 tty 侧的寄存器访问如何互斥 4126f149c48b:uart_port_lock() 包装里获取 nbcon
P6 arm64 等架构不能选 PREEMPT_RT d8fccd9ca5f9:"With the recent printk changes, the last known road block has been addressed."

6.11 为什么被拒

6.11 版的 emergency 段让段内 printk 只入缓冲、在段结束时统一刷出,目的是让一整段回溯连续输出。John Ogness 在线程里解释:对 RCU stall、lockdep 这类大段输出,刷出要等到大部分警告都存进缓冲之后。Linus 的回复(2024-07-23):"I really think the flushing needs to be way more aggressive for any oops","The 'flush at end' is not even remotely sane"。也就是说,如果机器在 oops 打印途中彻底死掉,「先存后刷」会让最关键的那几行永远出不来。

6.12 的 ecb5e1aa82c8 改为:"Perform direct console flushing at the emergency priority if the current CPU is in an emergency state and it is safe to do so." 每次 printk 都在 vprintk_emit() 里就地原子刷出。

方案与提交(6.12,按功能分组)

功能 关键提交 说明
选路 6690d6b52726 Add helper for flush type logic struct console_flush_type:nbcon_atomic / nbcon_offload / legacy_direct / legacy_offload
追踪控制台种类 bebd87ae27e0、97ea9bccfcbe、60013065fdc6 have_nbcon_console / have_boot_console / have_legacy_console;没有 legacy 时跳过 console_lock
原子刷出 d3a9f82ec5c0 Provide function to flush using write_atomic() 只刷到调用时最新的记录,防止一个 CPU 被别人的新消息拖住
emergency ecb5e1aa82c8 Implement emergency sections per-CPU 嵌套计数;__warn()、oops_enter()、lockdep、RCU stall 等接入
panic d2e85ca7a736 Flush nbcon consoles first;e35a8884270b Coordinate direct printing;5dde3b735413 Add unsafe flushing on panic nbcon 先、legacy 后,最后 hostile
驱动同步 7a16a7718907 device_lock;adf6f37d142e non-printing API;4126f149c48b port->lock wrapper nbcon_device_try_acquire/release()、uart_port_lock*() 接入
线程 76f258bf3f2a Introduce printer kthreads;13189fa73afa Rely on kthreads for normal operation;1529bbb6e261 nice -20 write_thread() 成为必选回调
RT 5f53ca3ff83b Implement legacy printer kthread for PREEMPT_RT force_legacy_kthread();pr/legacy
杂项 bd07d864522e reacquire;5102981d5e2a replay 提示;b7049d88c176 write_atomic 改 void;def84b446777 sysfs active —
架构 d8fccd9ca5f9 arm64;d2d6422f8bd1 x86;2638e4e6b182 riscv(合并 baeb9a7d8b60:"After twenty years of development we finally reached the point to enable PREEMPT_RT support in the mainline kernel.") select ARCH_SUPPORTS_RT

架构图:之前 / 之后

之前(6.11):调用者上下文打印;RT 靠树外补丁
之前(6.11):调用者上下文打印;RT 靠树外补丁
之后(6.12):四路选路 + 打印线程 + emergency + RT 进主线
之后(6.12):四路选路 + 打印线程 + emergency + RT 进主线

调用栈

之前 · 6.11 主线(与 6.6 相同,行号见 E1):

vprintk_emit()
  vprintk_store()
  preempt_disable(); console_trylock_spinning() → console_unlock()
    console_flush_all() → console_emit_next_record() → con->write()   ← 调用者上下文、关中断

之后 · 6.12(行号为本站基准 6.12.113):

vprintk_emit()                                                  printk.c:2342
  printk_get_console_flush_type(&ft)                            :2363 → internal.h:233
  vprintk_store()                                               :2374
  ft.nbcon_atomic  → nbcon_atomic_flush_pending()               :2377 → nbcon.c:1626
  ft.nbcon_offload → nbcon_kthreads_wake()                      :2380 → nbcon.c:1271
  ft.legacy_direct → console_trylock_spinning() → console_unlock()   :2397(非 RT)
  ft.legacy_offload → defer_console_output()                    :2403 → :4571(RT 上恒走这里)

── RT 上的 legacy 控制台 ──
wake_up_klogd_work_func()(irq_work)                            printk.c:4498
  force_legacy_kthread() → wake_up_interruptible(&legacy_wait)
legacy_kthread_func()                                           :3619(pr/legacy,legacy_kthread_create() :3634)
  wait_event_interruptible(legacy_wait, legacy_kthread_should_wakeup())   :3571
  console_lock()
  __console_flush_and_unlock()                                  :3252
    console_flush_all()                                         :3230   ← 6.12.113 仍一次刷完全部积压
      console_emit_next_record() → con->write()                 :3024   ← 在线程里,spinlock_t 睡眠也无妨

── nbcon 控制台 ──
nbcon_kthread_func()                                            nbcon.c:1186
  nbcon_emit_one(&wctxt, false)                                 :1106
    con->device_lock(); cant_migrate()
    nbcon_context_try_acquire() → nbcon_emit_next_record() → con->write_thread()
    nbcon_context_release(); con->device_unlock()

── emergency ──
__warn()                                                        panic.c:773
  nbcon_cpu_emergency_enter()                                   nbcon.c:1652
  printk → vprintk_emit → ft.nbcon_atomic → nbcon_atomic_flush_pending()
    nbcon_atomic_flush_pending_con()                            nbcon.c:1544
      local_irq_save();__nbcon_atomic_flush_pending_con()        :1496   ← 6.12:一次持有到刷完
  nbcon_cpu_emergency_exit()                                    nbcon.c:1667

── panic ──
panic()                                                         panic.c:340
  printk_legacy_allow_panic_sync()                              :430 → printk.c:2313
  console_flush_on_panic(CONSOLE_FLUSH_PENDING)                 :465 → printk.c:3449
  …
  console_flush_on_panic(); nbcon_atomic_flush_unsafe()         :520-521 → nbcon.c:1638

线程何时启动(6.12.113):printk_set_kthreads_ready() 是 early_initcall(printk.c:3765),调用 printk_kthreads_check_locked()(:3696):有 legacy 或 boot console 且是 RT,就建 pr/legacy;没有 boot console 且有 nbcon,就为每个 nbcon 建 pr/* 并置 printk_kthreads_running。7.3 把它推迟到 subsys_initcall,见 E5。

RT 与 printk 的关系,到这里理清了

问题 6.12 之前(树外 RT 补丁) 6.12 主线
调用者被串口拖住 RT 补丁自带打印线程 NORMAL 一律 offload:nbcon 走 pr/<con>,legacy 走 pr/legacy
legacy 驱动 write() 在 RT 上睡眠 补丁里线程化 force_legacy_kthread() 让 legacy 只在线程里被调用,驱动不用改
紧急 / panic 输出 补丁里给 8250 加了 write_atomic 框架齐备,但主线驱动要等 6.13+ 才转换
Kconfig 树外打开 select ARCH_SUPPORTS_RT(arm64 d8fccd9ca5f9)

4+1 增量

视图 E3 带来的变化
场景 S1、S2、S3、S7 第一次有了 nbcon 路径;S5 有了框架(但 ARM64 无驱动)
逻辑 console_flush_type;write_thread / device_lock;device context
进程 新执行体 pr/<con>、pr/legacy;emergency CPU;panic 次序
开发 internal.h 选路;serial_core.h 包装;printk.c legacy 线程;panic.c / lockdep / RCU 接入
物理 arm64 可选 PREEMPT_RT;但 pl011 / 8250 仍是 legacy(见 6.12.113 serial_core.h:632 恒为 false)

留下的问题:没有驱动就没有 nbcon 的可靠性收益;所有 legacy 控制台挤在一个 pr/legacy 里;原子刷出一次持有到刷完。见 E4、E5。