3. printk 高级用法:格式化输出、动态调试与封装宏

printk 是内核调试最基础、最常用的输出手段。但简单使用 printk(KERN_INFO "xxx\n") 远不能发挥其全部能力。本章深入讲解 printk 的格式化规范、动态调试机制以及 pr_debug/pr_info 等封装宏,帮助你在不同调试场景下精准、高效地获取日志。

3.1 printk 格式化输出规范

printk 的格式化语法与标准 C 库 printf 高度相似,但增加了内核特有的格式说明符,用于输出指针、MAC 地址、IP 地址等内核数据类型。

格式说明符 输出类型 示例 说明
%d / %u int / unsigned int printk("%d", var) 标准十进制整数
%lx / %llx unsigned long / long long printk("%lx", addr) 十六进制,常用于地址
%p 指针 printk("%p", ptr) 输出哈希后的指针(安全)
%px 指针(原始值) printk("%px", ptr) 输出真实地址(需特权)
%pK 指针(kptr_restrict) printk("%pK", ptr) 受 kptr_restrict 控制
%pI4 IPv4 地址 printk("%pI4", &ip) 输出 192.168.1.1 格式
%pI6 IPv6 地址 printk("%pI6", &ip6) 输出完整 IPv6 地址
%pM MAC 地址 printk("%pM", mac) 输出 xx:xx:xx:xx:xx:xx
%pE 错误码(字符串) printk("%pE", &err) 输出 -ENOMEM 等名称
调试建议:在发布版本中,默认使用 %p 而非 %px,避免泄露内核地址。仅在调试内核或拥有 CAP_SYSLOG 权限时使用 %px。

3.2 日志级别与控制台过滤

printk 通过日志级别(loglevel)控制消息的紧急程度。级别数字越小,优先级越高。内核根据 console_loglevel 决定哪些消息输出到控制台。

宏定义 级别值 含义 典型场景
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 调试消息 开发阶段日志

运行时可通过 /proc/sys/kernel/printk 调整控制台日志级别:

# 查看当前设置
cat /proc/sys/kernel/printk
# 输出示例:7 4 1 7
# 字段含义:控制台级别 | 默认级别 | 最低级别 | 启动时级别

# 设置控制台只显示 KERN_WARNING 及以上
echo "4 4 1 7" > /proc/sys/kernel/printk

3.3 动态调试(Dynamic Debug)

动态调试是内核提供的一种强大机制,允许在运行时动态启用或禁用内核代码中预置的 pr_debug() 和 dev_dbg() 输出,无需重新编译内核或模块。

核心优势:只在需要时开启特定文件的调试日志,避免海量无用输出淹没关键信息。

3.3.1 启用动态调试

内核配置需要开启 CONFIG_DYNAMIC_DEBUG。之后通过 debugfs 接口控制:

# 挂载 debugfs
mount -t debugfs none /sys/kernel/debug

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

# 启用 drivers/net/ 目录下所有 pr_debug
echo "file drivers/net/* +p" > /sys/kernel/debug/dynamic_debug/control

# 启用某个函数中的调试
echo "func skb_recv_datagram +p" > /sys/kernel/debug/dynamic_debug/control

# 启用某一行
echo "file drivers/net/loopback.c line 123 +p" > /sys/kernel/debug/dynamic_debug/control

3.3.2 匹配语法

