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 等封装宏
内核提供了一系列封装宏,统一了日志输出接口,并自动集成动态调试、日志级别、设备信息等功能。
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 中。
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 查看所有调试点,复制精确名称 |