3. procfs 与 sysfs 调试接口:把驱动状态“挂”出来

调试驱动的时候,最头疼的是什么?我个人觉得,是“黑盒”。驱动跑起来了,但内部状态到底对不对?中断有没有触发?寄存器写进去了没?总不能每次都插 JTAG 或者加 printk 重新编译吧。

其实 Linux 早就给了我们两个很趁手的工具——procfs 和 sysfs。说白了,就是让你在用户态通过读写文件,就能看到驱动内部的状态,甚至还能控制它。我当年刚接触驱动时,觉得这玩意儿很玄乎,后来自己动手写过一次,才发现:嗯,其实就是注册几个文件操作函数的事儿。

核心思想:驱动在内核里维护数据,通过 procfs 或 sysfs 暴露成文件。用户态 cat 一下,就看到状态了;echo 一下,就能改参数了。调试效率直接翻倍。

3.1 procfs:老牌调试利器

procfs 最早就是为调试而生的。它挂载在 /proc 下,内核里的很多信息都通过它暴露出来。比如 /proc/cpuinfo、/proc/meminfo。我们驱动开发者也可以在里面创建自己的节点。

3.1.1 创建 /proc 节点

核心 API 就两个:proc_create 和 remove_proc_entry。我习惯在模块初始化时创建,退出时删除。

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

static struct proc_dir_entry *proc_entry;

static ssize_t myproc_read(struct file *file, char __user *buf,
                           size_t count, loff_t *ppos)
{
    char kernel_buf[128];
    int len;

    // 模拟读取驱动内部状态
    len = snprintf(kernel_buf, sizeof(kernel_buf),
                   "driver_state: RUNNING\nirq_count: %d\n",
                   atomic_read(&irq_counter));

    return simple_read_from_buffer(buf, count, ppos, kernel_buf, len);
}

static const struct proc_ops myproc_fops = {
    .proc_read = myproc_read,
};

static int __init mydriver_init(void)
{
    proc_entry = proc_create("mydriver_status", 0444, NULL, &myproc_fops);
    if (!proc_entry)
        return -ENOMEM;
    return 0;
}

static void __exit mydriver_exit(void)
{
    remove_proc_entry("mydriver_status", NULL);
}

小技巧:proc_ops 结构体里还可以填 .proc_write,这样就能通过 echo 命令向驱动写数据,比如动态调整日志级别。我在调试一个网卡驱动时,就靠这个在不停机的情况下切换了调试模式。

3.1.2 实战经验:别滥用 procfs

procfs 虽然方便,但内核社区现在更推荐用 sysfs 来暴露单个值。为什么呢?因为 procfs 的节点通常用来展示“一大段文本”,比如多个统计值拼成的报表。而 sysfs 更强调“一个文件一个值”。

我曾经在一个项目里,把几十个调试参数全塞进了一个 proc 节点,结果用户态解析起来非常痛苦。后来全改成了 sysfs,每个参数一个文件,配合 shell 脚本,调试起来清爽多了。

注意:/proc/proc 下先建一个自己的目录,比如 proc_mkdir("mydriver", NULL),再把文件放进去。

3.2 sysfs:一个文件一个值

sysfs 是比 procfs 更年轻的文件系统,挂载在 /sys 下。它最大的特点是结构化——每个设备、每个驱动、每个属性都对应一个目录或文件。你想想看,一个驱动可能有几十个参数,如果用 procfs,要么全挤在一个文件里,要么起一堆乱七八糟的名字。而 sysfs 天然就是树形结构,清晰多了。

3.2.1 创建 /sys 节点

最常用的方式是使用 device_create_file 或者直接定义 DEVICE_ATTR 宏。我个人更喜欢 DEVICE_ATTR,因为它把读、写函数和文件名绑在一起,代码更紧凑。

static ssize_t reg_value_show(struct device *dev,
                              struct device_attribute *attr, char *buf)
{
    struct mydevice *mdev = dev_get_drvdata(dev);
    return snprintf(buf, PAGE_SIZE, "0x%08x\n", mdev->reg_val);
}

static ssize_t reg_value_store(struct device *dev,
                               struct device_attribute *attr,
                               const char *buf, size_t count)
{
    struct mydevice *mdev = dev_get_drvdata(dev);
    u32 val;

    if (kstrtou32(buf, 0, &val) < 0)
        return -EINVAL;

    mdev->reg_val = val;
    // 这里可以触发硬件寄存器写入
    writel(val, mdev->base_addr + REG_OFFSET);

    return count;
}

