2. printk基础

printk 是 Linux 内核中最核心的日志输出函数,其地位相当于用户空间的 printf。在嵌入式内核调试中,printk 是“最后一道防线”——当 kgdb 无法附加、硬件断点失效时,精心布置的 printk 日志往往是定位问题的唯一手段。本节从函数原型出发,深入解析日志级别机制与控制台配置方法。

2.1 printk 函数原型

printk 定义在 include/linux/printk.h 中,其核心原型如下:

// 标准原型(可变参数)
int printk(const char *fmt, ...);

// 带日志级别的调用格式
printk(KERN_ERR "DMA transfer failed, status: 0x%x\n", status);

// 不带日志级别的调用(默认使用 KERN_WARNING)
printk("Hello from kernel init\n");

关键特性:

  • 可变参数:与 printf 完全一致的格式化语法,支持 %d、%x、%s、%p 等。
  • 日志级别前缀:通过 KERN_* 宏指定,宏本质是字符串常量,在编译时拼接在 fmt 之前。
  • 返回值:成功打印的字符数(不含 '\0'),负值表示错误。
  • 原子安全:可在中断上下文、自旋锁持有期间调用(但需注意避免递归锁)。
⚠ 常见陷阱: 忘记在格式串末尾添加 \n 会导致日志被缓冲区延迟刷新,在系统崩溃时可能丢失最后几条关键日志。

2.2 日志级别(KERN_EMERG ~ KERN_DEBUG)

Linux 内核定义了 8 个日志级别,数字越小优先级越高。每个级别对应一个字符串宏,使用时直接拼接在 printk 格式串开头。

宏名称 数值 字符串 含义 典型场景
KERN_EMERG 0 "<0>" 紧急事件,系统不可用 内核 panic、致命硬件故障
KERN_ALERT 1 "<1>" 必须立即处理 严重内存损坏、文件系统崩溃
KERN_CRIT 2 "<2>" 临界条件 驱动初始化失败、DMA 超时
KERN_ERR 3 "<3>" 错误条件 设备 I/O 错误、内存分配失败
KERN_WARNING 4 "<4>" 警告条件 降级模式、配置不匹配
KERN_NOTICE 5 "<5>" 正常但重要 模块加载/卸载、链路状态变化
KERN_INFO 6 "<6>" 信息性消息 驱动版本、硬件探测结果
KERN_DEBUG 7 "<7>" 调试信息 变量跟踪、函数进入/退出
💡 调试建议: 在开发阶段,所有调试日志统一使用 KERN_DEBUG;发布驱动时,将稳定后的日志降级为 KERN_INFO 或移除。这样可以通过控制台日志级别一键开关调试输出。

2.3 控制台日志级别配置

控制台日志级别决定了哪些级别的 printk 消息能够真正输出到串口或虚拟终端。内核通过 console_loglevel 变量进行过滤:只有日志级别数值 小于 当前控制台级别的消息才会被打印。

2.3.1 运行时配置方式

  • 通过 /proc/sys/kernel/printk:四个整数值,依次为:控制台级别、默认消息级别、最小控制台级别、默认控制台级别。
  • 通过 sysrq 触发:echo 8 > /proc/sysrq-trigger 将控制台级别设为 8(打印所有消息)。
  • 通过内核启动参数:loglevel=8 或 debug(等价于 loglevel=8)。
# 查看当前 printk 配置
cat /proc/sys/kernel/printk
# 输出示例:7       4       1       7
#         ^       ^       ^       ^
#         |       |       |       └─ 默认控制台级别
#         |       |       └────────── 最小控制台级别
#         |       └────────────────── 默认消息级别
#         └────────────────────────── 当前控制台级别

# 设置控制台级别为 8(打印所有日志)
echo "8 4 1 7" > /proc/sys/kernel/printk

# 仅修改控制台级别(其他保持默认)
echo 8 > /proc/sys/kernel/printk

2.3.2 内核启动参数速查

启动参数 效果 等价控制台级别
quiet 仅打印 KERN_EMERG ~ KERN_ERR 4
loglevel=4 打印 KERN_EMERG ~ KERN_WARNING 4
loglevel=7 打印 KERN_EMERG ~ KERN_DEBUG 7
debug 打印所有日志(含 KERN_DEBUG) 8
ignore_loglevel 忽略所有日志级别过滤 无条件打印
🔴 生产环境警告: 在量产设备上,切勿将控制台级别设为 8 或使用 ignore_loglevel。大量调试日志会严重拖慢系统响应,甚至导致串口缓冲区溢出和 watchdog 超时复位。

2.4 printk 日志过滤与输出流程

下图展示了 printk 从调用到最终输出的完整路径,以及日志级别在各个环节的过滤作用:

printk 日志输出流程与级别过滤 printk("KERN_ERR ...") 提取日志级别 (0~7) 级别 < console_loglevel ? 写入 __log_buf 环形缓冲区 调用 console->write() 串口 / 虚拟终端 dmesg / /proc/kmsg 读取 是(满足条件) 否(不满足条件) 💡 关键理解: 控制台级别只影响“是否立即输出到控制台设备”;所有日志(无论级别)都会写入环形缓冲区,可通过 dmesg 回溯查看。

2.5 实践:嵌入式调试中的 printk 配置策略

在嵌入式系统调试中,建议按以下步骤配置 printk 环境:

  1. 开发阶段:在 bootargs 中添加 loglevel=8 ignore_loglevel,确保所有调试信息都能输出。
  2. 集成测试阶段:使用 loglevel=7,保留 KERN_DEBUG 但关闭无条件打印。
  3. 发布阶段:使用 loglevel=4 quiet,仅输出错误及以上级别,减少日志对实时性的影响。
  4. 现场问题复现:通过 sysrq 动态提升控制台级别,无需重启。
// 典型调试宏封装(头文件)
#define DBG_ERR(fmt, ...)  printk(KERN_ERR  "[DBG] " fmt, ##__VA_ARGS__)
#define DBG_WARN(fmt, ...) printk(KERN_WARNING "[DBG] " fmt, ##__VA_ARGS__)
#define DBG_INFO(fmt, ...) printk(KERN_INFO  "[DBG] " fmt, ##__VA_ARGS__)
#define DBG_TRACE(fmt, ...) printk(KERN_DEBUG "[DBG] " fmt, ##__VA_ARGS__)
🎯 调试技巧: 在驱动入口和出口处添加 DBG_TRACE("enter %s\n", __func__),配合 loglevel=8 可以快速定位驱动初始化/卸载的完整执行路径,无需单步调试。