nbcon 专题Linux v7.3 · 6.12.113

06 · E1 地基(5.10 → 6.6):先让「写日志」无锁,再拆小 console_lock

结论:nbcon 之前要先解决两个更底层的问题。写日志要拿全局锁:logbuf_lock 是 raw 自旋锁,NMI 里拿不了,只能另开 per-CPU safe 缓冲。console_lock 管得太多:链表、标志、打印全靠它串行。5.10 → 5.15 用无锁 printk_ringbuffer 依次替掉 log_buf、logbuf_lock 和 safe 缓冲;6.2 用 console_list_lock + SRCU 把控制台链表从 console_lock 里拆出来。中间 5.19 尝试过「每控制台一个打印线程」,在 rc4 被回退。它暴露的问题(早期控制台与正式控制台之间缺同步、console_lock 隐式串行了所有驱动)直接塑造了后来 nbcon 的设计:先有状态与优先级,再上线程。到 6.6,写日志已完全无锁,但打印仍是 console_lock + 调用者上下文,RT 依旧无解。

要解决的问题

# 问题 出处
P1 写日志要拿全局 raw 自旋锁 logbuf_lock,所有 CPU 争抢;NMI 里拿不了 5.9 printk.c:390 DEFINE_RAW_SPINLOCK(logbuf_lock)
P2 NMI / 递归 printk 只能写 per-CPU safe 缓冲,再经 irq_work 回灌;缓冲满就丢、顺序乱 5.9 printk_safe.c:45-49, 181
P3 console_lock 是 BKL:既保护控制台链表与 flags,又串行所有打印 4dc64682ad37 说明:"we really prefer to split up and reduce the responsibilities of console_lock rather than expand its complexity"
P4 打印在调用者上下文、关中断进行,持锁者要替所有 CPU 打完积压 5.9 printk.c:2395 console_unlock()

方案与提交

版本 提交 做了什么 解决
5.10 896fbe20b4e2 printk: use the lockless ringbuffer 用 desc ring + data ring 的无锁多写者结构替换 log_buf(锁暂时保留) P1 前提
5.13 996e966640dd printk: remove logbuf_lock "Since the ringbuffer is lockless, there is no need for it to be protected by @logbuf_lock." P1
5.15 93d102f094be printk: remove safe buffers "Messages can be stored from any context, so there is no need for the NMI and safe buffers anymore." P2
5.19-rc1 09c5ba0aa2fc printk: add kthread console printers 每控制台一个线程,SYSTEM_RUNNING 后由线程打印 P4(尝试)
5.19-rc4 5831788afb17 Revert "missing synchronization between early and regular console functionality … console lock serialized console drivers between each other" 回退
6.2 4dc64682ad37 console_list_lock;6c4afa79147e SRCU 准备;d9a4af5690e2 改 hlist 控制台增删用 mutex,遍历用 SRCU P3(部分)
6.6 51a1d258e50e、eacb04ff3c5b、9e70a5e109a4、696ffaf50e1f 非 panic CPU 让出 console_lock;panic 刷出不拿 console_lock;每控制台 suspended 状态;统一延后打印 为 nbcon 铺路

架构图:之前 / 之后

之前(≤5.9):logbuf_lock + printk_safe + console_lock
之前(≤5.9):logbuf_lock + printk_safe + console_lock
之后(6.6):无锁 prb + SRCU 链表,打印仍归 console_lock
之后(6.6):无锁 prb + SRCU 链表,打印仍归 console_lock

调用栈

之前 · 5.9(行号为 v5.9):

printk() → vprintk_func()                                       printk_safe.c:362
  ├─ NMI 上下文:vprintk_nmi()                                   printk_safe.c:291
  │     printk_safe_log_store(this_cpu_ptr(&nmi_print_seq))      :69  → 写 per-CPU 缓冲
  │     queue_flush_work() → irq_work → __printk_safe_flush()    :53 / :181(之后再拿 logbuf_lock 回灌)
  └─ 普通上下文:vprintk_default() → vprintk_emit()             printk.c:1987
        logbuf_lock_irqsave(flags)                              :2009(raw_spinlock)
        vprintk_store() → log_output() → log_store()            :1937 / :616
        logbuf_unlock_irqrestore(flags)
        preempt_disable()
        console_trylock_spinning()                              :1745
        console_unlock()                                        :2395
          for (;;) {                                            :2436
            printk_safe_enter_irqsave(); raw_spin_lock(&logbuf_lock)    :2441-2442
            msg_print_text();console_seq++
            raw_spin_unlock(&logbuf_lock)
            console_lock_spinning_enable(); stop_critical_timings()
            call_console_drivers() → con->write()               :2501 → :1804
          }

5.19-rc1 的线程化尝试(行号为 v5.19-rc1,随后被回退):

vprintk_emit()                                                  printk.c
  vprintk_store()
  if (!in_sched && allow_direct_printing())                     :2385(SYSTEM_RUNNING 前或 printk_prefer_direct 时才直接打)
      console_trylock_spinning() → console_unlock()
  wake_up_klogd() → 唤醒 log_wait 上的打印线程

printk_kthread_func(con)                                        :3750(每控制台一个,printk_start_kthread() :3865 创建)
  wait_event_interruptible(log_wait, printer_should_wake())
  mutex_lock_interruptible(&con->lock)                          每控制台一把 mutex
  console_kthread_printing_tryenter()                           与 console_lock 持有者互斥的计数
  console_emit_next_record(con, ...) → con->write()             :3718

缺陷(回退说明原话):"console lock serialized console drivers between each other"。线程化以后,同一硬件的 boot console 与正式 console 不再被 console_lock 隐式串行,而当时没有替代机制。今天 nbcon 的规定是「只要还有 boot console,就不启动打印线程」(v7.3 printk.c:3828-3838),正是这个教训。

之后 · 6.6(行号为 v6.6):

vprintk_emit()                                                  printk.c:2266
  vprintk_store()                                               无锁:prb_reserve() / prb_final_commit()
  preempt_disable()                                             :2299
  console_trylock_spinning()                                    :1920 / :2306
  console_unlock()                                              :3012
    console_flush_all()                                         :2945
      for_each_console_srcu(con)                                SRCU 遍历(6.2 起)
        console_emit_next_record(con, &handover, cookie)        :2864
          printk_get_next_message()
          con->write(con, outbuf, len)                          :2910

register_console()                                              (v6.2 printk.c:3312)
  console_list_lock()                                           v6.2 :249(mutex)
  hlist_add_head_rcu() / hlist_add_behind_rcu()                 v6.2 :3391-3399
  console_list_unlock() → synchronize_srcu()

4+1 增量

视图 E1 带来的变化
场景 NMI 回溯不再丢失(S4);控制台注册 / 注销不再与打印互锁
逻辑 引入无锁 prb 与 per-console seq;控制台链表的读写分离
进程 写日志可在任意上下文完成;打印仍在调用者上下文
开发 printk_ringbuffer.c 独立成模块;printk_safe.c 只剩上下文标记;console_list_lock / SRCU API
物理 NMI 安全依赖 cmpxchg;ARM64 当时还没有 ARCH_SUPPORTS_RT

仍未解决:打印没有状态与优先级,panic 只能「试试看」;RT 上 con->write() 里的 spinlock_t 会睡眠。这就是 E2 的起点。