3. procfs 核心数据结构:struct proc_dir_entry、proc_fops 与 proc_ops(内核 5.6+)
在 procfs 驱动开发中,理解其核心数据结构是编写正确、高效代码的前提。本章深入剖析三个关键结构体:struct proc_dir_entry(目录项描述符)、struct proc_fops(传统文件操作接口)以及内核 5.6 引入的 struct proc_ops(现代文件操作接口)。掌握这些结构体的字段含义、生命周期与使用场景,是构建可靠 proc 文件系统的基石。
3.1 struct proc_dir_entry:proc 文件系统的目录项
struct proc_dir_entry 是 procfs 中每个文件或目录在内核中的抽象表示。无论是通过 proc_create() 创建的文件,还是通过 proc_mkdir() 创建的目录,都会在内核中分配一个 proc_dir_entry 实例。该结构体记录了文件/目录的名称、权限、所有者、操作函数集以及父子关系等元数据。
name(文件名)、mode(权限位)、uid/gid(所有者)、proc_fops(文件操作指针)以及 parent/subdir/sibling(用于组织目录树的链表指针)。
| 字段名 | 类型 | 说明 |
|---|---|---|
name |
const char * |
文件或目录的名称,用于在 /proc 中定位 |
mode |
umode_t |
访问权限位,如 0444(只读)、0666(读写) |
uid / gid |
kuid_t / kgid_t |
文件所有者的用户 ID 和组 ID |
proc_fops |
const struct proc_ops * |
指向文件操作结构体的指针(内核 5.6+ 使用 proc_ops) |
parent |
struct proc_dir_entry * |
指向父目录的指针 |
subdir |
struct rb_root |
子目录的红黑树根节点,用于快速查找 |
sibling |
struct list_head |
兄弟目录项的链表节点 |
data |
void * |
私有数据指针,驱动可用来传递自定义上下文 |
data 字段是驱动与 proc 文件交互的“桥梁”。在 proc_create_data() 中传入的私有数据会存储在此,后续在 read/write 回调中可通过 PDE_DATA(inode) 宏获取。
3.2 struct proc_fops:传统文件操作接口
在 Linux 内核 5.6 之前,proc 文件系统复用 VFS 层的 struct file_operations 来定义文件操作回调。但 proc 文件系统有其特殊性:它不需要 llseek、mmap 等复杂操作,且 read/write 的语义与普通文件不同。为此,内核社区在 5.6 版本引入了专用的 struct proc_ops,但 struct proc_fops(本质是 file_operations 的别名)在旧内核中仍广泛使用。
struct file_operations(即 proc_fops)。对于 5.6+ 内核,强烈建议迁移到 struct proc_ops,以获得更清晰的语义和更小的结构体体积。
传统 proc_fops 中驱动最常实现的两个回调:
read:当用户态读取 proc 文件时触发。驱动需将数据从内核空间拷贝到用户空间缓冲区。write:当用户态写入 proc 文件时触发。驱动从用户空间接收数据并解析。
// 传统方式:使用 file_operations(内核 < 5.6)
static ssize_t mydev_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
// 实现读取逻辑
return 0;
}
static ssize_t mydev_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
// 实现写入逻辑
return count;
}
static const struct file_operations mydev_fops = {
.read = mydev_read,
.write = mydev_write,
};
// 创建 proc 文件
static int __init mydev_init(void)
{
struct proc_dir_entry *entry;
entry = proc_create("mydev_info", 0444, NULL, &mydev_fops);
if (!entry)
return -ENOMEM;
return 0;
}
3.3 struct proc_ops:内核 5.6+ 的现代接口
内核 5.6 引入的 struct proc_ops 是 procfs 专用的文件操作结构体。与 file_operations 相比,它精简了 proc 文件系统不需要的字段(如 llseek、mmap、flush 等),并增加了 proc_open、proc_release 等专有回调。这使得结构体更小、语义更清晰,也减少了驱动开发者的心智负担。
| 回调函数 | 说明 | 与 file_operations 的对应 |
|---|---|---|
proc_open |
文件打开时调用,可用于初始化私有数据 | 对应 open |
proc_release |
文件关闭时调用,用于清理资源 | 对应 release |
proc_read |
读取操作,语义与 read 相同 |
对应 read |
proc_write |
写入操作,语义与 write 相同 |
对应 write |
proc_lseek |
定位操作,很少使用 | 对应 llseek |
proc_poll |
轮询操作,用于 select/poll/epoll | 对应 poll |
file_operations 迁移到 proc_ops 时,只需将回调函数名前的 file 参数改为 struct inode * + struct file * 的形式,并将结构体类型从 file_operations 改为 proc_ops。内核提供了 proc_ops 宏来简化赋值。
// 现代方式:使用 proc_ops(内核 5.6+)
static ssize_t mydev_proc_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
// 实现读取逻辑
return 0;
}
static ssize_t mydev_proc_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
// 实现写入逻辑
return count;
}
static const struct proc_ops mydev_proc_ops = {
.proc_read = mydev_proc_read,
.proc_write = mydev_proc_write,
};
// 创建 proc 文件(接口不变)
static int __init mydev_init(void)
{
struct proc_dir_entry *entry;
entry = proc_create("mydev_info", 0444, NULL, &mydev_proc_ops);
if (!entry)
return -ENOMEM;
return 0;
}
3.4 数据结构关系与生命周期
理解这三个结构体之间的协作关系,是正确管理 proc 文件生命周期的关键。下图展示了从创建到移除的完整数据流:
- 创建阶段:
proc_create()分配proc_dir_entry,并将proc_ops(或file_operations)指针赋值给entry->proc_fops。 - 访问阶段:用户态打开 proc 文件时,VFS 层通过
proc_dir_entry找到对应的proc_ops,调用其中的proc_open、proc_read等回调。 - 移除阶段:
remove_proc_entry()根据名称和父目录找到proc_dir_entry,将其从目录树中摘除并释放内存。
remove_proc_entry() 之后,驱动必须确保没有正在进行的文件操作(如并发的 read/write)。否则,访问已释放的 proc_dir_entry 将导致内核崩溃。建议在模块卸载函数中先移除 proc 文件,再释放其他资源。
3.5 实战对比:proc_fops vs proc_ops 代码迁移
为了帮助你在实际项目中快速迁移,下表总结了两种接口的关键差异:
| 对比维度 | struct file_operations(旧) |
struct proc_ops(新) |
|---|---|---|
| 适用内核版本 | 所有版本(含 5.6 之前) | 5.6+ |
| 结构体大小 | 较大(约 30+ 个字段) | 较小(约 10+ 个字段) |
| 回调命名 | .read、.write、.open |
.proc_read、.proc_write、.proc_open |
| owner 字段 | 需要显式设置 .owner = THIS_MODULE |
不需要,由内核自动管理 |
| llseek 默认行为 | 需显式指定 no_llseek 或 default_llseek |
默认使用 default_llseek |
| 迁移难度 | — | 低,仅需重命名回调并修改结构体类型 |
struct proc_ops。对于需要兼容旧内核的项目,可以使用条件编译(#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,6,0))来分别定义两种结构体,实现平滑迁移。