Linux 虚拟文件系统介绍

2,350 阅读35分钟

1.1 概述

在 Linux 系统中,一切皆文件,除了通常所说的狭义的文件(文本文件和二进制文件) 以外,目录、设备、套接字和管道等都是文件。

文件系统在不同的上下文中有不同的含义:

  1. 在存储设备上组织文件的方法,包括数据结构和访问方法。到存储设备。
  2. 按照某种文件系统类型格式化的一块存储介质。我们常说在某个目录下挂载或卸载文件系统, 这里的文件系统就是这种意思。
  3. 内核中负责管理和存储文件的模块,即文件系统模块。

Linux文件系统的架构如下图所示,分为用户空间、内核空间和硬件3个层面:

注意上图中方块对齐关系,很多时候我们分不清内核文件系统中 “cache” 和 “buffer” 的区别,毕竟两者都可以翻译为 “缓存区”,但是从图中,就可以很清晰的看出所谓的 “cache” 其实指的就是图中的 “页缓存” 它是针对文件来说的,除了 “DAX"(直接访问方式的设备)它不使用 “缓存”,其他的闪存类,块设备类设备都会使用到 “页缓存” 也就是 “cache”,而 “buffer” 其实指的就是图中的 “块缓存” 它是针对块设备的。

虚拟文件系统中各数据结构之间的关系:

1.2 用户空间层面

应用程序可以直接使用内核提供的系统调用访问文件:

  1. 一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能访 问这个文件系统。 系统调用 mount 用来把文件系统挂载到内存中目录树的某个目录下。 以执行命令 “mount -t fstype device dir”,把文件系统挂载到某个目录下,mount 命令调 系统调用 mount 来挂载文件系统。
  2. 系统调用 umount 用来卸载某个目录下挂载的文件系统可以执行命令 “umount dir” 卸载文件系统, umount 命令调用系统调用 umount。
  3. 使用 open 打开文件。
  4. 使用 close 关闭文件。
  5. 使用 read 读文件。
  6. 使用 write 写文件。
  7. 使用 lseek 设置文件偏移。
  8. 当我们写文件的时候,内核的文件系统模块把数据保存在页缓存中,不会立即写 存储设备。我们可以使用 fsync 把文件修改过的属性和数据立即写到存储设备,或者使 fdatasync 把文件修改过的数据立即写到存储设备。

应用程序也可以使用 glibc 库封装的标准 IO 流函数访问文件,标准流提供了缓冲区, 目的是尽可能减少调用 read 和 write 的次数,提高性能 glic 库封装的标准流函数如下所示:

  1. 使用 fopen 打开流。
  2. 使用 fclose 关闭流。
  3. 使用 fread 读流。
  4. 使用 fwrite 写流。
  5. 使用 fseek 设置文件偏移。
  6. 使用 fwrite 可以把数据写到用户空间缓冲区,但不会立即写到内核。我们可以使 fush 冲刷流,即把写到用户空间缓冲区的数据立即写到内核。

1.3 硬件层面

外部存储设备分为块设备、闪存和 NVDIMM 设备 3 类。 块设备主要有以下两种。

  1. 机械硬盘:机械硬盘的读写单位是扇区。访问机械硬盘的时候,需要首先沿着半径 方向移动磁头寻找磁道,然后转动盘片找到扇区。
  2. 闪存类块设备:使用闪存作为存储介质,里面的控制器运行固化的驱动程序,驱动 程序的功能之一是闪存转换层(Flash Translation Layer,FTL),把闪存转换为块设备, 外表现为块设备。常见的闪存类块设备是在个人计算机和笔记本电脑上使用的固态硬盘 splid State Drives,SSD),以及在手机和平板电脑上使用的嵌入式多媒体存储卡(embedded Multi Media Card,eMMc)和通用闪存存储(Universal Flash Storage,UFS)。 闪存类块设备相对机械硬盘的优势是:访问速度快,因为没有机械操作:抗振性很高, 便于携带。

闪存(Flash Memory)的主要特点如下。

  1. 在写入数据之前需要擦除一个擦除块,因为向闪存写数据只能把一个位从 1 变成 0,不能从 0 变成 1,擦除的目的是把擦除块的所有位设置为 1
  2. 一个擦除块的最大擦除次数有限,NOR闪存的擦除块的最大擦除次数是 10^4~10^3, NAND 闪存的擦除块的最大擦除次数是 10^3~10^6

闪存按存储结构分为 NAND 闪存和 NOR 闪存,两者的区别如下。

  1. NOR闪存的容量小,NAND 闪存的容量大。
  2. NOR 闪存支持按字节寻址,支持芯片内执行(eXecute In Place,XIP),可以直接 在闪存内执行程序,不需要把程序读到内存中; NAND 闪存的最小读写单位是页或子页, 一个擦除块分为多个页,有的 NAND 闪存把页划分为多个子页。
  3. NOR 闪存读的速度比 NAND 闪存块,写的速度和擦除的速度都比 NAND 闪存慢
  4. NOR 闪存没有坏块;NAND 闪存存在坏块,主要是因为消除坏块的成本太高 NOR 闪存适合存储程序,一般用来存储引导程序比如 uboot 程序;NAND 闪存适 合存储数据。

为什么要针对闪存专门设计文件系统?主要原因如下。

  1. NAND 闪存存在坏块,软件需要识别并且跳过坏块。
  2. 需要实现损耗均衡( wear leveling),损耗均衡就是使所有擦除块的擦除次数均衡, 避免一部分擦除块先损坏。

机械硬盘和 NAND 闪存的主要区别如下。

  1. 机械硬盘的最小读写单位是扇区,扇区的大小一般是 512 字节:NAND 闪存的最 小读写单位是页或子页。
  2. 机械硬盘可以直接写入数据:NAND 闪存在写入数据之前需要擦除一个擦除块。
  3. 机械硬盘的使用寿命比 NAND 闪存长:机械硬盘的扇区的写入次数没有限制: NAND 闪存的擦除块的擦除次数有限。
  4. 机械硬盘隐藏坏的扇区,软件不需要处理坏的扇区:NAND 闪存的坏块对软件可 见,软件需要处理坏块。

