☰
printk打印等级全解析:解决内核日志控制台不输出的调试难题
2026/10/21 21:39:56 网站建设 项目流程

驱动里明明写了 printk,串口上却死活不出现;换到另一台设备上,同样的代码又刷得停不下来。这种被“同一段日志双重标准”支配的经历,我在做内核驱动调试的头一年里遇到过很多次。后来才明白,问题几乎都不在代码本身,而在 printk 打印等级这套规则上——内核给每条日志标了紧急程度,控制台又有一道接收阈值,两者共同决定你最终能看到什么。今天干脆把 printk 打印等级的来龙去脉一次讲透:八个日志级别怎么理解、/proc/sys/kernel/printk 四个数字到底怎么读、从启动到运行时谁在控制阈值,以及一套真正能落地的驱动调试姿势。无论你是刚接触内核模块开发的新手,还是被内核日志困扰的嵌入式工程师,照着排查都能少走弯路。

1. 一次“printk 静默”的排查:明明加了打印却什么都没输出

先说一次真实经历。我在某款新板卡的调试阶段,需要确认一个外设驱动有没有正常走 probe,就在入口处加了一行:

printk("probe start\n");

顺带在分支和错误路径上又加了几条打印。接上串口终端,开机,整屏日志里偏偏就没有我要的那条。第一反应是模块没加载,lsmod一看又是正常;检查设备树路径和总线地址,也没发现问题。直到我把 dmesg 翻出来:

dmesg | grep "probe start"

日志就安安静静躺在缓冲区里。这说明驱动确实跑了,printk 确实执行了,只是控制台不给我看。

1.1 从 dmesg 到 console:先看清两级输出路径

从这一刻起,我意识到 printk 的输出路径不是一根直通管,而是分成两段:第一段写入内核环形缓冲区,向系统所有日志使用者提供数据,dmesg 读的就是这个缓冲区;第二段是控制台输出,只有当消息级别满足当前控制台日志级别的条件时,才会真的被推到串口、VGA 这类 console 上。也就是说,第一段管“记不记”,第二段管“显不显”。

这个设计其实很合理。环形缓冲区是一个全局消息池,无论控制台输出与否,所有日志都应该先沉淀下来,方便事后回溯;控制台则是给“正在盯着设备的人”看的,身上没必要背那么多废话。问题在于:控制台输出的门槛,默认并不低。

1.2 裸 printk 的默认级别:第一道拦路虎

接着我查了控制台级别的状态:

cat /proc/sys/kernel/printk

新板卡给的是4 4 1 7。第一个数字 4 就是控制台日志级别 console_loglevel,只有级别比它“更小”的消息才会从 console 输出。我那行裸 printk 没带任何级别前缀,内核会把它当成默认级别。默认值多少呢?正是第二个数字,default_message_loglevel,常见配置为 4,对应 KERN_WARNING。现在尴尬的地方出现了:4 < 4 不成立,所以这条消息被判定为“不值得上 console”,交给 dmesg 之后就不管了。

要让它上 console,最直接的办法就是临时放宽阈值:

echo "8 4 1 7" > /proc/sys/kernel/printk

改完再执行同样的操作,串口立刻出现刚写的日志。到这里,之前所有“驱动没工作”“内核把模块吞了”的猜测全部作废,问题的本质就是打印等级。这个案例把核心规则自然带了出来:裸 printk 默认是 KERN_WARNING,而很多发行版默认 console_loglevel 也是 4,两者恰好相等,于是“裸 printk 明明写了却不显示”成了入门阶段最典型的一个坑。

2. 先搞清“紧急程度”:内核八个日志级别到底怎么排序

既然要谈打印等级,先把内核定义的八个级别一次看清楚。它们都定义在 include/linux/kern_levels.h 中,每个宏对应一个以数字标记开头的字符串:

级别宏嵌入字符串数值语义典型场景
KERN_EMERG<0>0系统不可用内核 panic、硬件损毁前最后呼喊
KERN_ALERT<1>1必须立即处理系统即将崩溃、必须人工介入
KERN_CRIT<2>2严重故障关键组件异常、子系统失效
KERN_ERR<3>3错误驱动初始化失败、硬件访问错误
KERN_WARNING<4>4警告有风险但还能继续运行
KERN_NOTICE<5>5普通但需要注意配置生效、状态变更
KERN_INFO<6>6信息驱动加载信息、常见运行信息
KERN_DEBUG<7>7调试仅供开发阶段跟踪的日志

