02 · 逻辑视图:对象、优先级与所有权状态机
结论:nbcon 的全部同步语义都压在每个控制台的一个 32 位原子字
nbcon_state里,字段为prio:2 | req_prio:2 | unsafe:1 | unsafe_takeover:1 | cpu:24。获取所有权只有三种方式:direct(无主,或对方优先级更低且处于安全点)、handover(对方在 unsafe 区:写req_prio后自旋等它让出,最长spinwait_max_us,原子刷出时为 2000 µs)、hostile(仅 panic 末尾,并永久标记unsafe_takeover)。优先级只有三级:NORMAL < EMERGENCY < PANIC,由「当前 CPU 是否在 panic / emergency 段」决定,而不是由调用者传入。丢失所有权的一方必须立即退出,接管者从头重印被打断的那条记录。这套语义从 6.7 引入至今没有改过,后续演进都是在它上面调整策略。
对象模型
| 对象 | 定义(v7.3) | 生命周期 | 作用 |
|---|---|---|---|
struct console 的 nbcon 部分 |
include/linux/console.h:464-472 |
register_console() → nbcon_alloc()(nbcon.c:1770)到 nbcon_free()(:1833) |
每控制台的状态字、打印进度、私有缓冲、线程、唤醒对象 |
struct nbcon_state |
console.h:215(static_assert ≤ int) |
只通过 nbcon_state_try_cmpxchg()(nbcon.c:156)修改 |
所有权的唯一事实来源 |
enum nbcon_prio |
console.h:248 |
— | NONE / NORMAL / EMERGENCY / PANIC |
struct nbcon_context |
console.h:272 |
调用方栈上,或 con->nbcon_device_ctxt(给 tty 侧用) |
一次获取 / 打印尝试的参数与结果 |
struct nbcon_write_context |
console.h:297 |
包着 nbcon_context,交给驱动回调 |
outbuf/len 是本次要写的文本;7.0 起带 cpu/pid/comm(CONFIG_PRINTK_EXECUTION_CTX) |
struct console_flush_type |
kernel/printk/internal.h:181 |
每次 printk 现算 | 本 CPU 此刻该走哪几条刷出路径 |
printk_buffers |
internal.h:271 |
每控制台一份;panic CPU 用 panic_nbcon_pbufs |
格式化缓冲:outbuf 2048 + scratchbuf 1024 |
进度与 ringbuffer 的关系:每个 nbcon 控制台有自己的 nbcon_seq(下一条要打印的记录号,nbcon_seq_read() nbcon.c:168),在 32 位机上只存低 32 位,再由 prb 补全。nbcon_seq_try_update()(:207)用 cmpxchg 推进,失败说明期间被别人接管或被 nbcon_seq_force() 重置。nbcon_alloc() 把初值设为 ULSEQ_MAX(:1794,6.12 的 fb9fabf3d862),注册流程再显式设定起点,避免「什么也没设就开始重放」。
被接管后的重印:nbcon_prev_seq 记住上一任所有者被分配到的记录号。新所有者在 nbcon_emit_next_record()(nbcon.c:980)里发现同一条记录被分配过两次,就调用 console_prepend_replay() 加上 ** replaying previous printk message ** 前缀(6.12 的 5102981d5e2a)。
优先级怎么来
nbcon_get_default_prio() nbcon.c:1442
panic_on_this_cpu() → NBCON_PRIO_PANIC
*cpu_emergency_nesting > 0 → NBCON_PRIO_EMERGENCY (per-CPU 计数,nbcon.c:1402)
否则 → NBCON_PRIO_NORMAL
- emergency 是per-CPU状态:CPU5 在 emergency 段里,CPU2 上的 printk 仍是 NORMAL(
ecb5e1aa82c8原话:"the emergency state is not system-wide")。6.19 增加了全局计数nbcon_cpu_emergency_cnt(nbcon.c:123),只用来阻塞打印线程,不改变别的 CPU 的优先级。 - 调用者不能自选优先级。driver 侧 API 只有 NORMAL(
nbcon_device_try_acquire():1882);KDB 有专用入口(nbcon_kdb_try_acquire():1965,6.19)。
所有权状态机
nbcon_context_try_acquire(ctxt, is_reacquire) nbcon.c:598
nbcon_state_read(con, &cur)
try_again:
err = nbcon_context_try_acquire_direct(ctxt, &cur, is_reacquire) :243
panic_on_other_cpu() 且非 reacquire → -EPERM
ctxt->prio <= cur.prio 或 <= cur.req_prio → -EPERM
cur.unsafe → -EBUSY(转 handover)
cmpxchg {prio=ctxt->prio, req_prio=0, cpu=this}
if (err == -EBUSY)
err = nbcon_context_try_acquire_handover(ctxt, &cur) :434
同 CPU / 已 unsafe_takeover / spinwait_max_us == 0 → -EBUSY
cmpxchg {req_prio = ctxt->prio}(失败 → -EAGAIN → try_again)
for (timeout = spinwait_max_us; timeout >= 0; timeout--)
nbcon_context_try_acquire_requested() :351
udelay(1)
超时:小心撤销 req_prio
if (err == -EBUSY)
err = nbcon_context_try_acquire_hostile(ctxt, &cur) :550
仅 allow_unsafe_takeover 且 prio == PANIC
cmpxchg {prio, cpu, unsafe |= ..., unsafe_takeover |= cur.unsafe}
成功:ctxt->pbufs = panic CPU ? &panic_nbcon_pbufs : con->pbufs
ctxt->seq = nbcon_seq_read(con)
所有者这一侧的义务:
- 进出 unsafe 区(
nbcon_enter_unsafe():885/nbcon_exit_unsafe():909),都经过__nbcon_context_update_unsafe()(:832)→nbcon_context_can_proceed()(:730)。 can_proceed()发现有等待者(req_prio≠ 0)且自己处于安全点时,就主动 release 并返回 false(:754-772),注释原话是 "Having a safe point for take over and eventually a few duplicated characters or a full line is way better than a hostile takeover"。- 返回 false 后,
wctxt->outbuf被清空(nbcon_write_context_set_buf(wctxt, NULL, 0)),驱动必须立即退出;如果需要复原硬件(例如 UART 的 CR / IER),用nbcon_reacquire_nobuf()(:938)以原优先级自旋拿回所有权,但不能续打。
两条不变式:
nbcon_owner_matches()(:635)只比较prio与cpu。所以 NORMAL 所有者如果允许被抢占或迁移,就会有 ABA 风险,注释(:653-661)给了完整反例。这就是device_lock()必须禁迁移、线程路径必须cant_migrate()的原因。- 发生过 hostile 之后,
unsafe_takeover会一直保持置位,控制台被视为永久不安全,直到重新初始化;之后的 direct 获取会触发WARN_ON_ONCE(:280)。
选路:console_flush_type
printk_get_console_flush_type(&ft) internal.h:194
NORMAL: nbcon 且无 boot console → printk_kthreads_running && !console_irqwork_blocked
? nbcon_offload : nbcon_atomic
legacy 或 boot console → is_printk_legacy_deferred() ? legacy_offload : legacy_direct
EMERGENCY: nbcon 且无 boot console → nbcon_atomic
legacy → 同上(direct / offload)
PANIC: nbcon 且无 boot console → nbcon_atomic
legacy → 未 legacy_allow_panic_sync 前保持沉默;panic 时不 offload
is_printk_legacy_deferred()(printk_safe.c:59)在 force_legacy_kthread()(RT 上为真,internal.h:31)、printk_deferred 上下文、NMI 或持有 printk_cpu_sync 时返回真。所以 RT 上 legacy 控制台永远走 offload,由 pr/legacy 打印,这是 E3 的核心设计。
与 6.12.113 的差异
| 项目 | 6.12.113 | 7.3 |
|---|---|---|
CON_NBCON_ATOMIC_UNSAFE |
无 | 有(6.19 187de7c212e5) |
| 全局 emergency 计数 / 阻塞线程 | 无 | 有(6.19 c41c0ebfa1e0) |
| 原子刷出粒度 | 整段持所有权、整段关中断(6.12.113 nbcon.c:1496-1586) |
逐条 acquire / release、逐条恢复中断 |
| panic 中 reacquire | 有(已回合,is_reacquire 参数) |
有 |
| irq_work | IRQ_WORK_INIT_LAZY(已回合,6.12.113 nbcon.c:1698) |
同 |
console_irqwork_blocked(suspend) |
无 | 有(6.19) |
nbcon_write_context 执行上下文字段 |
无 | 有(7.0) |