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 */};详细解释:
console_loglevel(当前控制台级别)- 作用:这是决定打印与否的“门槛”。
- 规则:只有
消息级别 < console_loglevel时,消息才会显示在终端上。 - 示例:若设为 4,则只有 0~3 级的消息会显示。
default_message_loglevel(默认消息级别)- 作用:当
printk("msg")没有指定级别宏时,赋予该消息的默认级别。 - 注意:通常默认为
KERN_WARNING(4)。
- 作用:当
minimum_console_loglevel- 作用:安全底线,防止用户把
console_loglevel设得太低导致连 Panic 都看不见。通常为 1。
- 作用:安全底线,防止用户把
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}...五、总结
- Printk 级别:由
console_loglevel控制,数值越小优先级越高。 - 动态调试:通过
/proc/sys/kernel/printk可以实时修改过滤规则。 - 核心机制:
- 先存:所有日志无条件存入
log_buf(dmesg可见)。 - 后显:只有满足
level < console_loglevel的日志才会推送到串口。
- 先存:所有日志无条件存入
- 源码路径:Linux 4.4 中,主要逻辑在
kernel/printk/printk.c,过滤逻辑位于call_console_drivers。