3. printk 进阶:格式化输出技巧、动态调试与 pr_debug/dev_dbg

printk 是内核调试最基础的输出手段,但多数开发者只停留在 printk(KERN_INFO "...") 的初级阶段。本章深入讲解格式化输出的高级控制、动态调试(dynamic_debug)的运行时开关机制,以及 pr_debug / dev_dbg 在内核驱动中的标准用法。掌握这些技巧,可以大幅减少“加 printk → 编译 → 重启”的迭代次数。

3.1 格式化输出:从基础到进阶

printk 的格式化语法与标准 C 库 printf 基本一致,但内核环境增加了若干专用格式说明符,用于输出指针、设备号、MAC 地址等内核特有类型。

3.1.1 常用格式说明符速查

格式符 用途 示例输出
%d / %u 有符号 / 无符号十进制整数 -123 / 456
%x / %X 十六进制小写 / 大写 1a2b / 1A2B
%p 指针(自动省略前导零) ffff800012345678
%pK 安全指针(根据 kptr_restrict 策略输出) 0000000000000000 或真实地址
%px 强制输出完整指针(调试用) ffff800012345678
%pI4 IPv4 地址 192.168.1.1
%pI6 IPv6 地址 fe80::1
%pM MAC 地址(冒号分隔) 00:11:22:33:44:55
%pMF MAC 地址(减号分隔) 00-11-22-33-44-55
%pE 转义字符串(不可见字符转义输出) hello\tworld
%pV struct va_format 格式化 可变参数字符串
安全提示:生产环境中应使用 %pK 而非 %px,避免内核地址泄露。调试阶段可在内核启动参数中设置 kptr_restrict=0 临时关闭地址保护。

3.1.2 日志级别与输出控制

printk 的日志级别通过宏定义在消息前指定,控制消息是否输出到控制台及写入内核日志缓冲区。

// 常用日志级别宏
#define KERN_EMERG   "<0>"   // 紧急消息,系统不可用
#define KERN_ALERT   "<1>"   // 需立即处理
#define KERN_CRIT    "<2>"   // 严重条件
#define KERN_ERR     "<3>"   // 错误条件
#define KERN_WARNING "<4>"   // 警告条件
#define KERN_NOTICE  "<5>"   // 正常但重要
#define KERN_INFO    "<6>"   // 信息性消息
#define KERN_DEBUG   "<7>"   // 调试消息

// 使用示例
printk(KERN_ERR "my_driver: DMA transfer failed, status=%d\n", status);
printk(KERN_DEBUG "my_driver: register value = 0x%08x\n", reg_val);

控制台日志级别由 /proc/sys/kernel/printk 控制,四个数字分别表示:控制台日志级别、默认消息日志级别、最小控制台日志级别、启动时控制台日志级别。

调试技巧:在驱动开发阶段,可将 /proc/sys/kernel/printk 设置为 7 7 1 7,使所有调试信息(KERN_DEBUG)都能输出到控制台。

3.2 动态调试(dynamic_debug)

动态调试是内核提供的一种运行时控制调试输出的机制,允许在不重新编译内核或模块的情况下,按文件、函数、行号、模块名等条件动态启用或禁用 pr_debug / dev_dbg 输出。

3.2.1 内核配置要求

使用动态调试前,需确保内核开启了以下配置:

CONFIG_DYNAMIC_DEBUG=y
CONFIG_DEBUG_FS=y

确认方法:

# 检查 /sys/kernel/debug/dynamic_debug/control 是否存在
ls -l /sys/kernel/debug/dynamic_debug/control

3.2.2 控制接口语法

动态调试的控制接口是 /sys/kernel/debug/dynamic_debug/control,通过写入格式化命令来开关调试输出。

命令格式 说明 示例
file FILENAME +p 启用指定文件的所有 pr_debug file my_driver.c +p
func FUNCTION_NAME +p 启用指定函数内的 pr_debug func my_probe +p
line LINE_NUMBER +p 启用指定行的 pr_debug line 123 +p
module MODULE_NAME +p 启用指定模块的所有 pr_debug module my_driver +p
format "PATTERN" +p 启用匹配格式字符串的 pr_debug format "dma" +p
-p 禁用匹配的调试输出 file my_driver.c -p
_ 通配符,匹配任意 file my_driver.c line 100-200 +p

3.2.3 实战操作示例

# 查看当前所有动态调试点
cat /sys/kernel/debug/dynamic_debug/control

# 启用 my_driver.c 中所有调试输出
echo "file my_driver.c +p" > /sys/kernel/debug/dynamic_debug/control

# 仅启用 my_probe 函数中的调试输出
echo "func my_probe +p" > /sys/kernel/debug/dynamic_debug/control

# 启用第 100 行到第 200 行的调试输出
echo "file my_driver.c line 100-200 +p" > /sys/kernel/debug/dynamic_debug/control

# 禁用所有调试输出(清空)
echo "" > /sys/kernel/debug/dynamic_debug/control
注意事项:动态调试仅对使用 pr_debug()、dev_dbg()、pr_devel() 等宏的代码有效。直接使用 printk(KERN_DEBUG ...) 不受动态调试控制。

3.3 pr_debug 与 dev_dbg 的使用

pr_debug 和 dev_dbg 是内核推荐的调试输出宏,它们与动态调试机制深度集成,在非调试编译时完全消除开销。

3.3.1 宏定义与行为

