1.proc 文件系统简介

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

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

1
2
3
4
5
6
7
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 结构,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
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 的核心数据结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 结构定义,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 内核块设备子系统的核心数据结构,用于抽象和管理系统中所有块设备,定义如下:

1
2
3
4
5
6
7
8
9
10
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 函数具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
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 proc 文件系统