NVDIMM(Nonn-Volatile DIMM,非易失性内存:DIMM 是 Dual-Inline-Memory-Modules 的缩写,表示双列直插式存储模块,是内存的一种规格)设备把 NAND 闪存、内存和超级 电容集成到一起,访问速度和内存一样快,并且断电以后数据不会丢失。在断电的瞬间, 超级电容提供电力,把内存中的数据转移到 NAND 闪存。

1.4 内核空间层面

在内核的目录 fs 下可以看到,内核支持多种文件系统类型。为了对用户程序提供统一的 文件操作接口,为了使不同的文件系统实现能够共存,内核实现了一个抽象层,称为虚拟文件 系统( Virtual File System,VFS),也称为虚拟文件系统切换( Virtual Filesystem Switch,VFS) 文件系统分为以下 4 种。

  1. 块设备文件系统,存储设备是机械硬盘和固态硬盘等块设备,常用的块设备文件 系统是 EXT 和 btrfs。EXT 文件系统是 Linux 原创的文件系统,目前有 3 个 成版本:EXT2、EXT3 和 EXT4。
  2. 闪存文件系统,存储设备是 NAND 闪存和 NOR 闪存,常用的闪存文件系统是 JFFS2 ,(日志型闪存文件系统版本2, Journalling Flash File System version2)和 UBIFS(无序区块镜像文件系统, Unsorted Block Image File System)。
  3. 内存文件系统,文件在内存中,断电以后文件丢失,常用的内存文件系统是 tmpfs, 用来创建临时文件。
  4. 伪文件系统,是假的文件系统,只是为了使用虚拟文件系统的编程接口,常用的 伪文件系统如下所示。
    1. sockfs,这种文件系统使得套接字(socket)可以使用读文件的接口 read 接收报文, 使用写文件的接口 write 发送报文。
    2. proc 文件系统,最初开发 proc 文件系统的目的是把内核中的进程信息导出到用户空间, 后来扩展到把内核中的任何信息导出到用户空间,通常把 proc 文件系统挂载在目录 “proc” 下。
    3. sysfs,用来把内核的设备信息导出到用户空间,通常把 sysfs 文件系统挂载在目录 “/sys”下。
    4. hugetlbfs,用来实现标准巨型页。
    5. cgroup 文件系统,控制组(control group cgroup)用来控制一组进程的资源, cgroup 文件系统使管理员可以使用写文件的方式配置 cgroup。
    6. cgroup2 文件系统, cgroup2 是 cgroup 的第二个版本, cgroup2 文件系统使管理员可 以使用写文件的方式配置 cgroup2。
  • 页缓存:访问外部存储设备的速度很慢,为了避免每次读写文件时访问外部存储设备,文件系 统模块为每个文件在内存中创建了一个缓存,因为缓存的单位是页,所以称为页缓存。
  • 块设备层:块设备的访问单位是块,块大小是扇区大小的整数倍。内核为所有块设备实现了统一 的块设备层。
  • 块缓存:为了避免每次读写都需要访问块设备,内核实现了块缓存,为每个块设备在内存中创 建一个块缓存。缓存的单位是块,块缓存是基于页缓存实现的。
  • IO 调度器:访问机械硬盘时,移动磁头寻找磁道和扇区很耗时,如果把读写请求按照扇区号排序, 可以减少磁头的移动,提高吞吐量。IO 调度器用来决定读写请求的提交顺序,针对不同的 使用场景提供了多种调度算法:NOOP(No Operation)、CFQ(完全公平排队, Complete Fair Queuing)和 deadline(限期)。NOOP 调度算法适合闪存类块设备,CFQ 和 deadline调度算 法适合机械硬盘。
  • 块设备驱动程序:每种块设备需要实现自己的驱动程序。

内核把闪存称为存储技术设备( Memory Technology Device,MTD),为所有闪存实现 了统一的 MTD 层,每种闪存需要实现自己的驱动程序。

针对 NVDIMM 设备,文件系统需要实现 DAX(Direct Access直接访问:X 代表 eXciting,没有意义,只是为了让名字看起来酷),绕过页缓存和块设备层,把 NVDIMM 设备里面的内存直接映射到进程或内核的虚拟地址空间。

libnvdimm 子系统提供对 3 种 NVDIMM 设备的支持:持久内存(persistent memory,PMEM) 模式的 NVDIMM 设备,块设备(block,BLK)模式的 NVDIMM 设备,以及同时支持PMEM 和 BLK 两种访问模式的 NVDIMM 设备。PMEM 访问模式是把 NVDIMM 设备当作内存,BLK 访问模式是把 NVDIMM 设备当作块设备。每种 NVDIMM 设备需要实现自己的驱动程序。

2 虚拟文件系统的数据结构

虽然不同文件系统类型的物理结构不同,但是虚拟文件系统定义了一套统一的数据结构。

  1. 超级块(super_block)。文件系统的第一块是超级块,描述文件系统的总体信息,挂载文件系统 的时候在内存中创建超级块的副本:结构体 super_block。
  2. 虚拟文件系统在内存中把目录组织为一棵树。一个文件系统,只有挂载到内存中目录 树的一个目录下,进程才能访问这个文件系统。每次挂载件系统,虚拟文件系统就会创建一个 挂载描述符: mount 结构体,并且读取文件系统的超级块,在内存中创建超级块的一个副本。
  3. 每种文件系统的超级块的格式不同,需要向虚拟文件系统注册文件系统类型 file_system_type,并且实现 mount 方法用来读取和解析超级块
  4. 索引节点(inode)。每个文件对应一个索引节点,每个索引节点有一个唯一的编号。当内 核访问存储设备上的一个文件时,会在内存中创建索引节点的一个副本:结构体 inode。
  5. 目录项(dentry)。文件系统把目录看作文件的一种类型,目录的数据是由目录项组成的, 每个目录项存储一个子目录文件 的名称以及对应的索引节点号。当内核访问存储设备上 的一个目录项时,会在内存中创建该目录项的一个副本:结构体 dentry。
  6. 当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:file 结构体,然后在进程的打开文件表中分配一个索引,这个索引称为文件描述符(fd),最后把文 件描述符和 file 结构体的映射添加到打开文件表中。

