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 函数原型 int printk(const char *fmt, ...) 8个日志级别 (0-7) EMERG → ALERT → CRIT → ERR → WARNING → NOTICE → INFO → DEBUG 与printf的区别 /proc/sys/kernel/printk 环形缓冲区 → dmesg %p %pM %pI4 等扩展 核心:日志级别决定显示,环形缓冲区决定存储 异步输出 · 多核安全 · 中断上下文需谨慎 公众号:蓝海资料掘金营,微信deep3321

printk虽然简单,但用好它需要理解日志级别、缓冲区机制和内核特有的格式。我建议你从今天开始,写驱动时养成指定日志级别的习惯,别偷懒用无级别的printk。等你遇到线上问题需要从海量日志里捞关键信息时,就知道这个习惯有多重要了。