4. printk实战

printk 是 Linux 内核中最基础、最常用的调试输出函数,其行为类似于用户空间的 printf,但专为内核上下文设计。本节将深入讲解如何在驱动模块中正确使用 printk,如何通过 dmesg 查看日志,以及如何通过 /proc/sys/kernel/printk 精细控制日志输出级别。

4.1 printk 函数原型与日志级别

printk 的核心区别在于它带有日志级别(loglevel),用于标识消息的严重程度。其基本调用格式如下:

printk(KERN_INFO "Hello, this is a info message\n");
printk(KERN_ERR "Error occurred at line %d\n", __LINE__);

若未指定日志级别,内核会使用默认级别 DEFAULT_MESSAGE_LOGLEVEL(通常为 4,即 KERN_WARNING)。

下表列出了内核预定义的 8 个日志级别宏及其含义:

宏定义 数值 含义 典型场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件故障
KERN_ALERT 1 必须立即处理 系统崩溃前兆
KERN_CRIT 2 严重条件 驱动初始化失败
KERN_ERR 3 错误条件 I/O 操作失败
KERN_WARNING 4 警告条件 资源使用接近上限
KERN_NOTICE 5 正常但重要的消息 模块加载/卸载
KERN_INFO 6 信息性消息 驱动版本信息
KERN_DEBUG 7 调试级别消息 函数入口/出口跟踪
实战建议:在开发阶段,优先使用 KERN_INFO 和 KERN_DEBUG;在发布版本中,仅保留 KERN_ERR 及以上级别,避免日志刷屏影响性能。

4.2 驱动模块中的 printk 实战示例

下面是一个完整的驱动模块示例,演示了不同级别 printk 的使用方法:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "my_driver: module loaded (level 6)\n");
    printk(KERN_DEBUG "my_driver: init function entered (level 7)\n");

    /* 模拟一个错误条件 */
    if (0) {
        printk(KERN_ERR "my_driver: critical resource missing (level 3)\n");
        return -ENODEV;
    }

    printk(KERN_NOTICE "my_driver: initialization successful (level 5)\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "my_driver: module unloaded (level 6)\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Expert");
MODULE_DESCRIPTION("printk level demo driver");
编译与测试步骤:
  1. 将上述代码保存为 my_driver.c,并编写对应的 Makefile。
  2. 执行 make 编译生成 my_driver.ko。
  3. 使用 sudo insmod my_driver.ko 加载模块。
  4. 使用 sudo rmmod my_driver 卸载模块。
  5. 运行 dmesg | tail -10 查看输出。

4.3 使用 dmesg 查看内核日志

dmesg 是读取内核环形缓冲区(kernel ring buffer)的标准工具。printk 输出的消息会写入该缓冲区,并通过 dmesg 呈现给用户。

常用 dmesg 命令选项:

命令 作用
dmesg 显示所有内核日志消息
dmesg -c 显示并清空内核环形缓冲区
dmesg -n 3 设置控制台日志级别为 3(KERN_ERR)
dmesg -l err 仅显示错误级别(KERN_ERR)及以上的消息
dmesg -T 显示人类可读的时间戳
dmesg -w 实时等待新消息(类似 tail -f)
注意:内核环形缓冲区大小有限(通常为 128KB)。如果驱动模块频繁输出大量调试信息,较早的消息会被覆盖。建议在调试时使用 dmesg -c 清空缓冲区,以便聚焦于当前测试的输出。

4.4 控制日志输出级别:/proc/sys/kernel/printk

Linux 内核通过 /proc/sys/kernel/printk 文件暴露了 4 个关键的日志控制值。理解这 4 个值是掌握 printk 行为的关键。

查看当前设置:

$ cat /proc/sys/kernel/printk
7       4       1       7

这 4 个数字的含义如下表所示:

字段 名称 默认值 说明
1 console_loglevel 7 只有日志级别 小于 此值的消息才会输出到控制台
2 default_message_loglevel 4 未指定级别的 printk 使用的默认级别
3 minimum_console_loglevel 1 控制台日志级别可设置的最小值(通常为 1)
4 default_console_loglevel 7 系统启动时的默认控制台日志级别

核心规则:只有 printk 的日志级别 数值小于 console_loglevel 时,消息才会在控制台(终端)上显示。数值越小,优先级越高。

例如,当 console_loglevel = 4 时:

  • KERN_ERR (3) → 3 < 4,会显示在控制台。
  • KERN_INFO (6) → 6 >= 4,不会显示在控制台,但会写入 dmesg。
调试技巧:在开发调试时,可以将 console_loglevel 设置为 8(大于所有级别),这样所有 printk 消息都会显示在控制台上:
echo 8 > /proc/sys/kernel/printk
或者使用更安全的命令:
dmesg -n 8

4.5 printk 日志过滤与输出控制流程图

下面的 SVG 图展示了 printk 消息从驱动模块发出到最终输出到控制台或 dmesg 的完整流程:

printk 消息过滤与输出流程图 驱动模块 printk 日志级别解析 级别 < console_loglevel? 控制台输出 内核环形缓冲区 dmesg 读取 是 否 说明 所有消息均写入环形缓冲区 控制台输出受 console_loglevel 控制

4.6 修改 printk 级别的实用方法

除了直接写入 /proc/sys/kernel/printk,还有以下几种方式可以控制日志输出:

  • 临时修改(运行时):
    # 只显示 KERN_ERR 及以上级别的消息到控制台
    echo "3 4 1 7" > /proc/sys/kernel/printk
    
    # 或者使用 sysctl
    sysctl -w kernel.printk="3 4 1 7"
  • 永久修改(重启生效): 编辑 /etc/sysctl.conf 或 /etc/sysctl.d/99-printk.conf,添加:
    kernel.printk = 3 4 1 7
  • 内核启动参数: 在 GRUB 内核命令行中添加 loglevel=3,效果等同于将 console_loglevel 设置为 3。
生产环境警告:在生产系统中,建议将 console_loglevel 设置为 3(KERN_ERR)或更低,以避免调试信息暴露敏感信息或影响控制台性能。调试完成后务必恢复为默认值。

4.7 常见问题与避坑指南

问题 原因 解决方案
控制台看不到 printk 输出 console_loglevel 太低,过滤掉了低优先级消息 echo 8 > /proc/sys/kernel/printk
dmesg 中看不到最近的输出 环形缓冲区被大量消息填满,旧消息被覆盖 使用 dmesg -c 清空后再测试
printk 导致系统卡顿 在中断上下文或高频率循环中调用 printk 使用 printk_ratelimited() 限制频率
printk 输出乱码 缺少换行符 \n,缓冲区未刷新 确保每个 printk 以 \n 结尾
进阶技巧:在调试高频代码路径时,可以使用 pr_debug() 和 pr_info() 等封装宏,它们支持动态调试(dynamic debug),可以在不重新编译内核的情况下按模块、函数或行号开启/关闭特定调试输出。