09 · printk:调用者不再亲自刷串口
结论:非 RT 内核里
printk()的调用者只要拿到console_lock就会在自己的上下文(可能是 hardirq、可能持着 raw 锁)把所有积压日志同步写到串口——115200 波特率下一行 100 字符约 9 ms,一次内核日志风暴能把系统最坏延迟拉到几十 ms,这就是 printk 被称为「RT 最后一块拼图」的原因。6.12 的 RT 内核把force_legacy_kthread()固定为 true:所有 legacy 控制台在pr/legacy内核线程里输出;已转换为 nbcon 的控制台各有一个pr/<name><idx>线程;只有 emergency/panic 走write_atomic()直接输出。调用者的成本降到「写 ring buffer」,代价是日志到达串口的时刻不再确定。
第一层
先看结论:printk 调用者不再亲自刷串口,legacy 与 nbcon 控制台各有专用线程输出,日志风暴不再制造几十 ms 的抖动。
第二层
先看结论:RT 侧只有两处 ifdef——
force_legacy_kthread()恒真、spinlock 允许函数为空;重活在 nbcon 的所有权状态机里。
RT 的两处开关
kernel/printk/ 里 CONFIG_PREEMPT_RT 只出现在两处:
kernel/printk/internal.h:24-35:
/*
* Identify if legacy printing is forced in a dedicated kthread. If true,
* all printing via console lock occurs within a dedicated legacy printer
* thread. The only exception is on panic, ...
*/
#ifdef CONFIG_PREEMPT_RT
# define force_legacy_kthread() (true)
#else
# define force_legacy_kthread() (false)
#endif
kernel/printk/printk.c:2984-3009:非 RT 下调用者上下文打印会「不尊重」raw_spinlock/spinlock 嵌套规则,需要printk_legacy_allow_spinlock_enter()压掉 lockdep 误报;RT 下调用者不打印,这两个函数是空的。
legacy 线程
printk.c:3596-3617 legacy_kthread_create():kthread_run(legacy_kthread_func, NULL, "pr/legacy"),sched_set_normal(…, -20)——SCHED_NORMAL nice -20,不是 FIFO。printk_kthreads_check_locked()(:3658-3687)在 RT 上必须创建成功,否则注销所有 legacy 控制台(:3677-3682)。
nbcon(6.12.107 已有线程框架)
nbcon.c由 Linutronix(John Ogness)与 Intel(Thomas Gleixner)贡献,是从 RT 补丁集上游化的「下一代控制台」。- 每个 nbcon 控制台一个线程:
nbcon_kthread_create()(:1326-1350)kthread_run(nbcon_kthread_func, con, "pr/%s%d", con->name, con->index),同样sched_set_normal(-20),注释说打印线程需要「在 printk 后很快被调度且有充裕的运行预算」。 - 回调(
include/linux/console.h:355-393):write_atomic()可在任意上下文(含 NMI)调用,不用锁,靠所有权状态字同步;write_thread()只在任务上下文、持device_lock()与NBCON_PRIO_NORMAL所有权时调用。 - 所有权优先级 NORMAL < EMERGENCY < PANIC:高优先级上下文可以接管(handover)甚至强夺(hostile takeover)正在打印的低优先级上下文。panic 时
__nbcon_atomic_flush_pending_con()(:1496)直接刷。 - 6.12.107 里没有任何串口驱动完成 nbcon 转换:
grep -rln 'write_atomic\s*=' drivers/为空;arm64 常用的 pl011(drivers/tty/serial/amba-pl011.c:2504)与 8250(drivers/tty/serial/8250/8250_core.c:502)都还是CON_PRINTBUFFER | CON_ANYTIME的 legacy 控制台。include/linux/serial_core.h:632-654的__uart_port_using_nbcon()对它们恒返回 false,uart_port_lock_irqsave()(:709-713)里的 nbcon 所有权获取路径编译进去了但永远不走。 - 对 arm64 RT 的实际含义:printk 调用者已经不被阻塞(
pr/legacy线程兜底),但 nbcon 的两个好处——每控制台独立线程、panic/emergency 时的write_atomic()原子输出——在 6.12.107 的 pl011/8250 上都拿不到;panic 时最后几行依赖 legacy 路径能否在崩溃前刷出。
printk_deferred
printk.c:4547-4550:vprintk_deferred() 以 LOGLEVEL_SCHED 写入,通过 irq_work 触发输出(:4528-4540)。它一直存在,但绝大多数调用点用的是普通 printk(),所以在非 RT 上不能指望它兜底。
性能影响
净影响:printk 成本降到 µs 以下;日志到达变晚、crash 前几行可能丢。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
printk() 调用成本 |
写 ring buffer + 可能刷完全部积压(ms – 数百 ms) | 写 ring buffer + 唤醒(< 1 µs 典型) | 核心收益 |
| 日志风暴对最坏延迟的影响 | 直接计入(若发生在 hardirq/关抢占区) | 无(线程是普通优先级) | |
| 日志到达串口的延迟 | 同步 | 取决于线程何时被调度:高 RT 负载下可达秒级 | 调试体验变差 |
| crash 前最后几行 | 通常能刷出 | 依赖 panic 的 atomic flush(nbcon);legacy 控制台可能丢 | |
console_lock 争用 |
打印者交接自旋 | 只在线程内 |
源码定位
| 主题 | 位置 |
|---|---|
force_legacy_kthread() |
kernel/printk/internal.h:24-35 |
| RT 下空的 spinlock 允许函数 | kernel/printk/printk.c:2984-3009 |
| legacy 线程 | kernel/printk/printk.c:3531, 3596-3617, 3658-3687 |
| nbcon 线程 | kernel/printk/nbcon.c:1157, 1181-1186, 1271, 1299, 1326-1350 |
| atomic 输出 | kernel/printk/nbcon.c:1045, 1496;include/linux/console.h:355-393 |
| arm64 串口驱动状态 | drivers/tty/serial/amba-pl011.c:2504;drivers/tty/serial/8250/8250_core.c:502;include/linux/serial_core.h:596-713 |
printk_deferred |
include/linux/printk.h:174-175, 497-498;kernel/printk/printk.c:4455-4456, 4528-4550 |
调优要点
- 排查 RT 抖动时用
trace_printk()/ftrace,不要用printk():前者只写 trace ring buffer。 - 生产 RT 系统降低
console_loglevel(kernel.printk),减少串口输出量;串口越慢(9600)越要这么做。 - arm64 板子上 pl011/8250 在 6.12.107 都是 legacy 控制台:要 nbcon 的 panic 原子输出,需要 6.13+(8250/pl011 的 nbcon 转换在后续版本合入)。
- 如果需要「日志一定要及时到串口」(如飞控黑匣子),接受它与延迟目标冲突,或把控制台换成 nbcon 驱动 + 更高的
pr/*线程优先级(chrt)。
小结
pr/legacy与pr/<con>线程都是 SCHED_NORMAL nice -20,不会与 RT 任务抢 CPU。- nbcon 的
write_atomic()保证 panic 时仍能输出;未转换的串口驱动走 legacy 线程。 - 调试 RT 抖动用 ftrace /
trace_printk,不要用printk。 - 承上启下:以上 9 章是机制;第 10 章从 Kconfig 角度画出 RT 内核的能力边界。