2. printk 基础:函数原型与日志级别详解
printk 是 Linux 内核中最基础、最常用的调试输出函数,其地位等同于用户空间的 printf。在驱动开发和内核调试中,printk 是定位问题的第一道防线。理解其函数原型和日志级别机制,是高效使用 printk 的前提。
2.1 printk 函数原型
printk 的定义位于 include/linux/printk.h,其核心原型如下:
// 标准原型
int printk(const char *fmt, ...);
// 带日志级别的宏定义(推荐使用)
#define printk(fmt, ...) \
printk(KERN_DEFAULT fmt, ##__VA_ARGS__)
printk 与用户空间的 printf 在格式化字符串方面完全兼容,支持 %d、%s、%x、%p 等常用格式说明符。关键区别在于:printk 的第一个参数通常是一个日志级别宏(如 KERN_INFO),该宏与格式化字符串之间没有逗号,直接拼接。
⚠️ 常见错误: 新手常写成
printk(KERN_INFO, "msg");(多了一个逗号),这会导致日志级别被当作普通字符串输出,且无法被内核日志系统正确解析。
2.2 日志级别详解
Linux 内核定义了 8 个日志级别,从最高优先级 KERN_EMERG 到最低优先级 KERN_DEBUG。每个级别对应一个数字(0-7),数字越小,优先级越高。
| 宏名称 | 数值 | 含义 | 典型使用场景 |
|---|---|---|---|
KERN_EMERG |
0 | 紧急事件,系统不可用 | 致命硬件故障、内核 panic 前兆 |
KERN_ALERT |
1 | 必须立即处理 | 严重数据损坏、关键资源耗尽 |
KERN_CRIT |
2 | 临界条件 | 驱动初始化失败、DMA 传输错误 |
KERN_ERR |
3 | 错误条件 | 设备响应超时、寄存器读写异常 |
KERN_WARNING |
4 | 警告,可能存在问题 | 降级模式运行、非关键配置缺失 |
KERN_NOTICE |
5 | 正常但重要的事件 | 设备热插拔、模块加载/卸载 |
KERN_INFO |
6 | 信息性消息 | 驱动版本号、硬件参数枚举 |
KERN_DEBUG |
7 | 调试信息 | 函数调用跟踪、变量中间值 |
💡 最佳实践: 生产环境驱动中,
KERN_DEBUG 级别的输出应通过 pr_debug() 或 dev_dbg() 宏控制,这些宏在非调试编译时会被完全移除,避免性能开销。
2.3 日志级别与控制台过滤
内核通过 console_loglevel 变量控制哪些级别的消息可以输出到控制台。只有日志级别数值小于当前控制台级别的消息才会被显示。
// 查看当前控制台日志级别
$ cat /proc/sys/kernel/printk
7 4 1 7
// 字段含义:控制台级别 | 默认消息级别 | 最低控制台级别 | 默认控制台级别
// 临时设置控制台级别为 8(显示所有消息)
$ echo 8 > /proc/sys/kernel/printk
四个字段的详细说明:
- 字段1(控制台级别): 当前控制台可显示的最高日志级别数值。设为 7 时,只有
KERN_DEBUG(7)及更高优先级(数值更小)的消息才能输出。 - 字段2(默认消息级别): 当 printk 未显式指定日志级别时,消息默认使用的级别。
- 字段3(最低控制台级别): 控制台级别可设置的最小值,防止完全关闭内核输出。
- 字段4(默认控制台级别): 系统启动时的初始控制台级别。
2.4 日志级别选择策略
在驱动开发中,合理选择日志级别能显著提升调试效率。以下是一个基于场景的选择指南:
- 致命错误: 使用
KERN_CRIT或KERN_ALERT,例如 DMA 引擎无法初始化、中断号冲突。 - 可恢复错误: 使用
KERN_ERR,例如设备返回错误状态码、超时重试。 - 异常但非错误: 使用
KERN_WARNING,例如使用降级模式、固件版本不匹配。 - 正常流程关键点: 使用
KERN_INFO,例如驱动加载成功、设备发现。 - 详细调试: 使用
KERN_DEBUG,并通过pr_debug()宏实现编译时开关。
🚨 性能警告: 在中断上下文或热路径(如网络收包、块设备 IO 完成)中使用 printk 会带来严重的性能下降。printk 内部会获取 spinlock 并可能触发控制台输出,导致延迟抖动。高频调试应优先使用 tracepoint 或 ftrace。
2.5 printk 日志处理流程
下图展示了 printk 从调用到最终输出的完整路径,帮助理解日志级别在其中的作用:
2.6 实际驱动代码示例
以下是一个虚拟网卡驱动中使用 printk 的典型片段,展示了不同日志级别的应用:
#include <linux/module.h>
#include <linux/netdevice.h>
static int virt_net_open(struct net_device *dev)
{
// 正常信息:驱动启动
printk(KERN_INFO "%s: 网卡启动,MAC=%pM\n", dev->name, dev->dev_addr);
// 尝试分配 DMA 缓冲区
if (!alloc_dma_buffer(dev)) {
// 严重错误:无法分配关键资源
printk(KERN_CRIT "%s: DMA 缓冲区分配失败,设备不可用\n", dev->name);
return -ENOMEM;
}
// 调试信息:仅在调试时关注
printk(KERN_DEBUG "%s: 寄存器基址=0x%lx\n", dev->name, dev->base_addr);
return 0;
}
static void virt_net_tx_timeout(struct net_device *dev)
{
// 警告:发送超时,但驱动会尝试恢复
printk(KERN_WARNING "%s: 发送超时,启动恢复流程\n", dev->name);
reset_hardware(dev);
}
🔧 调试技巧: 在开发阶段,可以将
/proc/sys/kernel/printk 设置为 8 4 1 7,确保所有级别的消息(包括 KERN_DEBUG)都能输出到控制台。发布前再恢复为 4 4 1 7,只显示 KERN_WARNING 及以上级别的消息。
2.7 日志级别与 pr_* 系列宏
为简化书写,内核提供了 pr_* 系列宏,自动附加对应的日志级别:
| 宏 | 等价形式 | 日志级别 |
|---|---|---|
pr_emerg() |
printk(KERN_EMERG ...) |
0 |
pr_alert() |
printk(KERN_ALERT ...) |
1 |
pr_crit() |
printk(KERN_CRIT ...) |
2 |
pr_err() |
printk(KERN_ERR ...) |
3 |
pr_warn() |
printk(KERN_WARNING ...) |
4 |
pr_notice() |
printk(KERN_NOTICE ...) |
5 |
pr_info() |
printk(KERN_INFO ...) |
6 |
pr_debug() |
printk(KERN_DEBUG ...) |
7 |
使用 pr_* 宏的优势在于:代码更简洁,且 pr_debug() 在未定义 DEBUG 宏时会被编译为空,零运行时开销。
// 推荐写法:使用 pr_* 宏
pr_info("网卡 %s 已注册\n", dev->name);
pr_err("DMA 分配失败,错误码=%d\n", ret);
// 不推荐:手动拼接日志级别(容易遗漏逗号)
printk(KERN_INFO "网卡 %s 已注册\n", dev->name);
2.8 本章小结
- printk 是内核调试的基础工具,其函数原型与 printf 兼容,但日志级别宏必须紧贴格式化字符串。
- 8 个日志级别(0-7)覆盖从系统崩溃到详细调试的全部场景,选择合适级别是专业驱动的标志。
- 控制台日志级别
console_loglevel决定哪些消息能实时输出,所有消息均会进入环形缓冲区。 - 优先使用
pr_*系列宏,特别是pr_debug()用于调试输出,避免生产环境性能损耗。