2.1 “数字越小越紧急”的直觉陷阱

第一个要纠正的直觉是:数字 7 不是“最高级”,而是“最低优先”。很多新手刚接触时看到 KERN_DEBUG 是 7,想当然以为它最严重,结果调试时拼命打 pr_debug,却不知道它是最不重要的那档。真正最严重的是 KERN_EMERG,数字 0,系统已经处于不可用状态。内核在判断要不要往控制台输出时,做的是“数值比较”,所以数字大小直接决定了日志的优先级位置。

这些<0>、<7>看起来是给人读的,其实是给 printk 解析器看的。printk 收到 fmt 字符串后,会发现参数开头有<数字>,把它剥离出来并解析成消息级别记录在日志头里,剩下内容才进入环形缓冲区。所以printk(KERN_ERR "fail\n")和手写printk("<3>fail\n")是等价的,正式代码里没人会手写数字,都用宏。

2.2 无级别消息与 KERN_DEFAULT

如果 printk 里不加任何级别宏,消息走默认级别 default_message_loglevel,也就是/proc/sys/kernel/printk第二个数字,通常为 4。所以裸 printk 在效果上约等于 KERN_WARNING。这里有个容易被忽略的细节:默认级别 4 在默认 console_loglevel 为 4 时,恰好卡在“严格小于”门槛之外,所以裸 printk 不出现在 console 上的概率非常高。这也是我第一章节里那个案例的数学根源。

除了 0~7,还有一个 KERN_DEFAULT,对应字符串<d>,表示使用默认级别;旧内核还有 KERN_CONT,用于把多段输出拼到同一个日志记录里,现在一般建议直接用 pr_cont() 宏处理续行。日常写驱动基本用不到这些特殊值,知道存在即可。

2.3 写代码时优先用 pr_xxx 宏

我不建议直接写printk(KERN_ERR ...),更统一的做法是使用 pr_xxx 宏:pr_emerg、pr_alert、pr_crit、pr_err、pr_warn、pr_notice、pr_info、pr_debug、pr_cont。它们把“级别”直接放进函数名里,代码清晰度高,也方便后续用工具批量扫描日志分级。需要注意 pr_debug 有个特殊行为:如果没有定义 DEBUG 宏,也没有开启内核动态调试,它会被编译成空操作,日志根本不会进入 dmesg。这和printk(KERN_DEBUG ...)不同,后者无论什么环境都会产生日志。这个差异是后面调试“看不见日志”的重要线索,先记在心里。

3. 控制台那扇门:/proc/sys/kernel/printk 四个数字一次讲透

cat /proc/sys/kernel/printk会输出四个数字,看起来平平无奇,其实前两个直接决定了你绝大多数日志的去向。我见过有人把第三个当成“控制台最小级别”硬性限制,被误导了很久,所以这里逐个拆清楚。

顺序字段名常见默认值作用
1console_loglevel4控制台日志级别,消息级别值小于它才允许上 console
2default_message_loglevel4无级别 printk 消息使用的默认级别
3minimum_console_loglevel1quiet 启动参数使用的目标级别,不是硬性下限
4default_console_loglevel7系统默认采用的控制台级别

3.1 “严格小于”才是放行条件

关键规则是:消息级别严格小于 console_loglevel 才会输出到控制台。

举例来说,console_loglevel 为 4 时:

  • KERN_EMERG(0)、KERN_ALERT(1)、KERN_CRIT(2)、KERN_ERR(3) 都能上 console;
  • KERN_WARNING(4) 本身等于门槛值,4 < 4 不成立,所以不上 console;
  • KERN_INFO(6)、KERN_DEBUG(7) 更不用说,全部被挡下。

很多人会把“控制台级别 4”理解成“4 级及以下的都输出”,方向完全反了。这个“严格小于”的细节,我在一次线上排查里差点搞错:当时怀疑驱动报错没打印,后来发现报错用的是 pr_warn,而 console_loglevel 恰好是 4,所以 dmesg 有记录,串口上就是没有。

3.2 调整与恢复:echo、dmesg -n、sysctl

最常用的调整方式是直接 echo:

# 只改第一个字段,把 console_loglevel 提到 8,放行所有 0~7 级消息 echo 8 > /proc/sys/kernel/printk # 一次性写四个字段,保持其他行为不变 echo "8 4 1 7" > /proc/sys/kernel/printk # 恢复常见默认 echo "4 4 1 7" > /proc/sys/kernel/printk

