Linux内核驱动调试 kgdb与printk联合使用 - 第2节

2. printk基础

printk 是 Linux 内核中最基础的日志输出接口,其函数原型定义在 <linux/printk.h> 中,行为与用户空间的 printf 高度相似,但增加了日志级别前缀和内核上下文约束。

// 完整原型(简化自内核源码)
int printk(const char *fmt, ...)
    __attribute__((format(printf, 1, 2)));
关键差异:printk 不返回输出字符数,而是返回 已缓冲的日志记录数;在原子上下文(中断、自旋锁)中调用时,不会触发调度,日志直接写入 __log_buf 环形缓冲区。

2.1.1 调用格式

// 带日志级别的标准调用
printk(KERN_ERR "DMA transfer failed, status=0x%x\n", status);

// 不带日志级别(默认使用 KERN_WARNING)
printk("This is a warning-level message\n");

日志级别通过宏常量拼接在格式字符串之前,宏与字符串之间没有逗号,这是 C 语言字符串字面量拼接的典型用法。

2.2 日志级别体系(KERN_EMERG ~ KERN_DEBUG)

Linux 内核定义了 8 个日志级别,数值越小优先级越高。驱动开发者通过选择正确的级别,控制消息是否输出到控制台以及写入系统日志的紧急程度。

宏名称 数值 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件故障、内存校验错误
KERN_ALERT 1 必须立即处理 看门狗超时、关键数据损坏
KERN_CRIT 2 严重条件 驱动初始化失败、IRQ 丢失
KERN_ERR 3 错误条件 DMA 超时、寄存器校验失败
KERN_WARNING 4 警告,可能存在问题 降级模式、非关键配置缺失
KERN_NOTICE 5 正常但重要的事件 驱动加载/卸载、链路状态变化
KERN_INFO 6 信息性消息 版本号、寄存器映射地址
KERN_DEBUG 7 调试信息 函数进入/退出、中间变量值
调试建议:驱动开发阶段统一使用 KERN_DEBUG 输出详细日志,配合 dmesg -n 7 或 echo 7 > /proc/sys/kernel/printk 使所有级别可见;发布前将调试日志移除或降级为 KERN_INFO。

2.3 控制台输出机制

printk 的输出并不直接到达控制台,而是经过三层过滤:日志缓冲区 → 控制台驱动 → 物理终端。理解这一链路对调试驱动至关重要。

printk 控制台输出流程 printk() 调用 __log_buf 环形缓冲区 console_drivers 链表 UART / VGA / 网络 console_loglevel 过滤 console_loglevel 默认值通常为 7(调试信息不输出),可通过 /proc/sys/kernel/printk 动态调整

2.3.1 控制台日志级别过滤

内核通过 /proc/sys/kernel/printk 文件暴露四个整数值,控制日志的过滤行为:

字段位置 名称 默认值 作用
1 console_loglevel 7 只有优先级高于此值的消息才输出到控制台
2 default_message_loglevel 4 未指定级别的 printk 默认使用此级别
3 minimum_console_loglevel 1 控制台允许设置的最低级别(硬限制)
4 default_console_loglevel 7 系统启动时的默认控制台级别
调试陷阱:在嵌入式系统中,如果 UART 控制台驱动注册较晚,启动早期的 printk 消息可能被丢弃。此时应使用 earlyprintk 内核参数,在控制台驱动就绪前通过硬件直接输出日志。

2.3.2 控制台驱动注册与输出

每个控制台驱动(如串口、VGA、网络控制台)通过 register_console() 注册到 console_drivers 链表。printk 在将消息写入 __log_buf 后,遍历该链表,调用每个驱动的 write() 回调函数。

// 控制台驱动结构体关键成员(位于 <linux/console.h>)
struct console {
    char    name[16];          // 驱动名称,如 "ttyS0"
    void    (*write)(struct console *, const char *, unsigned);
    struct  console *next;     // 链表指针
    short   flags;             // CON_ENABLED, CON_CONSDEV 等
    short   index;             // 设备索引
    // ...
};

2.4 日志缓冲区管理

内核使用 __log_buf 环形缓冲区存储所有 printk 输出,缓冲区大小由 CONFIG_LOG_BUF_SHIFT 配置(典型值 16,即 64KB)。

  • 环形特性:当缓冲区写满时,新消息覆盖最旧的消息,保证始终保留最新的日志。
  • 原子写入:在 NMI(不可屏蔽中断)上下文中,printk 使用 printk_nmi_direct 绕过锁机制直接写入。
  • 读取接口:用户空间通过 dmesg 命令或 /dev/kmsg 设备读取缓冲区内容。
性能提示:驱动中频繁调用 printk(如每秒数千次)会导致 logbuf_lock 争用,影响系统实时性。调试阶段建议使用 pr_debug / dev_dbg 动态开关,或通过 trace_printk 将日志输出到 ftrace 而非控制台。

2.5 本章小结

  • printk 原型与 printf 兼容,但返回值含义不同,且支持日志级别前缀。
  • 8 级日志体系从 KERN_EMERG(0) 到 KERN_DEBUG(7),驱动调试应优先使用 KERN_DEBUG。
  • 控制台输出经过 console_loglevel 过滤,通过 /proc/sys/kernel/printk 动态调整。
  • 环形缓冲区保证日志不丢失,但高频输出可能引入锁竞争。