2. printk 基础:函数原型与日志级别详解

printk 是 Linux 内核中最基础、最常用的调试输出函数,其地位等同于用户空间的 printf。在驱动开发和内核调试中,printk 是定位问题的第一道防线。理解其函数原型和日志级别机制,是高效使用 printk 的前提。

2.1 printk 函数原型

printk 的定义位于 include/linux/printk.h,其核心原型如下:

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

// 带日志级别的宏定义(推荐使用)
#define printk(fmt, ...) \
    printk(KERN_DEFAULT fmt, ##__VA_ARGS__)

printk 与用户空间的 printf 在格式化字符串方面完全兼容,支持 %d、%s、%x、%p 等常用格式说明符。关键区别在于:printk 的第一个参数通常是一个日志级别宏(如 KERN_INFO),该宏与格式化字符串之间没有逗号,直接拼接。

⚠️ 常见错误: 新手常写成 printk(KERN_INFO, "msg");(多了一个逗号),这会导致日志级别被当作普通字符串输出,且无法被内核日志系统正确解析。

2.2 日志级别详解

Linux 内核定义了 8 个日志级别,从最高优先级 KERN_EMERG 到最低优先级 KERN_DEBUG。每个级别对应一个数字(0-7),数字越小,优先级越高。

宏名称 数值 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件故障、内核 panic 前兆
KERN_ALERT 1 必须立即处理 严重数据损坏、关键资源耗尽
KERN_CRIT 2 临界条件 驱动初始化失败、DMA 传输错误
KERN_ERR 3 错误条件 设备响应超时、寄存器读写异常
KERN_WARNING 4 警告,可能存在问题 降级模式运行、非关键配置缺失
KERN_NOTICE 5 正常但重要的事件 设备热插拔、模块加载/卸载
KERN_INFO 6 信息性消息 驱动版本号、硬件参数枚举
KERN_DEBUG 7 调试信息 函数调用跟踪、变量中间值
💡 最佳实践: 生产环境驱动中,KERN_DEBUG 级别的输出应通过 pr_debug() 或 dev_dbg() 宏控制,这些宏在非调试编译时会被完全移除,避免性能开销。

2.3 日志级别与控制台过滤

内核通过 console_loglevel 变量控制哪些级别的消息可以输出到控制台。只有日志级别数值小于当前控制台级别的消息才会被显示。

// 查看当前控制台日志级别
$ cat /proc/sys/kernel/printk
7       4       1       7
// 字段含义:控制台级别 | 默认消息级别 | 最低控制台级别 | 默认控制台级别

// 临时设置控制台级别为 8(显示所有消息)
$ echo 8 > /proc/sys/kernel/printk

四个字段的详细说明:

  • 字段1(控制台级别): 当前控制台可显示的最高日志级别数值。设为 7 时,只有 KERN_DEBUG(7)及更高优先级(数值更小)的消息才能输出。
  • 字段2(默认消息级别): 当 printk 未显式指定日志级别时,消息默认使用的级别。
  • 字段3(最低控制台级别): 控制台级别可设置的最小值,防止完全关闭内核输出。
  • 字段4(默认控制台级别): 系统启动时的初始控制台级别。

2.4 日志级别选择策略

在驱动开发中,合理选择日志级别能显著提升调试效率。以下是一个基于场景的选择指南:

  1. 致命错误: 使用 KERN_CRIT 或 KERN_ALERT,例如 DMA 引擎无法初始化、中断号冲突。
  2. 可恢复错误: 使用 KERN_ERR,例如设备返回错误状态码、超时重试。
  3. 异常但非错误: 使用 KERN_WARNING,例如使用降级模式、固件版本不匹配。
  4. 正常流程关键点: 使用 KERN_INFO,例如驱动加载成功、设备发现。
  5. 详细调试: 使用 KERN_DEBUG,并通过 pr_debug() 宏实现编译时开关。
🚨 性能警告: 在中断上下文或热路径(如网络收包、块设备 IO 完成)中使用 printk 会带来严重的性能下降。printk 内部会获取 spinlock 并可能触发控制台输出,导致延迟抖动。高频调试应优先使用 tracepoint 或 ftrace。

2.5 printk 日志处理流程

下图展示了 printk 从调用到最终输出的完整路径,帮助理解日志级别在其中的作用:

printk 日志处理流程 printk(fmt, ...) 解析日志级别 级别 < console_loglevel? 写入环形缓冲区 控制台输出 syslog / dmesg 丢弃(不输出) 说明: 日志级别数值越小优先级越高。只有消息级别 < console_loglevel 时,才会触发控制台输出。 无论是否输出到控制台,所有消息都会写入内核环形缓冲区,可通过 dmesg 查看。

2.6 实际驱动代码示例

以下是一个虚拟网卡驱动中使用 printk 的典型片段,展示了不同日志级别的应用:

#include <linux/module.h>
#include <linux/netdevice.h>

static int virt_net_open(struct net_device *dev)
{
    // 正常信息:驱动启动
    printk(KERN_INFO "%s: 网卡启动,MAC=%pM\n", dev->name, dev->dev_addr);

    // 尝试分配 DMA 缓冲区
    if (!alloc_dma_buffer(dev)) {
        // 严重错误:无法分配关键资源
        printk(KERN_CRIT "%s: DMA 缓冲区分配失败,设备不可用\n", dev->name);
        return -ENOMEM;
    }

    // 调试信息:仅在调试时关注
    printk(KERN_DEBUG "%s: 寄存器基址=0x%lx\n", dev->name, dev->base_addr);

    return 0;
}

static void virt_net_tx_timeout(struct net_device *dev)
{
    // 警告:发送超时,但驱动会尝试恢复
    printk(KERN_WARNING "%s: 发送超时,启动恢复流程\n", dev->name);
    reset_hardware(dev);
}
🔧 调试技巧: 在开发阶段,可以将 /proc/sys/kernel/printk 设置为 8 4 1 7,确保所有级别的消息(包括 KERN_DEBUG)都能输出到控制台。发布前再恢复为 4 4 1 7,只显示 KERN_WARNING 及以上级别的消息。

2.7 日志级别与 pr_* 系列宏

为简化书写,内核提供了 pr_* 系列宏,自动附加对应的日志级别:

宏 等价形式 日志级别
pr_emerg() printk(KERN_EMERG ...) 0
pr_alert() printk(KERN_ALERT ...) 1
pr_crit() printk(KERN_CRIT ...) 2
pr_err() printk(KERN_ERR ...) 3
pr_warn() printk(KERN_WARNING ...) 4
pr_notice() printk(KERN_NOTICE ...) 5
pr_info() printk(KERN_INFO ...) 6
pr_debug() printk(KERN_DEBUG ...) 7

使用 pr_* 宏的优势在于:代码更简洁,且 pr_debug() 在未定义 DEBUG 宏时会被编译为空,零运行时开销。

// 推荐写法:使用 pr_* 宏
pr_info("网卡 %s 已注册\n", dev->name);
pr_err("DMA 分配失败,错误码=%d\n", ret);

// 不推荐:手动拼接日志级别(容易遗漏逗号)
printk(KERN_INFO "网卡 %s 已注册\n", dev->name);

2.8 本章小结

  • printk 是内核调试的基础工具,其函数原型与 printf 兼容,但日志级别宏必须紧贴格式化字符串。
  • 8 个日志级别(0-7)覆盖从系统崩溃到详细调试的全部场景,选择合适级别是专业驱动的标志。
  • 控制台日志级别 console_loglevel 决定哪些消息能实时输出,所有消息均会进入环形缓冲区。
  • 优先使用 pr_* 系列宏,特别是 pr_debug() 用于调试输出,避免生产环境性能损耗。