kernel/printk 内核日志(printk)机制与原理详解

kernel/printk 内核日志(printk)机制与原理详解

源码路径rk3588/kernel-6.1/kernel/printk/
内核版本:Linux 6.1(RK3588 平台)
平台:RK3588(4×Cortex-A76 + 4×Cortex-A55 big.LITTLE,ARM64)

该目录实现 Linux 内核 日志输出核心printk() / pr_info() 等宏的底层存储、环形缓冲区、控制台驱动调度、用户态读取接口(/dev/kmsgsyslog),以及 NMI/递归安全路径。Linux 6.1 采用 无锁 printk ringbuffer(prb) 替代早期单一 log_buf 方案,支持多读者、多写者并发。

定位:内核诊断与调试的「消息总线」;串口/UART 控制台驱动在 drivers/tty/serial/,通过 register_console() 接入本框架。


目录


一、源码目录结构

1.1 编译依赖(Makefile)

1
2
3
4
5
6
7
8
9
obj-y += printk.o

obj-$(CONFIG_PRINTK) += printk_safe.o
obj-$(CONFIG_PRINTK) += printk_support.o
printk_support-y := printk_ringbuffer.o
printk_support-$(CONFIG_SYSCTL) += sysctl.o

obj-$(CONFIG_A11Y_BRAILLE_CONSOLE) += braille.o
obj-$(CONFIG_PRINTK_INDEX) += index.o
配置项 说明
CONFIG_PRINTK 启用 printk(默认 y)
CONFIG_LOG_BUF_SHIFT log buffer 大小 = 1 << shift(默认 17 = 128KB)
CONFIG_PRINTK_TIME 日志带时间戳
CONFIG_PRINTK_TIME_FROM_ARM_ARCH_TIMER ARM 架构定时器时间源(RK 非 GKI)
CONFIG_PRINTK_CALLER 显示 caller pid/cpu
CONFIG_PRINTK_INDEX debugfs 导出 printk 格式索引
CONFIG_PRINTK_SAFE_LOG_BUF_SHIFT safe 模式备用 buffer
CONFIG_CONSOLE_LOGLEVEL_DEFAULT 控制台默认 loglevel
CONFIG_A11Y_BRAILLE_CONSOLE 盲文控制台

Kconfig 分散在 init/Kconfiglib/Kconfig.debug

1.2 源文件

文件 行数 功能
printk.c 4053 核心:存储、console、syslog、devkmsg、kmsg_dump
printk_ringbuffer.c 2124 无锁 ringbuffer 实现
printk_ringbuffer.h 384 prb 数据结构与 API
index.c 194 debugfs printk 格式索引
sysctl.c 85 /proc/sys/kernel/printk*
internal.h 60 内部 API、printk_safe 宏
printk_safe.c 47 NMI/递归上下文 vprintk 路由
braille.c 58 盲文控制台
console_cmdline.h 16 命令行 console 解析

相关头文件(目录外)

文件 功能
include/linux/printk.h printk() 宏、pr_* 系列
include/linux/console.h struct console、register_console
include/linux/kmsg_dump.h panic/oops 时 kmsg 转储
drivers/tty/serial/ UART 控制台(如 8250、DesignWare)

二、整体架构

2.1 数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
printk() / pr_info() / dev_printk()


vprintk() ──► [NMI/recursion?] ──► vprintk_deferred()
│ │
└─ vprintk_default() │ (仅写 ringbuffer)
│ │
▼ │
vprintk_emit() │
│ │
├── vprintk_store() ────┘ → prb_reserve / prb_commit
│ (写入 ringbuffer)

├── console_trylock_spinning()
│ console_unlock() → 各 console->write()

└── wake_up_klogd() → /dev/kmsg 读者

2.2 双阶段设计

阶段 说明
存储 所有消息写入 printk ringbuffer,带 seq、timestamp、level
输出 持有 console_sem 的线程从 prb 读取并调用 console->write()

存储与输出解耦:在 IRQ/NMI 中可安全写 buffer,控制台驱动稍后在进程上下文刷出。

2.3 启动阶段 buffer

1
2
3
4
早期: __log_buf[1 << CONFIG_LOG_BUF_SHIFT]  + printk_rb_static
setup_log_buf(early=1) — 启动极早阶段
后期: setup_log_buf(early=0) — 分配动态 prb,迁移 static 记录
prb = &printk_rb_dynamic

