第三章:procfs与sysfs调试——用户态与内核态的桥梁

调试驱动,说白了就是搞清楚内核里到底发生了什么。我个人习惯,第一件事就是看文件系统。不是看硬盘里的文件,而是看 procfs、sysfs 和 debugfs 这三个虚拟文件系统。它们把内核内部的数据结构、设备状态、调试信息,全都暴露给了用户态。你想想看,用 cat 命令就能看到内核里的变量,多方便。

3.1 procfs:最古老的调试利器

procfs 挂载在 /proc 下,最初是为了展示进程信息。后来大家发现,用它来暴露内核数据太方便了,于是各种驱动都往里塞东西。嗯,这里要注意:procfs 现在已经不推荐用于新驱动了,但老代码里到处都是,你得会读会改。

3.1.1 创建 proc 文件

核心 API 就两个:proc_create 和 remove_proc_entry。我在项目中遇到过,新手最容易忘的是在模块卸载时清理 proc 文件,导致内核崩溃。

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

static struct proc_dir_entry *proc_file;

static ssize_t my_read(struct file *file, char __user *buf, 
                       size_t count, loff_t *ppos) {
    char msg[] = "Hello from procfs!\n";
    return simple_read_from_buffer(buf, count, ppos, msg, strlen(msg));
}

static const struct proc_ops my_fops = {
    .proc_read = my_read,
};

static int __init my_init(void) {
    proc_file = proc_create("my_driver", 0444, NULL, &my_fops);
    if (!proc_file) {
        pr_err("Failed to create proc entry\n");
        return -ENOMEM;
    }
    return 0;
}

static void __exit my_exit(void) {
    remove_proc_entry("my_driver", NULL);
}
我的经验: proc_ops 结构体在 5.6 内核之后替代了旧的 file_operations。如果你在编译老驱动时遇到 proc_ops 未定义,记得加个内核版本判断。

3.1.2 读写接口实现

读操作很简单,写操作才是坑。我曾经调试一个网卡驱动,用户态写配置时数据没对齐,导致内核 oops。记住:用户态传过来的数据,一定要用 copy_from_user 安全拷贝。

static ssize_t my_write(struct file *file, const char __user *buf,
                        size_t count, loff_t *ppos) {
    char kbuf[128];
    if (count > sizeof(kbuf) - 1)
        return -EINVAL;
    if (copy_from_user(kbuf, buf, count))
        return -EFAULT;
    kbuf[count] = '\0';
    pr_info("Received: %s\n", kbuf);
    return count;
}

3.2 sysfs:设备模型的窗口

sysfs 挂载在 /sys 下,它和 procfs 最大的区别是:sysfs 是有层次结构的,每个目录对应一个 kobject,每个文件对应一个属性。说白了,sysfs 是给设备模型用的,procfs 是给进程和杂项用的。

3.2.1 属性文件的创建

每个设备驱动都可以在 /sys/devices/ 下创建自己的属性文件。我建议你优先用 sysfs 而不是 procfs,因为 sysfs 的命名空间更清晰,不容易冲突。

static ssize_t my_attr_show(struct device *dev,
                            struct device_attribute *attr, char *buf) {
    return scnprintf(buf, PAGE_SIZE, "Driver version: 1.0\n");
}

static ssize_t my_attr_store(struct device *dev,
                             struct device_attribute *attr,
                             const char *buf, size_t count) {
    pr_info("Setting driver parameter\n");
    return count;
}

static DEVICE_ATTR(my_param, 0644, my_attr_show, my_attr_store);
关键点: DEVICE_ATTR 宏会自动生成 dev_attr_my_param 这个变量名。你在 sysfs_create_file 里传的是 &dev_attr_my_param.attr,别搞混了。

3.2.2 属性组的使用

当你有多个属性时,用属性组批量创建更高效。我记得有一次调试一个 PCIe 驱动,要暴露 20 多个寄存器,用属性组一行代码就搞定了。

static struct attribute *my_attrs[] = {
    &dev_attr_my_param.attr,
    &dev_attr_status.attr,
    &dev_attr_debug.attr,
    NULL,
};

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

// 在 probe 函数中调用
sysfs_create_group(&dev->kobj, &my_attr_group);

3.3 debugfs:调试专用文件系统

debugfs 是我最常用的调试手段。它没有 procfs 的兼容性包袱,也没有 sysfs 的“每个文件一个属性”的严格限制。说白了,debugfs 就是给你随便玩的。

警告: debugfs 默认只挂载在调试内核上。生产环境的内核通常不挂载 debugfs,或者需要手动 mount -t debugfs none /sys/kernel/debug。

3.3.1 基本操作

#include <linux/debugfs.h>

