2. printk基础

printk 是 Linux 内核中最核心的日志输出函数,其地位等同于用户空间的 printf。驱动开发者通过 printk 输出调试信息、状态变更、错误告警等关键数据。理解 printk 的函数原型、日志级别以及控制台日志级别的配置机制,是进行内核日志分析的第一步。

2.1 printk 函数原型

printk 定义在 <linux/printk.h> 中,其函数原型与 printf 高度相似,但增加了日志级别参数。基本原型如下:

int printk(const char *fmt, ...);

参数说明:

  • fmt:格式化字符串,支持与 printf 相同的格式说明符(%d, %s, %x, %p 等)。
  • ...:可变参数,对应格式化字符串中的占位符。
  • 返回值:成功时返回输出的字符数(不包括结尾的 '\0');失败时返回负的错误码。
关键区别:printk 与 printf 最大的不同在于 printk 的第一个参数通常通过宏拼接的方式嵌入日志级别,例如 printk(KERN_INFO "Hello, world!\n");。日志级别宏与格式化字符串之间没有逗号,这是 C 语言字符串字面量拼接的语法特性。

2.2 日志级别(Log Levels)

Linux 内核定义了 8 个日志级别,从最高优先级 KERN_EMERG 到最低优先级 KERN_DEBUG。每个级别对应一个数字值(0~7),数字越小,优先级越高。日志级别决定了消息是否被输出到控制台,以及如何被 syslog 等用户空间工具处理。

宏名称 数值 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件故障、内核 panic 前的最后消息
KERN_ALERT 1 必须立即处理 严重内存错误、关键数据损坏
KERN_CRIT 2 临界条件 驱动初始化失败、DMA 传输错误
KERN_ERR 3 错误条件 设备 I/O 错误、资源分配失败
KERN_WARNING 4 警告条件 设备降级运行、配置不匹配
KERN_NOTICE 5 正常但重要的消息 驱动加载/卸载、设备热插拔事件
KERN_INFO 6 信息性消息 驱动版本信息、设备探测结果
KERN_DEBUG 7 调试消息 函数入口/出口、变量值跟踪
注意:如果 printk 调用中未指定日志级别(即直接以 printk("some message\n"); 形式调用),内核会使用默认级别 DEFAULT_MESSAGE_LOGLEVEL(通常为 4,即 KERN_WARNING)。这可能导致调试信息被意外抑制或关键信息被淹没,因此强烈建议始终显式指定日志级别。

2.3 控制台日志级别配置

控制台日志级别(console log level)决定了哪些级别的 printk 消息能够被输出到当前控制台(串口、VGA 终端等)。只有日志级别数值 小于或等于 当前控制台日志级别的消息才会被显示。例如,若控制台日志级别为 4(KERN_WARNING),则只有 KERN_EMERG(0)到 KERN_WARNING(4)的消息会出现在控制台上,而 KERN_NOTICE(5)及更低优先级的消息将被过滤。

控制台日志级别可以通过以下方式查看和修改:

  • 查看当前控制台日志级别:
    cat /proc/sys/kernel/printk
    该文件输出 4 个数字,依次为:控制台日志级别、默认消息日志级别、最小控制台日志级别、默认控制台日志级别。
  • 临时修改控制台日志级别:
    echo "7" > /proc/sys/kernel/printk
    将控制台日志级别设为 7,允许所有级别的消息(包括 KERN_DEBUG)输出到控制台。这在调试驱动时非常有用。
  • 通过内核启动参数设置:
    loglevel=7
    在 bootloader(如 U-Boot、GRUB)的内核命令行中添加该参数,可在系统启动时直接设置控制台日志级别。
  • 使用 dmesg 命令:
    dmesg -n 7
    设置控制台日志级别,效果等同于写入 /proc/sys/kernel/printk。
调试实践:在嵌入式系统开发中,通常将控制台日志级别设为 7(KERN_DEBUG),以便在串口终端上实时看到所有驱动调试输出。但需注意,大量调试消息会拖慢系统响应,生产环境中应恢复为默认级别(通常为 4 或 3)。

2.4 printk 日志处理流程

下图展示了 printk 消息从驱动调用到最终输出的完整路径,包括日志级别过滤、环形缓冲区存储以及控制台输出决策。

printk 日志处理流程 驱动调用 printk 解析日志级别宏 写入内核环形缓冲区 判断:级别 ≤ 控制台日志级别? 是 否 输出到控制台设备 仅保留在环形缓冲区 说明:dmesg 命令可读取环形缓冲区中的所有消息,不受控制台日志级别限制

2.5 常用调试技巧与注意事项

  • 使用 pr_* 宏简化调用:内核提供了 pr_emerg、pr_alert、pr_crit、pr_err、pr_warn、pr_notice、pr_info、pr_debug 等宏,自动拼接对应的日志级别。例如 pr_info("Device initialized\n"); 等价于 printk(KERN_INFO "Device initialized\n");。
  • pr_debug 的特殊性:pr_debug 在未定义 DEBUG 宏时会被编译为空,不会产生任何代码。在驱动开发中,可以通过在源文件顶部添加 #define DEBUG 或通过内核配置 CONFIG_DYNAMIC_DEBUG 来动态启用。
  • 避免在中断上下文使用过长格式:printk 在中断上下文(如中断处理函数、tasklet)中调用时,应避免输出过长的消息,因为 printk 可能引起调度延迟。建议仅输出关键错误码或状态标志。
  • 使用 %p 扩展格式:内核 printk 支持多种 %p 扩展格式,如 %p(指针)、%pK(受限指针)、%pI4(IPv4 地址)、%pM(MAC 地址)等,可简化常见数据类型的输出。
生产环境建议:在产品发布前,应将所有 pr_debug 和低优先级的 printk 移除或条件编译掉,避免日志输出影响系统性能。同时,通过内核启动参数 quiet 可抑制大部分控制台输出,仅保留 KERN_EMERG 级别的消息。