03 · 进程视图:上下文、线程与交接时序
结论:nbcon 把「谁来碰串口」分给四类执行体:打印线程(每控制台一个
pr/<name><idx>,SCHED_OTHERnice -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 时会出现关中断的原子输出窗口。
执行体与上下文
| 执行体 | 何时创建 / 由谁唤醒 | 调度属性 | 处理什么 | 源码(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 打断正在打印的线程
两侧调用栈(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
三个容易忽略的细节:
- handover 不跨同一颗 CPU:
try_acquire_handover()遇到cur->cpu == cpu直接返回 -EBUSY(nbcon.c:461),否则就是自己等自己。同 CPU 嵌套(中断里 WARN 打断本核线程)只能在安全点 direct 抢占;如果正好在 unsafe 区,emergency 这一次就放弃,由线程稍后输出。 - 等待有上限:原子刷出把
spinwait_max_us设为 2000(nbcon.c:1571)。超时后撤销req_prio;只有 PANIC 且允许 unsafe 时才升级为 hostile。 - 重印而不是续打:被抢的那条记录由新所有者从头重印,可能出现半行加一整行的重复,这是有意的取舍(
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 延迟的含义
- 调用者侧:NORMAL printk 的成本是一次 prb 写入,加上一次 irq_work 排队(LAZY)。在 nohz_full 核上,tick 已停时会补发一次自 IPI,并唤醒本核
irq_work/N。 - 打印线程侧:
pr/*是 nice -20 的普通线程,任何 RT 任务都能抢占它。RT 负载占满 housekeeping 核时,日志会无限滞后,但不会反过来影响 RT 任务。需要日志及时时,可以把pr/*绑到专门的 housekeeping 核,或者用chrt提到低 RT 优先级,前提是接受它反过来干扰同核 RT 任务。 - emergency 侧:WARN 发生的那颗 CPU 会关中断原子输出。6.12.113 是整段关中断(所有积压一口气刷完);6.19 起改为逐条恢复中断,最坏关中断时间从「全部积压」降到「一条记录」。在 115200 波特率下,一条 100 字符的记录约 9 ms。
- tty 侧:
uart_port_lock()现在多一次nbcon_device_try_acquire()cmpxchg;遇到 emergency 正在原子输出,会cpu_relax()自旋等待(serial_core.h:684)。