4. 内核模块调试

内核模块开发中,调试是绕不开的关键环节。与用户态程序不同,内核模块运行在特权级,无法直接使用 GDB 或标准输出。本章系统讲解三种最实用的内核调试手段:printk 日志级别控制、动态调试(dynamic debug)以及 /proc 与 /sysfs 调试接口。掌握这些工具,能让你在驱动开发中快速定位问题。

调试原则:生产环境慎用高频率 printk,避免日志风暴导致系统卡死;调试完成后及时关闭或降低日志级别。

4.1 printk 日志级别与控制

printk 是内核中最基础的日志输出函数,其行为由日志级别(loglevel)控制。每个 printk 语句都带有一个优先级,内核根据当前控制台日志级别决定是否将消息输出到控制台。

4.1.1 日志级别定义

宏定义 数值 含义 典型场景
KERN_EMERG 0 紧急事件,系统不可用 致命硬件错误
KERN_ALERT 1 必须立即处理 内核崩溃前
KERN_CRIT 2 严重条件 驱动初始化失败
KERN_ERR 3 错误条件 设备读写错误
KERN_WARNING 4 警告条件 资源不足
KERN_NOTICE 5 正常但重要 模块加载/卸载
KERN_INFO 6 信息性消息 驱动版本信息
KERN_DEBUG 7 调试信息 函数入口/出口

4.1.2 控制台日志级别

内核通过 /proc/sys/kernel/printk 文件控制日志输出行为,该文件包含 4 个整数值:

# cat /proc/sys/kernel/printk
7       4       1       7
# 字段含义:
# 1. 控制台日志级别(console_loglevel):只有优先级低于此值的消息才会输出到控制台
# 2. 默认消息日志级别(default_message_loglevel):未指定级别的 printk 使用此值
# 3. 最小控制台日志级别(minimum_console_loglevel):控制台级别可设置的最小值
# 4. 默认控制台日志级别(default_console_loglevel):系统启动时的控制台级别
调试技巧:开发阶段可将控制台日志级别设为 7(echo "7 4 1 7" > /proc/sys/kernel/printk),让所有调试信息直接显示在终端。生产环境建议设为 3 或 4,只输出错误及以上级别。

4.1.3 printk 使用示例

#include <linux/kernel.h>
#include <linux/module.h>