也可以用dmesg -n,它等价于只改第一个字段:

dmesg -n 8 # 等同于 echo 8 > /proc/sys/kernel/printk dmesg -n 0 # 控制台几乎完全静默,谨慎使用

通过 sysctl 也可以,适合脚本里写得比较语义化:

sysctl -w kernel.printk="4 4 1 7"

注意这些修改都不持久化,重启后内核会回到编译默认值和 cmdline 启动参数组合出的状态。如果你在调试产品时需要固定策略,可以写入/etc/sysctl.d/99-printk.conf:

kernel.printk = 4 4 1 7

3.3 第三个字段为什么容易让人误解

第三个字段 minimum_console_loglevel 常在默认配置里是 1,字面意思“最小控制台日志级别”特别容易让人以为是下限:难道 console_loglevel 不能设成 0?当然能。实际上它对应的是内核编译选项 CONFIG_CONSOLE_LOGLEVEL_QUIET,也就是 cmdline 传入 quiet 参数时使用的目标级别。quiet 的本意是让启动几乎保持安静,把 console_loglevel 落到 1,这样只有 KERN_EMERG(0) 和 KERN_ALERT(1) 等在非常危急时才会输出到控制台。所以它只是 quiet 模式的目标值,不是系统强制的下限。搞清楚这一点,看/proc/sys/kernel/printk时就不会被字段名带偏。

我把这个字段称为“quiet 落点”更贴切。第四个字段 default_console_loglevel 则是系统默认期望的控制台级别,常见值是 7,代表“默认想尽量多看日志”的调试友好策略。

4. 从 bootloader 到运行时:打印等级由谁决定

printk 打印等级不是运行到一半随机出来的,而是由一条从编译期到运行时的控制链决定。理解这条链路,你才能知道一开始拿到的4 4 1 7是从哪来的,以及在什么阶段去改最有效。

4.1 编译期默认值:内核自己带的“出厂设置”

自己编译内核时,以下配置影响打印等级:

  • CONFIG_CONSOLE_LOGLEVEL_DEFAULT:决定 console_loglevel 的初始值;
  • CONFIG_MESSAGE_LOGLEVEL_DEFAULT:决定 default_message_loglevel;
  • CONFIG_CONSOLE_LOGLEVEL_QUIET:对应 quiet 参数使用的目标级别;
  • CONFIG_LOG_BUF_SHIFT:决定日志环形缓冲区大小,默认偏小时早期日志容易被覆盖。

这些值会编译进内核,bootloader 不传参数时,系统就用它们初始化/proc/sys/kernel/printk。如果你拿到的是发行版内核,通常编译默认就是4 4 1 7,所以大多数人看到的初始值一致。

4.2 启动参数:调试阶段最高效的入口

bootloader 传给内核的 cmdline 里有几个参数,和打印等级直接相关:

参数效果适用场景
loglevel=8把 console_loglevel 设为 8,0~7 级全部上 console开发调试时全开
loglevel=4把 console_loglevel 设为 4保留 error 及更严重级别
quiet把 console_loglevel 设为 CONFIG_CONSOLE_LOGLEVEL_QUIET(常见为 1)量产设备减少刷屏
debug把 console_loglevel 提到很高,完整显示几乎全部标准级别内核早期调试、抓 panic 前日志
ignore_loglevel完全忽略 console_loglevel 阈值,强制输出所有消息必须看到一切日志的紧急调试

修改位置要看你的引导方式。U-Boot 下通常是改环境变量 bootargs,在末尾追加loglevel=8;GRUB 下可以临时按 e 编辑启动项,也可以写进/etc/default/grub的 GRUB_CMDLINE_LINUX_DEFAULT,更新配置后生效。调试完记得改回来,quiet或loglevel=3之类的会在生产环境帮你过滤掉大量噪声。

启动参数优先级高于编译默认。内核在引导早期解析 cmdline 后,就会覆盖 console_printk 数组里的初始值。运行时你再写/proc/sys/kernel/printk,优先级又高于 cmdline,但重启后失效。所以调 printk 等级的顺序通常是:优先用 cmdline 一次性解决,临时确认问题时用 sysctl 改当前值。

4.3 earlycon 与早期日志的“时间差”

还有一个许多人踩过的坑:你已经加了loglevel=8,但 U-Boot 结束之后、内核 console 驱动注册之前的那段日志,串口上还是看不到。这不是因为级别不对,而是因为那个阶段 console 根本还不存在,printk 消息暂时只能进环形缓冲区。