An individual dentry usually has a pointer to an inode. Inodes are filesystem objects such as regular files, directories, FIFOs and other beasts. They live either on the disc (for block device filesystems) or in the memory (for pseudo filesystems). Inodes that live on the disc are copied into the memory when required and changes to the inode are written back to disc. A single inode can be pointed to by multiple dentries (hard links, for example, do this).

单个 dentry 通常具有指向 inode 的指针。 inode 是文件系统对象,例如常规文件,目录,FIFO 和其他类型。 它们存在于 disc(用于块设备文件系统)或在内存中(用于伪文件系统)中。 存在于 disc 上的 inode 在需要时被复制到内存中,并且对 inode 的更改将被写回到 disc。 单个 inode 可以由多个 dentry 指向(硬链接,例如,这样做)。

注意: 这段话有三个重点:一是 inode 是用来表示文件系统对象,有些地方说是用来代表“文件”,这也没错,但却没有说这个“文件”不仅仅是指常规文件,它还包括 目录,FIFO(管道文件),socket,块设备,字符设备,符号链接一共七种类型文件,所以很容易产生误解,理解这点很重要。 二是 inode 的存储位置。三是一个 inode 或者说“文件” 可以被多个 dentry 指向,这里还特意提了是硬链接,其实也就说明硬链接其实是创建了一个 dentry ,而这个 dentry 的 d_inode 指向了同一个 inode,而对于符号链接它就是一个“文件” 也就是表示它有一个对应的 inode,而这个 inode 里面的内容是指向了另外一个 inode 的路径。

To look up an inode requires that the VFS calls the lookup() method of the parent directory inode. This method is installed by the specific filesystem implementation that the inode lives in. Once the VFS has the required dentry (and hence the inode), we can do all those boring things like open(2) the file, or stat(2) it to peek at the inode data. The stat(2) operation is fairly simple: once the VFS has the dentry, it peeks at the inode data and passes some of it back to userspace.

要查找 inode, VFS 需要调用父目录 inode 的 lookup() 方法。此方法由 inode 所在的特定文件系统实现安装。 一旦 VFS 有了目标 dentry(间接能够获得 inode),我们可以做所有那些无聊的事情,比如 open(2) 文件,或者 stat(2) 查看 inode 数据。stat(2) 操作相当简单:一旦 VFS 具有 dentry, 就可以获取 inode 的数据,并将其中一些传递回用户空间。

注意: 这段话的一个重点是我们是使用父目录 inode 对应的 lookup() 方法来查找 inode 的,也就是说在查找当前 inode 的时候,已经确保了父目录是没问题,但有时候在查找后也会再次检查父目录有没有发生改变。

2.1 超级块

文件系统的第一块是超级块,用来描述文件系统的总体信息。当我们把文件系统挂载 到内存中目录树的一个目录下时,就会读取文件系统的超级块,在内存中创建超级块的副 本:结构体 super_block,主要成员如下: [include/linux/fs.h]

struct super_block {
	struct list_head	s_list;		// Keep this first
	dev_t			s_dev;		// search index; _not_ kdev_t
	unsigned char		s_blocksize_bits;
	unsigned long		s_blocksize;
	loff_t			s_maxbytes;	// Max file size
	struct file_system_type* s_type;
	const struct super_operations* s_op;
	...
	unsigned long		s_flags;
	unsigned long		s_iflags;	// internal SB_I_* flags
	unsigned long		s_magic;
	struct dentry* s_root;

    ...
	struct hlist_bl_head	s_anon;		// anonymous dentries for (nfs) exporting
	struct list_head	s_mounts;	// list of mounts; _not_ for fs use
	struct block_device*  s_bdev;
	struct backing_dev_info*  s_bdi;
	struct mtd_info*  s_mtd;
	struct hlist_node	s_instances;

    ...
	void*  s_fs_info;	// Filesystem private info
	...
};
  • 成员 s_list 用来把所有超级块实例链接到全局链表 super_blocks。
  • 成员 sdev 和 bdev 保存文件系统所在的块设备,前者保存设备号,后者指向内 存中的一个 block_device 实例。
  • 成员 s_blocksize 是块长度,成员 blocksize_bits 是块长度以 2 为底的对数。
  • 成员 s_maxbytes 是文件系统支持的最大文件长度。
  • 成员 s_flags 是标志位。
  • 成员 s_type 指向文件系统类型。
  • 成员 s_op 指向超级块操作集合。
  • 成员 magic 是文件系统类型的魔幻数每种文件系统类型被分配一个唯一的魔幻数。
  • 成员 sroot 指向根目录的结构体 dentry。
  • 成员 sfs_info 指向具体文件系统的私有信息。
  • 成员 instances 用来把同一个文件系统类型的所有超级块实例链接在一起,链表 的头节点是结构体 fle_system_type 的成员 fs_supers。

超级块操作集合的数据结构是结构体 super_operations,主要成员如下: [include/linux/fs.h]

struct super_operations {
  struct inode *(*alloc_inode)(struct super_block *sb);
	void (*destroy_inode)(struct inode *);

  void (*dirty_inode) (struct inode *, int flags);
	int (*write_inode) (struct inode *, struct writeback_control *wbc);
	int (*drop_inode) (struct inode *);
	void (*evict_inode) (struct inode *);
	void (*put_super) (struct super_block *);
	int (*sync_fs)(struct super_block *sb, int wait);
	int (*freeze_super) (struct super_block *);
	int (*freeze_fs) (struct super_block *);
	int (*thaw_super) (struct super_block *);
	int (*unfreeze_fs) (struct super_block *);
	int (*statfs) (struct dentry *, struct kstatfs *);
	int (*remount_fs) (struct super_block *, int *, char *);
	int (*remount_fs2) (struct vfsmount *, struct super_block *, int *, char *);
	void *(*clone_mnt_data) (void *);
	void (*copy_mnt_data) (void *, void *);
	void (*umount_begin) (struct super_block *);

