3.3 利用 printk 进行调试
你可能觉得,向内核日志输出一条调试消息很简单——不就是发一条 KERN_DEBUG 级别的 printk 嘛。直觉没错,但现实比这复杂得多。
在内核开发者眼里,printk 只是冰山一角。真正的利器是 pr_debug() 和 dev_dbg()。当内核的“动态调试”特性开启时,这两个宏就不再是简单的打印语句,而是可控的调试开关。我们很快就会在后面的章节里见识到这个功能的强大之处。
不过,在深入那个“杀手级”功能之前,我们先要把基础打牢:如何正确地发出一条调试打印?有哪些进阶技巧能让我们的调试效率翻倍?
基础调试打印:pr_debug 与 pr_devel
回到上一节的那个 printk_loglevels 内核模块,你可能注意到了这两行代码:
pr_debug("Hello, debug world @ log-level KERN_DEBUG [%d]\n", LOGLEVEL_DEBUG);
pr_devel("Hello, debug world via the pr_devel() macro (eff @ KERN_DEBUG) [%d]\n", LOGLEVEL_DEBUG);
这两个宏看起来很像,它们确实都在 KERN_DEBUG 级别上向内核日志发送消息。但有一个巨大的前提:只有当代码编译时定义了 DEBUG 符号,它们才会真的输出东西;否则,它们就像空气一样——完全静默。
这正是我们想要的:
- 开发时,定义
DEBUG,信息铺天盖地。 - 发布时,去掉
DEBUG,内核保持安静,二进制文件里甚至不会留下这些字符串的痕迹。
⚠️ 注意
pr_devel() 这个宏主要是给内核内部开发用的。模块作者应该尽量使用 pr_debug()。pr_devel() 的设计初衷是:这些信息永远不应该出现在生产环境里。
那个 DEBUG 符号是在哪定义的?
如果你翻遍了源代码却找不到 DEBUG 的定义,别慌。它通常不在代码里,而是在 Makefile 里。
打开我们示例模块的 Makefile,答案就在这里(关键部分已高亮):
$ cd ch3/printk_loglevels ; cat Makefile
[ ... ]
FNAME_C := printk_loglevels
PWD := $(shell pwd)
obj-m += ${FNAME_C}.o
# EXTRA_CFLAGS deprecated; use ccflags-y
ccflags-y += -DDEBUG -g -ggdb -gdwarf-4 -Og -Wall -fno-omit-frame-pointer -fvar-tracking-assignments
[ ... ]
看这一行:ccflags-y += -DDEBUG ...。
-DDEBUG:告诉编译器,“定义DEBUG这个符号”。-UDEBUG:反之,如果你看到这个,意思是“取消定义DEBUG”。
这就是开关。当你准备发布模块时,只需把 -DDEBUG 去掉,或者像我在 Makefile 里做的那样,通过一个变量(比如 MYDEBUG)来控制它。
Debug Build vs. Production Build
这里有一个很容易踩的坑。
你的模块是怎么构建出来的,很大程度上取决于内核顶层 Makefile 里的 DEBUG_CFLAGS。这个变量的值又依赖于内核配置项 CONFIG_DEBUG_INFO。
- 在 Debug 内核上:如果你启动的是带调试信息的内核,那么编译模块时,各种符号信息、KASAN 插桩等都会自动加进去。
- 在 Production 内核上:如果在生产内核上构建,这些东西全都会被剥离。
数据不会骗人。同一个 printk_loglevels.ko:
- 在 Debug 内核上构建:221 KB。
- 在 Production 内核上构建:不到 8 KB! (巨大的体积差正是源于那些被剥离掉的调试符号和插桩。)
💡 小技巧
-
make V=1:想看清到底传给编译器了哪些参数?跑这个命令,信息量巨大。 -
readelf:想确认你的模块到底带了哪些调试信息?用这个工具看一眼:readelf --debug-dump <module_dbg.ko> | grep producer(注意:交叉编译环境下,记得用工具链里的
${CROSS_COMPILE}readelf。)
驱动开发者的最爱:dev_dbg()
现在,假设你在写一个真正的设备驱动。
有一个黄金法则:别再用 pr_debug() 了,用 dev_dbg()。
为什么?dev_dbg() 的第一个参数是 struct device *dev。这个指针不仅能让内核知道是哪个设备在说话,还能自动在日志里加上设备名、总线信息等上下文。这在有多个同类设备的系统里简直是救命稻草。
以内核里的 SSD1307 OLED 屏幕驱动为例(这种屏幕通常接在 I2C 总线上):
// drivers/video/fbdev/ssd1307fb.c
static int ssd1307fb_init(struct ssd1307fb_par *par)
{
[...]
/* Enable the PWM */
pwm_enable(par->pwm);
dev_dbg(&par->client->dev, "Using PWM%d with a %lluns period.\n",
par->pwm->pwm, pwm_get_period(par->pwm));
}
看,par->client->dev 就是那个设备指针。日志里自然就会带上“这个 I2C 设备”的信息。
还有一个现成的例子,内核的网络块设备驱动:
// drivers/block/nbd.c
dev_dbg(nbd_to_dev(nbd), "request %p: got reply\n", req);
这里的 nbd_to_dev() 是个内联函数,专门从 nbd_device 结构体里把嵌入的 device 结构体指针抠出来。
⚠️ 再次强调
写驱动时,把所有的 pr_*() 都换成对应的 dev_*():
dev_emerg(), dev_err(), dev_warn(), dev_notice(), dev_info(), dev_dbg()。
生产环境下的“闭门羹”
我们来做个实验。拿我们在第 1 章编译的那个“硬核”生产内核启动起来,试着加载刚才那个调试模块:
# make
[...]
# dmesg -C; insmod ./printk_loglevels.ko ; dmesg
insmod: ERROR: could not insert module ./printk_loglevels.ko: Operation not permitted
[ 1933.232266] Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
炸了。
这不是你的错,是我们的生产内核开启了 Lockdown 模式(内核安全锁定)。从 5.4 内核开始,如果开启了 CONFIG_SECURITY_LOCKDOWN_LSM=y,任何没签名的模块都别想加载——想都别想。
这也是为什么我们在生产内核上连测试都做不了。要破局,要么老老实实给模块签名(参考内核文档 Documentation/admin-guide/module-signing.html),要么(仅为了调试)把这些安全限制关掉。
别忘了:内核文档里有一份关于 lockdown 的详细说明,见
man kernel_lockdown.7。
炸屏危机:printk 的限速机制
调试打印虽好,但千万别贪杯。
想象一下,你在驱动里写了个 dev_dbg(),偏偏这段代码在一个高频执行路径上(比如中断处理函数或者定时器回调里)。后果是什么?
- 内核环形缓冲区溢出:缓冲区只有几十到几百 KB,瞬间被刷满,旧日志被覆盖,有用的信息丢了。
- 系统活锁:serial 输出太慢,CPU 全力以赴在打印字符,导致系统响应迟钝甚至完全假死。
- 日志刷屏:同一条错误重复打印一万遍,对解决问题没有任何帮助。
你可能会看到 systemd 报怨:
/dev/kmsg buffer overrun, some messages lost.
为了解决这种“自作孽”,内核社区搞出了 Rate Limiting(限速) 机制。
怎么限速?
内核提供了两个关键的 /proc 接口来控制阈值:
-
/proc/sys/kernel/printk_ratelimit- 含义:两次输出之间必须间隔的最小时间(单位:jiffies,通常是几秒)。
- 默认值:
5(秒)。设为0则关闭限速。
-
/proc/sys/kernel/printk_ratelimit_burst- 含义:在限速触发前,允许短时间内“爆发”输出的消息数量。
- 默认值:
10。
简单来说就是:5 秒内,最多让你爆发 10 条消息。超过这个数?后面的全部被掐掉,直到下一个 5 秒周期。
来看看怎么用。内核提供了这些封装好的宏(记得 include <linux/kernel.h>):
pr_*_ratelimited():对应pr_info,pr_err等。dev_*_ratelimited():对应dev_info,dev_err等。
⚠️ 注意
千万别直接用 printk_ratelimited(),内核文档明确警告过这个。用封装好的宏。
限速实测
我们写个简单的模块来验证一下(只截取关键部分):
// ch3/ratelimit_test/ratelimit_test.c
#define pr_fmt(fmt) "%s:%s():%d: " fmt, KBUILD_MODNAME, __func__, __LINE__
[…]
#include <linux/kernel.h>
#include <linux/delay.h>
static int num_burst_prints = 7;
module_param(num_burst_prints, int, 0644);
MODULE_PARM_DESC(num_burst_prints, "Number of printks to generate in a burst (defaults to 7).");
static int __init ratelimit_test_init(void)
{
int i;
pr_info("num_burst_prints=%d. Attempting to emit %d printks in a burst:\n",
num_burst_prints, num_burst_prints);
for (i=0; i<num_burst_prints; i++) {
pr_info_ratelimited("[%d] ratelimited printk @ KERN_INFO [%d]\n", i, LOGLEVEL_INFO);
mdelay(100); /* 故意加个延迟,把时间轴拉长 */
}
return 0;
}
默认参数是 7 条,这肯定没问题。我们玩点刺激的,把它设为 60:
# insmod ./ratelimit_test.ko num_burst_prints=60
你会看到类似的输出:
[ ...] ratelimit_test:ratelimit_test_init:58: num_burst_prints=60. Attempting to emit 60 printks in a burst:
[ ...] ratelimit_test:ratelimit_test_init:61: [0] ratelimited printk @ KERN_INFO [6]
[ ...] ratelimit_test:ratelimit_test_init:61: [1] ratelimited printk @ KERN_INFO [6]
...
[ ...] ratelimit_test:ratelimit_test_init:61: [9] ratelimited printk @ KERN_INFO [6]
[ ...] kernel: __ratelimit: 40 callbacks suppressed
看,打到第 10 条(索引 9)的时候,限速器启动了。40 callbacks suppressed 意味着后面的 40 条打印被内核无情吞掉了。
又过了 5 秒(因为我们在循环里加了 mdelay(100),把时间拖得很长),限速器重置,剩下的打印才又漏出来几条。总共 60 条,实际只出来了一小部分。
这就是限速的威力。
终极解决方案:trace_printk()
如果你真的需要在一个极高频的路径里打印,连限速都满足不了你,那 printk 可能真的不是合适的工具。
内核的 ftrace 子系统(我们在第 9 章会详细讲)提供了一个 trace_printk() API。
- 语法:跟
printf()一样(注意不是printk)。 - 优势:
- 极快:只写 RAM,不经过复杂的控制台逻辑。
- 缓冲区巨大:trace buffer 默认就很大,而且 root 可以动态调整。
如果你真的需要在这种场景下调试,记得考虑 trace_printk()。这不仅是换个函数的问题,它是另一种调试思维的体现。
掌握这些工具——pr_debug, dev_dbg, 限速宏——你就已经拥有了在内核里“插管子”监视流程的能力。这看起来已经够用了,直到你遇见那个真正的“大杀器”:动态调试。这才是让内核打印为你所用的终极形态。
下一节,我们来拆解它。