2. printk 基础:函数原型、日志级别与控制台输出机制

printk 是 Linux 内核中最基础、最常用的调试输出函数,其地位相当于用户空间的 printf。在 Android 内核驱动开发中,printk 是定位崩溃、跟踪流程、验证状态的“第一把钥匙”。本节将深入剖析 printk 的函数原型、日志级别体系以及控制台输出机制,为后续的动态调试打下坚实基础。

2.1 printk 函数原型

printk 的定义位于 include/linux/printk.h,其核心原型如下:

// 标准原型(可变参数)
int printk(const char *fmt, ...);

// 带日志级别的宏定义(实际使用形式)
#define printk(fmt, ...) \
    printk(KERN_DEFAULT fmt, ##__VA_ARGS__)

关键特性说明:

  • 可变参数:与 printf 完全一致的格式化语法,支持 %d、%s、%p(指针)、%lx 等。
  • 无返回值检查:printk 返回写入的字符数,但驱动中通常忽略返回值。
  • 日志级别前缀:通过宏定义在 fmt 前拼接级别字符串,如 printk(KERN_ERR "DMA failed: %d\n", ret);。
  • 原子安全:printk 可在中断上下文、自旋锁保护区域调用,不会导致死锁。
实战提示:printk 的格式化字符串中,%p 扩展支持多种内核专用格式:%pK(隐藏敏感指针)、%pI4(IP 地址)、%pV(设备树节点)。在驱动调试中,%p 直接打印指针地址是最常用的操作。

2.2 日志级别体系(KERN_EMERG ~ KERN_DEBUG)

printk 定义了 8 个日志级别,从最高紧急度到最低调试信息。每个级别对应一个字符串宏,在输出时拼接在消息头部。下表列出了所有级别及其含义:

宏定义 级别值 字符串 含义 典型场景
KERN_EMERG 0 "<0>" 紧急事件,系统不可用 内核 panic、致命硬件故障
KERN_ALERT 1 "<1>" 必须立即处理 严重数据损坏、关键资源耗尽
KERN_CRIT 2 "<2>" 临界条件 驱动初始化失败、DMA 超时
KERN_ERR 3 "<3>" 错误条件 设备 I/O 错误、内存分配失败
KERN_WARNING 4 "<4>" 警告条件 降级模式、非关键配置异常
KERN_NOTICE 5 "<5>" 正常但重要 驱动加载/卸载、电源状态切换
KERN_INFO 6 "<6>" 信息性消息 硬件版本识别、寄存器转储
KERN_DEBUG 7 "<7>" 调试信息 函数进入/退出、变量中间值
避坑指南:在 Android 生产内核中,KERN_DEBUG 级别的消息默认被编译时过滤(通过 CONFIG_DYNAMIC_DEBUG 或 CONFIG_DEBUG_KERNEL 控制)。如果驱动中大量使用 printk(KERN_DEBUG ...) 而看不到输出,请检查内核配置是否启用了调试支持。

2.3 控制台输出机制

printk 的输出并非直接写入终端,而是经过一套完整的消息管道。理解这个管道对调试至关重要。下图展示了 printk 从调用到控制台显示的完整路径:

printk 控制台输出机制流程图 驱动调用 printk(fmt, ...) 格式化字符串 + 提取日志级别 写入环形日志缓冲区 (__log_buf) console_loglevel 过滤:级别 ≤ 控制台级别? 是(通过过滤) 调用 console->write() 输出到终端 否(丢弃) 说明:console_loglevel 默认值通常为 7(包含 KERN_DEBUG),但 Android 产品内核可能设为 4(仅 KERN_ERR 及以上)

控制台输出机制的核心参数是 console_loglevel,它决定了哪些级别的消息能够真正显示在终端上。其判断逻辑为:

// 伪代码逻辑
if (msg_level < console_loglevel) {
    // 消息级别数值小于控制台级别(数值越小越紧急)
    // 例如:KERN_ERR (3) < console_loglevel (4) → 输出
    call_console_drivers(msg);
} else {
    // 消息被丢弃,仅保留在环形缓冲区
}
关键理解:日志级别的数值越小,紧急度越高,越容易被输出。例如,KERN_EMERG (0) 总是能通过过滤,而 KERN_DEBUG (7) 在大多数产品内核中被屏蔽。在调试驱动时,可以通过 echo "8 4 1 7" > /proc/sys/kernel/printk 临时提升控制台级别以查看调试信息。

2.4 实际使用示例

以下是一个典型的 Android 内核驱动调试片段,展示了不同级别 printk 的使用方式:

#include <linux/printk.h>

static int my_driver_probe(struct platform_device *pdev)
{
    int ret;
    struct resource *res;

    // 驱动加载通知(KERN_INFO)
    printk(KERN_INFO "my_driver: probe started\n");

    // 获取硬件资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        // 严重错误(KERN_ERR)
        printk(KERN_ERR "my_driver: failed to get memory resource\n");
        return -ENODEV;
    }

    // 寄存器映射
    void __iomem *base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
    if (!base) {
        // 临界错误(KERN_CRIT)
        printk(KERN_CRIT "my_driver: ioremap failed for region 0x%llx\n",
               (unsigned long long)res->start);
        return -ENOMEM;
    }

    // 读取硬件版本(KERN_DEBUG)
    u32 version = readl(base + 0x00);
    printk(KERN_DEBUG "my_driver: hardware version = 0x%08x\n", version);

    // 正常但重要的状态(KERN_NOTICE)
    printk(KERN_NOTICE "my_driver: probe completed successfully\n");

    return 0;
}

2.5 控制台级别动态调整

在 Android 设备上,可以通过 /proc/sys/kernel/printk 文件实时调整控制台输出行为。该文件包含 4 个整数值:

字段位置 名称 默认值 说明
1 console_loglevel 7 (或 4) 当前控制台可显示的最高级别(数值)
2 default_message_loglevel 4 未指定级别时的默认级别
3 minimum_console_loglevel 1 控制台级别可设置的最小值
4 default_console_loglevel 7 系统启动时的默认控制台级别

常用调试命令:

# 查看当前 printk 参数
adb shell cat /proc/sys/kernel/printk
# 输出示例:7 4 1 7

# 临时开启所有调试信息(包括 KERN_DEBUG)
adb shell "echo '8 4 1 7' > /proc/sys/kernel/printk"

# 仅显示 KERN_ERR 及以上级别(生产模式)
adb shell "echo '4 4 1 7' > /proc/sys/kernel/printk"
安全提醒:在 Android 用户设备上,/proc/sys/kernel/printk 通常受 SELinux 策略限制,只有 root 权限或 system 域进程才能修改。在调试工程机时,建议先执行 adb root 获取 root 权限。

2.6 本节总结

printk 是内核调试的基石,掌握其函数原型、8 级日志体系和控制台过滤机制,是高效定位驱动问题的前提。核心要点:

  • 级别选择:驱动开发中,KERN_ERR 用于错误路径,KERN_INFO 用于关键状态,KERN_DEBUG 用于详细跟踪。
  • 过滤规则:消息级别数值 ≤ console_loglevel 时输出;数值越小越紧急。
  • 动态控制:通过 /proc/sys/kernel/printk 可在运行时调整输出粒度,无需重新编译内核。
  • 性能影响:高频 printk(如中断处理函数中)会严重拖慢系统,生产环境应关闭或降级。