	int (*show_options)(struct seq_file *, struct dentry *);
	int (*show_options2)(struct vfsmount *,struct seq_file *, struct dentry *);
	int (*show_devname)(struct seq_file *, struct dentry *);
	int (*show_path)(struct seq_file *, struct dentry *);
	int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
	ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
	ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
	struct dquot **(*get_dquots)(struct inode *);
#endif
	int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
	long (*nr_cached_objects)(struct super_block *,
				  struct shrink_control *);
	long (*free_cached_objects)(struct super_block *,
				    struct shrink_control *);
};
  • 成员 alloc_inode 用来为一个索引节点分配内存并且初始化。
  • 成员 destroy_inode 用来释放内存中的索引接点。
  • 成员 dirty_inode 用来把索引节点标记为脏。
  • 成员 write_inode 用来把一个索引节点写到存储设备。
  • 成员 drop_inode 用来在索引节点的引用计数减到 0 时调用。
  • 成员 evict_inode 用来从存储设备上的文件系统中删除一个索引节点。
  • 成员 put_super 用来释放超级块。
  • 成员 sync_fs 用来把文件系统修改过的数据同步到存储设备。
  • 成员 statfs 用来读取文件系统的统计信息。
  • 成员 remountfs 用来在重新挂载文件系统的时候调用。
  • 成员 umount_begin 用来在卸载文件系统的时候调用。

2.2 挂载描述符

一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统 每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符: mount 结构体。挂载描述符 用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂 载到不同的目录下。 [include/linux/fs.h]

struct mount {
   struct hlist_node mnt_hash;
   struct mount *mnt_parent;
   struct dentry *mnt_mountpoint;
   struct vfsmount mnt;
   union {
   	struct rcu_head mnt_rcu;
   	struct llist_node mnt_llist;
   };
#ifdef CONFIG_SMP
   struct mnt_pcp __percpu *mnt_pcp;
#else
   int mnt_count;
   int mnt_writers;
#endif
   struct list_head mnt_mounts;	/* list of children, anchored here */
   struct list_head mnt_child;	/* and going through their mnt_child */
   struct list_head mnt_instance;	/* mount instance on sb->s_mounts */
   const char *mnt_devname;	/* Name of device e.g. /dev/dsk/hda1 */
   struct list_head mnt_list;
 ...
   struct mnt_namespace *mnt_ns;	/* containing namespace */
   struct mountpoint *mnt_mp;	/* where is it mounted */
   struct hlist_node mnt_mp_list;	/* list mounts with the same mountpoint */
 ...
};

假设我们把文件系统 2 挂载到目录 “/a” 下,目录 a 属于文件系统 1。目录 a 称为挂载 点,文件系统 2 的 mount 实例是文件系统 1 的 mount 实例的孩子,文件系统 1 的 mount 实 是文件系统 2 的 mount 实例的父亲。

  • 成员 mnt_parent 指向父亲,即文件系统 1 的 mount 实例。
  • 成员 mnt_mountpoint 指向作为挂载点的目录,即文件系统 1 的目录 a,目录 a 的 dentry 实例的成员 d_flags 设置了标志位 DCACHE_MOUNTED
  • 成员 mnt 的类型如下:
     struct vfsmount{
       struct dentry *mnt_root;
       struct super_block *mnt_sb;
       int mnt_flags;
    }
    
    mnt_root 指向文件系统 2 的根目录,mnt_sb 指向文件系统 2 的超级块。
  • 成员 mnt_hash 用来把挂载描述符加入全局散列表 mount_hashtable,关键字是{父载描述符,挂载点}。
  • 成员 mnt_mounts 是孩子链表的头节点。
  • 成员 mnt_child 用来加入父亲的孩子链表。
  • 成员 mnt_instance 用来把挂载描述符添加到超级块的挂载实例链表上,同一个存储设备上的文件系统,可以多次挂载,每次挂载到不同的目录下。
  • 成员 mnt_devname 指向存储设备的名称。
  • 成员 mnt_ns 指向挂载命名空间,后面会介绍。
  • 成员 mnt_mp 指向挂载点,类型如下:
    struct mountpoint{
      struct hlist_node m_hash;
      struct dentry *m_dentry;
      struct hlist_head m_list;
      int m_count;
    }
    
    m_dentry 指向作为挂载点的目录, m_list 用来把同一个挂载点下的所有挂载描述符链 接起来。为什么同一个挂载点下会有多个挂载描述符?这和挂载命名空间有关。
  • 成员 mnt_mp_list 用来把挂载描述符加入同一个挂载点的挂载描述符链表,链表 头节点是成员 mnt_mp 的成员 m_list。

2.3 文件系统类型

因为每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统注册 文件系统类型 file_system_type,并且实现 mount 方法用来读取和解析超级块。结构体 file_system_type 如下: [include/linux/fs.h]

struct file_system_type {
	const char *name;
	int fs_flags;
#define FS_REQUIRES_DEV		1
#define FS_BINARY_MOUNTDATA	2
#define FS_HAS_SUBTYPE		4
#define FS_USERNS_MOUNT		8	/* Can be mounted by userns root */
#define FS_USERNS_DEV_MOUNT	16 /* A userns mount does not imply MNT_NODEV */
#define FS_USERNS_VISIBLE	32	/* FS must already be visible */
#define FS_RENAME_DOES_D_MOVE	32768	/* FS will handle d_move() during rename() internally. */
	struct dentry *(*mount) (struct file_system_type *, int,
		       const char *, void *);
	struct dentry *(*mount2) (struct vfsmount *, struct file_system_type *, int,
			       const char *, void *);
	void *(*alloc_mnt_data) (void);
	void (*kill_sb) (struct super_block *);
	struct module *owner;
	struct file_system_type * next;
	struct hlist_head fs_supers;

