3. printk高级用法

printk 是 Linux 内核中最基础、最常用的调试手段。然而,仅仅使用简单的 printk(KERN_INFO "xxx\n") 远不能满足复杂内核调试的需求。本章将深入探讨 printk 的高级格式化技巧、日志级别控制、以及从 pr_debug 到 dynamic_debug 的现代化调试体系,帮助你在不同场景下精准、高效地获取内核运行信息。

3.1 printk 格式化输出与日志级别

printk 的格式化能力与用户空间的 printf 高度相似,但增加了内核特有的日志级别宏。日志级别决定了消息是否输出到控制台以及记录的紧急程度。

3.1.1 日志级别宏

每个 printk 调用都应附带一个日志级别宏,以标识消息的重要性。未指定级别时,内核会使用默认的 DEFAULT_MESSAGE_LOGLEVEL(通常为 KERN_WARNING)。

宏定义 数值 含义 典型场景
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 调试消息 函数入口/出口、变量中间值
实践建议: 生产环境通常将控制台日志级别设为 4(KERN_WARNING),这意味着只有级别 0-4 的消息会显示在控制台上。调试时可通过 echo 8 > /proc/sys/kernel/printk 临时开启所有级别。

3.1.2 格式化占位符

printk 支持 printf 系列的大部分格式符,并扩展了内核专用的占位符:

  • 基本类型: %d、%u、%x、%p(指针)、%s、%lu、%llu。
  • 内核专用指针格式: 使用 %p 加扩展字符可打印结构化信息,如 %pI4(IPv4 地址)、%pM(MAC 地址)、%pV(设备树节点)、%pK(受限指针,对非特权进程隐藏真实地址)。
  • 特殊格式: %*ph 打印十六进制缓冲区,%pE 打印错误码对应的字符串。
// 示例:使用内核专用格式符
#include <linux/printk.h>
#include <linux/device.h>

void print_debug_info(struct net_device *dev, int error) {
    // 打印 MAC 地址和错误码描述
    printk(KERN_DEBUG "Device %s MAC: %pM, error: %pE\n",
           dev->name, dev->dev_addr, ERR_PTR(error));
    
    // 打印 IPv4 地址
    __be32 ip = in_aton("192.168.1.1");
    printk(KERN_INFO "Server IP: %pI4\n", &ip);
}

3.2 pr_debug 与 pr_info 系列宏

直接使用 printk(KERN_DEBUG ...) 存在两个问题:一是代码冗长,二是无法在编译时或运行时统一控制调试输出。Linux 内核提供了 pr_* 系列宏来简化这一过程。

3.2.1 pr_* 宏家族

这些宏封装了 printk 与日志级别,并自动添加函数名、行号等上下文信息(取决于配置)。

宏 对应 printk 级别 编译时控制 运行时控制
pr_emerg() KERN_EMERG 始终编译 始终输出
pr_alert() KERN_ALERT 始终编译 始终输出
pr_crit() KERN_CRIT 始终编译 始终输出
pr_err() KERN_ERR 始终编译 始终输出
pr_warn() KERN_WARNING 始终编译 始终输出
pr_notice() KERN_NOTICE 始终编译 始终输出
pr_info() KERN_INFO 始终编译 始终输出
pr_debug() KERN_DEBUG 依赖 DEBUG 宏 依赖 dynamic_debug
pr_cont() 延续上一行 始终编译 始终输出
关键区别: pr_debug() 在未定义 DEBUG 宏且未开启 CONFIG_DYNAMIC_DEBUG 时,会被编译器完全优化掉,不产生任何代码。而 pr_info() 始终存在。因此,pr_debug() 是生产环境调试的首选——它不会带来性能开销。

3.2.2 使用 pr_debug 的最佳实践

// 在驱动中大量使用 pr_debug
#include <linux/printk.h>

#define DRIVER_NAME "mydrv"

static void process_data(struct my_device *dev, int len) {
    pr_debug("%s: %s: entering, len=%d\n", DRIVER_NAME, __func__, len);
    
    // ... 核心逻辑 ...
    
    if (unlikely(dev->state == ERROR)) {
        pr_err("%s: device in error state!\n", DRIVER_NAME);
        return;
    }
    
    pr_debug("%s: %s: exiting successfully\n", DRIVER_NAME, __func__);
}

编译时启用调试:在 Makefile 中添加 CFLAGS_<filename>.o := -DDEBUG,或在内核配置中为整个模块开启 DEBUG。

3.3 动态调试(Dynamic Debug)

动态调试(dynamic_debug)是 Linux 内核最强大的调试输出控制机制之一。它允许你在不重新编译内核或模块的情况下,在运行时精确控制哪些 pr_debug() 和 dev_dbg() 语句输出到内核日志。

3.3.1 动态调试的工作原理

当内核配置了 CONFIG_DYNAMIC_DEBUG 时,所有 pr_debug() 和 dev_dbg() 调用会被编译为一个特殊的调试描述符表。每个描述符包含文件名、函数名、行号、格式字符串以及一个启用标志。通过 debugfs 接口,你可以动态修改这些标志。

