3. procfs 核心数据结构:struct proc_dir_entry、proc_ops 与 inode/file_operations 关系
在 Linux 内核中,procfs 的每一个文件或目录都由一个核心数据结构 struct proc_dir_entry(简称 PDE)表示。理解 PDE、proc_ops 以及 VFS 层 inode/file_operations 之间的关系,是编写正确 proc 文件读写回调的基础。本章将深入剖析这三者的角色与协作机制。
3.1 struct proc_dir_entry:proc 文件系统的节点基石
struct proc_dir_entry 是 procfs 在内存中管理每一个条目的核心结构体。它记录了文件名、访问权限、所属目录、以及最重要的——操作函数集。虽然内核版本不同其内部字段略有差异,但关键成员始终保持一致。
| 字段名 | 类型 | 说明 |
|---|---|---|
name |
const char * |
文件名(不含路径) |
namelen |
u8 |
文件名长度 |
mode |
umode_t |
文件权限位(如 0444) |
nlink |
nlink_t |
硬链接计数 |
uid |
kuid_t |
文件所有者 UID |
gid |
kgid_t |
文件所属组 GID |
proc_iops |
const struct inode_operations * |
inode 操作(如 lookup) |
proc_fops |
const struct file_operations * |
文件操作(read/write 等) |
parent |
struct proc_dir_entry * |
指向父目录的指针 |
subdir |
struct rb_root |
子目录红黑树根 |
data |
void * |
私有数据指针,驱动可自由使用 |
关键点:
data 字段是驱动与 proc 文件之间传递上下文的重要桥梁。在注册 proc 文件时,可以将设备结构体、状态变量等任意指针存入 data,在读写回调中通过 PDE 取出使用。
3.2 proc_ops:现代 proc 文件的操作函数集
从 Linux 内核 5.6 版本开始,procfs 引入了 struct proc_ops 替代了传统的 struct file_operations 用于 proc 文件。这一变化的主要目的是将 proc 文件的操作接口与普通 VFS 文件操作解耦,使内核内部实现更清晰。
struct proc_ops {
unsigned int proc_flags;
int (*proc_open)(struct inode *, struct file *);
ssize_t (*proc_read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*proc_read_iter)(struct kiocb *, struct iov_iter *);
ssize_t (*proc_write)(struct file *, const char __user *, size_t, loff_t *);
loff_t (*proc_lseek)(struct file *, loff_t, int);
int (*proc_release)(struct inode *, struct file *);
__poll_t (*proc_poll)(struct file *, struct poll_table_struct *);
int (*proc_ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
int (*proc_mmap)(struct file *, struct vm_area_struct *);
unsigned long (*proc_get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
};
兼容性提示: 在 5.6 之前的内核中,proc 文件使用
struct file_operations。如果你的驱动需要支持旧内核,可以使用条件编译(#if LINUX_VERSION_CODE < KERNEL_VERSION(5,6,0))来分别处理。本课程以 proc_ops 为主。
3.3 inode 与 file_operations 的关系:VFS 层的桥接
当用户空间程序通过系统调用(如 open()、read())访问 proc 文件时,VFS 层会执行以下关键步骤:
- 路径查找: VFS 根据路径名找到对应的
dentry和inode。 - inode 初始化: procfs 在创建 inode 时,会将 PDE 中的
proc_fops赋值给 inode 的i_fop(即file_operations)。 - 打开文件:
open()系统调用创建struct file,其f_op指针指向 inode 的i_fop。 - 读写操作: 后续的
read()/write()通过file->f_op找到对应的回调函数执行。
下图展示了从 PDE 到 VFS 操作函数的完整链路:
3.4 注册与查找:PDE 的生命周期管理
驱动通过以下 API 创建和删除 proc 条目:
| 函数 | 说明 |
|---|---|
proc_create(name, mode, parent, proc_ops) |
创建 proc 文件并关联 proc_ops |
proc_create_data(name, mode, parent, proc_ops, data) |
创建 proc 文件并设置私有数据 data |
proc_mkdir(name, parent) |
创建 proc 目录 |
remove_proc_entry(name, parent) |
删除 proc 条目 |
proc_symlink(name, parent, target) |
创建符号链接 |
注意:
remove_proc_entry() 是同步操作,会等待所有正在使用该条目的进程退出后才释放内存。在模块卸载路径中,务必在释放相关资源之前调用此函数,否则可能导致内核崩溃。
3.5 回调函数中的上下文获取
在 proc_read 或 proc_write 回调中,驱动开发者通常需要获取注册时设置的私有数据。标准做法如下:
static ssize_t my_proc_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
// 方法1:通过 file->f_inode 获取 PDE,再取 data
struct proc_dir_entry *pde = PDE(file->f_inode);
struct my_device *dev = (struct my_device *)pde->data;
// 方法2:直接使用 file->private_data(需在 open 中设置)
struct my_device *dev = (struct my_device *)file->private_data;
// ... 读取 dev 中的数据并拷贝到用户空间 ...
return copied;
}
static int my_proc_open(struct inode *inode, struct file *file)
{
// 在 open 中将私有数据存入 file->private_data
struct proc_dir_entry *pde = PDE(inode);
file->private_data = pde->data;
return 0;
}
最佳实践: 推荐在
proc_open 中将 pde->data 赋值给 file->private_data,后续读写回调直接使用 file->private_data。这样既避免了每次重复查找 PDE,也与其他 VFS 文件操作的惯用风格保持一致。
3.6 数据结构关系总结
下表总结了三个核心结构体在 proc 文件系统中的角色与关联:
| 结构体 | 生命周期 | 主要职责 | 与驱动的关系 |
|---|---|---|---|
proc_dir_entry |
从 proc_create 到 remove_proc_entry |
管理 proc 文件元数据、权限、父子关系 | 驱动通过 data 字段绑定私有上下文 |
proc_ops |
通常为全局静态变量,常驻内存 | 定义 proc 文件的操作回调函数指针 | 驱动实现具体的 read/write 逻辑 |
inode/file |
每次打开创建,关闭释放 | VFS 层与进程交互的接口 | 驱动通过 file->private_data 访问上下文 |