
大家好,我是情报小哥~
今天给大家分享一下printk的使用技巧
做嵌入式 Linux 驱动,printk 是绕不开的第一课。 内核里几乎每个驱动都在用它打日志, 可日志是打了,能不能打明白,是另一回事。
级别乱用、输出被过滤、pr_debug 打了没反应, 这些问题几乎人人踩过。
先弄清楚一条 printk 从调用到上串口,走了什么路: 格式化消息,写进 log_buf,再交给 console->write() 输出。
看这张图:

(图1:printk 调用流程)
两条铁律先记住。
所有消息都会先写进 log_buf。 不管级别高低,先入环形缓冲区, 就算不上控制台,dmesg 照样翻得出来。 板子启动卡死、串口线没接,日志也一条不丢。
log_buf 多大,由内核配置决定。 CONFIG_LOG_BUF_SHIFT 默认 17,也就是 128KB, 能存最近几千条消息; 想多存点,启动参数加 log_buf_len=1M。
主线里控制台打印是同步的。 慢速串口会拖住调用线程。 5.10 那批改动把 log_buf 改成了无锁实现, 但真正把打印挪到独立线程异步处理, 是 2024 年 nbcon 那批改动之后的事,不少老内核还没有。 所以中断里、热路径上打日志要克制, 后面讲的限速就是干这个的。
读日志的三种方式:
dmesg | tail 看最新几条printk 一共 8 个级别,0 到 7,定义在 <linux/kern_levels.h>。
0 是 KERN_EMERG,系统不可用; 3 是 KERN_ERR,错误; 6 是 KERN_INFO,一般信息; 7 是 KERN_DEBUG,调试信息。 中间的级别看一眼头文件就懂。
关键在控制台的过滤规则:
一条消息要上控制台, 前提是它的级别小于 console_loglevel。
默认 console_loglevel 是 7, 所以级别 0~6 都能上串口, 唯独级别 7 的 KERN_DEBUG 被挡在门外。
你辛辛苦苦打的 pr_debug, 默认配置下根本不会出现在串口上。 "我明明打了日志,为什么看不到", 八成就是栽在这。

(图2:日志级别与控制台过滤)
当前配置怎么看:
$ cat /proc/sys/kernel/printk7 4 1 7# 当前控制台级别 默认消息级别 最低控制台级别 引导时默认级别想把 DEBUG 级别也放出来,改法有好几种:
$ echo 8 > /proc/sys/kernel/printk # 临时改,重启失效$ dmesg -n 8 # 效果一样# 要开机生效,写进内核命令行:loglevel=8直接看代码更直观, 同一条信息,级别不同,命运完全不同:
#include<linux/errno.h>#include<linux/kernel.h>#include<linux/module.h>staticint __init level_demo_init(void){/* 级别 3:默认配置下能上串口 */ printk(KERN_ERR "i2c read failed, err = %d\n", -EIO);/* 级别 7:不满足"< 7",只在 dmesg 里翻得到 */ printk(KERN_DEBUG "chip id = 0x%x\n", 0x42);return0;}staticvoid __exit level_demo_exit(void){}module_init(level_demo_init);module_exit(level_demo_exit);MODULE_LICENSE("GPL");一个忠告: 真正要紧的信息用 pr_err、pr_warn 打, 别都塞到 DEBUG 级别。 级别设错了,日志等于白打。
pr_xxx 就是一组包装宏, pr_info 等于 printk(KERN_INFO ...), pr_err 等于 printk(KERN_ERR ...)。
dev_xxx 更进一步,自动带上驱动名和设备名, 日志里一眼看出是哪个设备在说话。
pr_debug 和 dev_dbg 比较特殊: 默认编译进去是空操作, 除非你定义 DEBUG,或者开启动态调试。
内核开了 CONFIG_DYNAMIC_DEBUG=y 之后, 所有 pr_debug/dev_dbg 调用点的信息(文件、函数、行号) 都会被编译登记,存到 debugfs 里。 运行时往控制文件里写一条命令, 就能精准打开某几个调试点, 不用重编内核,不用重新烧板子。
$ mount -t debugfs debugfs /sys/kernel/debug# 按文件打开$ echo'file drivers/i2c/motor_drv.c +p' > /sys/kernel/debug/dynamic_debug/control# 按行打开$ echo'file drivers/i2c/motor_drv.c line 123 +p' > /sys/kernel/debug/dynamic_debug/control# 按模块打开$ echo'module motor_drv +p' > /sys/kernel/debug/dynamic_debug/control# 想关,把 +p 换成 -p
(图3:动态调试原理)
再说个好用的:pr_fmt。 在文件头部定义一次,所有 pr_* 输出自动带上函数名和行号:
/* 必须在 include 之前定义 */#define pr_fmt(fmt) "%s:%d: " fmt, __func__, __LINE__#include<linux/kernel.h>排查问题的时候,这条前缀能省太多事。
还有两类场景得单独说:太早,和太多。
太早。 系统启动初期,console 还没初始化, 普通 printk 进了 log_buf 却无处可去。 想早点看到启动日志,就得靠 earlycon:
earlycon=uart8250,mmio32,0x9000000x86 上还有更早的 earlyprintk,写法类似。 等正式 console 注册好,earlycon 会自动让位。 调启动早期死机、卡启动这类问题,这个真救命。
太多。 中断里、错误高频发生的热路径上, 每个中断都 printk 一下, 串口这种慢速外设根本扛不住, 日志刷屏不说,系统都被拖慢。
这种地方就得限速:
/* 中断处理函数内:硬件上报了一个无效中断 */if (irq_status & SPURIOUS_IRQ) { dev_err_ratelimited(&client->dev,"spurious irq, status = 0x%x\n", irq_status);}dev_err_ratelimited 默认每 5 秒最多放 10 条通过, 参数在 /proc/sys/kernel/printk_ratelimit 和 printk_ratelimit_burst 里调。 日志不刷屏了,关键的错误信息也留下来了。
星标公众号,第一时间看文章!
小哥搜集了一些嵌入式学习资料,公众号内回复【1024】即可找到下载链接!
推荐好文点击蓝色字体即可跳转
☞专辑|Linux应用程序编程大全 ☞ 专辑|学点网络知识 ☞ 专辑|手撕C语言 ☞ 专辑|手撕C++语言 ☞ 专辑|经验分享 ☞ 专辑|从单片机到Linux ☞ 专辑|电能控制技术 ☞ 专辑|嵌入式必备数学知识 ☞ MCU进阶专辑
☞ 经验分享