	struct lock_class_key s_lock_key;
	struct lock_class_key s_umount_key;
	struct lock_class_key s_vfs_rename_key;
	struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];

	struct lock_class_key i_lock_key;
	struct lock_class_key i_mutex_key;
	struct lock_class_key i_mutex_dir_key;
};
  • 成员 name 是文件系统类型的名称。
  • 方法 mount 用来在挂载文件系统的时候读取并且解析超级块。
  • 方法 kill_sb 用来在卸载文件系统的时候释放超级块。
  • 多个存储设备上的文件系统的类型可能相同,成员 fs_supers 用来把相同文件系统类型的 超级块链接起来。

2.4 索引节点

在文件系统中,每个文件对应一个索引节点,索引节点描述两类信息。

  1. 文件的属性,也称为元数据(metadata),例如文件长度、创建文件的用户的标识符、上一次 访问的时间和上一次修改的时间,等等。
  2. 文件数据的存储位置。

每个索引节点有一个唯一的编号。 当内核访问存储设备上的一个文件时,会在内存中创建索引节点的一个副本:结构体 inode,主要 成员如下: [include/linux/fs.h]

struct inode {
	umode_t			i_mode;
	unsigned short		i_opflags;
	kuid_t			i_uid;
	kgid_t			i_gid;
	unsigned int		i_flags;

#ifdef CONFIG_FS_POSIX_ACL
	struct posix_acl	*i_acl;
	struct posix_acl	*i_default_acl;
#endif

	const struct inode_operations	*i_op;
	struct super_block	*i_sb;
	struct address_space	*i_mapping;

    ...

	/* Stat data, not accessed from path walking */
	unsigned long		i_ino;
	/*
	 * Filesystems may only read i_nlink directly.  They shall use the
	 * following functions for modification:
	 *
	 *    (set|clear|inc|drop)_nlink
	 *    inode_(inc|dec)_link_count
	 */
	union {
		const unsigned int i_nlink;
		unsigned int __i_nlink;
	};
	dev_t			i_rdev;
	loff_t			i_size;
	struct timespec		i_atime;
	struct timespec		i_mtime;
	struct timespec		i_ctime;
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	unsigned short          i_bytes;
	unsigned int		i_blkbits;
	enum rw_hint		i_write_hint;
	blkcnt_t		i_blocks;

    ...
	struct hlist_node	i_hash;
	struct list_head	i_io_list;	/* backing dev IO list */
    ...
	struct list_head	i_lru;		/* inode LRU list */
	struct list_head	i_sb_list;
	union {
		struct hlist_head	i_dentry;
		struct rcu_head		i_rcu;
	};
	u64			i_version;
	atomic_t		i_count;
	atomic_t		i_dio_count;
	atomic_t		i_writecount;
#ifdef CONFIG_IMA
	atomic_t		i_readcount; /* struct files open RO */
#endif
	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct file_lock_context	*i_flctx;
	struct address_space	i_data;
	struct list_head	i_devices;
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
		char			*i_link;
	};
    ...
	void			*i_private; /* fs or device private pointer */
};

  • i_mode 是文件类型和访问权限,i_uid 是创建文件的用户的标识符,i_gid 是创建文件 用户所属的组标识符。
  • i_ino 是索引节点的编号。
  • i_size 是文件长度; i_blocks 是文件的块数,即文件长度除以块长度的商;i_bytes 是文件 长度除以块长度的余数;i_blkbits 是块长度以 2 为底的对数,块长度是 2 的 i_blkbits 次幂。
  • i_atime(access time)是上一次访问文件的时间, i_mtime(modified time)是上一次修改文 件数据的时间,i_ctime(change time)是上一次修改文件索引节点的时间。
  • i_sb 指向文件所属的文件系统的超级块。
  • i_mapping 指向文件的地址空间。
  • i_count 是索引节点的引用计数,i_nlink 是硬链接计数。
  • 如果文件的类型是字符设备文件或块设备文件,那么 i_rdev 是设备号,i_bdev 指向块, i_cdev 指向字符设备。

文件分为以下几种类型。

  • 普通文件(regular file):就是我们通常说的文件,是狭义的文件。
  • 目录:目录是一种特殊的文件,这种文件的数据是由目录项组成的,每个目录项 存储一个子目录文件 的名称以及对应的索引节点号。
  • 符号链接(也称为软链接):这种文件的数据是另一个文件的路径。
  • 字符设备文件。
  • 块设备文件。
  • 命名管道(FIFO)。
  • 套接字(socket)

字符设备文件、块设备文件、命名管道和套接字是特殊的文件,这些文件只有索引节点, 没有数据。字符设备文件和块设备文件用来存储设备号,直接把设备号存储在索引节点中。

内核支持两种链接。

  1. 软链接,也称为符号链接,这种文件的数据是另一个文件的路径。它是一个真正的文件,有自己的 inode 对象,inode 存储的数据就是符号链接指向路径的路径名。
  2. 硬链接,相当于给一个文件取了多个名称,多个文件名称对应同一个索引节点, 索引节点的成员 i_nlink 是硬链接计数。 索引节点的成员 iop 指向索引节点操作集合 inode operations,成员 ifop 指向文件操 作集合 file_operations 两者的区别是: inodeoperations用来操作目录(在一个目录下创建 或删除文件)和文件属性,file_operations 用来访问文件的数据 索引节点操作集合的数据结构是结构体 inode_operations,主要成员如下:
struct inode_operations {
	struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
	const char * (*follow_link) (struct dentry *, void **);
	int (*permission) (struct inode *, int);
	int (*permission2) (struct vfsmount *, struct inode *, int);
	struct posix_acl * (*get_acl)(struct inode *, int);

	int (*readlink) (struct dentry *, char __user *,int);
	void (*put_link) (struct inode *, void *);

