2. printk 基础:函数原型、日志级别与控制台输出机制

printk 是 Linux 内核中最基础、最常用的调试输出函数,其地位相当于用户空间的 printf。在内核开发与调试初期,printk 往往是开发者最先接触的工具。理解其函数原型、日志级别以及控制台输出机制,是掌握内核调试的第一步。

2.1 printk 函数原型

printk 定义在 <linux/printk.h> 中,其核心原型与 printf 高度相似,但增加了日志级别参数。

// 标准原型
int printk(const char *fmt, ...);

// 带日志级别的典型调用
printk(KERN_INFO "Hello, kernel! Value = %d\n", value);

关键特性:

  • 格式字符串:与 printf 兼容,支持 %d、%s、%x、%p(指针)等。
  • 返回值:成功时返回写入的字符数(不含 null 终止符),失败时返回负的错误码。
  • 日志级别宏:必须放在格式字符串的开头,且与格式字符串之间没有逗号。
  • 原子性:printk 可以在中断上下文、持有自旋锁等原子上下文中安全调用(但需注意避免死锁)。
⚠️ 常见错误:忘记在格式字符串前加日志级别宏,或错误地在宏与格式串之间加了逗号。例如 printk(KERN_INFO, "msg\n"); 是错误的,正确写法是 printk(KERN_INFO "msg\n");。

2.2 日志级别(Log Levels)

Linux 内核定义了 8 个日志级别,从最高紧急度 KERN_EMERG 到最低调试 KERN_DEBUG。每个级别对应一个数字,数字越小,优先级越高。

宏名称 数字值 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件错误、内核 panic 前
KERN_ALERT 1 必须立即采取行动 严重内存损坏、关键数据丢失
KERN_CRIT 2 临界条件 驱动初始化失败、IO 错误
KERN_ERR 3 错误条件 设备操作失败、系统调用返回错误
KERN_WARNING 4 警告条件 资源不足、配置不推荐
KERN_NOTICE 5 正常但重要的事件 模块加载/卸载、挂载文件系统
KERN_INFO 6 信息性消息 驱动版本信息、硬件探测结果
KERN_DEBUG 7 调试信息 函数入口/出口、变量值跟踪
💡 最佳实践:生产环境内核通常将控制台日志级别设为 4(KERN_WARNING),只显示错误及更高级别的消息。调试时可通过 echo 8 > /proc/sys/kernel/printk 临时调低控制台级别,以显示所有调试信息。

2.3 控制台输出机制

printk 的输出并非直接写入物理控制台,而是经过一个多层次的管道机制。理解这个流程,有助于定位日志丢失或延迟的问题。

printk 输出流程架构图 printk 输出流程架构 printk() 调用 格式化 & 日志级别解析 环形缓冲区 (log_buf) 控制台级别过滤 控制台驱动 (serial/VGA/net) 关键说明 • 所有 printk 先写入环形缓冲区 • 控制台级别过滤决定是否输出 • 可通过 /proc/kmsg 或 dmesg 读取 • 控制台驱动可动态注册/注销

2.4 控制台日志级别过滤

内核通过 /proc/sys/kernel/printk 文件控制哪些级别的消息可以输出到控制台。该文件包含 4 个整数值:

# cat /proc/sys/kernel/printk
4       4       1       7
# 字段含义:
# 1. 控制台日志级别 (console_loglevel) — 当前值: 4
# 2. 默认消息日志级别 (default_message_loglevel) — 当前值: 4
# 3. 最小控制台日志级别 (minimum_console_loglevel) — 当前值: 1
# 4. 默认控制台日志级别 (default_console_loglevel) — 当前值: 7
字段 含义 典型值
console_loglevel 只有日志级别数字 小于 此值的消息才会输出到控制台 4(显示 KERN_WARNING 及以上)
default_message_loglevel 当 printk 未显式指定日志级别时,使用此默认值 4
minimum_console_loglevel console_loglevel 可设置的最小值(防止完全关闭输出) 1
default_console_loglevel 系统启动时的默认 console_loglevel 7
🔧 调试技巧:在开发调试阶段,建议将 console_loglevel 设为 8 或 15(实际内核只用到 0-7,设为更大值会显示所有消息),以确保所有 KERN_DEBUG 消息都能输出到控制台:
echo "8 4 1 7" > /proc/sys/kernel/printk

2.5 环形缓冲区与 dmesg

无论消息是否输出到控制台,所有 printk 消息都会被写入内核的环形缓冲区(ring buffer,即 log_buf)。这意味着:

  • 即使控制台没有显示,消息仍然可以通过 dmesg 命令查看。
  • 缓冲区大小固定(可通过 CONFIG_LOG_BUF_SHIFT 配置,典型值为 128KB)。
  • 当缓冲区满时,新消息会覆盖最旧的消息。
# 查看所有内核日志
dmesg

# 查看最后 10 条消息
dmesg | tail -10

# 实时监控内核日志(类似 tail -f)
dmesg -w

# 清除日志缓冲区
dmesg -c

2.6 实际调试示例:驱动初始化跟踪

以下是一个典型的驱动调试场景,展示如何利用不同日志级别进行分层次调试:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/printk.h>

static int __init my_driver_init(void)
{
    int ret = 0;

    // 关键步骤:使用 KERN_INFO 报告正常启动
    printk(KERN_INFO "my_driver: initializing...\n");

    // 调试细节:使用 KERN_DEBUG 输出变量值
    printk(KERN_DEBUG "my_driver: config_param = %d\n", config_param);

    // 错误处理:使用 KERN_ERR 报告失败
    if (failed) {
        printk(KERN_ERR "my_driver: hardware init failed, error=%d\n", ret);
        return -ENODEV;
    }

    // 成功:使用 KERN_NOTICE 报告重要事件
    printk(KERN_NOTICE "my_driver: initialized successfully\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "my_driver: exiting\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
💡 分层调试策略:在开发阶段,使用 KERN_DEBUG 输出详细跟踪信息;在提交代码前,将稳定的调试信息降级为 KERN_INFO 或移除,仅保留必要的错误和警告输出。这样既不影响生产环境性能,又保留了调试能力。