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(行号为 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 的起点。