三、printk Ringbuffer(printk_ringbuffer.c)

3.1 结构

1
2
3
4
5
struct printk_ringbuffer {
struct prb_desc_ring desc_ring; /* 描述符 + printk_info 元数据 */
struct prb_data_ring text_data_ring; /* 文本数据环 */
atomic_long_t fail;
};

三个逻辑环

  1. desc_ringprb_desc(state_var + text_blk_lpos)+ printk_info(seq、ts、level、caller_id)
  2. text_data_ring:变长文本块,每块 [ID | padding | text]

3.2 描述符状态机

状态 含义
desc_reserved 写者正在写入
desc_committed 数据一致,写者可 reopen
desc_finalized 不可修改,读者可见
desc_reusable 已回收,可复用
desc_miss 读者查询 ID 不匹配(伪状态)

state_varID 与 state 打包atomic_long_t,实现无锁 CAS 更新。

3.3 写者 API

1
2
3
4
5
prb_rec_init_wr(&r, reserve_size);
prb_reserve(&e, prb, &r); /* 分配描述符+文本空间 */
/* 填充 r.text_buf, r.info */
prb_commit(&e); /* 提交,可 continuation */
prb_final_commit(&e); /* 提交并 finalize(带换行) */

LOG_CONT 续行prb_reserve_in_last() 追加到上一条未 finalize 的记录。

3.4 读者 API

1
2
3
prb_rec_init_rd(&r, &info, buf, size);
prb_read_valid(prb, seq, &r); /* 按 seq 读一条 */
prb_first_seq() / prb_next_seq() /* 迭代 */

四、日志写入路径(printk.c)

4.1 vprintk_store()

核心存储函数:

1
2
3
4
5
6
7
1. printk_enter_irqsave() — 递归计数 ≤ PRINTK_MAX_RECURSION(3)
2. ts_nsec = get_local_clock() — ARM 可用 arch_timer
3. vsnprintf 预估长度 → prb_reserve
4. printk_parse_prefix() — 解析 "<6>" level 与 "c" continuation
5. printk_sprint() — 格式化正文(syslog 前缀、device 信息等)
6. prb_final_commit() 或 prb_commit()
7. printk_exit_irqrestore()

4.2 vprintk_emit()

1
2
3
4
5
6
7
8
9
10
if (suppress_printk) return 0;
printed_len = vprintk_store(...);

if (!in_sched) {
preempt_disable();
if (console_trylock_spinning())
console_unlock(); /* 立即尝试刷 console */
preempt_enable();
wake_up_klogd();
}

LOGLEVEL_SCHED 路径 defer 控制台输出,避免调度器内死锁。

4.3 printk_parse_prefix()

解析内核日志前缀:

前缀 含义
<0><7> syslog level
<c> / <N c> continuation(续行)
KERN_* 编译期嵌入 level

4.4 console_printk[]

1
2
3
4
5
6
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel — 控制台可见最高 level */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN,
CONSOLE_LOGLEVEL_DEFAULT,
};

/proc/sys/kernel/printk 四个整数:console_logleveldefault_message_loglevelminimum_console_logleveldefault_console_loglevel

4.5 递归保护

1
2
3
4
5
#define PRINTK_MAX_RECURSION 3
static DEFINE_PER_CPU(u8, printk_count);
#ifdef CONFIG_HAVE_NMI
static DEFINE_PER_CPU(u8, printk_count_nmi);
#endif

NMI 与普通上下文 分开计数,防止 printk 触发 WARN 再次 printk 无限递归。


五、控制台输出(console)

5.1 struct console

1
2
3
4
5
6
7
8
9
10
struct console {
char name[16];
void (*write)(struct console *, const char *, unsigned);
int (*read)(struct console *, char *, unsigned);
struct device *dev;
u32 seq; /* 该 console 已输出到的 prb seq */
unsigned long dropped;
short flags;
/* CON_PRINTBUFFER, CON_BOOT, CON_CONSDEV, CON_BRL ... */
};

5.2 register_console()