	int (*create) (struct inode *,struct dentry *, umode_t, bool);
	int (*link) (struct dentry *,struct inode *,struct dentry *);
	int (*unlink) (struct inode *,struct dentry *);
	int (*symlink) (struct inode *,struct dentry *,const char *);
	int (*mkdir) (struct inode *,struct dentry *,umode_t);
	int (*rmdir) (struct inode *,struct dentry *);
	int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
	int (*rename) (struct inode *, struct dentry *,
			struct inode *, struct dentry *);
	int (*rename2) (struct inode *, struct dentry *,
			struct inode *, struct dentry *, unsigned int);
	int (*setattr) (struct dentry *, struct iattr *);
	int (*setattr2) (struct vfsmount *, struct dentry *, struct iattr *);
	int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
	int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
	ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
	ssize_t (*listxattr) (struct dentry *, char *, size_t);
	int (*removexattr) (struct dentry *, const char *);
	int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
		      u64 len);
	int (*update_time)(struct inode *, struct timespec *, int);
	int (*atomic_open)(struct inode *, struct dentry *,
			   struct file *, unsigned open_flag,
			   umode_t create_mode, int *opened);
	int (*tmpfile) (struct inode *, struct dentry *, umode_t);
	int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;

lookup 方法用来在一个目录下查找文件。 系统调用 open 和 creat 调用 create 方法来创建普通文件,系统调用 link 调用 link 方法 来创建硬链接,系统调用 symlink 调用 symlink 方法来创建符号链接,系统调用 mkdir 调用 mkdir 方法来创建目录,系统调用 mknod 调用 mknod 方法来创建字符设备文件、块设备文件、 命名管道和套接字。 系统调用 unlink 调用 unlink 方法来删除硬链接,系统调用 rmdir 调用 rmdir 方法来删除目录。 系统调用 rename 调用 rename 方法来给文件换一个名字。 系统调用 chmod 调用 setattr 方法来设置文件的属性,系统调用 stat 调用 getattr 方法来创建目录读取文件的属性。 系统调用 listxattr 调用 listxattr 方法来列出文件的所有的扩展属性。

2.5 目录项

文件系统把目录当做文件,这种文件的数据是有目录项组成的,每个目录项存储一个子目录或文件的名称以及对应的索引节点号。 当内核访问存储设备上的一个目录项时,会在内存中创建目录项的一个副本:结构体 dentry,主要成员如下: [include/linux/dcache.h]

struct dentry {
	/* RCU lookup touched fields */
	unsigned int d_flags;		/* protected by d_lock */
	seqcount_t d_seq;		/* per dentry seqlock */
	struct hlist_bl_node d_hash;	/* lookup hash list */
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name;
	struct inode *d_inode;		/* Where the name belongs to - NULL is
					 * negative */
	unsigned char d_iname[DNAME_INLINE_LEN];	/* small names */

	/* Ref lookup also touches following */
	struct lockref d_lockref;	/* per-dentry lock and refcount */
	const struct dentry_operations *d_op;
	struct super_block *d_sb;	/* The root of the dentry tree */
	unsigned long d_time;		/* used by d_revalidate */
	void *d_fsdata;			/* fs-specific data */

	struct list_head d_lru;		/* LRU list */
	struct list_head d_child;	/* child of parent list */
	struct list_head d_subdirs;	/* our children */
	/*
	 * d_alias and d_rcu can share memory
	 */
	union {
		struct hlist_node d_alias;	/* inode alias list */
	 	struct rcu_head d_rcu;
	} d_u;
};
  • d_name 存储文件名称,qstr是字符串的包装器,存储字符串的地址、长度和散列值;如果文件名称比较短,把文件名称存储在 d_iname;d_inode 指向文件的索引节点。
  • d_parent 指向父目录,d_child 用来把本目录加入父目录的子目录链表。
  • d_lockref 是引用计数。
  • d_subdirs 是子目录链表。
  • d_hash 用来把目录项加入散列表 dentry_hashtable
  • d_lru 用来把目录项加入超级块的最近最少使用(Least Recently Used,LRU)链表 s_dentry_lru 中,当目录项的引用计数减到 0 时,把目录项添加到超级块的 LRU 链表中。
  • d_alias 用来把同一个文件的所有硬链接对应的目录项链接起来。

以文件 “/a/c.txt” 为例,目录项和索引节点的关系如下:

目录项操作集合的数据结构是结构体 dentry_operations,其代码如下:

struct dentry_operations {
	int (*d_revalidate)(struct dentry *, unsigned int);
	int (*d_weak_revalidate)(struct dentry *, unsigned int);
	int (*d_hash)(const struct dentry *, struct qstr *);
	int (*d_compare)(const struct dentry *, const struct dentry *,
			unsigned int, const char *, const struct qstr *);
	int (*d_delete)(const struct dentry *);
	void (*d_release)(struct dentry *);
	void (*d_prune)(struct dentry *);
	void (*d_iput)(struct dentry *, struct inode *);
	char *(*d_dname)(struct dentry *, char *, int);
	struct vfsmount *(*d_automount)(struct path *);
	int (*d_manage)(struct dentry *, bool);
	struct inode *(*d_select_inode)(struct dentry *, unsigned);
	struct dentry *(*d_real)(struct dentry *, struct inode *);
	void (*d_canonical_path)(const struct path *, struct path *);
} ____cacheline_aligned;

参考 vfs.txt 文档:

d_revalidate: called when the VFS needs to revalidate a dentry. This is called whenever a name look-up finds a dentry in the dcache. Most local filesystems leave this as NULL, because all their dentries in the dcache are valid. Network filesystems are different since things can change on the server without the client necessarily being aware of it. This function should return a positive value if the dentry is still valid, and zero or a negative error code if it isn't. d_revalidate may be called in rcu-walk mode (flags & LOOKUP_RCU).If in rcu-walk mode, the filesystem must revalidate the dentry without blocking or storing to the dentry, d_parent and d_inode should not be used without care (because they can change and, in d_inode case, even become NULL under us). If a situation is encountered that rcu-walk cannot handle, return -ECHILD and it will be called again in ref-walk mode.

当 VFS 需要验证一个 dentry 有效性时会被调用。在 dcache 中找到 name 对应的 dentry 的时候就被调用。大多数本地文件系统直接返回 NULL,因为这类文件系统在 dcache 中所有的 dentry 都是有效的。但是网络文件系统不一样,因为事情会在服务器上已经发生了,但是客户端却不知道。这个函数应该返回一个正值来表示该 dentry 依然是有效的,0 或者 负数来表示错误。

