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

printk 是 Linux 内核中最核心的日志输出函数,其地位等同于用户空间的 printf。驱动开发者通过 printk 输出调试信息、状态变更、错误告警以及关键性能指标。理解 printk 的函数原型、日志级别定义以及控制台输出机制,是进行高效内核调试的第一步。

2.1 printk 函数原型

printk 定义在 include/linux/printk.h 中,其原型与标准 C 库的 printf 高度相似,但增加了日志级别前缀机制。

// 核心原型(简化)
int printk(const char *fmt, ...);

// 带日志级别的标准调用格式
printk(KERN_INFO "usb_driver: device probe succeeded, vid=0x%04x\n", vid);
参数 / 特性 说明
fmt 格式化字符串,支持 %d、%x、%s、%p 等标准格式符,以及内核扩展格式符(如 %pI4 打印 IPv4 地址)
... 可变参数,与 fmt 中的格式符一一对应
返回值 成功时返回输出的字符数(不含结尾 '\0');失败时返回负的错误码
日志级别前缀 通过宏定义(如 KERN_INFO)嵌入到 fmt 字符串头部,控制消息的紧急程度
关键区别:printk 与 printf 最大的不同在于:printk 的输出目标不是 stdout,而是内核日志缓冲区(log_buf),并且受日志级别和控制台日志级别的双重过滤。printk 可以在任何上下文(中断、原子上下文、调度器)中安全调用,但某些上下文下不能使用可能导致睡眠的变体(如 printk 的异步版本)。

2.2 日志级别定义(KERN_EMERG 到 KERN_DEBUG)

Linux 内核定义了 8 个日志级别宏,位于 include/linux/kern_levels.h。每个宏对应一个数字优先级,数字越小,优先级越高(越紧急)。

宏名称 数字值 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件故障、内核 panic 前输出
KERN_ALERT 1 必须立即处理 严重内存损坏、关键数据丢失
KERN_CRIT 2 临界条件 驱动探测失败导致设备不可恢复
KERN_ERR 3 错误条件 DMA 传输超时、寄存器读写校验失败
KERN_WARNING 4 警告条件 降级模式运行、非关键配置缺失
KERN_NOTICE 5 正常但重要 设备热插拔事件、驱动加载成功
KERN_INFO 6 信息性消息 驱动版本号、寄存器初始值、中断统计
KERN_DEBUG 7 调试信息 函数进入/退出、变量中间值、状态机跳转
常见错误:新手常犯的错误是忘记在 printk 中加日志级别前缀。此时内核会使用默认级别 DEFAULT_MESSAGE_LOGLEVEL(通常为 4,即 KERN_WARNING)。这会导致调试信息(本应使用 KERN_DEBUG)被当作警告输出,污染控制台并降低关键告警的可读性。始终显式指定日志级别。

2.3 控制台输出机制

printk 的输出并非直接到达终端或串口,而是经过一套分层过滤机制。理解这一机制,才能解释“为什么我 printk 了却看不到输出”这一经典问题。

2.3.1 核心过滤参数

参数名 文件路径(/proc/sys/kernel/) 默认值 作用
console_loglevel printk 7(最高) 控制台能显示的最高日志级别(数字越小越紧急)
default_message_loglevel printk 4 未指定级别时的默认日志级别
minimum_console_loglevel printk 1 控制台允许设置的最低级别(防止完全关闭输出)
default_console_loglevel printk 7 系统启动后的默认控制台级别

控制台日志级别可以通过 echo 命令动态调整:

# 查看当前 printk 参数(四个值依次对应上述四个参数)
cat /proc/sys/kernel/printk
# 输出示例:7  4  1  7

# 将控制台日志级别设为 4(只显示 KERN_WARNING 及以上)
echo "4 4 1 7" > /proc/sys/kernel/printk

# 临时开启所有调试信息(级别 8 表示显示所有,包括 KERN_DEBUG)
echo "8 4 1 7" > /proc/sys/kernel/printk

2.3.2 输出过滤流程

