1. 内核日志系统概述:printk机制、日志级别、日志缓冲区原理

大家好,我是你们的讲师。今天咱们来聊聊内核日志系统。说实话,搞驱动调试,日志就是你的眼睛。没有它,你就像在黑屋子里找一只黑猫——而且猫还不一定在屋里。

我个人习惯,拿到一个新板子,第一件事就是先把printk调通。为什么?因为后面所有调试手段,都得靠它来验证。你想想看,连日志都打不出来,你还能指望什么?

1.1 printk机制:内核的“printf”

printk,说白了就是内核版的printf。但跟用户态那个printf不一样,它有几个关键特性:

  • 能在任何上下文调用——中断、自旋锁、原子操作里都能用
  • 不需要用户态进程参与——内核自己就把活干了
  • 有日志级别控制——这个后面细说

我记得刚入行那会儿,有个前辈跟我说:“printk是驱动工程师的瑞士军刀。”当时我不信,后来踩了无数坑才明白——这话一点不夸张。

// 最简单的printk用法
printk("Hello, kernel world!\n");

// 带参数的printk
printk("irq = %d, data = 0x%x\n", irq, data);
注意:printk不需要像printf那样加\n就能刷新缓冲区?其实不是的。printk有自己的刷新机制,但建议还是加\n,养成好习惯。

1.2 日志级别:控制信息的“音量”

日志级别,你可以理解成音量旋钮。有些信息你希望永远能看到(比如系统崩溃),有些信息只在调试时需要(比如每次中断触发)。

内核定义了8个日志级别,从0到7,数字越小越紧急:

级别 宏定义 含义 我常用的场景
0 KERN_EMERG 紧急事件,系统不可用 内核panic前最后一条消息
1 KERN_ALERT 必须立即处理 硬件严重故障
2 KERN_CRIT 临界条件 DMA传输失败
3 KERN_ERR 错误条件 驱动初始化失败
4 KERN_WARNING 警告 寄存器值异常
5 KERN_NOTICE 正常但重要 设备热插拔事件
6 KERN_INFO 信息性消息 驱动版本信息
7 KERN_DEBUG 调试信息 寄存器读写跟踪
// 实际使用示例
printk(KERN_ERR "DMA transfer failed: status = 0x%x\n", status);
printk(KERN_INFO "Driver version %s loaded\n", DRV_VERSION);
printk(KERN_DEBUG "reg 0x%x = 0x%x\n", reg_addr, reg_val);
我的经验:调试阶段把所有printk都设成KERN_DEBUG,发布前再调整。我曾经因为忘记改级别,生产环境控制台被调试信息刷爆——嗯,那场面,挺尴尬的。

1.3 日志缓冲区原理:环形缓冲区

printk打出来的信息去哪了?答案是内核日志缓冲区。这个缓冲区是个环形结构,说白了就是个固定大小的数组,写满了就从开头覆盖。

为什么会这样设计?你想想看,内核日志可能在任何时刻产生——中断里、调度器里、甚至系统快崩溃的时候。这时候你不可能去分配内存、打开文件。环形缓冲区的好处就是:

  • 分配简单——编译时就定好了大小
  • 写入快——就是memcpy,没有锁竞争(早期版本)
  • 永不阻塞——写满了就覆盖,不会等

我记得有一次调试一个USB驱动,中断频率特别高,每秒几千次。printk打得太勤,结果环形缓冲区被刷得飞快,关键信息全被覆盖了。后来我学乖了——调试信息要控制频率,或者用tracepoint代替。

// 查看日志缓冲区大小(内核配置)
// CONFIG_LOG_BUF_SHIFT 决定大小,通常是17(128KB)
// 可以通过dmesg查看当前缓冲区使用情况
# dmesg | head -1
[    0.000000] Linux version 5.10.0 ...
核心要点:
  • 环形缓冲区大小固定,默认约128KB
  • 写满后覆盖最旧的数据
  • 可以通过内核参数log_buf_len调整大小
  • dmesg命令读取的就是这个缓冲区

1.4 实际调试中的坑与技巧

说了这么多理论,来点实战经验。我这些年踩过的坑,总结成几条:

  1. 别在中断里打太多日志——printk虽然快,但也不是零开销。高频率中断里打日志,轻则丢数据,重则系统卡死。
  2. 日志级别要分层——我习惯把驱动分成三层:初始化用KERN_INFO,运行时错误用KERN_ERR,调试用KERN_DEBUG。这样发布时只关掉DEBUG就行。
  3. 善用dmesg -w——这个命令可以实时查看日志,比cat /var/log/messages方便多了。
  4. 缓冲区不够怎么办?——加内核参数:log_buf_len=4M。我曾经在调试一个网络驱动时,把缓冲区扩到16MB才够用。
避坑指南:我曾经在生产环境上忘记关KERN_DEBUG级别的日志,结果环形缓冲区被调试信息填满,真正的错误信息全被覆盖了。排查了整整两天才发现问题——从那以后,我发布驱动前都会检查一遍所有printk的级别。

1.5 小结

printk机制,说白了就是内核给你留的一扇窗。通过它,你能看到内核里发生了什么。日志级别帮你控制信息的“音量”,环形缓冲区保证系统再忙也能记录。

下一章,咱们聊聊怎么用这些日志来定位驱动问题。到时候我会分享一个我印象特别深的案例——一个看起来是硬件问题,实际上是printk用错了的bug。

嗯,今天就到这儿。记住:调试驱动,日志先行。