static struct dentry *debug_dir;
static u32 my_reg_val = 0x1234;

static int my_reg_show(struct seq_file *s, void *v) {
    seq_printf(s, "Register value: 0x%08x\n", my_reg_val);
    return 0;
}

static int my_reg_open(struct inode *inode, struct file *file) {
    return single_open(file, my_reg_show, NULL);
}

static const struct file_operations my_reg_fops = {
    .owner = THIS_MODULE,
    .open = my_reg_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = single_release,
};

static int __init my_init(void) {
    debug_dir = debugfs_create_dir("my_driver", NULL);
    debugfs_create_file("registers", 0444, debug_dir, NULL, &my_reg_fops);
    debugfs_create_u32("counter", 0644, debug_dir, &my_reg_val);
    return 0;
}
小技巧: debugfs_create_u32 这类辅助函数可以直接暴露一个内核变量给用户态读写,省去自己实现 file_operations 的麻烦。我经常用它来快速调试寄存器值。

3.4 seq_file:优雅的内核输出接口

你有没有遇到过这种情况:用 sprintf 拼字符串,拼到一半发现缓冲区不够了?seq_file 就是来解决这个问题的。它允许你分多次输出,内核会自动帮你拼接。

3.4.1 为什么需要 seq_file

传统的 read 回调有个问题:一次 read 调用必须返回完整数据,如果数据太大,用户态要反复调用。而 seq_file 采用迭代器模式,每次只输出一小段,内核自动管理偏移量。

3.4.2 实现一个 seq_file 驱动

static void *my_seq_start(struct seq_file *s, loff_t *pos) {
    if (*pos >= 10)  // 假设有10条记录
        return NULL;
    return (void *)((unsigned long)*pos + 1);
}

static void *my_seq_next(struct seq_file *s, void *v, loff_t *pos) {
    (*pos)++;
    if (*pos >= 10)
        return NULL;
    return (void *)((unsigned long)*pos + 1);
}

static void my_seq_stop(struct seq_file *s, void *v) {
    // 不需要清理
}

static int my_seq_show(struct seq_file *s, void *v) {
    int idx = (int)((unsigned long)v - 1);
    seq_printf(s, "Entry %d: data = %d\n", idx, idx * 100);
    return 0;
}

static const struct seq_operations my_seq_ops = {
    .start = my_seq_start,
    .next  = my_seq_next,
    .stop  = my_seq_stop,
    .show  = my_seq_show,
};

static int my_seq_open(struct inode *inode, struct file *file) {
    return seq_open(file, &my_seq_ops);
}
核心思想: start 返回第一个元素,next 返回下一个,show 输出当前元素,stop 做清理。这四件套配合起来,可以输出任意复杂度的数据。

3.5 知识体系总览

下面这张图是我自己总结的,三种文件系统的定位和关系。你看一眼就能明白什么时候该用哪个。

调试文件系统选择指南 procfs 挂载点: /proc 适用场景: • 进程信息 • 内核参数 • 老驱动兼容 ⚠ 不推荐新驱动使用 sysfs 挂载点: /sys 适用场景: • 设备属性 • 驱动参数 • 设备模型 ✅ 推荐驱动使用 debugfs 挂载点: /sys/kernel/debug 适用场景: • 调试寄存器 • 内部状态 • 性能统计 🔧 调试专用 选择原则:设备属性用 sysfs,调试信息用 debugfs,老代码兼容用 procfs

3.6 实战避坑指南

最后分享几个我踩过的坑:

  • 权限问题: 我曾经在 sysfs 里创建了一个只读文件,但用户态程序试图写入,结果返回 -EPERM。记得在 store 回调里检查权限。
  • 缓冲区大小: seq_file 的缓冲区默认是 PAGE_SIZE(4KB)。如果你一次输出太多,会被截断。我建议单次 seq_printf 输出不超过 1KB。
  • 并发访问: 多个用户态进程同时读同一个 proc 文件,会导致 start/stop 被多次调用。确保你的迭代器是线程安全的。
  • 内存泄漏: debugfs_create_dir 返回的 dentry 指针,在模块卸载时一定要用 debugfs_remove_recursive 递归删除。我见过有人只删了文件没删目录,导致内核内存泄漏。
我的习惯: 调试新驱动时,我会先在 debugfs 里创建一个 registers 文件,把所有关键寄存器值都 dump 出来。等驱动稳定了,再决定哪些值要移到 sysfs 里作为正式接口。这样既方便调试,又不会污染正式接口。

好了,这三种文件系统加上 seq_file,基本上覆盖了 90% 的驱动调试场景。下次你遇到驱动行为异常,别急着加 printk,先想想能不能用文件系统暴露出来看看。


可可精品资料库