第三章: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 知识体系总览
下面这张图是我自己总结的,三种文件系统的定位和关系。你看一眼就能明白什么时候该用哪个。
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,先想想能不能用文件系统暴露出来看看。