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 从调用到最终输出的完整路径,以及日志级别在各个环节的过滤作用:
2.5 实践:嵌入式调试中的 printk 配置策略
在嵌入式系统调试中,建议按以下步骤配置 printk 环境:
- 开发阶段:在 bootargs 中添加
loglevel=8 ignore_loglevel,确保所有调试信息都能输出。 - 集成测试阶段:使用
loglevel=7,保留 KERN_DEBUG 但关闭无条件打印。 - 发布阶段:使用
loglevel=4 quiet,仅输出错误及以上级别,减少日志对实时性的影响。 - 现场问题复现:通过 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 可以快速定位驱动初始化/卸载的完整执行路径,无需单步调试。