nbcon 专题Linux v7.3 · 6.12.113

03 · 进程视图:上下文、线程与交接时序

结论:nbcon 把「谁来碰串口」分给四类执行体:打印线程(每控制台一个 pr/<name><idx>,SCHED_OTHER nice -20,处理 NORMAL)、pr/legacy(仅 PREEMPT_RT,所有 legacy 控制台共用一个)、处于 emergency / panic 的 CPU 自己(关中断调用 write_atomic()),以及 tty 持锁者(经 uart_port_lock() 以 NORMAL 持有 device 上下文)。它们之间不靠锁、只靠 nbcon_state 协调:线程路径持 device_lock() 并禁迁移,原子路径关本地中断,交接只发生在驱动标出的安全点。对 RT 而言,打印线程不是 RT 优先级,RT 任务一定能抢占它;代价是日志会滞后,emergency 时会出现关中断的原子输出窗口。

执行体与上下文

进程视图 ①:printk 调用者、打印线程、原子刷出、tty 侧与 panic CPU
进程视图 ①:printk 调用者、打印线程、原子刷出、tty 侧与 panic CPU
执行体 何时创建 / 由谁唤醒 调度属性 处理什么 源码(v7.3)
pr/<name><idx> nbcon_kthread_create();printk_kthreads_check_locked() 在无 boot console 时为每个 nbcon 建一个;由 rcuwait 唤醒 SCHED_OTHER nice -20(sched_set_normal(kt, -20)) NORMAL 记录,write_thread() nbcon.c:1375, 1220
pr/legacy legacy_kthread_create(),仅 force_legacy_kthread()(RT)且存在 legacy / boot console 同上 所有 legacy 控制台;有 boot console 时也替 nbcon 打 printk.c:3728, 3704
irq_work/N(RT) nbcon 的 con->irq_work(LAZY)与 wake_up_klogd_work 进入 lazy_list SCHED_FIFO 低优先级(sched_set_fifo_low) 只负责唤醒上面两个线程 kernel/irq_work.c:313-322
emergency CPU nbcon_cpu_emergency_enter() 之后的每次 printk 关抢占;原子刷出时逐条关中断 本 CPU 产生的 + 积压的记录,write_atomic() nbcon.c:1717, 1564
panic CPU vpanic() 关中断 全部积压;末尾允许 hostile panic.c:577-768
tty 持锁者 任何 uart_port_lock*() 调用者 持 port->lock 以 NORMAL 持 nbcon_device_ctxt 并标 unsafe serial_core.h:678-793

唤醒链为什么要经过 irq_work:printk 可能在任意上下文被调用(持 rq 锁、关中断、NMI),不能直接 wake_up_process()。所以 nbcon_kthreads_wake()(nbcon.c:1313)只做两件事:rcuwait_has_sleeper() 用全屏障确认线程正在睡;是的话 irq_work_queue()。7.3 起这个 irq_work 是 LAZY 的(560f4deda327,已回合 6.12.y),不再为每次 printk 发 IPI,而是等下一个 tick。理由是 NORMAL 消息本来就要等调度。

并发规则(谁和谁互斥、靠什么)

