4. printk 实战:在驱动模块中添加 printk、查看 dmesg 日志、使用 tail -f 实时监控

printk 是 Linux 内核中最基础、最直接的调试手段,也是驱动开发者的“第一把调试扳手”。本章通过一个完整的驱动模块实例,演示如何插入 printk、控制日志级别、查看 dmesg 缓冲区以及使用 tail -f 实现实时监控。

4.1 printk 基础与日志级别

printk 与用户空间的 printf 类似,但增加了日志级别(loglevel)机制,用于控制消息的紧急程度和输出目标。内核通过 /proc/sys/kernel/printk 文件定义四个控制值:

字段 含义 典型默认值
console_loglevel 允许输出到控制台的最低优先级 7(DEBUG)
default_message_loglevel 未指定级别时的默认优先级 4(KERN_WARNING)
minimum_console_loglevel 控制台可设置的最低优先级 1(KERN_ALERT)
default_console_loglevel 系统启动时的控制台级别 7(DEBUG)

printk 的日志级别宏定义在 <linux/kern_levels.h> 中,常用级别如下:

宏 数值 适用场景
KERN_EMERG 0 系统不可用,必须立即处理
KERN_ALERT 1 需要立即采取行动
KERN_CRIT 2 严重错误条件
KERN_ERR 3 驱动或硬件错误
KERN_WARNING 4 警告,可能存在问题
KERN_NOTICE 5 正常但重要的信息
KERN_INFO 6 信息性消息
KERN_DEBUG 7 调试信息,仅用于开发
最佳实践:开发阶段使用 KERN_DEBUG 或 KERN_INFO;发布驱动时,将常规日志降级为 KERN_WARNING 或 KERN_NOTICE,避免污染生产环境的内核日志缓冲区。

4.2 实战驱动模块:带 printk 的 hello_debug

下面编写一个包含多级别 printk 的简单驱动模块,演示模块加载和卸载时的日志输出。

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

static int __init hello_debug_init(void)
{
    printk(KERN_EMERG   "[HELLO] EMERG: 系统即将崩溃?\n");
    printk(KERN_ALERT   "[HELLO] ALERT: 立即处理!\n");
    printk(KERN_CRIT    "[HELLO] CRIT: 严重错误\n");
    printk(KERN_ERR     "[HELLO] ERR: 驱动错误\n");
    printk(KERN_WARNING "[HELLO] WARNING: 注意\n");
    printk(KERN_NOTICE  "[HELLO] NOTICE: 正常但重要\n");
    printk(KERN_INFO    "[HELLO] INFO: 模块加载成功\n");
    printk(KERN_DEBUG   "[HELLO] DEBUG: 调试信息\n");
    return 0;
}

static void __exit hello_debug_exit(void)
{
    printk(KERN_INFO "[HELLO] 模块卸载,再见!\n");
}

module_init(hello_debug_init);
module_exit(hello_debug_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("printk 级别演示模块");

编译后使用 insmod 和 rmmod 加载/卸载模块,观察不同级别的日志输出行为。

4.3 查看 dmesg 日志

dmesg 是读取内核环形缓冲区(ring buffer)的标准工具。常用操作如下:

  • 查看所有日志:dmesg
  • 查看最后 N 条:dmesg -n 10 或 dmesg | tail -n 20
  • 清空缓冲区:dmesg -c(清空后仅新日志可见)
  • 按级别过滤:dmesg -l err,warning(只显示错误和警告)
  • 人类可读时间戳:dmesg -T(将内核时间戳转为可读时间)
  • 跟踪新日志:dmesg -w(类似 tail -f,等待新日志)
示例:加载模块后执行 dmesg -l info,debug | grep HELLO,可快速过滤出本模块的调试信息。

4.4 使用 tail -f 实时监控内核日志

在驱动调试过程中,实时观察日志输出至关重要。tail -f 配合 /var/log/kern.log 或 dmesg -w 可实现类似效果:

# 方法一:监控内核日志文件(Ubuntu/Debian)
tail -f /var/log/kern.log

# 方法二:使用 dmesg 的跟踪模式(所有发行版)
dmesg -w

# 方法三:结合 grep 过滤特定模块
dmesg -w | grep HELLO

当执行 insmod hello_debug.ko 时,tail -f 终端会立即显示所有 printk 输出,无需手动刷新。

注意:如果使用 tail -f /var/log/kern.log,需要确保系统日志服务(rsyslog/systemd-journald)正在运行,并且内核日志被正确写入该文件。嵌入式系统中可能没有该文件,此时优先使用 dmesg -w。

4.5 printk 输出流程与环形缓冲区

下图展示了 printk 从驱动调用到控制台/日志文件的完整路径:

printk 输出流程与环形缓冲区 驱动模块 (printk) 日志级别判断 内核环形缓冲区 控制台输出 syslog / kern.log dmesg 读取 关键说明: 1. 只有优先级 ≥ console_loglevel 的消息才会输出到控制台。 2. 所有消息都会写入环形缓冲区,无论级别高低。 3. dmesg 直接从环形缓冲区读取,不受 console_loglevel 限制。 4. tail -f /var/log/kern.log 依赖 syslog 服务将缓冲区内容持久化到文件。

4.6 控制台日志级别动态调整

在调试过程中,可能需要临时调整控制台输出级别,避免被大量调试信息淹没:

# 查看当前控制台级别
cat /proc/sys/kernel/printk
# 输出示例:7  4  1  7

# 只允许 KERN_ERR 及以上级别输出到控制台
echo "3 4 1 7" > /proc/sys/kernel/printk

# 恢复默认(所有级别)
echo "7 4 1 7" > /proc/sys/kernel/printk
调试技巧:在开发阶段,可以将 console_loglevel 设为 7,配合 dmesg -w | grep YOUR_DRIVER 实时过滤,既能看到所有调试信息,又不会被无关日志干扰。

4.7 常见问题与避坑指南

问题 原因 解决方案
printk 不输出到控制台 console_loglevel 太低,过滤了低优先级消息 检查 /proc/sys/kernel/printk,临时调高 console_loglevel
dmesg 看不到最新日志 环形缓冲区被旧日志填满,新日志覆盖了旧日志 使用 dmesg -c 清空缓冲区,或增大缓冲区大小(内核配置)
tail -f 无输出 syslog 服务未运行,或日志未写入 kern.log 改用 dmesg -w,或检查 journalctl -kf(systemd 系统)
printk 导致系统卡顿 高频调用 printk,尤其在中断上下文 使用 printk_ratelimit() 或 pr_debug() 条件编译

4.8 本章小结