ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux 内核 printk 日志机制详解:日志级别、pr_* 宏体系与 console_loglevel 输出控制

Linux 内核 printk 日志机制详解:日志级别、pr_* 宏体系与 console_loglevel 输出控制 Linux 内核 printk 日志机制详解日志级别、pr_* 宏体系与 console_loglevel 输出控制【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文以 Linux 内核官方文档Documentation/core-api/printk-basics.rst为主体系统讲解内核消息日志函数printk()的使用方法、日志级别KERN_*与console_loglevel的过滤机制、pr_*()宏族与pr_fmt()格式化前缀以及热路径中滥用printk()导致系统锁死的规避策略。读完后你将能够在内核代码中规范地输出分级日志理解dmesg//proc/sys/kernel/printk背后的工作原理并掌握printk_deferred()、pr_*_ratelimited()、pr_*_once()等防锁死手段的适用场景。printk() 在内核日志体系中的位置printk()是 Linux 内核中最广为人知的函数之一是打印消息的标准工具通常也是最基本的跟踪与调试手段。它的用法和 C 库的printf(3)类似但存在两个关键的功能差异printk()消息可以指定日志级别log level格式字符串大体兼容 C99但并不遵循完全相同的规范它有一些扩展也有一些限制——不支持%n转换也不支持浮点转换说明符。格式说明符的完整规则见内核文档 printk-formats.rst。所有printk()消息都会先写入内核日志缓冲区kernel log buffer这是一个环形缓冲区ring buffer并通过/dev/kmsg导出到用户空间。用户空间读取它的惯用方式就是dmesg命令。printk()的典型用法如下printk(KERN_INFO Message: %s\n, arg);注意KERN_INFO并不是printk()的独立参数而是直接拼接到格式字符串前面的日志级别字符串。日志级别KERN_* 字符串与 LOGLEVEL_* 整数等价物可用的日志级别如下表完整继承自官方文档别名函数均可在 include/linux/printk.h 中找到定义名称字符串别名函数KERN_EMERG0pr_emerg()KERN_ALERT1pr_alert()KERN_CRIT2pr_crit()KERN_ERR3pr_err()KERN_WARNING4pr_warn()KERN_NOTICE5pr_notice()KERN_INFO6pr_info()KERN_DEBUG7pr_debug()定义了DEBUG时pr_devel()亦可用KERN_DEFAULT—KERN_CONTcpr_cont()从源码结构看这些级别在 include/linux/kern_levels.h 中的定义揭示了其本质——它们是SOHStart Of HeaderASCII\001字符加上一个级别数字的字符串拼接#define KERN_SOH \001 /* ASCII Start Of Header */ #define KERN_EMERG KERN_SOH 0 /* system is unusable */ #define KERN_ALERT KERN_SOH 1 /* action must be taken immediately */ #define KERN_CRIT KERN_SOH 2 /* critical conditions */ #define KERN_ERR KERN_SOH 3 /* error conditions */ #define KERN_WARNING KERN_SOH 4 /* warning conditions */ #define KERN_NOTICE KERN_SOH 5 /* normal but significant condition */ #define KERN_INFO KERN_SOH 6 /* informational */ #define KERN_DEBUG KERN_SOH 7 /* debug-level messages */ #define KERN_DEFAULT /* the default kernel loglevel */ #define KERN_CONT KERN_SOH c同一头文件还给出了一组整数等价物供内核内部代码如[kernel/printk/printk.c](https://link.gitcode.com/i/7629ea1a031626ae82de24bc7b1f2497)中的控制台过滤逻辑使用#define LOGLEVEL_SCHED -2 /* 调度代码的延迟消息使用的特殊级别 */ #define LOGLEVEL_DEFAULT -1 /* 默认或上一次使用的日志级别 */ #define LOGLEVEL_EMERG 0 ... #define LOGLEVEL_DEBUG 7其中两个特殊值值得注意LOGLEVEL_SCHED (-2)从调度器路径延迟输出的消息被标记为该特殊级别LOGLEVEL_DEFAULT (-1)表示“未显式指定级别”运行时沿用当前默认级别。KERN_CONT有特殊用途它用于标注“延续行”continued line只能紧跟在一条未以\n结尾的消息之后使用且官方注释明确说明它仅供早期启动阶段的 core/arch 代码使用——延续行在 SMP 环境下并不安全。对应的宏pr_cont()见 include/linux/printk.h#L595-L596。console_loglevel决定是否立即打到控制台的关键变量日志级别规定了消息的重要程度。内核根据消息级别与当前console_loglevel一个内核变量共同决定是否将消息立即显示打印到当前控制台当消息的优先级高于即日志级别数值小于console_loglevel时消息会被打印到控制台。如果写消息时省略了日志级别则该消息按KERN_DEFAULT级别处理。查看当前 console_loglevel$ cat /proc/sys/kernel/printk 4 4 1 7输出依次是四个值位置含义示例值第 1 个current当前生效的控制台级别4第 2 个default默认级别4第 3 个minimum允许用户设置的下限1第 4 个boot-time-default启动时默认级别7从源码看这四个值由console_loglevel、minimum_console_loglevel、default_console_loglevel三个内核变量支撑初始值在 kernel/printk/printk.c#L66-L69 定义通过 kernel/printk/sysctl.c 中的 sysctl 表暴露为/proc/sys/kernel/printk。修改 console_loglevel直接向/proc/sys/kernel/printk写入目标级别即可。例如让所有消息都打到控制台# echo 8 /proc/sys/kernel/printk另一个等效途径是使用dmesg# dmesg -n 5dmesg -n 5将console_loglevel设为 5即只有KERN_WARNING4及以上严重程度的消息才会打印到控制台。详见dmesg(1)。关于上限有两个值得了解的常量定义于 include/linux/printk.h#L54-L62CONSOLE_LOGLEVEL_MIN 1允许用户设置的最小级别下限CONSOLE_LOGLEVEL_MOTORMOUTH 15注释为 You cant shut this one up——即便设到这个级别某些级别也不允许被完全静音CONSOLE_LOGLEVEL_DEFAULT取自 Kconfig 选项CONFIG_CONSOLE_LOGLEVEL_DEFAULT即上表输出中的第 2 个值内核默认即以此为初始console_loglevel。pr_*() 宏族与 pr_fmt()更简洁、可统一前缀的写法作为printk()的替代方案推荐使用pr_*()别名宏族。这一族宏把日志级别内嵌在宏名中例如pr_info(Info message no. %d\n, msg_num);会输出一条KERN_INFO消息。它们在 include/linux/printk.h 中全部展开为printk(级别 pr_fmt(fmt), ...)的形式如pr_emerg()L523-L524、pr_err()L553-L554等。除写法更简洁外pr_*()还支持通过pr_fmt()宏为整个文件统一注入格式前缀。例如在源文件顶部在任何#include指令之前定义#define pr_fmt(fmt) %s:%s: fmt, KBUILD_MODNAME, __func__则该文件中每条pr_*()消息都会被自动加上模块名和发起消息的函数名作为前缀。pr_fmt()的默认定义就是恒等映射include/linux/printk.h#L401#define pr_fmt(fmt) fmt这意味着未定义pr_fmt()的源码行为完全不变定义它只是一个可选的增强。官方文档头文件注释中还给出了驱动开发者的建议写驱动时请优先使用dev_dbg等设备模型提供的调试接口而非裸pr_debug。条件编译的调试宏pr_debug() 与 pr_devel()调试目的有两个条件编译宏其展开行为在 include/linux/printk.h#L598-L642 中定义pr_devel()当编译单元定义了DEBUG时展开为printk(KERN_DEBUG pr_fmt(fmt), ...)否则展开为no_printk()即被完全编译剔除pr_debug()三级行为——开启CONFIG_DYNAMIC_DEBUG时展开为dynamic_pr_debug()支持运行时按模块/文件/函数动态启停无需重新编译否则若定义了DEBUG等价于一条KERN_DEBUG级别的printk否则被no_printk()剔除。官方文档的表述是这两个宏除非定义了DEBUGpr_debug()的情况下是DEBUG或CONFIG_DYNAMIC_DEBUG否则都会被编译掉compiled-out。函数参考官方文档的 Function reference 一节通过kernel-doc从 include/linux/printk.h 提取了以下函数的文档printk、pr_emerg、pr_alert、pr_crit、pr_err、pr_warn、pr_notice、pr_info、pr_fmt、pr_debug、pr_devel、pr_cont。结合头文件中的注释printk()自身的语义要点include/linux/printk.h#L487-L511printk()可以从任何上下文调用It can be called from any context. We want it to work.它尝试获取console_lock拿到锁就直接记录输出并调用控制台驱动拿不到锁时消息只放入日志缓冲区并返回由当前持有console_sem的代码在console_unlock()中看到新输出并补发到各控制台这种延迟打印有一个副作用“先printk()再修改console_loglevel”的代码可能不符合预期因为级别是在真正打印发生时才被检查的。宏定义形式上printk()本身是一个包装宏在开启 printk indexing格式串索引详见 printk-index.rst时会附带生成索引元数据#define printk(fmt, ...) printk_index_wrap(_printk, fmt, ##__VA_ARGS__) #define printk_deferred(fmt, ...) \ printk_index_wrap(_printk_deferred, fmt, ##__VA_ARGS__)避免热路径中过度 printk() 导致锁死注意原文档特别强调本节仅针对传统控制台驱动未使用 nbcon API 的驱动和非 PREEMPT_RT 内核。一旦所有控制台驱动都迁移到 nbcon这段文档即可移除。锁死是如何发生的在热路径hot path——例如中断处理程序、定时器回调、高频网络收包例程——中使用printk()配合传统控制台如consolettyS0可能引发锁死。传统控制台会同步获取console_sem并在冲刷flush消息期间阻塞可能长时间关中断从而触发硬/软锁死检测器hard/soft lockup detector。四种规避手段按文档推荐组织限频与一次性宏使用限频变体pr_*_ratelimited()如pr_err_ratelimited()或一次性宏pr_*_once()降低消息频率。源码中pr_*_once()统一基于DO_ONCE_LITE()实现include/linux/printk.h#L660-L669语义与WARN_ONCE()类似——同一条消息只真正打印一次pr_*_ratelimited()则走printk_ratelimited()路径由内核的 ratelimit 机制按时间窗折叠重复消息。降级 过滤给非关键消息分配较低级别如KERN_DEBUG再通过上文介绍的console_loglevel将其从控制台输出中过滤掉。printk_deferred()消息立即写入环形缓冲区但把控制台打印推迟到安全上下文执行。这是针对传统控制台的 workaround。它在 include/linux/printk.h#L512-L513 中定义为_printk_deferred()的索引包装。另外头文件还提供了printk_deferred_enter()/printk_deferred_exit()宏官方注释称其是特定场景下的 “hack”临时手段内核电源管理代码中即有实际应用示例如 include/linux/suspend.h#L550 在休眠流程中用printk_deferred(KERN_DEBUG pr_fmt(fmt), ...)输出。将传统控制台驱动移植到非阻塞的 nbcon API以CON_NBCON标志标识。这是首选方案nbcon 控制台把消息打印卸载offload到一个专用内核线程执行调用者不再因控制台 I/O 而阻塞。临时调试与长期方案的边界trace_printk()可以用于临时调试但官方明确要求它不得出现在 mainline主线代码中。更多背景见 debugging.rst如果热路径确实需要更持久的输出应使用trace events跟踪事件。相关文档与示例见 events.rst 和samples/trace_events/trace-events-sample.[ch]。实践速查需求推荐做法常规信息输出pr_info()/pr_notice()配合文件级pr_fmt()前缀开发期高频调试pr_debug()开CONFIG_DYNAMIC_DEBUG可运行时控制错误路径首次触发pr_err_once()热路径高频错误pr_err_ratelimited()或 trace event热路径且需完整入日志缓冲printk_deferred()传统控制台nbcon 控制台上则直接pr_*()查看/调整控制台输出门槛cat /proc/sys/kernel/printkecho 8 /proc/sys/kernel/printkdmesg -n level读取历史内核日志dmesg底层读/dev/kmsg环形缓冲区小结printk()及其pr_*()宏族构成了 Linux 内核消息日志的基础设施级别字符串以 SOH 前缀编码在格式串头部include/linux/kern_levels.h级别与console_loglevel的比较决定消息是否即时上屏kernel/printk/printk.c用户空间则经/dev/kmsg与dmesg消费环形缓冲区。开发实践中应遵循默认用pr_*()并统一pr_fmt()前缀调试用pr_debug()/pr_devel()热路径务必使用限频/一次性/延迟变体或 trace event避免传统控制台同步冲刷引发的锁死长期正解是推动控制台驱动使用 nbcon 非阻塞 API。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进