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 从调用到控制台显示的完整路径:
控制台输出机制的核心参数是 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(如中断处理函数中)会严重拖慢系统,生产环境应关闭或降级。