宏 定义位置 编译时行为(无 DEBUG) 编译时行为(有 DEBUG) 动态调试支持
pr_debug(fmt, ...) include/linux/printk.h 编译为空(零开销) 等价于 printk(KERN_DEBUG fmt, ...) 是
dev_dbg(dev, fmt, ...) include/linux/device.h 编译为空(零开销) 等价于 dev_printk(KERN_DEBUG, dev, fmt, ...) 是
pr_devel(fmt, ...) include/linux/printk.h 编译为空(零开销) 等价于 printk(KERN_DEBUG fmt, ...) 否(仅 DEBUG 时有效)

3.3.2 驱动中的标准用法

#include <linux/device.h>
#include <linux/printk.h>

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_private *priv;
    int ret;

    // 使用 dev_dbg 输出设备相关调试信息
    dev_dbg(dev, "probing device, IRQ=%d\n", platform_get_irq(pdev, 0));

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    // 使用 pr_debug 输出模块级调试信息
    pr_debug("my_driver: allocated private data at %pK\n", priv);

    ret = some_init_function(priv);
    if (ret < 0) {
        dev_err(dev, "init failed: %d\n", ret);
        return ret;
    }

    dev_dbg(dev, "probe successful\n");
    return 0;
}

3.3.3 编译时启用调试输出

在模块 Makefile 中添加 DEBUG 定义,可使 pr_debug / dev_dbg 在编译时直接生效:

# Makefile 片段
ccflags-y += -DDEBUG
# 或针对单个文件
CFLAGS_my_driver.o := -DDEBUG

此时调试输出等价于 printk(KERN_DEBUG ...),不受动态调试控制。

3.4 三种调试输出方式对比

特性 printk(KERN_DEBUG) pr_debug / dev_dbg(无 DEBUG) pr_debug / dev_dbg(有 DEBUG)
运行时开销(未启用) 有(字符串仍在内存) 零(编译为空) 有(同 printk)
动态调试控制 不支持 支持(需 CONFIG_DYNAMIC_DEBUG) 支持(但已全部输出)
生产环境安全性 低(可能泄露信息) 高(完全消除) 低(需手动移除 DEBUG)
推荐场景 临时快速调试 正式驱动开发 开发阶段全量输出
最佳实践:正式驱动代码中应统一使用 dev_dbg()(有设备上下文时)或 pr_debug()(无设备上下文时),并依赖动态调试在运行时按需开启。仅在需要全量输出且无法挂载 debugfs 的早期启动阶段,才考虑编译时添加 -DDEBUG。

3.5 动态调试工作流程

下图展示了动态调试从内核配置到运行时控制的完整流程:

动态调试(dynamic_debug)工作流程 内核编译阶段 CONFIG_DYNAMIC_DEBUG=y pr_debug / dev_dbg 调用 运行时控制接口 /sys/kernel/debug/ dynamic_debug/control echo "file xxx.c +p" > control 内核动态调试核心 匹配查询表 动态开关标志 调试输出 dmesg / 控制台 匹配条件示例 • file my_driver.c +p • func my_probe +p • line 100-200 +p 优势:无需重新编译,运行时按需开启,生产环境零开销

3.6 综合实战:驱动调试输出配置

以下是一个完整的驱动调试输出配置示例,展示如何结合动态调试与 dev_dbg 进行高效调试:

// my_driver.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/device.h>
#include <linux/printk.h>

#define DRIVER_NAME "my_driver"

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    int irq, ret;

    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(dev, "failed to get IRQ\n");
        return irq;
    }

    // 使用 dev_dbg,受动态调试控制
    dev_dbg(dev, "IRQ=%d, mem=0x%llx\n",
            irq, (u64)platform_get_resource(pdev, IORESOURCE_MEM, 0)->start);

    ret = devm_request_irq(dev, irq, my_isr, 0, DRIVER_NAME, pdev);
    if (ret) {
        dev_err(dev, "IRQ request failed: %d\n", ret);
        return ret;
    }

    dev_dbg(dev, "probe completed successfully\n");
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    dev_dbg(dev, "removing device\n");
    return 0;
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = DRIVER_NAME,
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Expert");
MODULE_DESCRIPTION("Dynamic debug example driver");

运行时调试操作:

# 加载模块
insmod my_driver.ko

# 启用 my_driver.c 中所有 dev_dbg 输出
echo "file my_driver.c +p" > /sys/kernel/debug/dynamic_debug/control

# 触发 probe(例如通过设备树 overlay 或 sysfs)
# 观察 dmesg 输出
dmesg | tail -20

# 仅启用 my_probe 函数中的调试输出
echo "func my_probe +p" > /sys/kernel/debug/dynamic_debug/control

# 调试完成后关闭
echo "file my_driver.c -p" > /sys/kernel/debug/dynamic_debug/control
效率提升:使用动态调试后,调试迭代从“修改代码 → 重新编译 → 重启/重载模块”缩短为“echo 命令 → 查看 dmesg”,单次调试周期从分钟级降至秒级。

3.7 常见问题与避坑指南

  • 动态调试不生效:检查内核是否开启 CONFIG_DYNAMIC_DEBUG,以及 debugfs 是否挂载(mount -t debugfs none /sys/kernel/debug)。
  • pr_debug 输出为空:确认代码中使用的确实是 pr_debug() 而非 printk(KERN_DEBUG ...),后者不受动态调试控制。
  • 生产环境性能影响:即使动态调试关闭,pr_debug 的参数表达式仍会被求值。使用 dev_dbg 时注意避免在参数中调用有副作用的函数。
  • 格式字符串匹配:format "PATTERN" 匹配的是格式字符串本身,而非格式化后的输出内容。
  • 模块卸载后控制点失效:动态调试控制点在模块卸载后自动移除,重新加载模块后需重新配置。
安全警告:切勿在生产环境中使用 echo "file *.c +p" 开启所有调试输出,这可能导致内核日志缓冲区被大量调试信息淹没,甚至引发性能问题或信息泄露。