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 动态调试工作流程
下图展示了动态调试从内核配置到运行时控制的完整流程:
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
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" 开启所有调试输出,这可能导致内核日志缓冲区被大量调试信息淹没,甚至引发性能问题或信息泄露。