桂林市网站建设_网站建设公司_漏洞修复_seo优化
2026/1/7 23:08:49 网站建设 项目流程

Linux Kernel 4.4printk源码分析与使用详解

  • 参考资料:百问网 - UART子系统
  • Kernel版本:Linux 4.4.154
  • 开发板:Firefly-RK3288
  • 关键文件kernel/printk/printk.c,include/linux/kern_levels.h

一、printk 的基本使用与打印级别

调试内核驱动最简单的方法就是使用printk函数。它与用户空间的printf格式类似,但多了一个**日志级别(Log Level)**的概念。

1.1 printk 使用示例

在驱动程序中,我们通常这样调用:

printk("This is an example\n");// 未指定级别,使用默认级别printk(KERN_WARNING"This is an example\n");// 指定为 WARNING 级别

底层原理
printk实际上支持在字符串头部加入\001n格式的字符来指定级别(n 为 0~7)。KERN_WARNING等宏本质上就是这个字符串前缀。

/* include/linux/kern_levels.h */#defineKERN_SOH"\001"/* ASCII Start Of Header */#defineKERN_WARNINGKERN_SOH"4"/* warning conditions */

1.2 打印级别定义

Linux 内核定义了 8 个打印级别(数值越小,优先级越高):

宏名称字符串说明
KERN_EMERG“0”系统不可用 (System is unusable)
KERN_ALERT“1”必须立即采取行动 (Action must be taken immediately)
KERN_CRIT“2”临界条件 (Critical conditions)
KERN_ERR“3”错误条件 (Error conditions)
KERN_WARNING“4”警告条件 (Warning conditions)
KERN_NOTICE“5”正常但重要的情况
KERN_INFO“6”信息性消息
KERN_DEBUG“7”调试级别消息

1.3 控制台打印控制(核心宏)

include/linux/kernel.h(实际上数据定义在kernel/printk/printk.c) 中,有四个核心宏决定了消息是否会打印到硬件控制台上。

#defineconsole_loglevel(console_printk[0])#definedefault_message_loglevel(console_printk[1])#defineminimum_console_loglevel(console_printk[2])#definedefault_console_loglevel(console_printk[3])

它们对应的数组定义如下:

/* kernel/printk/printk.c */intconsole_printk[4]={CONSOLE_LOGLEVEL_DEFAULT,/* console_loglevel */MESSAGE_LOGLEVEL_DEFAULT,/* default_message_loglevel */CONSOLE_LOGLEVEL_MIN,/* minimum_console_loglevel */CONSOLE_LOGLEVEL_DEFAULT,/* default_console_loglevel */};

详细解释:

  1. console_loglevel(当前控制台级别)

    • 作用:这是决定打印与否的“门槛”。
    • 规则:只有消息级别 < console_loglevel时,消息才会显示在终端上。
    • 示例:若设为 4,则只有 0~3 级的消息会显示。
  2. default_message_loglevel(默认消息级别)

    • 作用:当printk("msg")没有指定级别宏时,赋予该消息的默认级别。
    • 注意:通常默认为KERN_WARNING(4)。
  3. minimum_console_loglevel

    • 作用:安全底线,防止用户把console_loglevel设得太低导致连 Panic 都看不见。通常为 1。
  4. default_console_loglevel

    • 作用:系统启动时的初始console_loglevel

1.4 在用户空间修改打印级别

我们可以通过/proc文件系统动态查看和修改这 4 个值,无需重新编译内核。

查看当前值:

cat/proc/sys/kernel/printk# 输出示例: 4 4 1 7

这意味着当前只有级别 0~3 的消息会打印。

修改示例:打开所有调试信息
如果你想看到KERN_DEBUG信息,需要将第一个值设为 8(因为 7 < 8):

echo8>/proc/sys/kernel/printk# 或者一次性设置4个值echo"8 4 1 7">/proc/sys/kernel/printk

二、printk 的整体架构与数据流

理解printk最好的方式是跟踪数据流向。


(图源:百问网)

我们可以将上图分为四个阶段:

第一阶段:源头(驱动层)

  • 驱动调用printk
  • 如果未指定级别,内核自动补上default_message_loglevel

第二阶段:缓存(内核 Buffer 层)

  • 格式化:内核将消息封装结构体(包含长度.len、级别.level、内容"abc")。
  • 存入log_buf:这是全局环形缓冲区。
  • 关键点:无论级别高低,所有printk的内容都会存入log_buf。这也是为什么dmesg命令能看到所有历史日志的原因。