1
2
3
4
5
6
1. 匹配 cmdline / DT / SPCR preferred console
2. 插入 console_drivers 链表(CON_CONSDEV 在头)
3. 若 CON_PRINTBUFFER:从 syslog_seq 重放历史
4. 否则:seq = prb_next_seq(prb)(仅新消息)
5. console_unlock() 刷出
6. 真实 console 注册后 unregister bootconsole

RK3588 典型路径:earlycon=uart8250,... → boot console → ttyS* 驱动 register_console()

5.3 console_lock 与 spinning

传统 console_sem 串行化控制台访问。优化路径:

1
2
3
4
5
console_trylock_spinning()
→ 若当前持有者 same CPU 且可 schedule
→ console_waiter = 1,自旋等待 handover
console_lock_spinning_enable/disable()
→ 减少 printk 持锁期间阻塞其他 CPU

5.4 console_emit_next_record()

1
2
3
4
5
prb_read_valid(prb, con->seq, &r)
suppress_message_printing(level) → 高于 console_loglevel 则 skip
record_print_text() / info_print_ext_header()
call_console_driver(con, text, len)
con->seq++

5.5 特殊场景

场景 行为
oops_in_progress 放宽 console 锁检查
suppress_panic_printk panic 时非 panic CPU 抑制 printk
suspend_console() 睡眠前停止 console 输出
console_flush_on_panic() panic 时强制刷所有 console

六、printk-safe 与递归保护

6.1 printk_safe.c

1
2
3
4
5
6
7
asmlinkage int vprintk(const char *fmt, va_list args)
{
if (this_cpu_read(printk_context) || in_nmi())
return vprintk_deferred(fmt, args); /* 只写 buffer,不调 console */

return vprintk_default(fmt, args);
}

printk_safe_enter_irqsave() / __printk_safe_enter() 递增 per-CPU printk_context,标记「安全模式」—— 控制台驱动可能持锁,不可再调 console。

6.2 vprintk_deferred

仅调用 vprintk_store(),不触发 console_unlock();由 defer_console_output() 或后续 printk 刷出。


七、用户态接口

7.1 /dev/kmsg

字符设备(devkmsg_read/write):

  • :按 seq 读 ringbuffer 记录(含 facility、level、timestamp)
  • :用户态注入 kernel 日志(需 SYSLOG capability)
  • poll:新消息唤醒

devkmsg_logon / off / ratelimitprintk.devkmsg= 内核参数可永久锁定)

7.2 syslog 系统调用

1
int do_syslog(int type, char __user *buf, int len, int source)
type 功能
SYSLOG_ACTION_READ 读 ringbuffer
SYSLOG_ACTION_READ_ALL 读全部(含已读)
SYSLOG_ACTION_READ_CLEAR 读并清除
SYSLOG_ACTION_CLEAR 清除
SYSLOG_ACTION_SIZE_UNREAD 未读字节数

dmesg_restrict:限制非特权用户读 kernel 日志。

7.3 kmsg_dump

1
2
kmsg_dump_register(struct kmsg_dumper *dumper);
kmsg_dump(KMSG_DUMP_OOPS / PANIC / ...);

panic/oops 时 pstore、mtdoops、ramoops 等通过 kmsg_dump_get_line() 导出 ringbuffer。


八、Sysctl 与调试(sysctl.c / index.c)

8.1 /proc/sys/kernel/

节点 说明
printk 四个 loglevel 整数
printk_ratelimit 速率限制间隔
printk_ratelimit_burst burst 计数
printk_delay 每条 printk 延迟 ms(调试)
printk_devkmsg on/off/ratelimit
dmesg_restrict 限制 dmesg 访问
kptr_restrict %pK 指针隐藏级别

8.2 PRINTK_INDEX(index.c)

debugfs:/sys/kernel/debug/printk/index/vmlinux(及模块名)

导出编译期 .printk_index 段:

1
2
# <level/flags> filename:line function "format"
<6> sched/core.c:1234 schedule "scheduling..."

便于日志分析与静态格式检索。

8.3 其他调试

机制 说明
ignore_loglevel 忽略 console_loglevel(cmdline)
pm_test 分阶段 suspend 测试(power 子系统)
suspend_stats debugfs 与 power 共享统计思路
printk_time 控制台/syslog 显示时间戳

九、RK3588/ARM64 平台说明

9.1 时间戳源

