3. printk进阶:格式化输出技巧、动态调试(dynamic_debug)的开启与使用

printk 是 Linux 驱动开发中最基础、最常用的调试手段。然而,很多开发者对 printk 的使用仍停留在“printf 的内核版本”这一层面,未能充分利用其格式化输出技巧和动态调试(dynamic_debug)功能。本章将深入讲解 printk 的进阶用法,帮助你从“乱打日志”进化到“精准、高效、可控制”的调试境界。

3.1 printk 格式化输出技巧

printk 的格式化字符串与用户空间的 printf 类似,但增加了内核特有的格式说明符,用于输出指针、MAC 地址、IP 地址、资源大小等内核专用数据类型。

3.1.1 常用格式化说明符

格式说明符 用途 示例输出
%d / %u 有/无符号十进制整数 42
%x / %X 十六进制(小/大写) 2a / 2A
%p 指针(哈希化,安全) 00000000abcdef12
%px 指针(真实地址,需特权) ffff888012345678
%pK 内核指针(kptr_restrict 控制) 0000000000000000(受限时)
%pM MAC 地址(xx:xx:xx:xx:xx:xx) 00:11:22:33:44:55
%pI4 IPv4 地址(点分十进制) 192.168.1.1
%pI6 IPv6 地址(冒号分隔) fe80::1
%pE 转义字符串(安全打印) hello\nworld
%pa 物理地址(resource_size_t) 0x00000000fedc0000
%pap 带括号的物理地址范围 [0x00000000fedc0000-0x00000000fedc0fff]
安全提示: 生产环境中应优先使用 %p(哈希化)而非 %px(真实地址),以防止内核地址泄露。只有在调试阶段且确认安全时,才使用 %px。

3.1.2 日志级别与输出控制

printk 支持 8 个日志级别,通过 #define 宏定义在 <linux/printk.h> 中。级别越低,优先级越高。

宏 级别值 含义 典型场景
KERN_EMERG 0 紧急事件 系统崩溃前
KERN_ALERT 1 必须立即处理 严重硬件故障
KERN_CRIT 2 临界条件 驱动初始化失败
KERN_ERR 3 错误条件 IO 操作失败
KERN_WARNING 4 警告 资源不足
KERN_NOTICE 5 正常但重要 驱动加载成功
KERN_INFO 6 信息 设备探测信息
KERN_DEBUG 7 调试信息 函数调用跟踪
// 示例:使用不同日志级别
printk(KERN_ERR "my_driver: DMA transfer failed, status=%d\n", status);
printk(KERN_INFO "my_driver: device probed at 0x%pap\n", &res->start);
printk(KERN_DEBUG "my_driver: entering ioctl, cmd=0x%x\n", cmd);

3.2 动态调试(dynamic_debug)原理与架构

动态调试(dynamic_debug)是内核提供的一种运行时控制调试输出的机制。它允许你在不重新编译内核或模块的情况下,动态开启或关闭特定文件、函数、行号的 pr_debug() / dev_dbg() 输出。

动态调试(dynamic_debug)架构图 用户空间 debugfs 控制接口 内核动态调试核心 调试条目数据库(dynamic_debug 表) 每一条记录包含:文件名、函数名、行号、格式字符串、标志位 通过 __dynamic_pr_debug() 宏在编译时嵌入 驱动模块 A 驱动模块 B 驱动模块 C 核心思想:通过 debugfs 接口动态修改调试条目标志位,控制 pr_debug() 是否输出

3.3 动态调试的开启与配置

3.3.1 内核配置要求

要使用动态调试,内核必须开启以下配置项:

CONFIG_DYNAMIC_DEBUG=y          # 核心功能
CONFIG_DEBUG_FS=y               # debugfs 文件系统
# 如果使用模块,还需:
CONFIG_DYNAMIC_DEBUG_CORE=y     # 模块动态调试支持

3.3.2 驱动代码中的准备

驱动代码中必须使用 pr_debug() 或 dev_dbg() 替代 printk(KERN_DEBUG ...),才能被动态调试框架管理。

// 推荐方式:使用 dev_dbg(),自动关联设备信息
dev_dbg(&pdev->dev, "probe: reg_base=0x%p, irq=%d\n", reg_base, irq);

