3. procfs核心数据结构

在 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) 宏获取。
struct proc_dir_entry 内部布局与目录树关系 proc_dir_entry 实例 name: "mydev_info" mode: 0444 (只读) proc_fops → proc_ops data → 驱动私有数据 parent → /proc 根 subdir / sibling (树结构) /proc 根目录 driver/ mydev_info mydev_ctrl 目录树组织 parent 指向上层 subdir 管理子项 sibling 链接兄弟

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 的别名)在旧内核中仍广泛使用。

兼容性注意: 如果你的驱动需要支持内核 5.6 之前的版本,必须使用 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;
}
struct proc_ops vs struct file_operations(内核 5.6+) struct file_operations(传统) .owner = THIS_MODULE .llseek = no_llseek .read = mydev_read .write = mydev_write .open = mydev_open .release = mydev_release struct proc_ops(现代) .proc_open = mydev_proc_open .proc_release = mydev_proc_release .proc_read = mydev_proc_read .proc_write = mydev_proc_write .proc_lseek = default_llseek .proc_poll = NULL

3.4 数据结构关系与生命周期

理解这三个结构体之间的协作关系,是正确管理 proc 文件生命周期的关键。下图展示了从创建到移除的完整数据流:

  1. 创建阶段:proc_create() 分配 proc_dir_entry,并将 proc_ops(或 file_operations)指针赋值给 entry->proc_fops。
  2. 访问阶段:用户态打开 proc 文件时,VFS 层通过 proc_dir_entry 找到对应的 proc_ops,调用其中的 proc_open、proc_read 等回调。
  3. 移除阶段:remove_proc_entry() 根据名称和父目录找到 proc_dir_entry,将其从目录树中摘除并释放内存。
生命周期陷阱: 在调用 remove_proc_entry() 之后,驱动必须确保没有正在进行的文件操作(如并发的 read/write)。否则,访问已释放的 proc_dir_entry 将导致内核崩溃。建议在模块卸载函数中先移除 proc 文件,再释放其他资源。
procfs 数据结构生命周期与协作流程 ① 创建 proc_create() 分配 proc_dir_entry ② 绑定 entry->proc_fops 指向 proc_ops ③ 访问 用户态 open/read/write 回调 proc_ops 函数 ④ 移除 remove_proc_entry() 释放 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
迁移难度 — 低,仅需重命名回调并修改结构体类型
推荐做法: 对于新开发的内核模块,如果目标内核版本 ≥ 5.6,请直接使用 struct proc_ops。对于需要兼容旧内核的项目,可以使用条件编译(#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,6,0))来分别定义两种结构体,实现平滑迁移。