d_automount: called when an automount dentry is to be traversed (optional).This should create a new VFS mount record and return the record to the caller. The caller is supplied with a path parameter giving the automount directory to describe the automount target and the parent VFS mount record to provide inheritable mount parameters. NULL should be returned if someone else managed to make the automount first. If the vfsmount creation failed, then an error code should be returned. If -EISDIR is returned, then the directory will be treated as an ordinary directory and returned to pathwalk to continue walking.

当碰到一个自动挂载 automount 目录项时,d_automount 会被调用。(可选的)。它应该创建一个新的 VFS 挂载记录,并且把这个记录返回给调用者。为调用者提供了一个 path 参数,该参数给出了automount 目录(用于描述 automount 目标)和父 VFS 挂载记录(以提供可继承的挂载参数)。如果已经有其他调用者 在该 automount 做了处理,那么返回 NULL。如果 vfsmount 创建失败那么返回错误码。如果返回了 -EISDIR,那么该目录被当成普通目录并且返回到路径查找流程继续下一步查找。

If a vfsmount is returned, the caller will attempt to mount it on the mountpoint and will remove the vfsmount from its expiration list in the case of failure. The vfsmount should be returned with 2 refs on it to prevent automatic expiration - the caller will clean up the additional ref.

如果返回了 vfsmount,那么调用者将会尝试把它挂载在该挂载点上。并且如果挂载失败,则会将 vfsmount 从它的 expiration 列表中移除。vfsmount 在返回时应该带有两个引用(指向它自己的引用)用来防止 自动卸载(过期)。调用者会清除附加的引用。

This function is only used if DCACHE_NEED_AUTOMOUNT is set on the dentry. This is set by __d_instantiate() if S_AUTOMOUNT is set on the inode being added.

只有 dentry 被设置了 DCACHE_NEED_AUTOMOUNT 标志,该函数才会被使用。如果 inode 的 S_AUTOMOUNT 被设置了,那么可以通过 __d_instantiate() 函数来设置 DCACHE_NEED_AUTOMOUNT 标志。

d_manage: called to allow the filesystem to manage the transition from a dentry (optional). This allows autofs, for example, to hold up clients waiting to explore behind a 'mountpoint' whilst letting the daemon go past and construct the subtree there. 0 should be returned to let the calling process continue. -EISDIR can be returned to tell pathwalk to use this directory as an ordinary directory and to ignore anything mounted on it and not to check the automount flag. Any other error code will abort pathwalk completely.

d_manage 允许文件系统来管理一个 dentry 的转换。(可选的)。它允许使用 autofs。例如:挂起一个 等待去探索某 mountpoint 下路径的客户端,与此同时让守护进程通过该挂载点并且在其下构建子树。返回 0 表示让挂起的客户端继续执行。返回 -EISDIR,表示路径查找进程应该把该目录当做普通目录处理,并且忽略在其上挂载的任何东西,以及不会去检查 automou 标志。其它任何错误码表示应该中断本次路径查找。

If the 'rcu_walk' parameter is true, then the caller is doing a pathwalk in RCU-walk mode. Sleeping is not permitted in this mode, and the caller can be asked to leave it and call again by returning -ECHILD. -EISDIR may also be returned to tell pathwalk to ignore d_automount or any mounts.

如果 'rcu_walk' 参数为 true,那么调用者的路径查找处于 RCU 模式。这种模式下是不允许睡眠,所以 调用者被要求离开该函数并且返回 -ECHILD。同样的,返回 -EISDIR 告诉当前路径查找忽略 d_automount 或者其他任何挂载。

This function is only used if DCACHE_MANAGE_TRANSIT is set on the dentry being transited from.

该函数只有当前遍历的 dentry 被设置了 DCACHE_MANAGE_TRANSIT 标志是才能使用。

d_hash: called when the VFS adds a dentry to the hash table. The first dentry passed to d_hash is the parent directory that the name is to be hashed into.

VFS 添加一个 dentry 到散列表的时候被调用。第一个参数是父 dentry。

d_compare 用来比较两个目录项的文件系统。 d_delete 用来在目录项的引用计数减到 0 时判断是否可以释放目录项的内存。 d_release 用来在释放目录项的内存之前调用。 d_iput 用来释放目录项关联的索引节点。

2.6 文件的打开实例和打开文件表。

当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:file 结构体,主要成员如下:

struct file {
	union {
		struct llist_node	fu_llist;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	/*
	 * Protects f_ep_links, f_flags.
	 * Must not be taken from IRQ context.
	 */
	spinlock_t		f_lock;
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct list_head	f_ep_links;
	struct list_head	f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
} __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */
  1. f_path 存储文件在目录树中的位置,类型如下:
    struct path {
       struct vfsmount *mnt;
        struct dentry *dentry;
    };
    
    mnt 指向文件所属文件系统的挂载描述符的成员 mnt,dentry 是文件对应的目录项。
  2. f_inode 指向文件的索引节点。
  3. f_op 指向文件操作集合。
  4. f_count 是 file 结构体的引用计数。
  5. f_mode 是访问模式。
  6. f_pos 是文件偏移,即进程当前正在访问的位置。
  7. f_mapping 指向文件的地址空间。

文件的打开实例和索引节点的关系如图:

进程描述符有两个文件系统相关的成员:成员 fs 指向进程的文件系统信息结构体,主要是进程的根目录和当前工作目录;成员 files 指向打开文件表。

[include/linux/sched.h]

struct task_struct{
  ...
  struct fs_struct *fs;
  struct files_struct *files;
  ...
}

文件系统信息结构体的主要成员如下:

[include/linux/fs_struct.h]

