2. printk基础:函数原型、日志级别与printf的区别
说到内核调试,printk绝对是最基础、最常用的工具。我刚开始做内核开发时,第一件事就是学会用printk。说白了,它就是内核版的printf,但又不完全一样。今天咱们就把它掰开揉碎了讲清楚。
2.1 printk函数原型
先看原型,其实很简单:
#include <linux/kernel.h>
int printk(const char *fmt, ...);
嗯,这个声明是不是看着眼熟?跟printf几乎一模一样。但有个关键区别——printk多了一个日志级别参数。这个级别不是单独传参,而是嵌在格式字符串的开头。
核心用法:
printk(KERN_INFO "Hello, world! 当前进程: %s\n", current->comm);
注意:KERN_INFO和后面的格式字符串之间没有逗号,它们是拼接在一起的。
我在项目中见过不少新手在这里犯错,把KERN_INFO当成单独参数传进去,结果日志级别根本没生效。你想想看,内核又不会报错,只是日志全跑到了默认级别,排查起来特别坑。
2.2 日志级别详解
内核定义了8个日志级别,从0到7,数字越小越紧急。我习惯把它们分成三档:
| 宏定义 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
| KERN_EMERG | 0 | 紧急事件 | 系统崩溃前最后一句话 |
| KERN_ALERT | 1 | 必须立即处理 | 严重硬件故障 |
| KERN_CRIT | 2 | 临界条件 | 驱动加载失败 |
| KERN_ERR | 3 | 错误 | 函数返回错误码 |
| KERN_WARNING | 4 | 警告 | 资源即将耗尽 |
| KERN_NOTICE | 5 | 普通提示 | 模块加载/卸载 |
| KERN_INFO | 6 | 信息 | 驱动初始化信息 |
| KERN_DEBUG | 7 | 调试信息 | 变量值跟踪 |
我的经验:开发阶段多用KERN_DEBUG,发布前记得删掉或改成KERN_INFO。我曾经把一个KERN_ERR级别的调试信息留在生产代码里,结果客户看到控制台刷屏,以为是系统要炸了……
为什么会分这么多级别?说白了就是为了控制输出。内核通过/proc/sys/kernel/printk这个文件控制哪些级别的日志能打印到控制台。默认值通常是:
$ cat /proc/sys/kernel/printk
7 4 1 7
这四个数字分别代表:
- 控制台日志级别:比这个值小的日志才会显示在控制台(默认7,即所有都显示)
- 默认消息日志级别:没指定级别的printk用这个值(默认4)
- 最小控制台日志级别:控制台能设置的最低级别(默认1)
- 默认控制台日志级别:系统启动时的控制台级别(默认7)
避坑指南:我曾经在调试时发现printk死活不打印,查了半天才发现是控制台级别被改成了3。KERN_INFO(级别6)比3大,所以被过滤掉了。记住:数字越小越优先,级别高于控制台级别才能显示。
2.3 printk与printf的区别
这个问题我面试时经常问。区别其实不少,我挑几个关键的说说:
2.3.1 运行环境不同
printf跑在用户空间,有完整的libc支持。printk跑在内核空间,没有libc可用。这意味着:
- printk不支持浮点数格式化(%f)
- printk不支持宽字符(%lc、%ls)
- printk有自己的一套扩展格式,比如%p用来打印指针
2.3.2 输出目标不同
printf输出到stdout,printk输出到内核日志缓冲区(一个环形缓冲区)。这个缓冲区可以通过dmesg查看,也会根据日志级别决定是否写到控制台。
关键点:printk是异步的!它先把日志放进缓冲区就返回了,真正写控制台是另一个进程(klogd)的事。所以别指望printk能精确测量时间。
2.3.3 格式说明符扩展
内核给printk加了不少私货,我常用的有:
// 打印指针,自动去除非规范地址
printk("ptr: %p\n", ptr);
// 打印MAC地址
printk("mac: %pM\n", mac_addr);
// 打印IPv4地址
printk("ip: %pI4\n", &ip_addr);
// 打印设备号
printk("dev: %d:%d\n", MAJOR(dev), MINOR(dev));
这些扩展格式在写驱动时特别有用。我记得有一次调试网卡驱动,用%pM打印MAC地址,一眼就看出寄存器读出来的值字节序反了。要是用printf那套自己拼格式,估计得debug半天。
2.3.4 并发安全性
printk内部有自旋锁保护,多核同时调用不会乱。但这也意味着——在中断上下文或持有锁时调用printk要小心,搞不好就死锁了。我早期就踩过这个坑,在自旋锁里调printk,结果printk也要拿同一个锁,直接死锁。
建议:中断上下文用printk_deferred(),它把日志放到一个专用队列,避免死锁。
2.4 知识体系总览
下面这张图把printk的核心知识点串起来了,建议保存下来:
printk虽然简单,但用好它需要理解日志级别、缓冲区机制和内核特有的格式。我建议你从今天开始,写驱动时养成指定日志级别的习惯,别偷懒用无级别的printk。等你遇到线上问题需要从海量日志里捞关键信息时,就知道这个习惯有多重要了。