调试基础: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);
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
调试时的实用技巧
最后分享几个我常用的调试技巧:
- 用 find 命令定位 sysfs 文件:
find /sys -name "*my_driver*",比手动翻目录快多了。 - 用 watch 监控变化:
watch -n 1 cat /proc/my_driver/status,实时看数据变化。 - 用 dd 测试大文件读写:
dd if=/proc/my_driver/big_data bs=4096 count=100,测缓冲区处理。 - 用 strace 跟踪用户态操作:看看你的驱动到底收到了什么系统调用。
嗯,procfs 和 sysfs 就聊这么多。说白了,它们就是内核和用户态之间的「桥梁」。你搭好了这座桥,调试起来就顺风顺水。下一章咱们聊聊更高级的调试手段——ftrace,那才是真正的大杀器。