2. printk基础
printk 是 Linux 内核中最核心的日志输出函数,其地位等同于用户空间的 printf。驱动开发者通过 printk 输出调试信息、状态变更、错误告警以及关键性能指标。理解 printk 的函数原型、日志级别定义以及控制台输出机制,是进行高效内核调试的第一步。
2.1 printk 函数原型
printk 定义在 include/linux/printk.h 中,其原型与标准 C 库的 printf 高度相似,但增加了日志级别前缀机制。
// 核心原型(简化)
int printk(const char *fmt, ...);
// 带日志级别的标准调用格式
printk(KERN_INFO "usb_driver: device probe succeeded, vid=0x%04x\n", vid);
| 参数 / 特性 | 说明 |
|---|---|
| fmt | 格式化字符串,支持 %d、%x、%s、%p 等标准格式符,以及内核扩展格式符(如 %pI4 打印 IPv4 地址) |
| ... | 可变参数,与 fmt 中的格式符一一对应 |
| 返回值 | 成功时返回输出的字符数(不含结尾 '\0');失败时返回负的错误码 |
| 日志级别前缀 | 通过宏定义(如 KERN_INFO)嵌入到 fmt 字符串头部,控制消息的紧急程度 |
关键区别:printk 与 printf 最大的不同在于:printk 的输出目标不是 stdout,而是内核日志缓冲区(log_buf),并且受日志级别和控制台日志级别的双重过滤。printk 可以在任何上下文(中断、原子上下文、调度器)中安全调用,但某些上下文下不能使用可能导致睡眠的变体(如 printk 的异步版本)。
2.2 日志级别定义(KERN_EMERG 到 KERN_DEBUG)
Linux 内核定义了 8 个日志级别宏,位于 include/linux/kern_levels.h。每个宏对应一个数字优先级,数字越小,优先级越高(越紧急)。
| 宏名称 | 数字值 | 含义 | 典型使用场景 |
|---|---|---|---|
| KERN_EMERG | 0 | 紧急事件,系统不可用 | 致命硬件故障、内核 panic 前输出 |
| KERN_ALERT | 1 | 必须立即处理 | 严重内存损坏、关键数据丢失 |
| KERN_CRIT | 2 | 临界条件 | 驱动探测失败导致设备不可恢复 |
| KERN_ERR | 3 | 错误条件 | DMA 传输超时、寄存器读写校验失败 |
| KERN_WARNING | 4 | 警告条件 | 降级模式运行、非关键配置缺失 |
| KERN_NOTICE | 5 | 正常但重要 | 设备热插拔事件、驱动加载成功 |
| KERN_INFO | 6 | 信息性消息 | 驱动版本号、寄存器初始值、中断统计 |
| KERN_DEBUG | 7 | 调试信息 | 函数进入/退出、变量中间值、状态机跳转 |
常见错误:新手常犯的错误是忘记在 printk 中加日志级别前缀。此时内核会使用默认级别
DEFAULT_MESSAGE_LOGLEVEL(通常为 4,即 KERN_WARNING)。这会导致调试信息(本应使用 KERN_DEBUG)被当作警告输出,污染控制台并降低关键告警的可读性。始终显式指定日志级别。
2.3 控制台输出机制
printk 的输出并非直接到达终端或串口,而是经过一套分层过滤机制。理解这一机制,才能解释“为什么我 printk 了却看不到输出”这一经典问题。
2.3.1 核心过滤参数
| 参数名 | 文件路径(/proc/sys/kernel/) | 默认值 | 作用 |
|---|---|---|---|
| console_loglevel | printk | 7(最高) | 控制台能显示的最高日志级别(数字越小越紧急) |
| default_message_loglevel | printk | 4 | 未指定级别时的默认日志级别 |
| minimum_console_loglevel | printk | 1 | 控制台允许设置的最低级别(防止完全关闭输出) |
| default_console_loglevel | printk | 7 | 系统启动后的默认控制台级别 |
控制台日志级别可以通过 echo 命令动态调整:
# 查看当前 printk 参数(四个值依次对应上述四个参数)
cat /proc/sys/kernel/printk
# 输出示例:7 4 1 7
# 将控制台日志级别设为 4(只显示 KERN_WARNING 及以上)
echo "4 4 1 7" > /proc/sys/kernel/printk
# 临时开启所有调试信息(级别 8 表示显示所有,包括 KERN_DEBUG)
echo "8 4 1 7" > /proc/sys/kernel/printk
2.3.2 输出过滤流程
2.3.3 控制台设备注册
控制台输出最终由 struct console 驱动完成。每个控制台设备(串口、VGA、虚拟终端、网络控制台)都需要注册一个 console 结构体。printk 在决定输出后,会遍历已注册的 console 列表,调用其 write 回调函数。
// include/linux/console.h
struct console {
char name[16]; // 控制台名称,如 "ttyS0"
void (*write)(struct console *, const char *, unsigned);
int (*read)(struct console *, char *, unsigned);
struct device *device; // 关联的设备
int (*setup)(struct console *, char *);
short flags; // CON_ENABLED, CON_CONSDEV 等
short index; // 设备索引
int cflag; // 串口控制标志
void *data;
struct console *next;
};
调试要点:如果 printk 消息在 dmesg 中可见,但串口终端看不到,通常原因有:(1) console_loglevel 设置过低;(2) 串口 console 驱动未注册或注册顺序不对;(3) 串口硬件流控或波特率不匹配。使用
console=ttyS0,115200 内核启动参数可强制指定控制台设备。
2.4 日志缓冲区(log_buf)与 dmesg
所有 printk 消息无论是否输出到控制台,都会写入内核日志缓冲区 log_buf。该缓冲区是一个环形缓冲区,默认大小为 256 KB(可配置)。用户态通过 dmesg 命令读取该缓冲区内容。
| 特性 | 说明 |
|---|---|
| 缓冲区类型 | 环形缓冲区(ring buffer),新消息覆盖最旧消息 |
| 默认大小 | CONFIG_LOG_BUF_SHIFT 控制,通常为 18(256 KB) |
| 访问方式 | dmesg、/proc/kmsg、syslog 系统调用 |
| 持久性 | 掉电丢失;可通过 pstore/ramoops 在死机后保留 |
| 时间戳 | 每条消息记录精确到微秒的时间戳(需开启 CONFIG_PRINTK_TIME) |
# 查看所有日志(含时间戳)
dmesg -T
# 实时跟踪新日志
dmesg -w
# 只显示 KERN_ERR 及以上级别
dmesg -l err
# 清除缓冲区
dmesg -c
2.5 实践建议与常见陷阱
- 始终指定日志级别:不要写裸
printk("hello\n"),应写printk(KERN_DEBUG "hello\n"),避免被默认级别污染。 - 调试信息使用 KERN_DEBUG:生产环境中可通过调整 console_loglevel 关闭调试输出,无需重新编译内核。
- 注意格式安全:printk 的格式化字符串中不要使用用户态传入的未校验字符串,防止格式串漏洞。
- 原子上下文中慎用 printk:虽然 printk 本身是原子的,但某些控制台驱动(如 USB 串口)的 write 回调可能睡眠。在中断上下文或自旋锁保护区域中,优先使用
printk_deferred()或pr_emerg()等原子安全变体。 - 使用 pr_* 宏简化代码:内核提供了
pr_emerg、pr_alert、pr_err、pr_warn、pr_info、pr_debug等封装宏,自动添加日志级别前缀,推荐在驱动开发中使用。
// 推荐写法:使用 pr_* 宏
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt // 可选:添加模块名前缀
pr_info("usb_driver: probe succeeded, vid=0x%04x\n", vid);
pr_err("dma transfer timeout, status=0x%08x\n", status);
pr_debug("irq handler entered, pending=%d\n", pending);
快速验证:在驱动初始化函数中加入
pr_info("Hello from my_driver!\n");,然后执行 dmesg | tail 确认输出。如果看不到,检查 cat /proc/sys/kernel/printk 的第一个值是否 ≥ 6(KERN_INFO 的级别)。