  struct fs_struct{
    ...
    struct path root, pwd;
  }

成员 root 存储进程的根目录,成员 pwd 存储进程的当前工作目录。 假设首先调用系统调用 chroot,把目录“/a” 设置为进程的根目录,然后创建子进程,子进程继承 父进程的文件系统信息,那么把子进程能看到的目录范围限制为以目录 “/a” 为根的子树。但子进程打开文件 “/b.txt”(文件路径是绝对路径,以“/”开头)时,真实的文件路径是 “/a/b.txt”。 假设调用系统调用 chdir,把目录 “/c” 设置为进程的当前工作目录,当子进程打开文件 “d.txt” (文件路径是相对路径,不以“/”开头)时,真实的文件路径是 “/c/d.txt”。

打开文件表也称为文件描述符表,数据结构如下图:

结构体 files_struct 是打开文件表的包装器,主要成员如下: [include/linux/fdtable.h]

struct files_struct {
  /*
   * read mostly part
   */
	atomic_t count;
	bool resize_in_progress;
	wait_queue_head_t resize_wait;

	struct fdtable __rcu *fdt;
	struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
	spinlock_t file_lock ____cacheline_aligned_in_smp;
	int next_fd;
	unsigned long close_on_exec_init[1];
	unsigned long open_fds_init[1];
	unsigned long full_fds_bits_init[1];
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

成员 count 是结构体 files_struct 的引用计数。 成员 fdt 指向打开文件表。 当进程刚刚创建的时候,成员 fdt 指向成员 fdtab,运行一段时间后,进程打开的文件数量超过了 NR_OPEN_DEFAULT,就会扩大打开文件表,重新分配 fdtable 结构体,成员 fdt 指向新的 fdtable 结构体。 打开文件表的数据如下: [include/linux/fdtable]

struct fdtable {
	unsigned int max_fds;
	struct file __rcu **fd;      /* current fd array */
	unsigned long *close_on_exec;
	unsigned long *open_fds;
	unsigned long *full_fds_bits;
	struct rcu_head rcu;
};

成员 max_fds 是打开文件表的当前大小,即成员 fd 指向的 file 指针数组的大小。随着进程 打开文件的数量增加,打开文件表逐步扩大。 成员 fd 指向 file 指针数组。当进程调用 open 打开文件的时候,返回的文件描述符是 file 指针 数组的索引。 成员 close_on_exec 指向一个位图,指示在指向 execve() 以装载新程序的时候需要关闭哪些文件 描述符。 成员 open_fds 指向文件描述符位图,指示哪些文件描述符被分配。

2.7 目录项缓存 Directory Entry Cache (dcache)

The VFS implements the open(2), stat(2), chmod(2), and similar system calls. The pathname argument that is passed to them is used by the VFS to search through the directory entry cache (also known as the dentry cache or dcache). This provides a very fast look-up mechanism to translate a pathname (filename) into a specific dentry. Dentries live in RAM and are never saved to disc: they exist only for performance.

VFS实现 open(2)、stat(2)、chmod(2) 和类似的系统调用。传递给这些函数的路径名参数被 VFS 用来在目录项缓存(也称为 dentry 缓存或 dcache)进行搜索目录项。这提供了一个非常快速的查找机制,将路径名(filename)转换为一个特定的 dentry。这些 dentry 存在内存中,并没有保存到磁盘:它们的存在于只是为了提高性能。

The dentry cache is meant to be a view into your entire filespace. As most computers cannot fit all dentries in the RAM at the same time, some bits of the cache are missing. In order to resolve your pathname into a dentry, the VFS may have to resort to creating dentries along the way, and then loading the inode. This is done by looking up the inode.

dentry 缓存是指向整个文件空间的视图。由于大多数计算机不能同时在 RAM 中容纳所有的 dentry,缓存中的一些位丢失了。为了将路径名解析为 dentry, VFS 可能不得不在此过程中创建 dentry,然后加载 inode。这是通过查找 inode 完成的。

有几个文件系统用来维护目录项的函数:

dget:对一个已经存在的 dentry 打开一个新的句柄(handle)(只是增加 dentry 的使用计数)。

dput: 对一个 dentry 关闭一个句柄(减少使用计数)。如果使用计数变为 0,并且这个 dentry 还是它父目录的 hash 表中,那么 "d_delete" 方法被调用,用来检查它是否应该被缓存。如果 不应该被缓存,或者该 dentry 没有被 hash,那么删除它。否则把该已缓存的 dentry 移动到 LRU 链表中,将来当内存不够时可以回收它。

d_drop:它用来从父目录的 hash 表中 unhashes 一个 dentry。如果此时它的使用计数(usage count) 已经降到 0,随后调用 dput() 来释放这个(deallocate) dentry,;

d_delete:删除一个 dentry。如果它没有其他开放的引用,那么它变成一个 negative dentry, 然后 d_iput() 被调用。如果有其他引用,那么调用 d_drop。

d_add: 增加一个 dentry 到它的父目录的 hash 列表中,然后调用 d_instantiate()。

d_instantiate: 增加一个 dentry 到相应 inode 的 alise hash 链表中,并且更新 d_inode 成员。 inode 的 i_count 成员应该被 “set/incremented”。如果该 dentry 的 inode 指针为空, 那么该 dentry 称为 "negative dentry"。该函数通常在为 "negative dentry" 创建一个 inode 时被调用。

d_lookup: 给定一个父目录和路径名称分量,然后查找相应的 dentry。它从缓存(decahe)的散列表中 通过给定的名称查找相应的子项。如果查找到了,增加引用计数并返回 dentry。当完成使用后, 调用者必须调用 dput() 来释放这个 dentry。

2.8 注册文件系统类型

因为每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统注册文件系统类型 file_system_type, 实现 mount 方法用来读取和解析超级块。 函数 register_filesystem 用来注册文件系统类型: int register_filesystem(struct file_system_type *fs); 函数 unregister_filesystem 用来注销文件系统类型: int unregister_filesystem(struct file_system_type *fs); 管理员可以执行命令 “cat/proc/filesystems” 来查看已经注册的文件系统类型。

最后给出两张数据结构图:第一张基于 Linux 4.4,后一张基于 Linux 3.x 版本。图片较大,可先点击放大后再右键“在新标签页中打开图片”,可查看高清图。

虚拟文件系统版本4.x
虚拟文件系统版本3.x

参考:

Linux虚拟文件系统简介

Linux虚拟文件系统源码分析

Linux kernel 文档

《Linux 内核深度解析》余华兵 著