第三阶段:分发与过滤(Console 驱动层)

  • 数据从log_buf取出。
  • 过滤判断:在此处进行if (level < console_loglevel)的判断。
  • 如果不满足条件,流程终止(只存不打)。
  • 如果满足条件,调用具体驱动的write函数。

第四阶段:物理输出(硬件层)

  • Console 驱动:如ttyS0(串口) 或tty0(屏幕)。
  • 调用底层的 UART 寄存器操作将字符发送出去。

三、Kernel 4.4 源码深度剖析

让我们深入kernel/printk/printk.c看看这一切是如何实现的。

3.1 入口函数printk

/* kernel/printk/printk.c */asmlinkage __visibleintprintk(constchar*fmt,...){printk_func_tvprintk_func;va_list args;intr;va_start(args,fmt);// 获取当前CPU的打印函数指针vprintk_func=this_cpu_read(printk_func);r=vprintk_func(fmt,args);va_end(args);returnr;}EXPORT_SYMBOL(printk);

3.2 为什么使用函数指针vprintk_func

这里涉及到一个设计细节:防止 NMI(不可屏蔽中断)死锁

  • 默认情况下,printk_func指向vprintk_default
  • 场景:如果系统正在打印(持有锁)时发生 NMI,NMI 处理程序如果也调用printk,尝试获取同一个锁,就会导致死锁。
  • 机制:在 NMI 上下文中,内核会将该指针临时切换为vprintk_nmi,将数据写入临时的 NMI 安全缓冲区,从而避免死锁。

3.3 核心处理vprintk_emit

vprintk_default最终会调用vprintk_emit,这是核心大管家。

asmlinkageintvprintk_emit(intfacility,intlevel,...){// 1. 将数据写入 log_buf (Ring Buffer)// 无论级别如何,先存下来!printed_len+=log_store(0,2,LOG_PREFIX|LOG_NEWLINE,0,NULL,0,text,text_len);// 2. 尝试唤醒控制台驱动进行输出if(!in_sched){// 获取 console 信号量/锁if(console_trylock_for_printk())console_unlock();// 重点在这里}returnprinted_len;}

3.4 消费与输出console_unlock

数据存好了,现在要发给硬件。这个工作由console_unlock完成。它是一个循环,不断从log_buf取数据。

voidconsole_unlock(void){for(;;){// ... 从 log_buf 读取一条 msg ...// 格式化消息len+=msg_print_text(msg,...);// ... 释放 logbuf_lock (允许并发写 buffer) ...// 调用驱动发送数据// 注意:这里传入了 msg->levelcall_console_drivers(level,ext_text,ext_len,text,len);}}

3.5 真正的过滤逻辑call_console_drivers

在 Linux 4.4 中,打印级别的判断逻辑被封装在call_console_drivers内部。

staticvoidcall_console_drivers(intlevel,constchar*text,size_tlen,...){// --- 核心过滤逻辑 ---// 如果 消息级别 >= console_loglevel,且没有强制忽略级别// 则直接返回,不进行硬件操作。#ifndefCON_PSTOREif(level>=console_loglevel&&!ignore_loglevel)return;#endif// 遍历所有 console (如串口、屏幕)for_each_console(con){if(con->write)con->write(con,text,len);// 最终操作硬件}}

总结执行链:
printk->vprintk_emit->log_store(存入内存) ->console_unlock->call_console_drivers(检查级别) ->uart_console_write(硬件输出)。


四、硬件选择:内核怎么知道往哪打?

内核可能有多个输出设备(VGA、串口、网络),它通过console参数来决定。

4.1 命令行参数 (cmdline)

在系统启动日志或/proc/cmdline中可以看到:

cat/proc/cmdline# 输出: ... console=ttyFIQ0 ...

这表示内核选择名为ttyFIQ0的设备作为控制台。

4.2 参数来源

这些参数通常来自Device Tree (设备树)chosen节点,或者由U-Boot动态传递。

设备树示例:

/ { chosen { bootargs = "console=ttymxc1,115200"; }; };

U-Boot 环境变量示例 (IMX6ULL):

=>print consoleconsole=ttymxc0=>print mmcargsmmcargs=setenv bootargsconsole=${console},${baudrate}...

五、总结

  1. Printk 级别:由console_loglevel控制,数值越小优先级越高。
  2. 动态调试:通过/proc/sys/kernel/printk可以实时修改过滤规则。
  3. 核心机制
    • 先存:所有日志无条件存入log_buf(dmesg可见)。
    • 后显:只有满足level < console_loglevel的日志才会推送到串口。
  4. 源码路径:Linux 4.4 中,主要逻辑在kernel/printk/printk.c,过滤逻辑位于call_console_drivers

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询