组合 机制 说明
线程 vs 线程 每控制台只有一个 pr/* 不同控制台互不影响:慢串口不再拖累网络控制台
线程 vs tty 持锁者 device_lock()(如 port->lock) 二者都以 NORMAL 获取,先拿驱动锁,再拿所有权
线程 / tty vs emergency / panic nbcon_state 优先级 高优先级在安全点 direct 抢占,或在 unsafe 区请求 handover
原子路径 vs 本 CPU 中断 local_irq_save / scoped_guard(irqsave) 原子刷出不持驱动锁;如果被中断打断,再进驱动去拿所有权会死锁(nbcon.c:1577-1583 注释)
NORMAL 所有权 vs 抢占 / 迁移 device_lock() 必须禁迁移;cant_migrate() 防 nbcon_owner_matches() 的 ABA(逻辑视图)
打印线程 vs emergency(6.19+) nbcon_cpu_emergency_cnt > 0 时线程不醒 避免反复交接把输出撕碎
线程注册 / 注销 console_list_lock + SRCU 线程整个打印过程持 console_srcu_read_lock(),synchronize_srcu() 就能保证它已停(nbcon.c:1262)

交接时序:WARN 打断正在打印的线程

进程视图 ②:pr/ttyAMA0(NORMAL)被 CPU5 的 WARN(EMERGENCY)请求交接
进程视图 ②:pr/ttyAMA0(NORMAL)被 CPU5 的 WARN(EMERGENCY)请求交接

两侧调用栈(7.3,pl011):

── CPU2 · pr/ttyAMA0 ──────────────────────────────────────────────────────
nbcon_kthread_func()                                              nbcon.c:1220
 nbcon_emit_one(&wctxt, false)                                    nbcon.c:1129
  pl011_console_device_lock() → __uart_port_lock_irqsave()        amba-pl011.c:2713
  nbcon_context_try_acquire(NORMAL)                               nbcon.c:598
  nbcon_emit_next_record(wctxt, false)                            nbcon.c:980
   pl011_console_write_thread()                                   amba-pl011.c:2673
    for (i = 0; i < len; i++) {
      nbcon_enter_unsafe()                    ← t1:unsafe = 1,写一个字节
      uart_console_write(..., 1, pl011_console_putchar)
      nbcon_exit_unsafe()                     ← t2:看到 req_prio = EMERGENCY
        __nbcon_context_update_unsafe() → nbcon_context_can_proceed()   nbcon.c:832 / 730
          nbcon_context_release()  →  返回 false → break
    }
    while (!nbcon_enter_unsafe(wctxt))        ← t3:自旋等回所有权
      nbcon_reacquire_nobuf(wctxt)                                nbcon.c:938
    等 TX 空,恢复 UART CR;nbcon_exit_unsafe()                    ← t4
  !wctxt->outbuf → nbcon_context_release();返回 false            nbcon.c:1072
  device_unlock()

── CPU5 · WARN ────────────────────────────────────────────────────────────
__warn() → nbcon_cpu_emergency_enter()                            panic.c:1077
 printk → vprintk_emit → nbcon_atomic_flush_pending()             nbcon.c:1689
  __nbcon_atomic_flush_pending_con(con, stop_seq)                 nbcon.c:1564
   scoped_guard(irqsave) {
     nbcon_context_try_acquire(EMERGENCY)                         spinwait_max_us = 2000
       try_acquire_direct()   → -EBUSY(对方 unsafe)              ← t1
       try_acquire_handover() → 写 req_prio,udelay(1) 自旋        ← t2
         try_acquire_requested() 成功                              ← t3
     nbcon_emit_next_record(wctxt, true)
       console_prepend_replay()(同一条被分配过两次)
       pl011_console_write_atomic()                               amba-pl011.c:2645
     nbcon_context_release()
   }  // 每条都重复以上过程(6.19 起)
 nbcon_cpu_emergency_exit() → 计数归零 → nbcon_kthreads_wake()     nbcon.c:1734

三个容易忽略的细节:

  1. handover 不跨同一颗 CPU:try_acquire_handover() 遇到 cur->cpu == cpu 直接返回 -EBUSY(nbcon.c:461),否则就是自己等自己。同 CPU 嵌套(中断里 WARN 打断本核线程)只能在安全点 direct 抢占;如果正好在 unsafe 区,emergency 这一次就放弃,由线程稍后输出。
  2. 等待有上限:原子刷出把 spinwait_max_us 设为 2000(nbcon.c:1571)。超时后撤销 req_prio;只有 PANIC 且允许 unsafe 时才升级为 hostile。
  3. 重印而不是续打:被抢的那条记录由新所有者从头重印,可能出现半行加一整行的重复,这是有意的取舍(nbcon.c:755 注释)。

panic 时的进程视图

[panic CPU] vpanic()                                              panic.c:577
  panic_other_cpus_shutdown()          其他 CPU 被 IPI(或 pseudo-NMI)停住
      此后 non-panic CPU 的 printk 被丢弃(printk.c:2439)
      它们若卡在 nbcon_reacquire_nobuf() 自旋:6.17 起允许 direct reacquire 退出(571c1ea91a73)
  printk_legacy_allow_panic_sync()     legacy 从此允许在本上下文直接打印
  console_flush_on_panic()             nbcon(安全)→ legacy(不拿 console_lock)
  ...
  nbcon_atomic_flush_unsafe()          允许 hostile;ATOMIC_UNSAFE 驱动此刻才会被调用

6.19 起,打印线程在 panic_in_progress() 时不再尝试获取(nbcon_kthread_should_wakeup() nbcon.c:1196-1198),避免与 panic CPU 抢所有权。

对 RT 延迟的含义