1
2
3
4
5
6
7
8
9
#ifdef CONFIG_PRINTK_TIME_FROM_ARM_ARCH_TIMER
static u64 get_local_clock(void)
{
/* 使用 ARM architected timer (CNTVCT_EL0 / arch_timer_read_counter) */
return ns_from_counter(arch_timer_read_counter(), cntfrq);
}
#else
static inline u64 get_local_clock(void) { return local_clock(); }
#endif

CONFIG_PRINTK_TIME_FROM_ARM_ARCH_TIMER 依赖 ARM_ARCH_TIMER && NO_GKI,RK 非 GKI 内核可选,使 printk 时间与 arch timer 一致(boot 阶段更稳定)。

9.2 控制台与 earlycon

RK3588 常用配置:

1
2
3
console=ttyFIQ0,1500000
earlycon=uart8250,mmio32,0xfeb50000
loglevel=8
组件 路径
FIQ debugger / early console drivers/tty/serial/rockchip_fiq_debugger.c
8250/DesignWare UART drivers/tty/serial/8250/
register_console() 驱动 probe 后注册

9.3 log buffer 大小

默认 CONFIG_LOG_BUF_SHIFT=17(128KB)。RK3588 嵌入式若日志量大,可增大至 18–20(256KB–1MB),通过 log_buf_len= 或 menuconfig。

9.4 多核与 big.LITTLE

  • caller_id:task 为 pid;非 task 上下文为 0x80000000 + cpu_id
  • per-CPU printk_count:各 CPU 独立递归计数
  • console spinning handover:减少 big.LITTLE 上高并发 printk 时的锁竞争
  • panic 抑制suppress_panic_printk 防止多核同时 printk 拖慢 panic 处理

9.5 常用调试命令

1
2
3
4
5
6
7
8
9
10
11
# 查看内核 ringbuffer
dmesg -w
dmesg -T # 人类可读时间(用户态转换)

# 实时跟踪
cat /dev/kmsg

# 调整控制台级别(立即生效)
echo 8 > /proc/sys/kernel/printk # 仅第一个整数

# 忽略 level 刷全部(需 cmdline ignore_loglevel 或 sysctl)

十、完整 printk 时序

console->writeconsole_unlockprintk_ringbuffervprintk_storevprintk_emit调用者console->writeconsole_unlockprintk_ringbuffervprintk_storevprintk_emit调用者pr_info("msg")vprintk_store()prb_reserve + prb_final_commitconsole_trylock_spinning()prb_read_valid(con->seq)con->write(text)wake_up_klogd()

NMI 路径vprintkvprintk_deferred → 仅 vprintk_store,不进入 Console 分支;稍后由普通 printk 或 defer_console_output() 刷出。


十一、总结

组件 职责
printk_ringbuffer.c 无锁 prb:reserve/commit/read,支持 continuation
printk.c 格式化、存储、console 调度、syslog/devkmsg、kmsg_dump
printk_safe.c NMI/递归上下文路由到 deferred 路径
sysctl.c loglevel、ratelimit、dmesg_restrict 等 sysctl
index.c debugfs 导出编译期 printk 格式

Linux 6.1 printk 以 ringbuffer 为中心,将「快速存储」与「慢速 console 输出」分离,在 SMP、NMI、panic 场景下保持可预测行为。RK3588 平台通过 UART/earlycon 接入 register_console(),可选 ARM arch timer 作为时间戳源。


附录:源文件清单

文件 行数
kernel/printk/printk.c 4053
kernel/printk/printk_ringbuffer.c 2124
kernel/printk/printk_ringbuffer.h 384
kernel/printk/index.c 194
kernel/printk/sysctl.c 85
kernel/printk/internal.h 60
kernel/printk/braille.c 58
kernel/printk/braille.h 56
kernel/printk/printk_safe.c 47
kernel/printk/console_cmdline.h 16
kernel/printk/Makefile 10
合计 ~7077

关联源码(目录外)

文件 说明
include/linux/printk.h 公共 printk 宏
include/linux/console.h console 驱动接口
lib/Kconfig.debug PRINTK_TIME 等选项
init/Kconfig LOG_BUF_SHIFT、PRINTK
drivers/tty/serial/*.c UART 控制台驱动

文章互动

阅读 --

留言

0 条留言

正在加载留言…