调试基础:procfs与sysfs接口,通过文件系统与内核模块交互

说到内核调试,我第一个想聊的就是 procfs 和 sysfs。为什么?因为这是最直观、最上手的调试手段。你想想看,内核跑在后台,你没法像用户态程序那样直接 print 看日志。那怎么办?通过文件系统!把内核的数据暴露成文件,你 cat 一下就能看到,echo 一下就能改。多方便。

我个人习惯,拿到一个新模块,第一件事就是看它有没有注册 proc 或 sysfs 接口。有的话,调试效率直接翻倍。没有?那才叫头疼。

procfs:老牌选手,但依然能打

procfs 全称是 Process File System,最早是为进程信息设计的。但后来大家发现,哎,这玩意儿太好用了,什么内核数据都能往里塞。于是它就成了内核调试的万能接口。

在驱动里创建 proc 文件,核心就几步:

#include <linux/proc_fs.h>

static struct proc_dir_entry *my_proc_dir;
static struct proc_dir_entry *my_proc_file;

static int my_proc_show(struct seq_file *m, void *v) {
    seq_printf(m, "当前驱动状态: %s\n", "running");
    seq_printf(m, "计数器值: %d\n", my_counter);
    return 0;
}

static int my_proc_open(struct inode *inode, struct file *file) {
    return single_open(file, my_proc_show, NULL);
}

static const struct proc_ops my_proc_fops = {
    .proc_open    = my_proc_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

static int __init my_init(void) {
    my_proc_dir = proc_mkdir("my_driver", NULL);
    if (!my_proc_dir) return -ENOMEM;

    my_proc_file = proc_create("status", 0444, my_proc_dir, &my_proc_fops);
    if (!my_proc_file) return -ENOMEM;

    return 0;
}

这里有个细节要注意——proc_ops 是 5.6 内核之后的新结构。老内核用的是 file_operations。嗯,这里要注意,如果你在维护老项目,记得检查内核版本。

我的小技巧:seq_file 接口写 proc 文件,比直接操作 read 回调省心多了。它帮你处理了缓冲区管理,你只需要在 show 函数里 seq_printf 就行。我在项目中遇到过,有人自己实现 read 回调,结果缓冲区溢出,内核直接 oops。用 seq_file 就没这烦恼。

sysfs:更规范,更现代

sysfs 是 2.6 内核引入的,比 procfs 晚,但设计更合理。它把内核对象组织成目录树,每个目录代表一个 kobject,每个文件代表一个属性。说白了,就是让内核数据有了「结构化」的展示方式。

驱动里用 sysfs,通常有两种方式:

方式一:使用 DEVICE_ATTR 宏

static ssize_t my_attr_show(struct device *dev,
                            struct device_attribute *attr,
                            char *buf) {
    return scnprintf(buf, PAGE_SIZE, "%d\n", my_value);
}

static ssize_t my_attr_store(struct device *dev,
                             struct device_attribute *attr,
                             const char *buf, size_t count) {
    sscanf(buf, "%d", &my_value);
    return count;
}

static DEVICE_ATTR(my_value, 0644, my_attr_show, my_attr_store);

static int __init my_init(void) {
    // 假设 my_dev 已经注册
    device_create_file(my_dev, &dev_attr_my_value);
    return 0;
}

方式二:使用 attribute_group

static struct attribute *my_attrs[] = {
    &dev_attr_my_value.attr,
    &dev_attr_my_status.attr,
    NULL,
};

static const struct attribute_group my_attr_group = {
    .attrs = my_attrs,
};

// 在 probe 中
sysfs_create_group(&dev->kobj, &my_attr_group);
重点: sysfs 的权限控制很严格。你创建文件时指定的权限(比如 0644),用户态必须匹配才能读写。我曾经遇到过,有人把 sysfs 文件设成 0444,结果写操作静默失败,排查了半天才发现是权限问题。

procfs vs sysfs:什么时候用哪个?

维度 procfs sysfs
设计初衷 进程信息、任意内核数据 设备模型、驱动属性
目录结构 扁平或简单层级 严格按 kobject 树组织
推荐用途 调试信息、统计、临时接口 设备属性、配置接口
内核社区态度 不推荐新增 proc 文件 推荐使用
性能 一般 较好(属性缓存)

我个人建议:新驱动优先用 sysfs。除非你只是临时调试,或者数据量特别大、不适合按设备组织,才考虑 procfs。说白了,procfs 是「万能胶」,sysfs 是「专业工具」。

实战:一个完整的调试接口示例

咱们来个综合的例子。假设你写了一个字符设备驱动,想暴露几个调试接口:

  • 读取当前缓冲区使用率
  • 重置统计计数器
  • 动态调整日志级别

用 sysfs 实现:

// 缓冲区使用率(只读)
static ssize_t buf_usage_show(struct device *dev,
                              struct device_attribute *attr,
                              char *buf) {
    int usage = get_buffer_usage();
    return scnprintf(buf, PAGE_SIZE, "%d%%\n", usage);
}
static DEVICE_ATTR_RO(buf_usage);

// 重置计数器(只写)
static ssize_t reset_store(struct device *dev,
                           struct device_attribute *attr,
                           const char *buf, size_t count) {
    reset_statistics();
    return count;
}
static DEVICE_ATTR_WO(reset);

// 日志级别(读写)
static ssize_t log_level_show(struct device *dev,
                              struct device_attribute *attr,
                              char *buf) {
    return scnprintf(buf, PAGE_SIZE, "%d\n", current_log_level);
}
static ssize_t log_level_store(struct device *dev,
                               struct device_attribute *attr,
                               const char *buf, size_t count) {
    int level;
    if (sscanf(buf, "%d", &level) != 1)
        return -EINVAL;
    if (level < 0 || level > 4)
        return -EINVAL;
    current_log_level = level;
    return count;
}
static DEVICE_ATTR_RW(log_level);

static struct attribute *debug_attrs[] = {
    &dev_attr_buf_usage.attr,
    &dev_attr_reset.attr,
    &dev_attr_log_level.attr,
    NULL,
};

static const struct attribute_group debug_attr_group = {
    .name = "debug",
    .attrs = debug_attrs,
};

用户态操作:

# 查看缓冲区使用率
cat /sys/devices/.../debug/buf_usage

# 重置计数器
echo 1 > /sys/devices/.../debug/reset

# 调整日志级别
echo 3 > /sys/devices/.../debug/log_level
避坑指南: 我曾经在 sysfs 的 store 函数里忘了检查用户输入的范围,结果有人 echo 了一个超大值,直接导致内核内存分配失败。记住:永远不要信任用户态传来的数据!做边界检查、做类型检查、做错误返回。

调试时的实用技巧

最后分享几个我常用的调试技巧:

  1. 用 find 命令定位 sysfs 文件find /sys -name "*my_driver*",比手动翻目录快多了。
  2. 用 watch 监控变化watch -n 1 cat /proc/my_driver/status,实时看数据变化。
  3. 用 dd 测试大文件读写dd if=/proc/my_driver/big_data bs=4096 count=100,测缓冲区处理。
  4. 用 strace 跟踪用户态操作:看看你的驱动到底收到了什么系统调用。

嗯,procfs 和 sysfs 就聊这么多。说白了,它们就是内核和用户态之间的「桥梁」。你搭好了这座桥,调试起来就顺风顺水。下一章咱们聊聊更高级的调试手段——ftrace,那才是真正的大杀器。