Linux Kernel 4.4 printk 源码分析与使用详解
- 参考资料 :百问网 - 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 使用示例
在驱动程序中,我们通常这样调用:
c
printk("This is an example\n"); // 未指定级别,使用默认级别
printk(KERN_WARNING "This is an example\n"); // 指定为 WARNING 级别
底层原理 :
printk 实际上支持在字符串头部加入 \001n 格式的字符来指定级别(n 为 0~7)。KERN_WARNING 等宏本质上就是这个字符串前缀。
c
/* include/linux/kern_levels.h */
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_WARNING KERN_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) 中,有四个核心宏决定了消息是否会打印到硬件控制台上。
c
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])
它们对应的数组定义如下:
c
/* kernel/printk/printk.c */
int console_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 个值,无需重新编译内核。
查看当前值:
bash
cat /proc/sys/kernel/printk
# 输出示例: 4 4 1 7
这意味着当前只有级别 0~3 的消息会打印。
修改示例:打开所有调试信息
如果你想看到 KERN_DEBUG 信息,需要将第一个值设为 8(因为 7 < 8):
bash
echo 8 > /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
c
/* kernel/printk/printk.c */
asmlinkage __visible int printk(const char *fmt, ...)
{
printk_func_t vprintk_func;
va_list args;
int r;
va_start(args, fmt);
// 获取当前CPU的打印函数指针
vprintk_func = this_cpu_read(printk_func);
r = vprintk_func(fmt, args);
va_end(args);
return r;
}
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,这是核心大管家。
c
asmlinkage int vprintk_emit(int facility, int level, ...)
{
// 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(); // 重点在这里
}
return printed_len;
}
3.4 消费与输出 console_unlock
数据存好了,现在要发给硬件。这个工作由 console_unlock 完成。它是一个循环,不断从 log_buf 取数据。
c
void console_unlock(void)
{
for (;;) {
// ... 从 log_buf 读取一条 msg ...
// 格式化消息
len += msg_print_text(msg, ...);
// ... 释放 logbuf_lock (允许并发写 buffer) ...
// 调用驱动发送数据
// 注意:这里传入了 msg->level
call_console_drivers(level, ext_text, ext_len, text, len);
}
}
3.5 真正的过滤逻辑 call_console_drivers
在 Linux 4.4 中,打印级别的判断逻辑被封装在 call_console_drivers 内部。
c
static void call_console_drivers(int level, const char *text, size_t len, ...)
{
// --- 核心过滤逻辑 ---
// 如果 消息级别 >= console_loglevel,且没有强制忽略级别
// 则直接返回,不进行硬件操作。
#ifndef CON_PSTORE
if (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 中可以看到:
bash
cat /proc/cmdline
# 输出: ... console=ttyFIQ0 ...
这表示内核选择名为 ttyFIQ0 的设备作为控制台。
4.2 参数来源
这些参数通常来自 Device Tree (设备树) 的 chosen 节点,或者由 U-Boot 动态传递。
设备树示例:
dts
/ {
chosen {
bootargs = "console=ttymxc1,115200";
};
};
U-Boot 环境变量示例 (IMX6ULL):
bash
=> print console
console=ttymxc0
=> print mmcargs
mmcargs=setenv bootargs console=${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。