4. 内核模块调试:printk日志级别、动态调试(dynamic debug)、/proc与/sysfs调试接口
内核模块开发中,调试是绕不开的关键环节。与用户态程序不同,内核模块运行在特权级,无法直接使用 GDB 或标准输出。本章系统讲解三种最实用的内核调试手段:printk 日志级别控制、动态调试(dynamic debug)以及 /proc 与 /sysfs 调试接口。掌握这些工具,能让你在驱动开发中快速定位问题。
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):系统启动时的控制台级别
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);
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 |
匹配格式字符串包含特定关键词的语句 |
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() |
| 推荐用途 | 一次性状态快照 | 设备属性、运行时配置 |
| 内核文档 | 不推荐新驱动使用 | 推荐新驱动使用 |
| 权限控制 | 文件权限位 | 文件权限位 + 设备模型 |
4.4 调试接口选择策略
在实际开发中,根据调试阶段和需求选择合适的接口:
- 开发初期:使用 printk + 高日志级别,快速验证基本功能
- 功能调试:使用动态调试,按模块/函数精确控制调试输出
- 状态监控:使用 /sysfs 创建运行时属性,实时查看驱动状态
- 性能分析:关闭所有调试输出,使用 tracepoint 或 ftrace
- 确认内核配置:
CONFIG_DYNAMIC_DEBUG=y、CONFIG_DEBUG_FS=y - 挂载 debugfs:
mount -t debugfs none /sys/kernel/debug - 检查 printk 级别:
cat /proc/sys/kernel/printk - 创建 /sysfs 属性文件用于运行时控制
- 调试完成后清理:删除 /proc 条目、移除 /sysfs 属性文件