要让早期日志也上串口,需要配置早期控制台:

  • 在 cmdline 加earlycon,很多板卡用earlycon=uart8250,mmio32,0x...指定串口地址;
  • 老式内核还有earlyprintk=serial参数。
  • 内核需要开启对应的 CONFIG_SERIAL_EARLYCON、CONFIG_DEBUG_LL 等选项。

从实践看,loglevel 和 earlycon 是两个独立问题:loglevel 管“哪些级别能上”,earlycon 管“还没有正式 console 的时候能不能先输出”。调试启动阶段挂死问题,这两个参数经常要同时上。

5. 写驱动时 printk 的正确打开方式

级别和阈值讲完了,再聊一下真正写驱动时该注意的姿势。很多驱动代码里全是裸 printk,垃圾日志一多,dmesg 里跟大海捞针一样。我推荐的组合拳是:pr_fmt + pr_xxx / dev_xxx + rate limit + dynamic debug。

5.1 pr_fmt 让日志自带“门牌号”

每个源文件顶部定义 pr_fmt,之后所有 pr_xxx 都会自动带上模块名前缀:

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> static int __init hello_init(void) { pr_info("loading hello module\n"); pr_err("simulated error\n"); return 0; } module_init(hello_init);

KBUILD_MODNAME 是 kbuild 编译时自动给出的模块名,不需要手动写死。定义 pr_fmt 必须要放在包含 printk.h 之前,实际就是在文件头部,后续日志出来形如hello: loading hello module,配合dmesg | grep hello一抓一个准。

如果手头有 struct device 指针,用 dev_xxx 更好:dev_info、dev_err、dev_dbg 等会自动带上设备和驱动名称,多个同类型设备时的日志区分度会高很多。比如:

dev_err(&pdev->dev, "reg base=%px\n", base);

5.2 防刷屏:rate limit 宏是用来救命的

驱动里最常见的刷屏场景是中断处理函数:硬件异常时中断风暴可能瞬间让 printk 把 CPU 拖死。所以凡是会在高频路径里执行的打印,都要用 ratelimited 版本:

pr_err_ratelimited("interrupt too frequent\n"); dev_info_ratelimited(&pdev->dev, "reset count=%d\n", count);

这类宏默认约每 5 秒最多放行一小批日志,具体受内核的 printk 限速机制控制。它们的价值不在于“一定打印出来”,而在于“既保留现场线索,又不至于把系统拖垮”。我曾在一次硬件误触发频繁中断的调试里,看到满屏同一个错误刷了十几万行,才开始后悔没用 ratelimited。好的驱动在发布时,高频路径里几乎只有 ratelimited 打印。

5.3 dynamic debug:不用改代码就能开调试日志

pr_debug 默认是空操作,所以在开发调试时常常出现“代码里写了 pr_debug 但 dmesg 什么都没有”的假象。两种情况能让它输出:

一是临时在源文件顶部加一行#define DEBUG,之后该文件的 pr_debug 和 dev_dbg 会生效,但需要重新编译。

二是使用 dynamic debug,不用重新编译,靠运行期控制。前提是内核开启 CONFIG_DYNAMIC_DEBUG:

# 挂载 debugfs mount -t debugfs none /sys/kernel/debug # 查看当前所有调试点 cat /sys/kernel/debug/dynamic_debug/control # 开启某个模块的全部调试打印 echo -n 'module hello +p' > /sys/kernel/debug/dynamic_debug/control # 只开某个函数的调试打印 echo -n 'func hello_probe +p' > /sys/kernel/debug/dynamic_debug/control # 关闭 echo -n 'module hello -p' > /sys/kernel/debug/dynamic_debug/control

模块加载前也可以直接在 cmdline 里写dyndbg="module hello +p"。比起改代码加#define DEBUG,这套方案最大的好处是对发行版内核友好,也不需要维护两套源码。开发驱动时,我习惯把调试信息全部用 pr_debug/dev_dbg 写,然后根据现场情况动态开关特定模块或函数,效率比反复重新编译高很多。

5.4 printk 格式化里的几个坑

printk 格式化整体像 printf,但内核指针处理有差异。裸%p出于安全考虑默认输出成哈希值,想看到真实地址要用%px;想在普通用户态隐藏地址可以用%pK,配合 kptr_restrict 使用。驱动里打印寄存器地址、资源地址时,我常用%px、%pR(资源)这类扩展格式,否则只看到一个雾里看花的哈希值,根本没法对寄存器手册查地址。

