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 时快速确定应该使用哪个级别:

printk 日志级别选择决策树 printk 日志级别选择决策树 消息是否紧急? 系统即将崩溃? KERN_EMERG (0) KERN_ALERT (1) 是否影响功能? KERN_ERR (3) KERN_WARNING (4) 用户需要知道? KERN_NOTICE (5) KERN_INFO (6) KERN_DEBUG (7) 💡 使用建议 • 生产环境:只保留 KERN_ERR 及以上 • 开发调试:临时使用 KERN_DEBUG • 驱动发布前:将 KERN_DEBUG 改为动态调试

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条实战准则

  1. 致命错误用 KERN_EMERG/ALERT:只有系统即将崩溃或数据已经损坏时才使用。Android 内核中极少使用,通常由内核核心代码触发。
  2. 驱动功能失败用 KERN_ERR:probe 失败、DMA 超时、中断注册失败等。这是驱动开发中最常用的错误级别。
  3. 异常但可恢复用 KERN_WARNING:驱动降级运行、配置不兼容、资源紧张。用户应该知道但不需要立即处理。
  4. 正常事件用 KERN_NOTICE/INFO:设备插拔、电源状态切换、模块加载。INFO 用于常规信息,NOTICE 用于更重要的里程碑事件。
  5. 调试信息用 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 文件,可以按模块、函数、文件名、行号精确控制哪些调试信息输出。
  • 生产环境内核可以保留所有调试信息而不影响性能(仅增加少量内存),只在需要时动态开启。