大家好,这里是物联网心球。

    本期文章,我们来深入学习 Linux proc 文件系统。

1.proc 文件系统简介

    proc 文件系统一种伪文件系统,文件内容存储在内存(不占用磁盘空间,不能永久存储)。它用于向用户空间暴露内核数据结构、进程状态及系统配置信息,是系统监控、调试与性能调优的核心工具。

    proc文件系统的内核定义如下:

static struct file_system_type proc_fs_type = {
    .name = "proc",
    .init_fs_context = proc_init_fs_context,
    .parameters = proc_fs_parameters,
    .kill_sb = proc_kill_sb,
    .fs_flags = FS_USERNS_MOUNT | FS_DISALLOW_NOTIFY_PERM,
};

    内核初始化过程中,initramfs init 脚本会挂载 proc 文件系统。proc 文件系统中常见的文件有: 

(1)系统信息

  • /proc/cpuinfo:CPU 硬件信息。
  • /proc/meminfo:内存总大小、空闲、缓存、Swap 等状态。
  • /proc/loadavg:系统平均负载。
  • /proc/uptime:系统启动总时间、空闲时间。
  • /proc/cmdline:内核启动参数。
  • /proc/filesystems:内核支持的文件系统列表。
  • /proc/stat:系统整体 CPU 使用率、中断、进程切换统计。
  • /proc/partitions:块设备与分区信息。
  • /proc/mounts:当前已挂载文件系统列表。

(2)进程信息

  • /proc/[PID]/cmdline:进程启动命令与参数。
  • /proc/[PID]/status:进程状态。
  • /proc/[PID]/maps:进程虚拟内存映射。
  • /proc/[PID]/fd/:进程打开的文件描述符列表。
  • /proc/[PID]/environ:进程环境变量。
  • /proc/[PID]/limits:进程资源限制。
  • /proc/[PID]/io:进程 I/O 统计。
  • /proc/[PID]/sched:进程调度信息与优先级。

(3)网络信息

  • /proc/net/dev:网卡流量、错误、丢包统计。
  • /proc/net/tcp:TCP 连接表。
  • /proc/net/udp:UDP 连接表。
  • /proc/net/route:内核路由表。
  • /proc/net/arp:ARP 表。
  • /proc/net/netstat:网络栈统计。
  • /proc/net/sockstat:套接字使用统计。

2.从内核看 proc 文件系统

   理解 proc 文件系统的关键在于理解 proc 文件系统内部树,所谓内部树是指独立于 VFS(虚拟文件系统)的文件树,具体情况如图1所示。

图1    proc 文件系统内核实现原理

    内核初始化时会调用 start_kernel 函数,该函数会调用 proc_root_init 函数来初始化 proc 内部文件树。proc 内部文件树的节点类型(文件或目录)为 struct proc_dir_entry 结构,其定义如下:

struct proc_dir_entry {
    conststruct inode_operations *proc_iops;    /* inode操作函数表 */
    union {
        conststruct proc_ops *proc_ops;        /* 内核5.6+新增proc专用文件操作函数表 */
        conststruct file_operations *proc_dir_ops;    /* 文件操作函数表 */
    };
    conststruct dentry_operations *proc_dops;    /* 目录项(dentry)操作函数表 */
    union {
        conststruct seq_operations *seq_ops;      /* seq_file操作函数表,用于复杂文件(多个记录)*/
        int (*single_show)(struct seq_file *, void *); /* 简单显示函数,用于简单文件(单个记录) */
    };
    void *data; /* 私有数据指针 */
    nlink_t nlink; /* 硬链接数 */
    kuid_t uid; /* 用户 ID */
    kgid_t gid; /* 组 ID */
    loff_t size; /* 文件大小 */
    struct proc_dir_entry *parent;          /* 父目录指针,指向包含此节点的父目录 */
    struct rb_root subdir;                /* 子目录红黑树根 */
    struct rb_node subdir_node;            /* 红黑树节点,子节点插入父目录红黑树的节点 */
    char *name; /* 指向文件/目录名字符串 */
    umode_t mode; /* 表示文件类型和权限 */
    u8 flags; /* 标志位 */
    u8 namelen; /* 文件名长度 */
    ......
};

    struct proc_dir_entry(简称 PDE)是 Linux 内核管理 /proc 文件系统所有节点(文件、目录、链接)的数据结构体,每个节点在内核中都对应一个PDE,PDE用于存储节点的名称、权限、所属目录、操作回调、私有数据等关键信息。

    如果 PDE 是一个目录,那么 PDE 的 subdir 成员将会生效,subdir 是子节点(文件和目录)的红黑树根,子节点通过 PDE 的 subdir_node 成员插入父节点红黑树,这样就构成了树形结构。

    proc 内部文件树的文件节点和 VFS(虚拟文件系统)中的文件是对应关系,proc 的文件由内核创建,内核通过 proc_mkdir 和 proc_create 函数来创建 proc 文件节点。当用户程序需要访问 proc 文件时,内核会基于 proc 文件节点动态生成 struct inode 结构,并将访问 proc 文件的方法(函数表)也赋值给inode。这样用户程序就能够通过 VFS 来访问 proc 文件了。

    proc 内部文件树的根节点是 /proc 目录,它是一个特殊的PDE,其定义如下:

struct proc_dir_entry proc_root = {
    .low_ino = PROC_ROOT_INO,
    .namelen = 5,
    .mode = S_IFDIR | S_IRUGO | S_IXUGO,
    .nlink = 2,
    .refcnt = REFCOUNT_INIT(1),
    .proc_iops = &proc_root_inode_operations,
    .proc_dir_ops = &proc_root_operations,
    .parent = &proc_root,
    .subdir = RB_ROOT,
    .name = "/proc",
}; 

    proc_root 是 Linux 内核中 /proc 文件系统根节点,所有 /proc 下的文件/目录都是挂载到这个根节点下的子节点,它是整个 proc 内部文件树的 “根”。

3.seq_file 机制 

    seq_file 是用于简化 /proc 文件的创建和读取操作的机制。它解决了传统 proc 文件处理中的缓冲区管理难题,提供了一种迭代式、分块输出的方式来生成动态内容。

    seq_file 的核心数据结构为:

struct seq_file {
    char *buf; // 输出缓冲区
    size_t size; // 缓冲区大小
    size_t from; // 用户空间已读取的位置
    size_t count; // buf 中有效数据量
    size_t pad_until; // 填充位置
    loff_t index; // 当前迭代位置
    loff_t read_pos; // 当前读取位置
    u64 version; // 版本号
    struct mutex lock;// 互斥锁
    conststruct seq_operations *op;// 操作函数集
    int poll_event; // poll 事件
    conststruct file *file;// 关联的文件
    void *private; // 私有数据
};

    简单来说,seq_file 就是一个内核缓冲区,proc 文件中的数据需要先格式化输出到 seq_file 内核缓冲区,再拷贝至用户缓冲区。格式化输出的过程需要使用特定的方法,这种方法由 struct seq_operations 结构定义,如下:

struct seq_operations {
    /* 遍历起始回调:找到第pos个数据节点(遍历的起点)*/
    void * (*start) (struct seq_file *m, loff_t *pos);

    /* 遍历结束回调:清理资源(解锁、释放内存等)*/
    void (*stop) (struct seq_file *m, void *v);

    /* 遍历下一个回调:获取当前节点的下一个节点 */
    void * (*next) (struct seq_file *m, void *v, loff_t *pos);

    /* 内容输出回调:将单个节点的数据格式化输出到seq_file缓冲区 */
    int (*show) (struct seq_file *m, void *v);
};

    seq_operations 的使用方式如图2所示。

图 2    seq_file机制    

    用户程序(shell)执行 cat /proc/xxx 命令查看 proc 文件时,首先会打开文件,打开文件的过程会创建 file 对象,然后将 file 对象的私有指针指向 proc 文件对应的 seq_file。接着,cat 命令会读取 proc 文件数据,读取的过程需要用到 seq_file 对象中的 op 函数表 (struct struct seq_operations),具体过程如下:

  • 步骤1:调用 start 函数开始迭代,获取第一个数据项(整型数、字符串、结构体等),并调用 show 函数格式化输出该数据项至 seq_file 内核缓冲区。
  • 步骤2:调用 next 函数获取下一个数据项,并调用 show 函数格式化输出该数据项至 seq_file 内核缓冲区。
  • 步骤3:持续执行步骤2,直至所有数据项都格式化输出到 seq_file 内核缓冲区。
  • 步骤4:调用 stop 函数停止迭代,清理资源。
  • 步骤5::将 seq_file 收集到数据拷贝至用户缓冲区。

4. /proc/partitions 示例

    我们以读取 /proc/partitions 文件为例,来进一步了解 proc 文件的读取过程。/proc/partitions 是 proc 文件系统中专门用于暴露系统块设备(磁盘 / 分区)信息的核心虚拟文件,如图3所示。

图3    读取 /proc/partitions 文件

    /proc/partitions 文件的数据项是 struct gendisk 结构,struct gendisk(通用磁盘结构体)是 Linux 内核块设备子系统的核心数据结构,用于抽象和管理系统中所有块设备,定义如下:

struct gendisk
{
    int major; // 主设备号
    int first_minor; // 起始次设备号
    int minors; // 次设备号
    char disk_name[DISK_NAME_LEN]; // 磁盘名称
    struct xarray part_tbl; // 分区表
    struct block_device *part0; //主设备块设备对象
    ......
};

    一台主机通常会有多个块设备,调用 start 函数后,内核会查询块设备列表,获取到第一个块设备。 接着,内核调用 show 函数格式化输出块设备信息,该场景下的 show 函数具体实现如下:

static int show_partition(struct seq_file *seqf, 
void *v) {
    struct gendisk *sgp = v;
    struct block_device *part;
    unsigned long idx;
    xa_for_each(&sgp->part_tbl, idx, part) {
        seq_printf(seqf, "%4d %7d %10llu %pg\n",
      MAJOR(part->bd_dev), MINOR(part->bd_dev),
               bdev_nr_sectors(part) >> 1, part);
    }
    return 0;
}

    show 函数中循环(xa_for_each语言)格式化输出(调用 seq_printf 函数)每个分区的信息至 seq_file 内核缓冲区。一个块设备处理完毕后,调用 next 函数定位到下一个块设备,然后按照相同的方式处理下一个块设备,直至所有块设备的信息都格式化输出到 seq_file 内核缓冲区,最后将 seq_file 内核缓冲区的数据拷贝至用户缓冲区,用户程序就能够查看到内核的块设备分区信息了。

最后:

    我的新书《图解Linux网络编程》发布了,我对Linux网络编程的应用开发技术以及内核源码进行了深入的研究,并以图解方式创作了《图解Linux网络编程》这本书,如果你想系统性地学习Linux网络编程,从底层原理到上层应用彻底通关Linux网络编程,欢迎入手我的新书。

图书已经在各大电商平台上线,搜索“图解Linux网络编程”购买。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