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 三者的关系和使用场景。我在画这张图时,特意把“驱动内部状态”放在中间,因为不管用哪种方式,最终目的都是把内核态的数据暴露给用户态。
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) 来做兼容。