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 | 调试消息 | 函数入口/出口、变量中间值 |
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 接口,你可以动态修改这些标志。
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 并重新编译模块。使用动态调试,你可以零编译完成定位:
- 确认动态调试已启用:
cat /proc/config.gz | gunzip | grep CONFIG_DYNAMIC_DEBUG应返回y。 - 挂载 debugfs:
mount -t debugfs none /sys/kernel/debug。 - 启用目标调试语句:
echo "func phy_state_machine +pflm" > /sys/kernel/debug/dynamic_debug/control。这会输出函数名、行号、模块名和消息。 - 观察内核日志:
dmesg -w | grep phy_state_machine。 - 复现问题: 触发网络操作,观察日志输出。
- 定位后关闭调试:
echo "func phy_state_machine -p" > /sys/kernel/debug/dynamic_debug/control,避免生产环境日志泛滥。
3.6 本章小结
- 日志级别: 使用
KERN_*宏控制消息紧急程度,生产环境建议将控制台级别设为 4。 - 格式化输出: 善用
%pI4、%pM、%pE等内核专用格式符,减少手动转换代码。 - pr_debug 系列: 优先使用
pr_debug()和dev_dbg(),它们可在编译时或运行时被移除,零性能开销。 - 动态调试: 通过
debugfs接口在运行时精确控制调试输出,支持按文件、函数、行号过滤,是内核调试的利器。