3. printk高级用法
在掌握了基础的 printk 日志输出后,本章将深入探讨其高级特性。这些特性不仅能让调试信息更加精准、高效,还能在无需重新编译内核的情况下动态控制日志输出,极大提升调试效率。我们将重点学习格式化输出的技巧、动态调试(dynamic_debug) 机制以及内核 5.5 引入的 printk 索引(index) 功能。
3.1 格式化输出:从简单字符串到结构化数据
printk 的格式化能力与标准 C 库的 printf 类似,但针对内核环境扩展了若干专用格式说明符。正确使用这些格式符,是编写可读性强、信息量大的调试日志的基础。
3.1.1 常用格式说明符速查
| 格式符 | 数据类型 | 示例输出 | 说明 |
|---|---|---|---|
%d / %i |
int | -123 |
有符号十进制整数 |
%u |
unsigned int | 456 |
无符号十进制整数 |
%x / %X |
unsigned int | 1a / 1A |
无符号十六进制(小写/大写) |
%p |
void * | 0xffff888123456789 |
指针地址(自动加 0x 前缀) |
%pK |
void * | 0000000000000000 或真实地址 |
安全指针,根据 kptr_restrict 决定是否显示 |
%px |
void * | ffff888123456789 |
强制打印真实指针(需谨慎使用) |
%s |
char * | hello |
字符串 |
%pI4 |
struct in_addr * | 192.168.1.1 |
IPv4 地址 |
%pI6 |
struct in6_addr * | 0001:0002:...:0003 |
IPv6 地址 |
%pM |
unsigned char * | 00:11:22:33:44:55 |
MAC 地址 |
%pG |
struct page * | 0x1234 |
页描述符地址 |
%p,它会在启用了地址随机化(KASLR)的内核中自动隐藏真实地址,避免信息泄露。仅在调试阶段且确认安全时,才使用 %px 获取真实物理地址。
3.1.2 格式化输出示例
#include <linux/printk.h>
#include <linux/device.h>
void print_device_info(struct device *dev, int irq) {
// 基础信息打印
printk(KERN_INFO "Device name: %s, IRQ: %d\n", dev_name(dev), irq);
// 打印指针地址(安全模式)
printk(KERN_DEBUG "Device struct ptr: %p\n", dev);
// 打印 DMA 地址(假设有 dma_addr_t 变量 dma_addr)
// printk(KERN_DEBUG "DMA addr: %pad\n", &dma_addr);
// 打印资源信息(假设有 struct resource *res)
// printk(KERN_INFO "Region: [%pa, %pa]\n", &res->start, &res->end);
}
3.2 动态调试(dynamic_debug):运行时控制日志输出
动态调试是内核提供的一种强大机制,允许开发者在系统运行时,通过 debugfs 文件系统动态地启用或禁用内核代码中特定的 pr_debug()、dev_dbg() 等调试打印语句,而无需重新编译内核或模块。这在大规模、复杂系统的调试中极为有用。
3.2.1 核心概念与工作流程
CONFIG_DYNAMIC_DEBUG 选项,并且 debugfs 文件系统需要挂载(通常挂载在 /sys/kernel/debug/)。
动态调试的核心文件是 /sys/kernel/debug/dynamic_debug/control。通过向该文件写入特定的查询命令,可以控制匹配到的调试语句的启用状态。
3.2.2 常用控制命令
向 control 文件写入的命令格式为:match_spec [flags]。其中 match_spec 用于匹配调试语句,flags 用于设置状态。
- 匹配格式:
module <模块名>、func <函数名>、file <源文件路径>、line <行号>。 - 标志位:
+p启用打印,-p禁用打印;+f添加函数名,+l添加行号,+t添加线程 ID。
3.2.3 实战示例
# 查看当前所有动态调试点的状态
cat /sys/kernel/debug/dynamic_debug/control
# 启用 usb_storage 模块中所有调试信息
echo "module usb_storage +p" > /sys/kernel/debug/dynamic_debug/control
# 启用特定文件中的调试信息(路径从内核源码根目录开始)
echo "file drivers/usb/storage/transport.c +p" > /sys/kernel/debug/dynamic_debug/control
# 启用特定函数中的调试信息,并同时打印函数名和行号
echo "func usb_stor_invoke_transport +pfl" > /sys/kernel/debug/dynamic_debug/control
# 禁用所有调试信息
echo "module usb_storage -p" > /sys/kernel/debug/dynamic_debug/control
pr_debug()、dev_dbg() 等宏的语句生效。对于普通的 printk(KERN_DEBUG ...) 无效。此外,在生产环境中应谨慎使用,避免大量日志输出导致性能下降。
3.3 printk 索引(index)功能:精准定位日志来源
从 Linux 内核 5.5 版本开始,引入了 printk 索引功能。它为每个 printk() 调用点分配一个唯一的、持久化的索引号。这个索引号在编译时确定,不会因代码行号变化(如添加注释)而改变,为日志的自动化分析和定位提供了极大的便利。
3.3.1 索引的工作原理
在内核编译时,printk 索引子系统会扫描所有 printk() 调用,并为每个调用点生成一个唯一的 u64 类型的索引值。这个索引与调用点的文件名、函数名、行号等信息一起,被记录在一个特殊的 ELF 段(.printk_index)中。
3.3.2 索引的优势与应用场景
| 特性 | 传统方式(行号) | printk 索引 |
|---|---|---|
| 稳定性 | 随代码行号变化(添加/删除注释、空行) | 持久化,不随无关修改变化 |
| 唯一性 | 同一文件内唯一,跨文件可能重复 | 全局唯一 |
| 可读性 | 需要结合源码文件查看 | 可通过工具直接映射到源码位置 |
| 自动化 | 难以被自动化工具稳定引用 | 非常适合日志分析、监控系统集成 |
3.3.3 查看与使用索引
内核编译完成后,可以通过 printk 索引导出工具(如 scripts/printk-index.py)生成索引与源码位置的映射文件。运行时,可以通过 /proc/printk_index 接口查看当前内核的索引信息。
# 在内核源码根目录下,导出索引映射文件
python3 scripts/printk-index.py vmlinux > printk_index.map
# 查看 printk_index.map 文件内容(示例)
# 0x0000000000000001 drivers/base/dd.c:123 driver_probe_device "Probing device %s\n"
# 0x0000000000000002 kernel/printk/printk.c:456 vprintk_emit "printk: console [%s%d] enabled\n"
# 运行时查看当前内核索引(需要 root 权限)
cat /proc/printk_index
printk 索引(格式通常为 #<index>),你可以直接使用 grep 在 printk_index.map 文件中查找该索引,从而立即定位到对应的内核源码文件和行号,无需手动搜索日志字符串。
3.4 本章小结
本章深入探讨了 printk 的三个高级特性:
- 格式化输出 提供了丰富的格式说明符,特别是针对内核数据结构的专用格式符,使日志信息更加清晰、结构化。
- 动态调试 赋予了开发者在运行时动态控制调试信息输出的能力,是调试复杂、难以复现问题的利器。
- printk 索引 为每个
printk调用点提供了稳定、唯一的标识,极大地简化了日志的自动化分析和源码定位工作。
掌握这些高级用法,将使你在内核调试过程中更加得心应手,能够更快速、更精准地定位问题根源。