static DEVICE_ATTR(reg_value, 0644, reg_value_show, reg_value_store);

// 在 probe 函数中创建
static int mydriver_probe(struct platform_device *pdev)
{
    // ... 初始化设备 ...
    device_create_file(&pdev->dev, &dev_attr_reg_value);
    return 0;
}

// 在 remove 函数中删除
static int mydriver_remove(struct platform_device *pdev)
{
    device_remove_file(&pdev->dev, &dev_attr_reg_value);
    // ... 清理 ...
    return 0;
}

用法示例:

  • 读状态:cat /sys/devices/platform/mydevice/reg_value
  • 写参数:echo 0x1234 > /sys/devices/platform/mydevice/reg_value

配合 watch -n 1 cat /sys/.../reg_value,就能实时监控寄存器变化。我在调试 I2C 设备时,就靠这个盯着设备端的配置寄存器,一眼就发现了初始化顺序的问题。

3.2.2 sysfs 的目录结构

sysfs 的路径是有规律的,调试时能帮你快速定位:

路径 内容 调试用途
/sys/bus/ 总线类型(platform、i2c、spi 等) 查看设备挂在哪条总线上
/sys/devices/ 所有设备,按拓扑结构排列 直接找到你的设备节点
/sys/class/ 按设备类别分类(如 input、net、gpio) 快速找到同类设备
/sys/kernel/debug/ debugfs(另一个调试文件系统) 更灵活的调试接口,但需要手动挂载

3.3 procfs vs sysfs:怎么选?

很多初学者会纠结这个问题。我直接说结论:

  • 展示多行统计信息(比如驱动运行时长、中断次数、错误计数)→ 用 procfs,一个文件全搞定。
  • 暴露单个可读/可写的属性(比如寄存器值、配置参数、状态标志)→ 用 sysfs,一个文件一个值。
  • 需要复杂的数据结构交互(比如数组、链表)→ 可以考虑 debugfs,它比 procfs 更灵活,但需要手动挂载。

我的经验法则:如果用户态只需要 cat 一下看个大概,用 procfs;如果用户态需要写脚本批量读取或修改,用 sysfs。前者是“给人看的”,后者是“给程序用的”。

3.4 一张图看懂调试文件系统

下面这张图梳理了 procfs、sysfs、debugfs 三者的关系和使用场景。我在画这张图时,特意把“驱动内部状态”放在中间,因为不管用哪种方式,最终目的都是把内核态的数据暴露给用户态。

驱动调试文件系统选择指南 驱动内部状态 procfs sysfs debugfs procfs 特点 • 多行文本展示 • 适合统计信息 • 挂载于 /proc sysfs 特点 • 一个文件一个值 • 树形结构 • 挂载于 /sys debugfs 特点 • 最灵活 • 支持任意数据结构 • 需手动挂载 选择原则:给人看用 procfs,给程序用 sysfs,复杂场景用 debugfs

3.5 避坑指南:我曾经踩过的坑

最后分享几个我实际项目中遇到的教训,希望能帮你少走弯路。

  • 缓冲区溢出:procfs 的读函数里,buf 大小是 PAGE_SIZE(通常是 4096 字节)。如果你 snprintf 的内容超过了这个大小,用户态读到的东西会被截断。我曾经有个驱动,统计信息特别多,结果 cat 出来只有前半段,排查了半天才发现是缓冲区不够。后来改用 seq_file 接口解决了。
  • 并发访问:多个进程同时读写你的 proc/sys 节点,会不会出问题?会的。如果读函数里访问了共享数据,记得加锁。我见过一个同事的驱动,不加锁直接读一个正在被中断修改的计数器,结果读出来的值忽大忽小,非常诡异。
  • 权限控制:proc_create 和 DEVICE_ATTR 都可以指定文件权限。调试阶段用 0644 没问题,但产品发布前一定要改成更严格的权限,或者干脆删掉调试节点。否则被用户 cat 出敏感信息,那就尴尬了。
  • 别忘了清理:模块卸载时,一定要调用 remove_proc_entry 和 device_remove_file。否则节点残留,下次 insmod 时会报错,甚至导致 kernel panic。

重要提醒:procfs 和 sysfs 的 API 在不同内核版本间有变化。比如 5.6 之后 proc_ops 取代了老的 file_operations。写代码前,先确认一下你的内核版本。我一般在代码里加个 #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,6,0) 来做兼容。