// 或使用 pr_debug(),适用于非设备上下文
pr_debug("my_driver: ioctl cmd=0x%x, arg=0x%lx\n", cmd, arg);
注意: 即使开启了 CONFIG_DYNAMIC_DEBUG,printk(KERN_DEBUG ...) 也不会被动态调试管理。必须使用 pr_debug() 或 dev_dbg() 系列宏。

3.4 动态调试的使用方法

动态调试通过 debugfs 文件系统提供控制接口,默认挂载在 /sys/kernel/debug/dynamic_debug/control。

3.4.1 查看当前所有调试条目

# cat /sys/kernel/debug/dynamic_debug/control
# 输出示例:
# drivers/my_driver/my_module.c:123 [my_driver]my_func =_ "entered my_func, arg=%d\012"
# drivers/my_driver/my_module.c:456 [my_driver]my_ioctl =_ "ioctl cmd=0x%x\012"

3.4.2 基本控制语法

控制命令格式:match_type match_value flags flag_value

匹配类型 匹配值示例 说明
file my_module.c 匹配文件名(支持通配符 *)
func my_func 匹配函数名
line 123 或 100-200 匹配行号或范围
module my_driver 匹配模块名
format ioctl 匹配格式字符串内容
标志 含义
+p 开启该条目的调试输出
-p 关闭该条目的调试输出
+f 输出时显示函数名
+l 输出时显示行号
+m 输出时显示模块名
+t 输出时显示线程 ID

3.4.3 常用操作示例

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

# 开启 my_func 函数中所有调试输出,并显示函数名和行号
echo "func my_func +pfl" > /sys/kernel/debug/dynamic_debug/control

# 开启 my_driver 模块中 100-200 行的调试输出
echo "module my_driver line 100-200 +p" > /sys/kernel/debug/dynamic_debug/control

# 关闭所有调试输出(静默模式)
echo "file * -p" > /sys/kernel/debug/dynamic_debug/control

# 开启包含 "ioctl" 格式字符串的调试输出
echo "format ioctl +p" > /sys/kernel/debug/dynamic_debug/control

3.5 动态调试进阶技巧

3.5.1 使用 dynamic_debug.verbose=1 内核参数

在引导时添加内核参数,可以在系统启动早期就开启动态调试:

# 在 GRUB 命令行或 /etc/default/grub 中添加
dynamic_debug.verbose=1
# 或更精细的控制:
dyndbg="file my_driver.c +p"

3.5.2 模块加载时动态调试

在加载模块时传递参数:

# 加载模块时开启调试
modprobe my_driver dyndbg="file my_module.c +p"

# 或使用 insmod
insmod my_driver.ko dyndbg="func my_func +pfl"

3.5.3 结合 dev_dbg() 的设备级控制

dev_dbg() 会自动关联设备名称,可以通过 format 匹配设备名:

# 只开启特定设备的调试输出
echo "format 'my_device_name' +p" > /sys/kernel/debug/dynamic_debug/control
实战建议:
  1. 开发阶段: 在驱动中大量使用 dev_dbg(),通过 echo "file *.c +p" 一键开启所有调试。
  2. 问题定位: 使用 func 和 line 精确锁定可疑函数或代码行。
  3. 性能敏感场景: 只开启关键路径的调试,避免 file * +p 导致日志洪流。
  4. 生产环境: 默认关闭所有调试,仅在需要时通过 format 匹配特定错误码或状态。

3.6 printk 与 dynamic_debug 对比总结

特性 printk(KERN_DEBUG) pr_debug() / dev_dbg()
编译时控制 通过 #define DEBUG 或 CONFIG_DYNAMIC_DEBUG 通过 CONFIG_DYNAMIC_DEBUG
运行时控制 不支持(需重新编译) 支持(通过 debugfs)
精细度 文件级别(#define DEBUG) 函数/行/格式字符串级别
性能开销 始终存在(即使不输出) 几乎为零(关闭时)
日志格式 需手动添加文件名/行号 可通过 +f +l 标志自动添加
适用场景 快速调试、早期启动 复杂系统、生产环境问题定位
最佳实践: 新驱动开发应优先使用 dev_dbg() 配合动态调试。只有在系统早期启动(如 arch 代码、中断控制器初始化)或 debugfs 不可用时,才退而使用 printk(KERN_DEBUG)。