4. printk 基础用法:日志级别与格式化输出

在嵌入式 Linux 内核调试中,printk 是最直接、最可靠的输出手段。与用户空间的 printf 不同,printk 需要显式指定日志级别,并支持针对设备树节点的专用格式化符号 %pOF。本章将系统讲解这两项核心技能,为后续跟踪 of_ 函数调用路径打下基础。

调试原则: 在设备树解析路径中,优先使用 printk 而非 dev_dbg 或 pr_debug,因为后者可能被动态调试机制关闭,而 printk 在大多数内核配置下始终可用。

4.1 日志级别:控制消息的“优先级”

每个 printk 调用都带有一个日志级别宏,它决定了消息是否显示在控制台以及写入系统日志的紧急程度。内核使用 前缀来标记级别,级别数字越小,优先级越高。

日志级别宏 数值 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件错误、内核崩溃前
KERN_ALERT 1 必须立即处理 严重设备故障、内存耗尽
KERN_CRIT 2 临界条件 驱动初始化失败、DMA 错误
KERN_ERR 3 错误条件 设备树解析失败、of 函数返回 NULL
KERN_WARNING 4 警告,可能有问题 设备树属性缺失但可降级运行
KERN_NOTICE 5 正常但重要的消息 设备树节点匹配成功、驱动绑定
KERN_INFO 6 信息性消息 打印设备树节点名称、寄存器地址
KERN_DEBUG 7 调试消息 跟踪 of 函数内部路径、变量中间值
设备树调试中的级别选择:
  • KERN_ERR:用于 of_ 函数返回 NULL、of_property_read_* 失败等不可恢复的错误。
  • KERN_DEBUG:用于跟踪 of_find_node_by_name 的遍历路径、of_get_property 的返回值等调试细节。
  • KERN_INFO:用于打印成功解析到的节点全路径(配合 %pOF)。
printk 日志级别过滤流程 printk(KERN_xxx, ...) console_loglevel >= msg_level ? (控制台日志级别过滤) 否 是 丢弃消息 输出到控制台 写入内核日志缓冲区 说明:KERN_DEBUG(7) 消息需要将 console_loglevel 设为 ≥7 才能在控制台看到 可通过 echo 8 > /proc/sys/kernel/printk 临时提升控制台日志级别

4.2 格式化输出:从基础到设备树专用

printk 的格式化语法与 printf 基本一致,但在内核调试中,有几个格式符需要特别掌握,尤其是针对设备树的 %pOF。

4.2.1 基础格式符回顾

格式符 类型 示例 输出
%s 字符串 printk(KERN_INFO "name = %s", np->name); name = uart0
%d / %i 有符号十进制整数 printk(KERN_DEBUG "irq = %d", irq); irq = 42
%u 无符号十进制整数 printk(KERN_INFO "size = %u", size); size = 4096
%x / %X 十六进制整数 printk(KERN_ERR "reg = 0x%x", reg); reg = 0xffe0
%p 指针地址 printk(KERN_DEBUG "np = %p", np); np = 00000000c1234567
%s + of_node_full_name() 节点全路径(旧方式) printk(KERN_INFO "%s", of_node_full_name(np)); /soc/uart@ffe00000

4.2.2 设备树专用格式符:%pOF

%pOF 是内核为设备树节点提供的专用格式化扩展,它可以直接接收一个 struct device_node * 指针,自动输出节点的全路径。相比手动调用 of_node_full_name(),%pOF 更简洁、更安全,且在内核 4.x 之后广泛支持。

推荐用法:
struct device_node *np;
np = of_find_node_by_name(NULL, "uart0");
if (np) {
    printk(KERN_INFO "找到节点: %pOF\n", np);   // 输出: /soc/uart@ffe00000
    printk(KERN_ERR "节点 %pOF 解析失败\n", np); // 错误时也打印路径
    of_node_put(np);
}

%pOF 的额外优势:

  • 自动处理 NULL 指针:如果传入 NULL,输出 (null),不会崩溃。
  • 与日志级别无缝配合:可以在 KERN_ERR、KERN_DEBUG 等任何级别中使用。
  • 可读性强:直接输出类似 /soc/i2c@ffd00000/rtc@51 的完整路径,便于定位。
避坑提醒: 不要对 %pOF 传入 of_node_get 后的临时指针而不释放,否则会导致节点引用计数泄漏。始终在打印后配对 of_node_put。
%pOF 格式化输出内部流程 printk("%pOF", np) struct device_node *np np == NULL ? 安全检查 输出 "(null)" of_node_full_name(np) 获取全路径字符串 输出 "/soc/uart@..." 说明:%pOF 内部自动调用 of_node_full_name(),并处理 NULL 指针异常 无需手动调用 of_node_full_name(),减少代码冗余和出错概率

4.3 实战组合:在 of 函数路径中使用 printk

将日志级别与格式化输出结合,可以构建出清晰的调试信息。以下是一个在设备树节点查找函数中插入 printk 的典型模式:

struct device_node *np;
int ret;

np = of_find_node_by_name(NULL, "uart0");
if (!np) {
    printk(KERN_ERR "of_find_node_by_name: 未找到节点 uart0\n");
    return -ENODEV;
}
printk(KERN_INFO "成功找到节点: %pOF\n", np);

/* 读取属性 */
ret = of_property_read_u32(np, "reg", ®_val);
if (ret) {
    printk(KERN_ERR "节点 %pOF 读取 reg 属性失败, ret=%d\n", np, ret);
    of_node_put(np);
    return -EINVAL;
}
printk(KERN_DEBUG "节点 %pOF reg = 0x%x\n", np, reg_val);

of_node_put(np);
调试技巧: 在开发阶段,可以将 KERN_DEBUG 消息临时改为 KERN_INFO 或 KERN_NOTICE,以便在默认日志级别下直接看到调试输出。调试完成后改回 KERN_DEBUG 并提交。

4.4 日志级别控制:让调试消息可见

即使代码中使用了 KERN_DEBUG,如果当前控制台日志级别低于 7,消息也不会显示在终端上。以下是常用的控制方法:

方法 命令 说明
查看当前级别 cat /proc/sys/kernel/printk 输出 4 个数字:控制台级别、默认级别、最低级别、启动时级别
临时提升级别 echo 8 > /proc/sys/kernel/printk 使所有级别(包括 KERN_DEBUG)都输出到控制台
启动参数设置 loglevel=8 在 bootargs 中添加,开机即生效
忽略级别输出 echo "file drivers/of/* +p" > /sys/kernel/debug/dynamic_debug/control 动态调试,仅适用于 pr_debug/dev_dbg
生产环境注意: 不要在生产内核中长时间开启 loglevel=8,大量 KERN_DEBUG 消息会拖慢系统并填满日志缓冲区。调试完成后应恢复默认级别(通常为 4 或 7)。