Linux驱动调试系列 · 第 01 篇
驱动调试第一课,从 printk 开始。这是内核中最基础、最常用的打印函数,几乎所有驱动工程师的调试之旅都从它起步。本文从工作原理、日志级别、实战命令到常见踩坑,帮你把 printk 用到位。
感兴趣 Linux 底层驱动可以私信我:瑞芯微 Linux 驱动一对一答疑辅导。
一、printk 是什么?
printk 是 Linux 内核提供的日志打印函数,类似于用户空间的 printf。它是 Kernel Log 机制的基础 API,几乎所有内核调试都从它开始。
与 printf 不同的是,printk 运行在内核态,可以在中断上下文、进程上下文中自由使用。它不依赖 glibc 等用户态运行环境,也不需要用户态内存分配,是驱动开发中最基础也最重要的调试手段。
1.1 为什么不用 printf?
| 对比项 | printf | printk |
|---|---|---|
| 运行空间 | 用户态 | 内核态 |
| 可调用上下文 | 进程上下文 | 中断/进程上下文均可 |
| 输出目标 | stdout | 内核 Log Buffer / Console |
| 是否阻塞 | 可能 | 写入缓冲区是原子的,输出到 Console 时可能阻塞 |
| 格式化能力 | 完整 | 类似,但 %s 等指针需内核地址 |
注意:printk 与 printf 的格式化不完全等价。printk 不支持浮点数,指针打印用
%p(默认会哈希,如需原始地址用%px,但生产环境慎用)。
二、printk 工作流程
printk 将日志输出到 Log Buffer(循环缓冲区),然后根据日志级别决定是否输出到 Console(控制台)。dmesg 命令读取的就是这个 Log Buffer,而控制台输出还要受 console_loglevel 控制。
# 查看当前 Log Buffer 中所有级别的消息
$ dmesg
# 实时跟踪新日志(类似 tail -f)
$ dmesg -w
# 只显示 err 及以上级别的日志
$ dmesg -l err,crit,alert,emerg
小技巧:调试驱动时通常先 dmesg -w 再加载/操作设备,这样能第一时间看到 printk 输出。
三、8 个日志等级
printk 支持 8 个日志等级,数值越小级别越高(越紧急)。只有数值严格小于控制台级别(console_loglevel)的消息才会输出到控制台;等于或大于的只写入 Log Buffer,需用 dmesg 查看。
| 等级 | 宏定义 | 数值 | 说明 |
|---|---|---|---|
| 紧急 | KERN_EMERG |
0 | 紧急事件,系统崩溃前的提示 |
| 警报 | KERN_ALERT |
1 | 必须立即采取行动 |
| 临界 | KERN_CRIT |
2 | 临界状态 |
| 错误 | KERN_ERR |
3 | 错误状态,常用于硬件错误 |
| 警告 | KERN_WARNING |
4 | 警告信息 |
| 注意 | KERN_NOTICE |
5 | 正常但重要的消息 |
| 信息 | KERN_INFO |
6 | 一般信息 |
| 调试 | KERN_DEBUG |
7 | 调试信息 |
3.1 推荐写法:pr_* 宏
直接写 printk(KERN_ERR ...) 虽然可行,但容易漏写级别或拼错宏。更规范的做法是使用内核封装好的 pr_* 系列宏:
pr_emerg("系统不可用n");
pr_alert("需要立即处理n");
pr_crit("严重错误n");
pr_err("操作失败: %dn", ret);
pr_warn("发生异常但可继续n");
pr_notice("重要状态n");
pr_info("普通信息n");
pr_debug("调试信息n"); // 默认编译时不输出
pr_debug()默认不会输出,只有开启CONFIG_DYNAMIC_DEBUG或DEBUG宏时才生效,下一篇会细讲。
四、控制台日志级别
通过 /proc/sys/kernel/printk 可以查看和修改控制台日志级别,这个文件有 4 个值:
$ cat /proc/sys/kernel/printk
4 4 1 7
| 位置 | 含义 | 默认值 |
|---|---|---|
| 第1个 | 控制台日志级别(数值小于它的消息才会打印到控制台) | 4 |
| 第2个 | 默认消息日志级别(printk 未指定级别时使用) | 4 |
| 第3个 | 最低的控制台日志级别 | 1 |
| 第4个 | 启动时默认的控制台日志级别 | 7 |
修改控制台级别:
# 将控制台级别改为 8,让所有级别(含 debug)都输出到控制台
$ echo 8 > /proc/sys/kernel/printk
# 等价的另一种写法(dmesg -n 与写 /proc/sys/kernel/printk 等效)
$ dmesg -n 8
改动
/proc/sys/kernel/printk只在当前运行环境生效,重启后恢复默认值。如需永久生效,可在内核启动参数里加loglevel=8。
五、printk 使用示例
基本用法:
#include <linux/kernel.h>
// 带级别打印
printk(KERN_ERR "这是一条错误信息n");
printk(KERN_INFO "这是一条信息: %dn", value);
// 推荐使用内核封装的宏
pr_err("错误信息n");
pr_warn("警告信息n");
pr_info("普通信息n");
pr_debug("调试信息n");
常用格式化符:
| 格式化符 | 类型 | 说明 |
|---|---|---|
%d / %ld |
int / long | 十进制有符号整数 |
%u / %lu |
unsigned int / long | 十进制无符号整数 |
%x / %lx |
unsigned int / long | 十六进制小写 |
%p |
指针 | 内核指针地址(已哈希,安全) |
%px |
指针 | 原始地址(生产环境慎用) |
%s |
字符串 | 字符串,必须是内核地址 |
%zu |
size_t | 无符号大小 |
六、实战建议
先看 dmesg,再看代码:驱动出问题,第一反应不是加打印,而是 dmesg -w 复现。很多硬件错误、段错误、资源冲突已经被内核打印出来。
少而精地打印关键路径:在 probe、remove、中断处理函数、关键寄存器读写处放 pr_info() 或 pr_debug(),追踪执行流即可,不要大面积刷屏。
善用日志级别:错误用 pr_err(),状态变更用 pr_info(),调试细节用 pr_debug()。级别用对了,生产日志不会变得无法阅读。
清理发布版本:发布前用 grep -nE "pr_debug|printk" 检查,移除或降级明显是调试用的打印,避免性能开销和敏感信息泄露。
七、常见踩坑
Log Buffer 被覆盖:默认缓冲区 128KB 或更大,在高频 printk 场景下旧日志会被快速覆盖。调试关键问题时应及时 dmesg > log.txt 保存。
printk 导致时序变化:在竞态条件、中断频繁的地方,printk 的延迟会让 bug 无法复现,必要时改用 trace_printk() 或 ftrace。
%s 打印用户态指针可能 oops:printk 的格式化函数运行在内核态,%s 只能打印内核地址。传入用户态地址可能触发缺页甚至 oops(取决于架构与 SMAP/PAN),需先用 copy_from_user 拷贝到内核缓冲区后再打印。
浮点数禁用:printk 不支持 %f,内核态也不建议进行浮点运算。
中文字符输出乱码:如果串口终端编码与内核日志编码不一致,中文会显示为乱码。调试输出尽量使用 ASCII 英文。
八、面试考点
printk 与 printf 的本质区别是什么?
/proc/sys/kernel/printk
四个字段分别是什么含义?为什么推荐使用 pr_err()、pr_info()、pr_debug() 而不是直接 printk()?为什么 pr_debug() 默认看不到输出?如何开启?在 ISR(中断上下文)里能调用 printk 吗?有什么风险?printk 支持浮点数打印吗?为什么?
九、下篇预告
下一篇介绍 dynamic debug(动态打印),让你在不重新编译、不修改代码的情况下,随时开关某个文件甚至某一行 printk/pr_debug 的输出,是生产环境中排查问题的利器。
驱动调试系列导航
第01篇:printk 内核打印 →当前· 第02篇:dynamic debug 动态打印 · 第03篇:内核调用栈调试 · 第04篇:休眠唤醒 standby 调试 · 第05篇:access_ok 地址检测 · 第06篇:分支预测优化 · 第07篇:procfs 虚拟文件系统 · 第08篇:debugfs 调试文件系统 · 第09篇:devmem 直接读写寄存器 · 第10篇:Linux 性能工具大全 · 第11篇:CPU 使用率详解 · 第12篇:perf 性能优化
144