前言

在 linux 中常见的文件系统 有很多, 如下 

基于磁盘的文件系统, ext2, ext3, ext4, xfs, btrfs, jfs, ntfs 
内存文件系统, procfs, sysfs, tmpfs, squashfs, debugfs 
闪存文件系统, ubifs, jffs2, yaffs  

文件系统这一套体系在 linux 有一层 vfs 抽象, 用户程序不用关心 底层文件系统的具体实现, 用户只用操作 open/read/write/ioctl/close 的相关 系统调用, 这一层系统调用 会操作 vfs 来处理响应的业务 

vfs 会有上面各种文件系统对应的 读写 相关服务, 进而 将操作下沉到 具体的文件系统 

我们这里 来看一下 proc 文件系统, 这是一个 基于 内核内存的文件系统, 读写的都是 内核的相关信息项 

如何分配 inode ?

inode 的创建是 懒加载 的, 也就是 第一次访问的时候 vfs 在 i_ops 中预留了 lookup, 基于这个函数 来实现的懒加载, 如果没有找到, 则新建给定的文件对应的 inode, 以及初始化 i_ops, f_ops 的相关操作 

这里传入的 ptr, 是当前访问的 进程文件 的一个配置信息, 如 stat, cmdline 对应于不同的配置, 对应于 pid_entry 

40 proc 文件系统_vfs

各个 pid_entry 的配置如下, 比如 cmdline, 定义了文件标志, 用户/组/其他用户的读写执行权限, 具体当前文件的 读写 相关 api 的具体实现 

40 proc 文件系统_proc_02

比如说 /proc/$pid/cmdline 文件的相关 api 如下 

40 proc 文件系统_数据_03

我们来看一下 inode 的相关处理, inode 的分配是来自于 proc 对应的 fs 的 super_block 

这里初始化了 读写权限, inode, m/i/c/time, i_op, f_op, pid, uid, gid 等等相关属性 

其中 f_op 在上面 cmdline 的例子中就对应于 proc_pid_cmdline_ops, 然后实际操作的时候 vfs 会将操作委托给 这个函数 

40 proc 文件系统_linux_04

新增了 inode 之后, d_add 会将 inode 注册到 dcache.dentry_hashtable 中

40 proc 文件系统_linux_05

/proc/$pid 文件夹中的各个文件什么时候创建的?

是在 你访问的时候创建的, 比如你 直接读取 "/proc/$pid/cmdline" 或者 "ls /proc/$pid" 的时候创建的 

如下为 ls "/proc/321" 的时候, 会遍历 ents 来创建对应的文件, 这的 ents 对应于上面的 tgid_base_stuff 的列表, 对应于 进程需要创建的 pid 相关的所有文件的信息 

40 proc 文件系统_proc_06

如何分配 存储的空间?

大多数的 proc 中的文件是不单独占用存储空间 

是通过对应的 读写函数 直接读取业务数据结构的相关信息 直接返回 

如何 读写数据?

根据文件路径 查询到文件对应的 inode, 封装 file, file->f_op 为 inode->f_op 

对应于 /proc/$pid/cmdline 读取函数为 proc_pid_cmdline_ops, 如下图 即为完整的调用栈 

40 proc 文件系统_linux_07

申请一个临时物理页, 然后读取给定的进程的 mm 的 arg_start + offset 的数据, 读取到 argv_end 

读取出来的内容其实就是 cmdline 的内容, 即所有的参数 

其中 argv[0] 为当前执行程序, argv[1], argv[2], ..., argv[n] 为用户传入的程序参数  

这里读取到了 21 个字符 "./Test05SocketServer"

40 proc 文件系统_数据_08

然后将数据从 物理页 拷贝到 用户空间的 buffer, 得到 "./Test05SocketServer" 

40 proc 文件系统_文件系统_09

用户程序这边 cat 输出结果为 

40 proc 文件系统_文件系统_10

再比如 /proc/$pid/oom_score, 的读取 

使用的另外一种方式来读取文件的数据, 输出数据到 seq_file, 这个是 封装 file 的时候, linux 构造的一个容器 

40 proc 文件系统_数据_11

输出如下 

40 proc 文件系统_proc_12

再比如 /proc/$pid/status, 的读取 

40 proc 文件系统_vfs_13

输出如下 

40 proc 文件系统_数据_14

根据 path 获取到 inode 的过程 

上面提到了 新建的 proc 的 inode 放在了 dcache.dentry_hashtable 

那么 linux 又是怎么讲 path 和 dcache.dentry_hashtable 关联起来的呢? 

在常见的 ext4 文件系统中, 文件夹的 inode 信息是放在了 inode 的文件内容中的, 那么 proc 文件系统是怎么做的呢 ? 

path_init 的时候初始化 nd 中为 根文件系统的 inode, dentry, 然后 link_path_walk 一个文件夹一个文件夹的向后遍历, 调整 nd, do_last 处理目标文件/文件夹 

所以这里 "/proc/310/cmdline" 会 调用一次 path_init, 两次 link_path_walk, 第一次 do_last 

我们这里 着重关注 link_path_walk 

40 proc 文件系统_数据_15

第一次 link_path_walk 的处理, __d_lookup_rcu 的时候, 在 rootfs  "/" 下面的 rootfs "/proc" 

注意这里的 __d_lookup_rcu 查找的是 dcache.dentry_hashtable

40 proc 文件系统_文件系统_16

follow_mount_rcu 之后, 查询 "/proc" 是否有挂载点, 如果有 更新到对应的挂载点上面去 

nd 中的 dentry 信息, 由 rootfs "/proc" 更新为了 proc "/", 这里 mount 信息是记录在 namespace.mount_hashtable 中, 在 mount 的时候会记录相关信息 

40 proc 文件系统_proc_17

第二次 link_path_walk 的处理, 是在 proc "/" 下面查找 "310", 这里找到了这个 proc "310" 的 dentry 

注意这里的 __d_lookup_rcu 查找的是 dcache.dentry_hashtable, 因为在创建 inode 节点的时候, 向 dcache.dentry_hashtable 中注册了 dentry 的信息, 因此这里能够查到 

40 proc 文件系统_vfs_18

proc "/310" 上面没有其他的挂载点, 因此获取到的数据 还是原有的 proc "310" 对应的 dentry, inode 

40 proc 文件系统_vfs_19

do_last 中 lookup_fast 从 dcache.dentry_hashtable 中在 proc "310" 下面查找 "cmdline" 

能够找到 proc "/310/cmdline" 

40 proc 文件系统_proc_20

do_dentry_open 中根据上下文的相关信息, 填充 file 对象  

40 proc 文件系统_数据_21

然后最外层 sys_open 中建立 fd -> file 的映射 

这里放进去的就是 read/write 中根据 fd 拿到的 file 

file.f_ops 为对应的 inode.f_ops 

40 proc 文件系统_linux_22

完