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

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 */
};

详细解释:

  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 个值,无需重新编译内核。

查看当前值:

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} ...

五、总结

  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
相关推荐
EndingCoder5 分钟前
函数基础:参数和返回类型
linux·前端·ubuntu·typescript
CAU界编程小白17 分钟前
Linux系统编程系列之动静态库
linux
济61719 分钟前
linux(第十三期)--filezilla使用方法(实现ubuntu和windows11文件互传)-- Ubuntu20.04
linux·运维·ubuntu
HIT_Weston20 分钟前
91、【Ubuntu】【Hugo】搭建私人博客:侧边导航栏(五)
linux·运维·ubuntu
oMcLin23 分钟前
如何在 Rocky Linux 8.6 上配置并调优 Nginx 与 Lua 脚本,提升 API 网关的性能与并发处理能力
linux·nginx·lua
Yana.nice32 分钟前
Linux目录结构说明
linux
EndingCoder36 分钟前
箭头函数和 this 绑定
linux·前端·javascript·typescript
食咗未39 分钟前
Linux iptables工具的使用
linux·运维·服务器·驱动开发·网络协议·信息与通信
tech-share44 分钟前
【无标题】IOMMU功能测试软件设计及实现 (二)
linux·架构·系统架构·gpu算力
时兮兮时1 小时前
Linux 服务器后台任务生存指南
linux·服务器·笔记