static int __init my_driver_init(void)
{
    printk(KERN_INFO "my_driver: module loaded, version 1.0\n");
    printk(KERN_DEBUG "my_driver: entering init function\n");

    /* 模拟错误场景 */
    if (some_error_condition()) {
        printk(KERN_ERR "my_driver: hardware init failed, error=%d\n", ret);
        return -EIO;
    }
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_NOTICE "my_driver: module unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
printk 日志级别过滤流程 printk(msg, level) 提取日志级别值 level < console_loglevel? 输出到控制台 写入内核日志缓冲区 控制台日志级别可通过 /proc/sys/kernel/printk 动态调整,无需重新编译内核

4.2 动态调试(Dynamic Debug)

动态调试是内核提供的一种高级调试机制,允许在不重新编译模块的情况下,动态启用或禁用特定的调试打印语句。它比 printk 更精细,可以按文件名、函数名、行号或模块名进行过滤。

4.2.1 启用动态调试

内核配置需要开启 CONFIG_DYNAMIC_DEBUG 和 CONFIG_DEBUG_FS。模块编译时使用 pr_debug()、dev_dbg() 等宏:

/* 在模块中使用动态调试宏 */
#include <linux/device.h>

pr_debug("my_driver: entering ioctl, cmd=%d\n", cmd);
dev_dbg(&pdev->dev, "DMA transfer complete, size=%zu\n", size);

4.2.2 控制动态调试

动态调试通过 /sys/kernel/debug/dynamic_debug/control 文件进行控制:

# 查看当前所有动态调试语句
cat /sys/kernel/debug/dynamic_debug/control

# 启用 my_driver 模块的所有调试信息
echo "module my_driver +p" > /sys/kernel/debug/dynamic_debug/control

# 启用 my_driver.c 文件中第 100-200 行的调试信息
echo "file my_driver.c line 100-200 +p" > /sys/kernel/debug/dynamic_debug/control

# 启用 my_driver_init 函数中的调试信息
echo "func my_driver_init +p" > /sys/kernel/debug/dynamic_debug/control

# 禁用所有调试信息
echo "module my_driver -p" > /sys/kernel/debug/dynamic_debug/control
注意事项:
  • 动态调试依赖 debugfs 文件系统,需要挂载:mount -t debugfs none /sys/kernel/debug
  • 使用 pr_debug() 而非 printk(KERN_DEBUG),后者不受动态调试控制
  • 生产环境建议关闭动态调试,避免性能开销

4.2.3 动态调试过滤语法

过滤条件 语法示例 说明
按模块 module my_driver +p 匹配指定模块中的所有调试语句
按文件 file drivers/usb/core/hub.c +p 匹配指定源文件中的调试语句
按函数 func hub_irq +p 匹配指定函数中的调试语句
按行号 file hub.c line 120-150 +p 匹配指定行号范围的调试语句
按格式 format "DMA" +p 匹配格式字符串包含特定关键词的语句
动态调试(Dynamic Debug)控制流程 用户空间控制 echo "module xxx +p" debugfs 接口 /sys/kernel/debug/ dynamic_debug/control 动态调试核心 匹配过滤条件 目标内核模块 pr_debug() / dev_dbg()

4.3 /proc 与 /sysfs 调试接口

/proc 和 /sysfs 是内核暴露给用户空间的两个重要虚拟文件系统,驱动开发者可以通过它们创建自定义调试接口,实现运行时状态查询和参数调整。

4.3.1 /proc 文件系统

/proc 主要用于进程信息和内核状态,驱动也可以在其中创建文件用于调试。推荐使用 proc_create() 和 remove_proc_entry():

#include <linux/proc_fs.h>
#include <linux/uaccess.h>

static struct proc_dir_entry *proc_entry;

static ssize_t proc_read(struct file *file, char __user *buf,
                         size_t count, loff_t *pos)
{
    char data[128];
    int len;

    if (*pos > 0)
        return 0;

    len = snprintf(data, sizeof(data),
                   "Driver state: %s\n"
                   "IRQ count: %lu\n"
                   "Error count: %u\n",
                   state ? "active" : "idle",
                   irq_count, err_count);

    if (copy_to_user(buf, data, len))
        return -EFAULT;

    *pos = len;
    return len;
}

static const struct proc_ops proc_fops = {
    .proc_read = proc_read,
};

static int __init my_proc_init(void)
{
    proc_entry = proc_create("my_driver_debug", 0444, NULL, &proc_fops);
    if (!proc_entry)
        return -ENOMEM;
    return 0;
}

static void __exit my_proc_exit(void)
{
    remove_proc_entry("my_driver_debug", NULL);
}

4.3.2 /sysfs 文件系统

/sysfs 是比 /proc 更结构化的接口,每个设备在 /sys/devices/ 下都有对应目录。驱动通过 device_create_file() 或 sysfs_create_group() 创建属性文件:

#include <linux/device.h>

/* 读属性 */
static ssize_t debug_level_show(struct device *dev,
                                struct device_attribute *attr, char *buf)
{
    return snprintf(buf, PAGE_SIZE, "%d\n", debug_level);
}

/* 写属性 */
static ssize_t debug_level_store(struct device *dev,
                                 struct device_attribute *attr,
                                 const char *buf, size_t count)
{
    int ret, val;

    ret = kstrtoint(buf, 10, &val);
    if (ret)
        return ret;

    if (val < 0 || val > 3)
        return -EINVAL;

    debug_level = val;
    return count;
}

static DEVICE_ATTR(debug_level, 0644, debug_level_show, debug_level_store);

/* 在 probe 函数中创建 */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    int ret;

    ret = device_create_file(dev, &dev_attr_debug_level);
    if (ret)
        return ret;

    return 0;
}

/* 在 remove 函数中删除 */
static int my_remove(struct platform_device *pdev)
{
    device_remove_file(&pdev->dev, &dev_attr_debug_level);
    return 0;
}

4.3.3 /proc 与 /sysfs 对比

特性 /proc /sysfs
设计目标 进程信息、内核状态 设备模型、驱动属性
文件位置 /proc/<name> /sys/devices/.../<attr>
创建函数 proc_create() device_create_file()
推荐用途 一次性状态快照 设备属性、运行时配置
内核文档 不推荐新驱动使用 推荐新驱动使用
权限控制 文件权限位 文件权限位 + 设备模型
最佳实践:新开发的驱动优先使用 /sysfs 接口,它更符合设备模型架构,且与 udev、systemd 等用户空间工具集成更好。只有在需要读取大量一次性数据时才考虑 /proc。
/proc 与 /sysfs 调试接口架构 用户空间(User Space) /proc/my_driver_debug /sys/devices/.../debug_level proc_fops(proc_ops) device_attribute(sysfs_ops) proc_read / proc_write show / store 回调函数 内核模块(Kernel Module)

4.4 调试接口选择策略

在实际开发中,根据调试阶段和需求选择合适的接口:

  • 开发初期:使用 printk + 高日志级别,快速验证基本功能
  • 功能调试:使用动态调试,按模块/函数精确控制调试输出
  • 状态监控:使用 /sysfs 创建运行时属性,实时查看驱动状态
  • 性能分析:关闭所有调试输出,使用 tracepoint 或 ftrace
调试清单:
  1. 确认内核配置:CONFIG_DYNAMIC_DEBUG=y、CONFIG_DEBUG_FS=y
  2. 挂载 debugfs:mount -t debugfs none /sys/kernel/debug
  3. 检查 printk 级别:cat /proc/sys/kernel/printk
  4. 创建 /sysfs 属性文件用于运行时控制
  5. 调试完成后清理:删除 /proc 条目、移除 /sysfs 属性文件