3. 日志级别宏详解:pr_emerg 到 pr_cont

在 Linux 内核开发与调试中,printk 是最基础也是最常用的日志输出接口。为了对日志进行分级管理和动态过滤,内核定义了一套完整的日志级别宏。本章将逐一剖析从 pr_emerg 到 pr_cont 的 9 个核心宏,涵盖其语义、使用场景、数值对应关系以及实际调试中的选择策略。

3.1 日志级别与数值映射

每个 printk 日志级别都对应一个整数,数值越小表示优先级越高(紧急程度越高)。系统根据当前控制台的日志级别(loglevel)决定哪些消息能够被显示。下表列出了所有标准日志级别及其宏定义:

宏名称 数值 含义 典型使用场景
pr_emerg 0 系统不可用 内核崩溃、致命硬件错误
pr_alert 1 必须立即采取行动 严重数据损坏、系统即将挂起
pr_crit 2 临界条件 严重驱动故障、内存耗尽临界
pr_err 3 错误条件 设备 I/O 错误、系统调用失败
pr_warning 4 警告条件 资源不足、配置异常、降级运行
pr_notice 5 正常但重要的条件 驱动加载成功、热插拔事件
pr_info 6 信息性消息 版本号、参数初始化、状态变更
pr_debug 7 调试消息 函数入口/出口、变量值跟踪
pr_cont — 续行消息 多行日志拼接、进度指示
调试建议: 在产品内核中,默认控制台日志级别通常设为 4(KERN_WARNING),这意味着 pr_info 和 pr_debug 默认不会输出到控制台,但会写入内核日志缓冲区(可通过 dmesg 查看)。调试阶段建议通过 loglevel=8 内核参数临时提升级别。

3.2 核心宏详解

3.2.1 pr_emerg — 系统不可用

这是最高优先级的日志级别,仅在系统完全无法继续运行时使用。例如内核 panic、检测到不可恢复的硬件故障。使用 pr_emerg 输出的消息会立即显示在所有控制台上,且通常伴随系统挂起或重启。

// 示例:检测到致命 CPU 缓存错误
if (cache_error_fatal) {
    pr_emerg("CPU%d: L2 cache parity error, system halted\n", smp_processor_id());
    panic("Fatal cache error");
}

3.2.2 pr_alert — 必须立即行动

表示系统处于危险状态,需要管理员立即介入。例如文件系统检测到严重元数据损坏、关键硬件 watchdog 超时。与 pr_emerg 的区别在于系统可能仍能运行有限时间。

// 示例:RAID 阵列丢失多个磁盘
pr_alert("RAID array /dev/md0: %d disks failed, data at risk\n", failed_disks);

3.2.3 pr_crit — 临界条件

用于临界状态,例如内存分配连续失败、中断处理超时、驱动进入未知状态。这些错误如果不处理,很快就会升级为系统不可用。

// 示例:DMA 分配失败
if (!dma_buffer) {
    pr_crit("Unable to allocate DMA buffer for NIC, network may stall\n");
    return -ENOMEM;
}

3.2.4 pr_err — 错误条件

最常用的错误报告宏,表示某个操作失败但系统整体仍可运行。例如设备 probe 失败、系统调用返回错误、文件读写异常。调试时 pr_err 是定位问题的第一道防线。

// 示例:I2C 设备通信失败
ret = i2c_transfer(client->adapter, &msg, 1);
if (ret < 0) {
    pr_err("i2c: failed to write to device %s, err=%d\n", client->name, ret);
    return ret;
}

3.2.5 pr_warning — 警告条件

表示系统遇到非预期情况,但能够自动恢复或降级运行。例如内存碎片化、时钟漂移、驱动使用过时 API。警告消息应附带可操作的修复建议。

// 示例:中断处理耗时过长
pr_warning("IRQ%d: handler took %lu us, consider using threaded IRQ\n",
           irq, duration_us);

3.2.6 pr_notice — 正常但重要

用于记录系统正常运行时的重要事件,例如模块加载/卸载、设备热插拔、网络链路状态变化。这些消息对系统管理员有参考价值,但不会指示错误。

// 示例:USB 设备插入
pr_notice("usb %s: new high-speed USB device number %d using %s\n",
          dev_name(dev), number, hcd->driver->description);

3.2.7 pr_info — 信息性消息

最通用的信息输出宏,用于打印驱动版本、硬件配置参数、初始化状态等。在调试阶段,pr_info 常用于确认代码执行路径。

// 示例:驱动初始化
pr_info("e1000e: %s NIC detected, MAC %pM\n", pci_name(pdev), mac_addr);

3.2.8 pr_debug — 调试消息

仅在调试配置(DEBUG 定义或 dynamic_debug 启用)下才编译或输出的消息。用于函数入口/出口、变量值跟踪、循环状态等细粒度调试。生产内核中默认不包含 pr_debug 输出。