另一个细节是换行。printk 不会自动补换行,习惯上每条日志末尾都要写\n。缺了换行,消息会跟下一条日志黏在一起,控制台显示的时机也会变得捉摸不定,尤其 panic 时可能丢掉尾段。别嫌啰嗦,这五行代码就能避免很多诡异现象。

6. printk 不打印的十层排查路径

最后给一套我实际排查“printk 不打印”问题时的路径。核心是先分清楚是“dmesg 没有”还是“dmesg 有但 console 没有”,两条分支查的东西完全不同。

6.1 第一分支:dmesg 能看到,但控制台看不到

这种情况说明 printk 执行了,消息也进了环形缓冲区,问题出在控制台输出环节。按顺序查:

  1. 当前 console_loglevel 是不是太小:cat /proc/sys/kernel/printk,如果第一个数是 4,那 KERN_WARNING 及以下级别都不上 console。
  2. 启动参数里有没有quiet、低版本的loglevel:cat /proc/cmdline确认。
  3. 消息本身是什么级别:裸 printk 默认是 KERN_WARNING,这在 console_loglevel=4 时正好被卡住。改成 pr_err 或放开 console_loglevel 都能解决。
  4. 是不是 console 根本没注册:cat /proc/consoles看看有没有对应的 console;早期启动日志看不到时考虑 earlycon。
  5. 系统 suspend 期间 console 被挂起:如果设备进入低功耗,日志可能延后或丢失,必要时用 no_console_suspend 参数。

这类问题占比最高的是第 1、3 条,尤其是“裸 printk + 默认 console_loglevel=4”的组合,几乎解释掉一半以上的控制台静默现象。

6.2 第二分支:dmesg 里也找不到

如果连 dmesg 都没有,问题就不是级别,而是日志根本没进缓冲区。按这个思路往下:

  1. 代码是否真的执行了:把打印放到更明确一定会跑的入口,比如 module_init、probe 的最前面,先用普通 pr_info 而不是 pr_debug。
  2. 是不是用了 pr_debug / dev_dbg 但没开启:检查 dynamic_debug 控制文件,或者临时加#define DEBUG。
  3. 是不是被 rate limit 吞了:如果打印内容在中断高频路径上,检查是否用了 ratelimited 版本,以及 dmesg 里有没有相关抑制提示。
  4. 日志环形缓冲区是否太小:早期日志被后续覆盖,可通过 cmdline 加log_buf_len=4M之类参数扩大。
  5. 权限问题:/proc/sys/kernel/dmesg_restrict为 1 时普通用户 dmesg 看不到,要用 root。
  6. 内核配置:CONFIG_PRINTK 被关闭,或者动态调试相关配置缺失。这个比较少见,但自编译内核时要注意。

6.3 排查步骤速查

现象核心检查高概率原因解决方向
dmesg 有,console 无cat /proc/sys/kernel/printkconsole_loglevel 太小echo "8 4 1 7" > ...
dmesg 有,console 无cat /proc/cmdline存在 quiet/loglevel 低值参数放开
dmesg 也没有lsmod + 临时 pr_info代码未执行找到真正入口
只有 pr_debug 不输出cat control动态调试未开启echo 'module xx +p' > control
日志存在但被覆盖dmesg | taillog_buf 太小log_buf_len= 调大
中断中大量丢失查看频率rate limit 生效确认合理的限速

把这十层走一遍,大多数 printk 静默问题都能定位到根因。而实际经验里,前四层(dmesg 是否存在、级别、console_loglevel、cmdline)能解决掉九成问题,剩余的多半是动态调试和缓冲区大小。

最后一点经验

现在每拿到一块新板卡,我会先按顺序做三件事:cat /proc/cmdline看启动参数、cat /proc/sys/kernel/printk看当前阈值、dmesg | tail -50看既有日志。确认这些之后再去加打印,能少走非常多的冤枉路。还有个私藏小技巧:如果临时改了/proc/sys/kernel/printk,记得把它和对应的 patch 或试验记录放在一起,因为这类修改不持久化,隔一个重启就“消失”,下次再调试时很容易为同一件事折腾第二遍。printk 打印等级本身不复杂,但它是整个内核日志体系的基石,花半小时把它彻底理清,后面所有跟内核日志打交道的日子都会顺畅得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询