3. printk 日志级别详解:8个日志级别含义、使用场景与选择策略
printk 是内核调试最基础、最直接的输出手段。但很多开发者只会在代码里随手写一句 printk("xxx\n"),结果要么控制台刷屏,要么关键信息被淹没。要真正用好 printk,必须先理解它的日志级别(Log Level)机制。
日志级别决定了这条消息的紧急程度,以及它最终会出现在哪个输出目标(控制台、dmesg、日志文件)。Linux 内核定义了 8 个级别,从 KERN_EMERG(0)到 KERN_DEBUG(7),数字越小越紧急。
💡 核心原则: 日志级别不是“重要性排名”,而是“输出过滤优先级”。高紧急度的消息几乎一定会显示,低紧急度的消息可能被内核参数或用户空间工具过滤掉。
3.1 8个日志级别速查表
| 级别名称 | 数值 | 宏定义 | 含义 | 典型使用场景 |
|---|---|---|---|---|
| KERN_EMERG | 0 | KERN_EMERG |
系统不可用,即将崩溃或已崩溃 | 致命硬件错误、内核 panic 前兆、内存校验失败 |
| KERN_ALERT | 1 | KERN_ALERT |
必须立即采取行动 | 严重数据损坏、关键设备丢失、安全违规 |
| KERN_CRIT | 2 | KERN_CRIT |
严重错误条件 | 驱动 probe 失败、DMA 超时、中断风暴 |
| KERN_ERR | 3 | KERN_ERR |
错误条件,但系统可继续运行 | I/O 操作失败、内存分配失败、设备响应异常 |
| KERN_WARNING | 4 | KERN_WARNING |
警告,可能存在问题 | 驱动降级模式、配置不兼容、资源接近耗尽 |
| KERN_NOTICE | 5 | KERN_NOTICE |
正常但重要的事件 | 设备热插拔、电源状态切换、模块加载/卸载 |
| KERN_INFO | 6 | KERN_INFO |
信息性消息 | 驱动版本号、硬件配置参数、初始化进度 |
| KERN_DEBUG | 7 | KERN_DEBUG |
调试信息,仅用于开发 | 寄存器读写追踪、函数调用路径、变量中间值 |
📌 注意: 数值 0-7 是内核内部使用的优先级数字。在代码中必须使用宏定义(如
KERN_ERR),不要直接写数字 printk("3", ...),因为不同架构的日志格式可能不同。
3.2 日志级别过滤机制:控制台与 dmesg
内核通过两个关键参数控制哪些级别的消息能输出到控制台:
- console_loglevel:控制台允许显示的最高日志级别(数值)。只有级别数值 小于等于 此值的消息才会打印到串口/HDMI 控制台。
- default_message_loglevel:当 printk 未显式指定级别时,默认使用的级别(通常是 KERN_WARNING,即 4)。
例如,若 console_loglevel = 4,则只有 KERN_EMERG(0) 到 KERN_WARNING(4) 的消息会显示在控制台,KERN_NOTICE(5) 及以下的调试信息只写入 dmesg 环形缓冲区。
⚠️ 常见陷阱: 很多新手在驱动里写
printk(KERN_DEBUG "debug info\n"),然后抱怨控制台看不到输出。原因就是默认 console_loglevel 通常为 4 或 7(取决于内核配置),而 KERN_DEBUG 是 7,很可能被过滤掉。调试阶段建议临时降低 console_loglevel 或使用动态调试。
3.3 日志级别选择决策树
下面这张 SVG 流程图可以帮助你在写 printk 时快速确定应该使用哪个级别:
3.4 实际代码示例:不同级别的正确写法
下面是一个 Android 内核 I2C 触摸屏驱动中的 printk 使用示例,展示了不同场景下如何选择日志级别:
#include <linux/kernel.h>
static int touch_probe(struct i2c_client *client)
{
int ret;
/* 正常但重要的事件:驱动加载成功 */
printk(KERN_NOTICE "touch: probe started, addr=0x%02x\n", client->addr);
/* 信息性消息:硬件版本 */
printk(KERN_INFO "touch: firmware version %d.%d\n",
ver_major, ver_minor);
/* 错误条件:I2C 通信失败 */
ret = i2c_master_recv(client, buf, sizeof(buf));
if (ret < 0) {
printk(KERN_ERR "touch: i2c read failed, err=%d\n", ret);
return ret;
}
/* 警告:降级模式 */
if (quirks & TOUCH_QUIRK_NO_GESTURE) {
printk(KERN_WARNING "touch: gesture not supported, "
"running in basic mode\n");
}
/* 调试信息:寄存器值(开发阶段使用) */
printk(KERN_DEBUG "touch: reg[0x%02x] = 0x%02x\n", reg, value);
return 0;
}
3.5 如何选择合适的日志级别:5条实战准则
- 致命错误用 KERN_EMERG/ALERT:只有系统即将崩溃或数据已经损坏时才使用。Android 内核中极少使用,通常由内核核心代码触发。
- 驱动功能失败用 KERN_ERR:probe 失败、DMA 超时、中断注册失败等。这是驱动开发中最常用的错误级别。
- 异常但可恢复用 KERN_WARNING:驱动降级运行、配置不兼容、资源紧张。用户应该知道但不需要立即处理。
- 正常事件用 KERN_NOTICE/INFO:设备插拔、电源状态切换、模块加载。INFO 用于常规信息,NOTICE 用于更重要的里程碑事件。
- 调试信息用 KERN_DEBUG:仅在开发阶段使用。发布前必须删除或替换为
pr_debug()/dev_dbg()等动态调试接口。
🔧 Android 内核调试建议: 在 Android 内核开发中,强烈建议使用
dev_err()、dev_warn()、dev_info()、dev_dbg() 等设备模型专用宏。它们会自动附加设备名称,比裸 printk 更容易定位问题来源。例如:dev_err(&client->dev, "probe failed\n")。
3.6 日志级别与动态调试的关系
理解日志级别是掌握动态调试(Dynamic Debug)的基础。动态调试的核心思想就是:将 KERN_DEBUG 级别的消息在编译时保留,但在运行时通过控制文件动态开关。这意味着:
- 使用
pr_debug()、dev_dbg()编写的调试信息,默认不会输出到任何地方。 - 通过
/sys/kernel/debug/dynamic_debug/control文件,可以按模块、函数、文件名、行号精确控制哪些调试信息输出。 - 生产环境内核可以保留所有调试信息而不影响性能(仅增加少量内存),只在需要时动态开启。