// 示例:跟踪函数调用
pr_debug("%s: entering, arg=%d\n", __func__, arg);
// ... 业务逻辑 ...
pr_debug("%s: exiting, ret=%d\n", __func__, ret);
注意: pr_debug 在未定义 DEBUG 且未启用动态调试时,会被预处理器完全移除,不会产生任何运行时开销。这是与 pr_info 的关键区别——pr_info 始终存在,而 pr_debug 可以做到零开销。

3.2.9 pr_cont — 续行消息

这是一个特殊宏,用于将日志消息续接到上一行末尾,而不添加换行符。常用于打印进度条、多行拼接或避免日志行被其他 CPU 的输出打断。

// 示例:打印内存检测进度
pr_info("Memory test: ");
for (i = 0; i < num_pages; i++) {
    if (i % 100 == 0)
        pr_cont(".");
}
pr_cont(" done (%d pages)\n", num_pages);

3.3 日志级别选择策略

在实际开发中,选择合适的日志级别直接影响调试效率和系统性能。下图展示了从开发到生产的不同阶段,各级别宏的推荐使用策略:

日志级别选择策略流程图 日志级别选择策略流程图 开发阶段 测试阶段 生产阶段 pr_debug / pr_info 全量输出,跟踪执行路径 pr_err / pr_warning 错误与警告,定位缺陷 pr_cont 进度拼接,避免日志碎片 pr_info / pr_notice 关键信息,验证功能正确 pr_err / pr_warning 保留所有错误与警告 pr_debug(动态调试) 按模块按需开启 pr_err / pr_warning 仅保留错误与警告 pr_notice 重要事件记录 pr_info(选择性保留) 仅保留关键状态 核心原则 开发阶段:全量输出 → 测试阶段:按需过滤 → 生产阶段:最小化输出 使用动态调试(dynamic_debug)实现运行时级别切换,无需重新编译 图3-1 日志级别选择策略:从开发到生产的演进路径

3.4 日志级别与动态调试的协同

在 Linux 内核中,pr_debug 与动态调试(dynamic_debug)机制深度集成。当内核配置了 CONFIG_DYNAMIC_DEBUG 时,pr_debug 不再是简单的条件编译,而是变为一个可以在运行时通过 debugfs 控制开关的调用点。这意味着:

  • 零开销生产部署:默认所有 pr_debug 调用点被禁用,不产生任何日志输出和性能开销。
  • 运行时按需开启:通过 echo 'module mydriver +p' > /sys/kernel/debug/dynamic_debug/control 即可开启指定模块的所有调试消息。
  • 精细过滤:支持按文件名、函数名、行号、模块名进行精确控制,避免日志洪流。
// 动态调试控制示例
// 开启 drivers/net/ 目录下所有 pr_debug 输出
# echo 'file drivers/net/* +p' > /sys/kernel/debug/dynamic_debug/control

// 仅开启 e1000e 驱动中 e1000_open 函数的调试消息
# echo 'module e1000e function e1000_open +p' > /sys/kernel/debug/dynamic_debug/control

// 关闭所有调试输出
# echo 'module e1000e -p' > /sys/kernel/debug/dynamic_debug/control
实战要点: 在开发新驱动时,建议将所有调试信息使用 pr_debug 输出,而非 pr_info。这样在开发阶段可以通过 dynamic_debug 灵活控制输出范围,进入生产阶段后无需修改代码即可完全关闭调试日志,实现“一次编写,两阶段使用”。

3.5 常见误区与避坑指南

误区 错误做法 正确做法
级别滥用 所有调试信息都用 pr_err 输出 根据严重程度选择:错误用 pr_err,跟踪用 pr_debug
忽略 pr_cont 竞争 多 CPU 同时使用 pr_cont 拼接日志 使用 pr_cont 前加锁,或使用 seq_file 替代
生产环境保留 pr_debug 生产内核中 pr_debug 默认开启 确保 CONFIG_DYNAMIC_DEBUG 启用,默认关闭所有调试点
日志行不换行 连续使用 pr_info 打印多行,每行末尾不加 \n 每条完整日志末尾加 \n,续行使用 pr_cont
忽略日志缓冲区溢出 高频循环中大量使用 pr_info 使用 pr_debug 或 rate_limit 变体(printk_ratelimited)

3.6 小结

本章详细解析了 Linux 内核中 9 个日志级别宏的定义、数值、使用场景和选择策略。核心要点包括:

  • 日志级别从 0(pr_emerg)到 7(pr_debug),数值越小优先级越高。
  • pr_debug 与动态调试机制结合,可实现运行时零开销的调试日志控制。
  • pr_cont 用于续行拼接,但需注意多核竞争问题。
  • 开发阶段全量输出,测试阶段按需过滤,生产阶段最小化输出——这是日志级别选择的核心策略。