匹配关键字 示例 说明
file file drivers/usb/* 匹配文件名(支持通配符)
func func usb_submit_urb 匹配函数名
line line 200-300 匹配行号范围
module module usbcore 匹配模块名
format format "error %d" 匹配格式化字符串
注意:动态调试只对使用 pr_debug()、dev_dbg() 等宏的代码有效。直接使用 printk(KERN_DEBUG ...) 不受动态调试控制。

3.4 pr_debug / pr_info 等封装宏

内核提供了一系列封装宏,统一了日志输出接口,并自动集成动态调试、日志级别、设备信息等功能。

printk 封装宏体系 printk 通用日志宏(pr_* 系列) pr_emerg / pr_alert pr_info / pr_warn pr_debug / pr_err 设备日志宏(dev_* 系列) dev_err / dev_warn dev_info / dev_notice dev_dbg / dev_vdbg 关键区别 pr_* 系列:全局输出,不关联设备;dev_* 系列:自动包含设备名称,便于识别来源

3.4.1 通用日志宏(pr_* 系列)

这些宏封装了 printk 并自动添加日志级别,同时支持动态调试:

// 紧急级别
pr_emerg("System halted: %s\n", reason);

// 错误级别
pr_err("DMA transfer failed: status=%x\n", status);

// 警告级别
pr_warn("Firmware version %d is deprecated\n", ver);

// 信息级别
pr_info("Module loaded, version %s\n", VERSION);

// 调试级别(受动态调试控制)
pr_debug("tx ring head=%d tail=%d\n", head, tail);

3.4.2 设备日志宏(dev_* 系列)

驱动开发中最常用的宏,自动输出设备名称,便于日志溯源:

#include <linux/device.h>

// 错误:自动包含驱动名和设备名
dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);

// 信息
dev_info(&spi->dev, "SPI device probed at %d Hz\n", spi->max_speed_hz);

// 调试(受动态调试控制)
dev_dbg(&netdev->dev, "Packet received: len=%d\n", skb->len);
最佳实践:驱动开发优先使用 dev_* 系列宏。它们不仅自动添加设备标识,还能在 dev_dbg 中无缝集成动态调试,无需额外修改代码。

3.5 动态调试与封装宏的协同工作

动态调试与 pr_debug / dev_dbg 深度集成。当内核配置了 CONFIG_DYNAMIC_DEBUG 时,这些宏在编译时被转换为动态调试点,默认不输出。只有通过 debugfs 显式启用后,日志才会出现在控制台或 dmesg 中。

动态调试与 pr_debug 协同流程 pr_debug("...") 编译为动态调试点 debugfs 控制接口 echo "func xxx +p" 动态调试点被启用 pr_debug 输出生效 dmesg 控制台输出 默认行为 未通过 debugfs 启用时,pr_debug 不产生任何输出,零运行时开销

3.6 实战:驱动调试中的日志策略

以下是一个典型的网络驱动调试场景,展示如何组合使用不同级别的日志宏:

static int netdrv_open(struct net_device *ndev)
{
    struct netdrv_priv *priv = netdev_priv(ndev);
    int ret;

    // 信息级别:记录关键状态变化
    dev_info(&ndev->dev, "Opening device\n");

    // 调试级别:详细寄存器操作(默认关闭)
    dev_dbg(&ndev->dev, "MAC address: %pM\n", ndev->dev_addr);

    ret = request_irq(priv->irq, netdrv_isr, 0, ndev->name, ndev);
    if (ret) {
        // 错误级别:必须记录失败原因
        dev_err(&ndev->dev, "Failed to request IRQ %d: %d\n",
                priv->irq, ret);
        return ret;
    }

    // 调试级别:记录成功路径细节
    dev_dbg(&ndev->dev, "IRQ %d registered successfully\n", priv->irq);

    return 0;
}

运行时控制:

# 只开启该驱动的调试日志
echo "file drivers/net/netdrv.c +p" > /sys/kernel/debug/dynamic_debug/control

# 查看 dmesg 过滤
dmesg -w | grep "netdrv"
策略总结:
  • dev_err / dev_warn:用于必须记录的错误和警告,始终输出。
  • dev_info:用于模块加载、卸载等关键生命周期事件。
  • dev_dbg:用于函数入口出口、寄存器值、数据包详情等高频调试信息,通过动态调试按需开启。

3.7 常见问题与避坑

问题 原因 解决方案
pr_debug 不输出 动态调试未启用或未配置 检查 CONFIG_DYNAMIC_DEBUG;通过 debugfs 启用
dev_dbg 不输出 同 pr_debug,或 device 指针为 NULL 确保 dev 指针有效;启用动态调试
日志刷屏无法控制 console_loglevel 过高 降低控制台级别:echo "3 3 1 7" > /proc/sys/kernel/printk
%p 输出哈希值 内核安全机制默认开启 使用 %px 或设置 kptr_restrict=0
动态调试匹配不到 文件名或函数名拼写错误 先 cat control 查看所有调试点,复制精确名称