动态调试(Dynamic Debug)架构图 内核源码 pr_debug("msg1") pr_debug("msg2") 编译 调试描述符表 文件: mydrv.c, 行: 42, 标志: 0 文件: mydrv.c, 行: 58, 标志: 0 文件: core.c, 行: 120, 标志: 0 ... 控制 debugfs 接口 /sys/kernel/debug/ dynamic_debug/ control 内核日志输出 仅输出标志=1 的调试消息 用户写入控制命令

3.3.2 通过 debugfs 控制动态调试

动态调试的控制接口位于 /sys/kernel/debug/dynamic_debug/control。你可以通过读写该文件来查询和修改调试语句的状态。

查询当前所有调试语句:

# cat /sys/kernel/debug/dynamic_debug/control
# 输出示例:
# drivers/net/phy/phy.c:1023 [phy]phy_state_machine =_ "phy_state_machine: state=%s\n"
# drivers/net/phy/phy.c:1040 [phy]phy_state_machine =_ "phy_state_machine: state=%s\n"
# net/core/dev.c:4567 [core]dev_queue_xmit_nit =_ "dev_queue_xmit_nit: skb %p len %u\n"

启用特定文件的调试输出:

# 启用 drivers/net/phy/phy.c 中所有调试语句
echo "file drivers/net/phy/phy.c +p" > /sys/kernel/debug/dynamic_debug/control

# 启用特定函数的调试
echo "func phy_state_machine +p" > /sys/kernel/debug/dynamic_debug/control

# 启用特定行号的调试
echo "file phy.c line 1023 +p" > /sys/kernel/debug/dynamic_debug/control

# 启用模块中所有调试
echo "module phy +p" > /sys/kernel/debug/dynamic_debug/control

禁用调试输出: 将 +p 替换为 -p 即可。

3.3.3 动态调试的格式化控制标志

标志 含义 示例
p 启用打印(核心标志) +p 开启输出,-p 关闭
f 在输出中添加函数名 +pf 输出 "func: msg"
l 在输出中添加行号 +pl 输出 "line: msg"
m 在输出中添加模块名 +pm 输出 "module: msg"
t 在输出中添加线程 ID +pt 输出 "[pid]: msg"
_ 无额外标志(仅消息) 默认行为
注意事项: 动态调试依赖于 debugfs 文件系统。确保内核已挂载 debugfs:mount -t debugfs none /sys/kernel/debug。在嵌入式系统中,如果内核镜像未开启 CONFIG_DYNAMIC_DEBUG,则 pr_debug() 调用会被完全移除,无法通过任何手段启用。

3.4 dev_dbg:设备驱动专用的调试宏

对于设备驱动开发者,dev_dbg() 比 pr_debug() 更实用。它自动关联到 struct device,输出时包含设备名称,便于在多设备系统中区分消息来源。

// dev_dbg 示例
#include <linux/device.h>

static int my_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;
    
    dev_dbg(dev, "probing device\n");
    
    // ... 初始化 ...
    
    if (IS_ERR(dev->driver_data)) {
        dev_err(dev, "failed to allocate resources\n");
        return PTR_ERR(dev->driver_data);
    }
    
    dev_info(dev, "device initialized successfully\n");
    return 0;
}

dev_dbg() 同样受动态调试控制,且输出格式自动包含设备名,如 mydrv mydev.0: probing device。

3.5 实战:使用动态调试定位网络驱动问题

假设你正在调试一个网络驱动,怀疑数据包在 phy_state_machine 函数中状态转换异常。传统做法是添加 printk 并重新编译模块。使用动态调试,你可以零编译完成定位:

  1. 确认动态调试已启用: cat /proc/config.gz | gunzip | grep CONFIG_DYNAMIC_DEBUG 应返回 y。
  2. 挂载 debugfs: mount -t debugfs none /sys/kernel/debug。
  3. 启用目标调试语句: echo "func phy_state_machine +pflm" > /sys/kernel/debug/dynamic_debug/control。这会输出函数名、行号、模块名和消息。
  4. 观察内核日志: dmesg -w | grep phy_state_machine。
  5. 复现问题: 触发网络操作,观察日志输出。
  6. 定位后关闭调试: echo "func phy_state_machine -p" > /sys/kernel/debug/dynamic_debug/control,避免生产环境日志泛滥。
效率对比: 传统方法需要修改代码 → 重新编译 → 下载到目标板 → 重启 → 复现,耗时约 5-10 分钟。动态调试方法仅需 3 条命令,耗时 30 秒。在大型内核开发中,这种效率提升至关重要。

3.6 本章小结

  • 日志级别: 使用 KERN_* 宏控制消息紧急程度,生产环境建议将控制台级别设为 4。
  • 格式化输出: 善用 %pI4、%pM、%pE 等内核专用格式符,减少手动转换代码。
  • pr_debug 系列: 优先使用 pr_debug() 和 dev_dbg(),它们可在编译时或运行时被移除,零性能开销。
  • 动态调试: 通过 debugfs 接口在运行时精确控制调试输出,支持按文件、函数、行号过滤,是内核调试的利器。