4. printk实战
printk 是 Linux 内核中最基础、最常用的调试输出函数,其行为类似于用户空间的 printf,但专为内核上下文设计。本节将深入讲解如何在驱动模块中正确使用 printk,如何通过 dmesg 查看日志,以及如何通过 /proc/sys/kernel/printk 精细控制日志输出级别。
4.1 printk 函数原型与日志级别
printk 的核心区别在于它带有日志级别(loglevel),用于标识消息的严重程度。其基本调用格式如下:
printk(KERN_INFO "Hello, this is a info message\n");
printk(KERN_ERR "Error occurred at line %d\n", __LINE__);
若未指定日志级别,内核会使用默认级别 DEFAULT_MESSAGE_LOGLEVEL(通常为 4,即 KERN_WARNING)。
下表列出了内核预定义的 8 个日志级别宏及其含义:
| 宏定义 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
KERN_EMERG |
0 | 紧急事件,系统不可用 | 致命硬件故障 |
KERN_ALERT |
1 | 必须立即处理 | 系统崩溃前兆 |
KERN_CRIT |
2 | 严重条件 | 驱动初始化失败 |
KERN_ERR |
3 | 错误条件 | I/O 操作失败 |
KERN_WARNING |
4 | 警告条件 | 资源使用接近上限 |
KERN_NOTICE |
5 | 正常但重要的消息 | 模块加载/卸载 |
KERN_INFO |
6 | 信息性消息 | 驱动版本信息 |
KERN_DEBUG |
7 | 调试级别消息 | 函数入口/出口跟踪 |
实战建议:在开发阶段,优先使用
KERN_INFO 和 KERN_DEBUG;在发布版本中,仅保留 KERN_ERR 及以上级别,避免日志刷屏影响性能。
4.2 驱动模块中的 printk 实战示例
下面是一个完整的驱动模块示例,演示了不同级别 printk 的使用方法:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "my_driver: module loaded (level 6)\n");
printk(KERN_DEBUG "my_driver: init function entered (level 7)\n");
/* 模拟一个错误条件 */
if (0) {
printk(KERN_ERR "my_driver: critical resource missing (level 3)\n");
return -ENODEV;
}
printk(KERN_NOTICE "my_driver: initialization successful (level 5)\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "my_driver: module unloaded (level 6)\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Debug Expert");
MODULE_DESCRIPTION("printk level demo driver");
编译与测试步骤:
- 将上述代码保存为
my_driver.c,并编写对应的 Makefile。 - 执行
make编译生成my_driver.ko。 - 使用
sudo insmod my_driver.ko加载模块。 - 使用
sudo rmmod my_driver卸载模块。 - 运行
dmesg | tail -10查看输出。
4.3 使用 dmesg 查看内核日志
dmesg 是读取内核环形缓冲区(kernel ring buffer)的标准工具。printk 输出的消息会写入该缓冲区,并通过 dmesg 呈现给用户。
常用 dmesg 命令选项:
| 命令 | 作用 |
|---|---|
dmesg |
显示所有内核日志消息 |
dmesg -c |
显示并清空内核环形缓冲区 |
dmesg -n 3 |
设置控制台日志级别为 3(KERN_ERR) |
dmesg -l err |
仅显示错误级别(KERN_ERR)及以上的消息 |
dmesg -T |
显示人类可读的时间戳 |
dmesg -w |
实时等待新消息(类似 tail -f) |
注意:内核环形缓冲区大小有限(通常为 128KB)。如果驱动模块频繁输出大量调试信息,较早的消息会被覆盖。建议在调试时使用
dmesg -c 清空缓冲区,以便聚焦于当前测试的输出。
4.4 控制日志输出级别:/proc/sys/kernel/printk
Linux 内核通过 /proc/sys/kernel/printk 文件暴露了 4 个关键的日志控制值。理解这 4 个值是掌握 printk 行为的关键。
查看当前设置:
$ cat /proc/sys/kernel/printk
7 4 1 7
这 4 个数字的含义如下表所示:
| 字段 | 名称 | 默认值 | 说明 |
|---|---|---|---|
| 1 | console_loglevel | 7 | 只有日志级别 小于 此值的消息才会输出到控制台 |
| 2 | default_message_loglevel | 4 | 未指定级别的 printk 使用的默认级别 |
| 3 | minimum_console_loglevel | 1 | 控制台日志级别可设置的最小值(通常为 1) |
| 4 | default_console_loglevel | 7 | 系统启动时的默认控制台日志级别 |
核心规则:只有 printk 的日志级别 数值小于 console_loglevel 时,消息才会在控制台(终端)上显示。数值越小,优先级越高。
例如,当 console_loglevel = 4 时:
KERN_ERR (3)→ 3 < 4,会显示在控制台。KERN_INFO (6)→ 6 >= 4,不会显示在控制台,但会写入 dmesg。
调试技巧:在开发调试时,可以将 console_loglevel 设置为 8(大于所有级别),这样所有 printk 消息都会显示在控制台上:
echo 8 > /proc/sys/kernel/printk
或者使用更安全的命令:
dmesg -n 8
4.5 printk 日志过滤与输出控制流程图
下面的 SVG 图展示了 printk 消息从驱动模块发出到最终输出到控制台或 dmesg 的完整流程:
4.6 修改 printk 级别的实用方法
除了直接写入 /proc/sys/kernel/printk,还有以下几种方式可以控制日志输出:
- 临时修改(运行时):
# 只显示 KERN_ERR 及以上级别的消息到控制台 echo "3 4 1 7" > /proc/sys/kernel/printk # 或者使用 sysctl sysctl -w kernel.printk="3 4 1 7" - 永久修改(重启生效): 编辑
/etc/sysctl.conf或/etc/sysctl.d/99-printk.conf,添加:kernel.printk = 3 4 1 7 - 内核启动参数: 在 GRUB 内核命令行中添加
loglevel=3,效果等同于将 console_loglevel 设置为 3。
生产环境警告:在生产系统中,建议将 console_loglevel 设置为 3(KERN_ERR)或更低,以避免调试信息暴露敏感信息或影响控制台性能。调试完成后务必恢复为默认值。
4.7 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 控制台看不到 printk 输出 | console_loglevel 太低,过滤掉了低优先级消息 | echo 8 > /proc/sys/kernel/printk |
| dmesg 中看不到最近的输出 | 环形缓冲区被大量消息填满,旧消息被覆盖 | 使用 dmesg -c 清空后再测试 |
| printk 导致系统卡顿 | 在中断上下文或高频率循环中调用 printk | 使用 printk_ratelimited() 限制频率 |
| printk 输出乱码 | 缺少换行符 \n,缓冲区未刷新 |
确保每个 printk 以 \n 结尾 |
进阶技巧:在调试高频代码路径时,可以使用
pr_debug() 和 pr_info() 等封装宏,它们支持动态调试(dynamic debug),可以在不重新编译内核的情况下按模块、函数或行号开启/关闭特定调试输出。