printk 消息输出过滤流程 printk("KERN_XXX ...") 提取日志级别 L (0-7) L <= console_loglevel ? (数字越小越紧急) 否 仅写入内核日志缓冲区 是 写入 log_buf + 控制台设备输出 用户态:dmesg / syslog 读取 console_loglevel 默认 7 KERN_DEBUG(7) 默认可见

2.3.3 控制台设备注册

控制台输出最终由 struct console 驱动完成。每个控制台设备(串口、VGA、虚拟终端、网络控制台)都需要注册一个 console 结构体。printk 在决定输出后,会遍历已注册的 console 列表,调用其 write 回调函数。

// include/linux/console.h
struct console {
    char    name[16];          // 控制台名称,如 "ttyS0"
    void    (*write)(struct console *, const char *, unsigned);
    int     (*read)(struct console *, char *, unsigned);
    struct  device *device;    // 关联的设备
    int     (*setup)(struct console *, char *);
    short   flags;             // CON_ENABLED, CON_CONSDEV 等
    short   index;             // 设备索引
    int     cflag;             // 串口控制标志
    void    *data;
    struct  console *next;
};
调试要点:如果 printk 消息在 dmesg 中可见,但串口终端看不到,通常原因有:(1) console_loglevel 设置过低;(2) 串口 console 驱动未注册或注册顺序不对;(3) 串口硬件流控或波特率不匹配。使用 console=ttyS0,115200 内核启动参数可强制指定控制台设备。

2.4 日志缓冲区(log_buf)与 dmesg

所有 printk 消息无论是否输出到控制台,都会写入内核日志缓冲区 log_buf。该缓冲区是一个环形缓冲区,默认大小为 256 KB(可配置)。用户态通过 dmesg 命令读取该缓冲区内容。

特性 说明
缓冲区类型 环形缓冲区(ring buffer),新消息覆盖最旧消息
默认大小 CONFIG_LOG_BUF_SHIFT 控制,通常为 18(256 KB)
访问方式 dmesg、/proc/kmsg、syslog 系统调用
持久性 掉电丢失;可通过 pstore/ramoops 在死机后保留
时间戳 每条消息记录精确到微秒的时间戳(需开启 CONFIG_PRINTK_TIME)
# 查看所有日志(含时间戳)
dmesg -T

# 实时跟踪新日志
dmesg -w

# 只显示 KERN_ERR 及以上级别
dmesg -l err

# 清除缓冲区
dmesg -c

2.5 实践建议与常见陷阱

  • 始终指定日志级别:不要写裸 printk("hello\n"),应写 printk(KERN_DEBUG "hello\n"),避免被默认级别污染。
  • 调试信息使用 KERN_DEBUG:生产环境中可通过调整 console_loglevel 关闭调试输出,无需重新编译内核。
  • 注意格式安全:printk 的格式化字符串中不要使用用户态传入的未校验字符串,防止格式串漏洞。
  • 原子上下文中慎用 printk:虽然 printk 本身是原子的,但某些控制台驱动(如 USB 串口)的 write 回调可能睡眠。在中断上下文或自旋锁保护区域中,优先使用 printk_deferred() 或 pr_emerg() 等原子安全变体。
  • 使用 pr_* 宏简化代码:内核提供了 pr_emerg、pr_alert、pr_err、pr_warn、pr_info、pr_debug 等封装宏,自动添加日志级别前缀,推荐在驱动开发中使用。
// 推荐写法:使用 pr_* 宏
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt   // 可选:添加模块名前缀

pr_info("usb_driver: probe succeeded, vid=0x%04x\n", vid);
pr_err("dma transfer timeout, status=0x%08x\n", status);
pr_debug("irq handler entered, pending=%d\n", pending);
快速验证:在驱动初始化函数中加入 pr_info("Hello from my_driver!\n");,然后执行 dmesg | tail 确认输出。如果看不到,检查 cat /proc/sys/kernel/printk 的第一个值是否 ≥ 6(KERN_INFO 的级别)。