您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
   
 
 订阅
Linux文件系统与VFS:探索数据管理的奥秘
 
作者:往事敬秋风
  38  次浏览      3 次
 2025-2-24
 
编辑推荐:
本文主要介绍了Linux文件系统与VFS相关内容。 希望对您的学习有所帮助。
本文来自于微信公众号深度Linux,由火龙果软件Linda编辑、推荐。

Linux 文件系统以其独特的设计理念和丰富的功能,在操作系统领域占据着重要的地位。它采用树状目录结构,将文件和目录有序地组织起来,每个目录都有其特定的用途,使得系统的管理和使用更加高效。inode 与数据块的协同工作,实现了文件的存储和访问,inode 记录文件的元数据,数据块存储文件的实际内容,两者通过块指针紧密相连。超级块则作为文件系统的 “掌控者”,存储着文件系统的全局信息,对文件系统的正常运行起着关键作用。

虚拟文件系统 VFS 是 Linux 文件系统的核心组件,它为 Linux 系统带来了强大的灵活性和兼容性。VFS 诞生于多种文件系统并存的背景下,旨在解决不同文件系统操作不统一的问题。它向上提供统一的文件访问接口,使得应用程序可以使用相同的系统调用(如 open、read、write 等)来操作不同类型的文件系统,无需关心底层文件系统的具体实现细节。向下,VFS 通过与各种实际文件系统的交互,实现了跨文件系统的透明访问。VFS 的四大核心对象 —— 超级块、inode、dentry 和 file,相互协作,共同完成了文件系统的各种操作,从文件系统的注册与挂载,到文件的打开、读取、写入和关闭等操作,都离不开这四个核心对象的协同工作。

一、Linux文件系统基础

1.1 什么是文件系统

在计算机的世界里,文件系统就像是一个超级智能的图书管理员,而存储设备(比如硬盘、固态硬盘)则是一座巨大的图书馆。文件系统负责管理存储设备上的文件,为用户和应用程序提供数据存储和访问的机制。它定义了文件的组织结构、命名规则、权限管理以及数据的存储方式等。

当我们在计算机上创建一个文件时,文件系统会为这个文件分配存储空间,并记录文件的相关信息,如文件名、文件大小、创建时间、修改时间等。就好比图书馆管理员为每一本新书分配书架位置,并记录书名、作者、出版时间等信息。当我们需要访问一个文件时,文件系统会根据文件名找到对应的文件,并将文件中的数据读取出来,就像我们告诉管理员书名,管理员就能找到对应的书籍并递给我们。

从操作系统的角度来看,文件系统是操作系统与存储设备之间的桥梁。操作系统通过文件系统来管理存储设备上的空间,对文件进行创建、读取、写入、删除等操作。同时,文件系统也为操作系统提供了一种抽象,使得操作系统无需关心存储设备的具体物理细节,只需要与文件系统进行交互即可。

1.2 Linux 文件系统的独特之处

与其他操作系统的文件系统相比,Linux 文件系统有着独特的魅力。其中最著名的理念就是 “一切皆文件”。在 Linux 的世界里,不仅仅是普通的文本文件、二进制文件被视为文件,就连硬件设备(如硬盘、串口、USB 设备)、进程信息、网络套接字等也都被抽象成文件来处理。这意味着我们可以使用相同的文件操作命令和系统调用来访问和管理这些不同类型的资源。

例如,在 Linux 中,硬盘设备通常被表示为/dev/sda、/dev/sdb等文件,我们可以像操作普通文件一样对这些设备文件进行读写操作(当然,需要有足够的权限)。进程信息则可以通过/proc目录下的文件来获取,每个进程在/proc目录下都有一个对应的目录,目录名就是进程的 PID(进程标识符),通过读取这些目录下的文件,我们可以获取进程的状态、内存使用情况、打开的文件描述符等信息。

Linux 还支持多种不同类型的文件系统,常见的有 EXT 系列(包括 ext2、ext3、ext4)、XFS、Btrfs 等。每种文件系统都有其特点和适用场景:

EXT 系列文件系统:这是 Linux 中非常经典的文件系统家族。ext2 是第二代扩展文件系统,它具有简单、高效的特点,但是没有日志功能,在系统突然断电或崩溃时,可能会导致文件系统不一致。ext3 在 ext2 的基础上加入了日志功能,大大提高了文件系统的可靠性和稳定性,能够在系统故障时快速恢复文件系统。ext4 则是 ext3 的进一步升级,它支持更大的文件和分区大小,具有更高的性能和更好的扩展性,是目前很多 Linux 发行版的默认文件系统。

XFS 文件系统:这是一种高性能的日志文件系统,特别适合大型存储设备和高负载服务器环境。XFS 具有出色的 I/O 性能,能够快速处理大量的文件和大文件,而且它的文件系统检查和修复速度非常快,支持在线文件系统扩展功能,即在不影响系统运行的情况下可以扩展文件系统的大小。因此,在需要处理大规模数据和高并发读写的场景中,XFS 表现得非常出色。

Btrfs 文件系统:这是一个相对较新的文件系统,它具有许多先进的特性,如支持数据校验和、快照功能、数据压缩和数据去重等。数据校验和可以保护数据不被损坏,快照功能允许我们在不占用大量额外空间的情况下创建文件系统的备份,以便在需要时还原到之前的状态。数据压缩和数据去重功能则可以有效地节省存储空间。Btrfs 还支持在线扩展和收缩文件系统,以及动态添加或删除磁盘的功能,使得它在存储管理方面非常灵活。

二、Linux文件系统核心原理

2.1 文件与目录结构剖析

Linux 文件系统采用了独特的树状目录结构,就像一棵枝繁叶茂的大树,而根目录 “/” 就是这棵大树的根。从根目录出发,衍生出众多的子目录,每个子目录都有着特定的功能和用途,它们相互协作,共同构建起了 Linux 文件系统的架构。

/bin 目录是 “Binary” 的缩写,这里存放着系统中最常用的二进制可执行文件,这些文件包含了各种基本命令,如 ls(用于列出目录内容)、cd(用于切换目录)、rm(用于删除文件或目录)等。这些命令就像是我们在 Linux 系统中探索和操作的工具,无论是普通用户还是超级用户,在日常使用系统时都会频繁地调用这些命令。而且,这些命令在系统启动的单用户模式下也可以正常执行,这保证了在系统出现故障需要进行紧急修复时,我们依然能够使用这些基本工具来进行操作。

/sbin 目录同样存放着二进制可执行文件,不过这里的文件主要是系统管理命令,例如 ifconfig(用于配置网络接口)、reboot(用于重启系统)、shutdown(用于关闭系统)等。这些命令对于系统的正常运行和管理至关重要,通常只有超级用户(root)才能执行。因为这些命令涉及到系统的核心配置和关键操作,赋予普通用户执行权限可能会带来安全风险,所以只有拥有最高权限的超级用户才能使用这些工具来对系统进行全面的管理和维护。

/etc 目录是系统配置文件的存放地,这里几乎包含了所有系统级别的配置文件,是系统运行的重要 “指挥中心”。比如,/etc/passwd 文件存储着用户账号信息,包括用户名、用户 ID、用户所属组等;/etc/fstab 文件定义了文件系统的挂载信息,系统在启动时会根据这个文件来自动挂载各个文件系统;/etc/hosts 文件则用于本地的 IP 地址与主机名映射,当我们在系统中访问某个主机名时,系统会首先查看这个文件来确定对应的 IP 地址。此外,各种服务和应用程序的配置文件也大多存放在 /etc 目录下的子目录中,例如 Apache 服务器的配置文件通常在 /etc/apache2 / 目录下,MySQL 数据库的配置文件在 /etc/mysql/ 目录下。通过修改这些配置文件,我们可以对系统和各种服务进行个性化的设置,以满足不同的需求。

/home 目录是普通用户的主目录,每个用户在 Linux 系统中都有一个属于自己的独立目录,这个目录以用户的账号命名。例如,用户 “user1” 的主目录就是 /home/user1。在用户的主目录下,通常会包含一些常见的子目录,如 Documents(用于存放用户的文档)、Downloads(用于存放用户下载的文件)、Pictures(用于存放用户的图片)等。此外,用户的个人配置文件也会存放在主目录下,比如.bashrc 文件,它是 Bash Shell 的配置文件,用户可以在这个文件中设置自己的环境变量、别名等。用户主目录的权限设置非常严格,只有用户本人和超级用户(root)有权限访问和修改其中的内容,这有效地保护了用户的隐私和数据安全。

/root 目录是系统管理员,也就是超级权限者(root 用户)的主目录。与普通用户的主目录不同,/root 目录直接位于根目录下,它是 root 用户的专属工作区域。root 用户在这个目录下拥有完全的权限,可以进行任何系统操作,包括修改系统关键文件、安装和卸载系统软件等。由于 root 用户的权限极高,所以在使用 /root 目录时需要格外小心,避免误操作导致系统出现严重问题。

/boot 目录存放着启动 Linux 系统时所使用的一些核心文件,这些文件对于系统的启动至关重要。其中,最重要的文件包括 Linux 内核文件(通常命名为 vmlinuz),它是 Linux 操作系统的核心,负责管理系统的硬件资源、进程调度、内存管理等重要功能;还有引导加载程序(如 GRUB 或 LILO)的配置文件,引导加载程序的作用是在系统启动时,负责加载内核文件并将系统控制权交给内核。如果 /boot 目录中的文件损坏或丢失,系统可能无法正常启动,因此需要特别注意对这个目录的保护和维护。

/dev 目录是设备文件的保存位置,在 Linux 系统中,一切皆文件,包括硬件设备。这个目录中存放着表示各种硬件设备的文件,通过这些设备文件,用户和应用程序可以像操作普通文件一样对硬件设备进行访问和控制。例如,/dev/sda 通常表示第一个 SCSI 硬盘,我们可以通过对这个文件进行读写操作来访问硬盘上的数据;/dev/tty 表示终端设备,我们在终端上输入和输出的内容实际上就是通过这个设备文件进行传输的;/dev/null 是一个特殊的设备文件,也被称为 “空设备”,它会丢弃所有写入其中的数据,通常用于丢弃不需要的输出信息或者将命令的输出重定向到这个设备文件,以避免输出信息对屏幕造成干扰。

/tmp 目录用于存放临时文件,系统和应用程序在运行过程中会产生一些临时数据,这些数据通常只在程序运行期间有用,程序结束后就可以删除。/tmp 目录为这些临时数据提供了一个存放的空间,它的特点是在系统每次启动时,其中的内容都会被自动清理,以防止临时文件占用过多的磁盘空间。例如,当我们使用一些文本编辑器时,编辑器可能会在 /tmp 目录下创建临时文件来保存我们正在编辑的内容,当我们关闭编辑器时,这些临时文件就会被自动删除。不过,需要注意的是,虽然 /tmp 目录中的文件会在系统重启时被清理,但在系统运行期间,用户仍然可以手动删除这些临时文件,以释放磁盘空间。

/media 目录是系统用于自动挂载可移动设备的目录,当我们将 U 盘、光驱等可移动设备插入到 Linux 系统中时,系统会自动识别这些设备,并将它们挂载到 /media 目录下的相应子目录中。例如,当我们插入一个 U 盘时,系统可能会将其挂载到 /media/usb0 目录下(具体的挂载目录名称可能会因系统设置和设备不同而有所差异),这样我们就可以通过访问 /media/usb0 目录来查看和操作 U 盘中的文件。这种自动挂载机制使得用户在使用可移动设备时非常方便,无需手动进行复杂的挂载操作。

/mnt 目录是系统提供的一个临时挂载点,它主要用于让用户手动挂载其他文件系统。与 /media 目录不同,/mnt 目录下通常不会自动挂载设备,而是需要用户根据自己的需求,使用 mount 命令将外部的存储设备(如移动硬盘、网络共享文件夹等)挂载到这个目录下。例如,我们可以使用以下命令将一个移动硬盘挂载到 /mnt 目录下:mount /dev/sdb1 /mnt,其中 /dev/sdb1 是移动硬盘的设备文件,/mnt 是挂载点。挂载完成后,我们就可以通过访问 /mnt 目录来访问移动硬盘中的内容。当我们不再需要使用这个设备时,可以使用 umount 命令将其从 /mnt 目录下卸载。

/opt 目录是给主机额外安装软件所摆放的目录,一些第三方软件或自定义应用程序通常会安装在这个目录下。例如,我们在安装 Oracle 数据库时,就可以将其安装到 /opt 目录下。这个目录的存在使得系统的软件安装更加灵活,我们可以将一些不常用或者非系统核心的软件安装在 /opt 目录下,而不会影响到系统其他部分的正常运行。同时,/opt 目录下的软件通常具有相对独立的运行环境,便于管理和维护。

/usr 目录是用户级应用程序和文件的存放目录,它包含了大量的二进制文件、库文件、文档和其他资源,是 Linux 系统中非常重要的一个目录。/usr/bin 目录存放着用户级别的命令二进制文件,这些命令通常是一些常用的应用程序命令,如 gcc(GNU 编译器)、perl(Perl 解释器)等,它们是用户在进行软件开发、文本处理等工作时经常会用到的工具;/usr/sbin 目录存放着系统管理命令的二进制文件,这些命令与 /sbin 目录中的命令类似,但它们不是系统启动时必须的,通常用于一些高级的系统管理任务;/usr/lib 目录是库文件目录,存放着应用程序和系统所需的共享库,这些库文件包含了一些预先编写好的函数和代码,应用程序在运行时可以调用这些库文件来实现各种功能;/usr/share 目录是共享数据目录,存放着不特定于某个用户或系统的共享数据,如文档、图标、声音等,这些数据可以被多个用户或应用程序共享使用;/usr/local 目录则用于存放本地安装的软件和文件,用户可以在这个目录下自行安装和管理一些软件,而不会影响到系统其他部分的正常运行。

/var 目录用于存放系统运行时产生的可变数据,这些数据会随着系统的运行而不断变化。/var/log 目录是系统日志文件的存放目录,其中包含了各种系统日志,如 syslog(记录系统的一般信息和事件)、auth.log(记录用户认证相关的信息)、kern.log(记录内核相关的信息)等,通过查看这些日志文件,我们可以了解系统的运行状态、排查系统故障;/var/mail 目录用于存放用户的邮件,当我们使用邮件客户端接收邮件时,邮件通常会被存储在这个目录下;/var/spool 目录是队列目录,用于存放打印任务、邮件队列等,当我们提交一个打印任务时,打印任务会首先被放入 /var/spool/lpd 目录下的队列中,等待打印机进行处理;/var/cache 目录用于存放应用程序的缓存文件,这些缓存文件可以加快应用程序的运行速度,减少对磁盘的读写次数。由于 /var 目录中的数据会不断增长,所以需要定期对其进行清理和维护,以防止磁盘空间不足。

2.2 文件存储的奥秘:inode 与数据块

在 Linux 文件系统中,inode(索引节点)和数据块是文件存储的关键组成部分,它们就像是文件存储的 “幕后英雄”,默默地为文件的存储和访问提供支持。

inode 是一个非常重要的数据结构,它用于存储文件的元数据信息。每个文件和目录在文件系统中都有一个唯一的 inode 编号,就像每个人都有一个唯一的身份证号码一样。inode 中不包含文件名,而是包含了文件的所有其他重要信息,这些信息对于文件的管理和访问至关重要:

文件类型:inode 记录了文件的类型,比如是普通文件、目录、符号链接等。不同类型的文件在系统中的处理方式和权限设置都有所不同,通过 inode 中的文件类型信息,系统可以正确地识别和处理文件。

文件权限:inode 中存储了文件的读、写、执行权限,这些权限决定了哪些用户可以对文件进行何种操作。例如,文件所有者可以设置文件的权限为其他人只读,这样其他人就只能读取文件的内容,而不能对其进行修改或执行。

文件所有者和组:inode 记录了文件的属主和属组,通过这些信息,系统可以确定文件的访问权限和所有者的身份。文件所有者可以对文件进行更多的操作,而其他用户则需要根据文件的权限来进行相应的操作。

文件大小:inode 中保存了文件的实际大小(以字节为单位),这使得系统在读取文件时能够准确地知道需要读取多少数据。

时间戳:inode 包含了文件的三个重要时间戳,分别是创建时间(ctime)、修改时间(mtime)和访问时间(atime)。创建时间记录了文件被创建的时间,修改时间记录了文件内容最后一次被修改的时间,访问时间记录了文件最后一次被访问的时间。这些时间戳对于文件的管理和追踪非常有用,例如,我们可以通过查看文件的修改时间来判断文件是否被更新过。

链接数:inode 记录了指向该 inode 的硬链接数量。硬链接是指多个文件名可以指向同一个 inode,这意味着它们共享相同的文件数据。当我们创建一个硬链接时,实际上是创建了一个新的文件名,这个文件名指向同一个 inode,因此它们的文件内容是完全相同的。删除其中一个文件名不会影响其他文件名,只有当所有硬链接都被删除时,文件数据才会被删除。

块指针:inode 中包含了指向存储文件数据的实际磁盘块的指针,这些指针就像是地图上的标记,告诉系统文件的数据存储在哪些磁盘块中。通过这些指针,系统可以快速地定位和读取文件的数据。

数据块则是存储文件实际数据的地方。当我们创建一个文件时,文件系统会为文件分配若干个数据块,这些数据块会根据文件的大小和磁盘的使用情况进行分配。数据块的大小通常是固定的,常见的大小有 4KB、8KB 等。文件的数据会被分割成若干个小块,分别存储在这些数据块中。例如,一个大小为 10KB 的文件,可能会被存储在 3 个 4KB 的数据块中,其中前两个数据块会被填满,第三个数据块会存储剩余的 2KB 数据。

inode 与数据块之间存在着紧密的映射关系。每个文件都有一个对应的 inode,inode 中包含了指向文件数据块的指针。当我们访问一个文件时,系统首先会根据文件名找到对应的 inode 号码,然后通过 inode 号码获取 inode 信息,其中就包括了指向文件数据块的指针。最后,系统根据这些指针找到文件数据所在的数据块,将数据读取出来。这种映射关系使得文件系统能够高效地管理和访问文件,提高了文件存储和读取的效率。

2.3 超级块:文件系统的 “掌控者”

超级块在 Linux 文件系统中扮演着至关重要的角色,它就像是文件系统的 “大脑”,掌控着文件系统的全局信息和运行状态。

超级块是文件系统的关键组成部分,它存储了文件系统的元数据信息,这些信息对于文件系统的正常运行和管理至关重要。超级块通常位于文件系统的开头,作为文件系统的 “开篇之章”,它描述了整个文件系统的结构和状态。

超级块中包含了丰富的文件系统信息,这些信息就像是文件系统的 “身份证” 和 “健康报告”,为操作系统提供了全面了解文件系统的依据:

文件系统大小:超级块记录了整个文件系统的容量大小,这使得操作系统能够清楚地知道文件系统中可用于存储文件的空间有多少。

每个块的大小:超级块中保存了数据块的大小信息,这对于文件系统在分配和管理数据块时非常重要。因为不同的文件系统可能会使用不同大小的数据块,了解数据块的大小可以帮助操作系统正确地进行文件的读写操作。

inode 表的大小:超级块还记录了 inode 表的大小,inode 表是存储 inode 的地方,了解 inode 表的大小可以帮助操作系统合理地管理 inode 资源,避免 inode 耗尽的情况发生。

文件系统布局:超级块中描述了文件系统的布局结构,包括数据块、inode、目录等在文件系统中的分布情况。这使得操作系统能够快速地定位和访问文件系统中的各种资源。

文件系统名称:超级块中保存了文件系统的名称,这有助于用户和操作系统识别文件系统的类型和用途。

创建时间和最近修改时间:超级块记录了文件系统的创建时间和最近一次修改的时间,这些时间信息对于文件系统的管理和维护非常有用,例如,我们可以通过查看创建时间来了解文件系统的历史,通过查看最近修改时间来判断文件系统是否被更新过。

文件系统挂载状态、读写状态和错误状态:超级块中还包含了文件系统的状态信息,这些信息就像是文件系统的 “健康指示灯”,可以帮助操作系统及时发现和处理文件系统的问题。如果文件系统处于挂载状态,操作系统就可以对其进行读写操作;如果文件系统处于只读状态,操作系统就只能读取文件系统中的数据,而不能进行写入操作;如果文件系统出现错误,超级块中的错误状态信息就会提示操作系统进行相应的处理。

在 Linux 系统中,当一个文件系统挂载到某个挂载点时,操作系统会首先读取文件系统的超级块信息,并将其加载到内存中。这就像是将文件系统的 “使用说明书” 和 “健康报告” 先读取到内存中,以便操作系统能够随时查阅和使用。一旦超级块被加载到内存中,操作系统就可以根据超级块中的信息来访问文件系统的数据、索引和目录结构,从而进行文件的读取、写入和管理操作。例如,当我们需要读取一个文件时,操作系统会根据超级块中的文件系统布局信息,找到文件对应的 inode,然后通过 inode 中的块指针找到文件的数据块,将数据读取出来。同样,当我们需要创建一个新文件时,操作系统会根据超级块中的空闲块数量和 inode 数量信息,为新文件分配合适的数据块和 inode,并在超级块中更新相关的信息。

超级块在文件系统的一致性检查和修复过程中也起着关键作用。当文件系统出现故障或错误时,系统管理员可以使用一些工具(如 fsck 命令)来检查和修复文件系统。在这个过程中,fsck 工具会读取超级块中的信息,根据文件系统的结构和状态信息来检查文件系统中的各种数据结构是否正确,如 inode 表、目录结构、数据块等。如果发现问题,fsck 工具会根据超级块中的信息进行修复,确保文件系统的一致性和完整性。例如,如果 fsck 工具发现某个 inode 的块指针指向了错误的数据块,它就可以根据超级块中的文件系统布局信息,找到正确的数据块,并更新 inode 中的块指针。

三、虚拟文件系统VFS:统一的抽象层

VFS是一个抽象层,其向上提供了统一的文件访问接口,而向下则兼容了各种不不同类型的文件系统。不仅仅是诸如Ext2、Ext3、Ext4、XFS、windows家族的NTFS和Btrfs等常规意义上的文件系统,还可以是比如上图的proc等伪文件系统和设备,也可以是诸如NFS、CIFS等网络文件系统。

3.1 VFS 的诞生背景

Linux 以其强大的兼容性和扩展性而闻名,其中一个重要的体现就是它能够支持多种不同类型的文件系统。从 Linux 早期的 ext 文件系统,到后来的 ext2、ext3、ext4,以及 XFS、Btrfs 等,还有与其他操作系统兼容的 FAT32、NTFS 等文件系统,Linux 如同一个庞大的文件系统博物馆,容纳了各种各样的文件系统。

然而,多种文件系统的存在也带来了一个问题,那就是如何统一对这些文件系统的操作和管理。不同的文件系统有着不同的设计理念、数据结构和操作方式,如果应用程序需要针对每一种文件系统编写不同的代码来进行文件操作,那将是一场噩梦。例如,读取 ext4 文件系统中的文件和读取 NTFS 文件系统中的文件,可能需要使用完全不同的函数和方法,这无疑增加了应用程序开发的难度和复杂性。

为了解决这个问题,Linux 引入了虚拟文件系统(VFS,Virtual File System)。VFS 就像是一个智能的翻译官,它在应用程序和各种实际文件系统之间搭建了一座桥梁,提供了一个统一的接口。通过这个接口,应用程序可以使用相同的方式来操作不同类型的文件系统,而无需关心底层文件系统的具体实现细节。无论是 ext4、XFS 还是 NTFS,在 VFS 的抽象下,都呈现出一致的文件操作界面,使得用户和应用程序能够以一种通用的方式来访问和管理文件,大大提高了系统的灵活性和可扩展性。VFS 采用标准的Linux系统调用读写位于不同物理介质上的不同文件系统,即为各类文件系统提供了一个统一的操作界面和应用编程接口, VFS是一个内核软件层 。VFS是一个可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作的 抽象层 ,如下图所示:

3.2 VFS 的关键作用

VFS 在 Linux 文件系统中扮演着至关重要的角色,它的主要作用可以概括为两个方面:向上提供统一的文件访问接口,向下兼容各种不同类型的文件系统。

向上,VFS 为用户空间的应用程序提供了一套标准的文件操作接口,这些接口以系统调用的形式呈现,如 open(打开文件)、read(读取文件内容)、write(写入文件内容)、close(关闭文件)、lseek(移动文件指针)等。这些系统调用就像是一套通用的工具,无论底层是何种文件系统,应用程序都可以使用这些工具来进行文件操作。

例如,当我们使用 Python 的内置函数open()来打开一个文件时,实际上就是通过 VFS 提供的接口来实现的。无论这个文件是存储在 ext4 文件系统的硬盘上,还是存储在 XFS 文件系统的固态硬盘上,open()函数的使用方式都是一样的,应用程序无需关心文件系统的具体类型和实现细节。

向下,VFS 负责与各种实际的文件系统进行交互,它将应用程序的文件操作请求转换为对具体文件系统的操作。不同的文件系统在实现上可能有很大的差异,比如文件的存储方式、元数据的管理、目录结构的组织等。但是,VFS 通过抽象和封装,屏蔽了这些差异,使得各种文件系统都能够以统一的方式被访问和管理。当应用程序调用read()系统调用来读取文件内容时,VFS 会根据文件所在的文件系统类型,调用相应文件系统的读取函数来完成实际的读取操作。

对于 ext4 文件系统,VFS 会调用 ext4 文件系统的读取函数,从 ext4 文件系统的存储介质中读取数据;对于 XFS 文件系统,VFS 则会调用 XFS 文件系统的读取函数,从 XFS 文件系统的存储介质中读取数据。而这些具体的文件系统操作对于应用程序来说是透明的,应用程序只需要关心文件的逻辑操作,而不需要了解底层文件系统的物理实现。

通过这种方式,VFS 实现了跨文件系统的透明访问,使得应用程序可以在不同的文件系统之间自由切换,而无需进行任何修改。例如,我们可以将一个文件从 ext4 文件系统的分区移动到 XFS 文件系统的分区,而应用程序在读取和写入这个文件时,不需要做任何额外的处理,因为 VFS 已经为我们处理了文件系统之间的差异。这种设计大大提高了 Linux 系统的灵活性和可扩展性,使得 Linux 能够轻松地支持新的文件系统,并且能够在不同的存储设备和文件系统之间实现无缝的集成。

3.3 VFS 的四大核心对象

虚拟文件系统在磁盘中并没有对应的存储的信息。尽管 Linux 支持多达几十种文件系统,但这些真实的文件系统并不是一下子都挂在系统中的,它们实际上是按需挂载的。另外,这些实的文件系统只有安装到系统中,VFS 才予以认可,也就是说,VFS 只管理挂载到系统中的实际文件系统。

VFS 通过四个核心对象来实现对文件系统的抽象和管理,它们分别是超级块(Superblock)、索引节点(inode)、目录项对象(dentry)和文件对象(file)。这四个对象相互协作,共同构成了 VFS 的核心架构,就像一个精密的机器,每个部件都有着不可或缺的作用。

VFS 有 4 个主要对象:

超级块(Superblock) :存放系统中已安装文件系统的有关信息。

文件索引节点(inode) :存放关于具体文件的一般信息。

目录项对象(dentry) :存放目录项与对应文件进行链接的信息。

文件对象(file):存放打开文件与进程之间进行交互的有关信息。

路径中的每一个部分被称作目录项,例如 /home/clj/myfile 中,根目录是 / ,而 home,clj 和文件 myfile 都是目录项;超级块是对一个文件系统的描述;索引节点是对一个文件物理属性的描述;而目录项是对一个文件逻辑属性的描述。

(1)超级块 super_block

超级块用来描述整个文件系统的信息,包括文件系统的大小、有多少是空的和已经填满的占多少,以及他们各自的总数和其他诸如此类的信息。超级块占用1号物理块,就是文件系统的控制块 ,要 使用一个分区来进行数据访问,那么第一个要访问的就是超级块 。所以,超级块坏了,那磁盘也就基本没救了。

当内核在对一个文件系统进行初始化和注册时 在内存为其 分配一个超级块 。此时的超级块为 VFS 超级块 。也就是说,VFS 超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时被自动删除。VFS 超级块只存放在内存中 。

对于每个具体的文件系统来说,都有各自的超级块,如 Ext2 超级块和 Ext3 超级块,它存放在磁盘上,内容包括:文件系统的大小、空闲块数目、空闲块索引表、空闲i节点数目、空闲i节点索引表、封锁标记等。超级块是系统为文件分配存储空间、回收存储空间的依据。这一部分的拓扑结构如下图:

其中 Block Group 存储的各部分含义如下:

indoe bitmap (indoe对照表): 用来记录当前文件系统的indoe哪些是已经使用的,哪些又是未使用的。

block bitmap (块对照表):用来记录当前文件系统哪些block已经使用,哪些又是未使用的。

inode table (inode 表格):inode是用来记录文件的属性以及该文件实际数据所在的block的号码。

GDT(Global Descriptor Table):用来描述每个block group开始和结束的block号码以及每个区段位于哪一个block号码之间。相当于文件系统描述的是每个block group的信息。

data blocks:数据块,用于存放数据

超级块的数据结构定义如下:

struct super_block
{
dev_t s_dev; //
unsigned long s_blocksize; // 以字节为单位数据块的大小
unsigned char s_blocksize_bits; // 块大小的值所占用的位数,

...

struct list_head s_list; // 指向超级块链表的指针

struct file_system_type *s_type; // 指向文件系统的 file_system_type 的指针

struct super_operation *s_op; // 指向具体文件系统的用于超级块操作的函数集合

struct mutex s_lock;

struct list_head s_dirty;

...

void *s_fs_info; // 指向具体文件系统的超级块
};

 

从上面定义的数据结构可知:所有的超级块对象都以双向循环链表的形式链接在一起。链表中第一个元素用 super_blocks 变量来表示,与超级块关联的方法就是所谓的超级块操作表,其数据结构是 super_operations,定义如下:

struct super_operations
{
void (*write_super) (struct super_block *); // 将超级块的信息写回磁盘
void (*put_super) (struct super_block *); // 释放超级块对象
void (*read_inode) (struct inode *); // 读取某个文件系统的索引节点
void (*write_inode) (struct inode *, int); // 把索引节点写回磁盘
void (*put_inode) (struct inode *); // 逻辑上释放索引节点
void (*delete_inode) (struct inode *); // 从磁盘上删除索引节点

};

(2)索引节点 inode

文件系统处理文件所需要的所有信息都存放在索引节点中。在同一个文件系统中,每个索引节点号都是唯一的 。具体文件系统的索引节点是存放在磁盘上,是一种静态结构,要使用它,必须调入内存,填写 VFS 的索引节点,因此,也称 VFS 索引节点是 动态节点 。

我们的磁盘在进行分区、格式化的时候会分为两个区域, 一个是 数据区 ,用于存储文件中的数据 ;另一个是 inode区 ,用于存放 inode table (inode表) , inode table 中存放的是一个一个的 inode (也称为inode节点),不同的 inode 就可以表示不同的文件,每一个文件都必须对应一个 inode , inode 实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如:

文件字节大小

文件所有者

文件对应的读/写/执行权限

文件时间戳(创建时间、更新时间等)

文件类型

文件数据存储的block(块)位置

inode 结构体定义在<linux/fs.h>中,主要包含:存放的内容如下:

struct inode
{
struct list_head i_hash; // 指向哈希表的指针
struct list_head i_list; // 指向索引节点链表的指针
struct list_head i_dentry; // 指向目录项链表的指针
...

unsigned long i_ino; // 索引节点号
umode_t i_mode; // 文件的类型与访问权限
kdev_t i_rdev; // 实际设备标识号
uid_t i_uid; // 文件拥有者标识号
gid_t i_gid; // 文件拥有者所在组的标识号
...
struct inode_operations *i_op; // 指向对该节点进行操作的一组函数

struct super_block *i_sb; // 指向该文件系统超级块的指针

atomic_t i_count; // 当前使用该节点的进程数,计数为0时,表明该节点可丢弃或重新使用

struct file_operations *i_fop; // 指向文件操作的指针
...
struct vm_area_struct *i_op; // 指向对文件进行映射所使用的虚存区指针

unsigned long i_state; // 索引节点的状态标志
unsigned int i_flags; // 文件系统的安装标志

union // 联合结构体,其成员指向具体文件系统的 inode 结构
{
struct minix_inode_info minix_i;
struct Ext2_inode_info Ext2_i;
}
};

 

inode的索引流程如图所示(注意,文件名并不是记录在inode中,而是目录项dentry中):

所以由此可知, inode table 本身也需要占用磁盘的存储空间。在同一个文件系统中,每一个文件都有唯一的一个 inode ,每一个 inode 都有一个与之相对应的数字编号 ,内核可以根据索引节点号的哈希值查找其 inode 结构,前提是内核要知道索引节点号和对应文件所在文件系统的超级块对象的地址。在Linux系统下,我们可以通过"ls -i"命令查看文件的 inode编号 ,如下所示:

上图中ls打印出来的信息中,每一行前面的一个数字就表示了对应文件的inode编号。除此之外,还可以使用stat命令查看,用法如下:

与索引节点关联的方法叫索引节点操作表,由inode_operations结构来描述:

struct inode_operations
{
// 创建一个新的磁盘索引节点
int (*create) (struct inode *, struct dentry *, int);

// 查找一个索引节点所在的目录
struct dentry * (*lookup) (struct inode *, struct dentry *);

// 创建一个新的硬链接
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 *, int);

// 删除一个目录项的索引节点
int (*rmdir) (struct inode *, struct dentry *);
};

(3)目录项 dentry

每个文件除了有一个索引节点inode数据结构外,还有一个目录项dentry数据结构。目录项反应了文件系统的树状结构,目前主流的操作系统基本都是用树状结构来组织文件的。linux也不例外。dentry表示一个目录项,目录项下面又有子目录。

目录在文件系统中的存储方式与常规文件类似,常规文件包括了 inode节点 以及 文件内容数据存储块(block) ;但对于目录来说,其存储形式则是由 inode节点 和 目录块 所构成, 目录块当中记录了有哪些文件组织在这个目录下,记录它们的 文件名 以及对应的 inode编号 。所以对此总结如下:

普通文件由 inode节点 和 数据块 构成

目录由 inode节点 和 目录块 构成

其存储形式如下图所示:

对于 dentry 和 inode 的区别可以如此总结:

dentry 结构代表的是逻辑意义上的文件,描述的是文件逻辑上的属性, 目录项对象在磁盘上并没有对应的映像 。

inode 结构代表的是物理意义上的文件,记录的是物理上的属性,对于一个具体的文件系统, 其inode结构在磁盘上就有对应的映像 。

当打开一个文件时,按照目录树搜索的过程如下:

这一流程大概如下图所示:

个索引节点对象可能对应多个目录项对象(因为路径的每一部分称作目录项,而文件的路径很长)。目录项由dentry结构体标识,定义在<linux/dcache.h>中,主要包含:

struct dentry
{
atomic_t d_count; // 目录项引用器
unsigned int d_flags; // 目录项标志
struct inode *d_inode; // 与文件名关联的索引节点
struct dentry *d_parent; // 父目录的目录项

struct list_head d_hash; // 目录项形成的哈希表
struct list_head d_lru; // 未使用的 LRU 链表
struct list_head d_child; // 父目录的子目录项所形成的链表
struct list_head d_subdirs; // 该目录项的子目录所形成的的链表
struct list_head d_alias; // 索引节点别名的链表

int d_mounted; // 目录项的安装点
struct qstr d_name; // 目录项名(可快速查找)
struct dentry_operations *d_op; // 操作目录项的函数
struct super_block *d_sb; // 目录项树的根
unsigned long d_vfs_flags;
void *d_fsdata; // 具体文件系统的数据
unsigned char d_iname[DNAME_INLINE_LEN]; // 短文件名
...

};

 

目录项有三种状态:

被使用:该目录项指向一个有效的索引节点,并有一个或多个使用者,不能被丢弃。

未被使用:也对应一个有效的索引节点,但VFS还未使用,被保留在缓存中。如果要回收内存的话,可以撤销未使用的目录项。

负状态:没有对应有效的索引节点,因为索引节点被删除了,或者路径不正确,但是目录项仍被保留了。

将整个文件系统的目录结构解析成目录项,是一件费力的工作,为了节省VFS操作目录项的成本,内核会将目录项缓存起来。

(4)文件 file

文件对象是进程打开的文件在内存中的实例。Linux用户程序可以通过open()系统调用来打开一个文件,通过close()系统调用来关闭一个文件。由于多个进程可以同时打开和操作同一个文件, 所以同一个文件,在内存中也存在多个对应的文件对象,但对应的索引节点和目录项是唯一的 。

一个进程所处的位置是由 fs_strcut 来描述的 , 而一个进程(或者用户)打开的文件是由 files_struct / fdtable 来描述的 , 而整个系统所打开的文件是由 file 结构来描述的 。

文件对象由file结构体表示,file 结构形成了一个双链表,称为系统打开文件表。其定义在<linux/fs.h>中,主要包含:

struct file
{
struct list_head f_list; // 所有打开的文件形成一个链表
struct dentry *f_dentry; // 与文件相关的目录项对象
struct vfsmount *f_mount; // 该文件所在的已安装文件系统
struct file_operations *f_op; // 指向文件操作表的指针
mode_t f_mode; // 文件的打开模式
loff_t f_pos; // 文件的当前位置
unsigned short f_flags; // 打开文件时所指定的标志
unsigned short f_count; // 使用该结构的进程数

...
};

对文件进行操作的一组函数叫文件操作表,由file_operations结构描述,如下:

struct file_operations
{
// 修改文件指针
loff_t (*llseek) (struct file *, loff_t, int);

// 从文件中读取若干个字节
ssize_t (*read) (struct file *, char *, size_t, loff_t *);

// 给文件中写若干个字节
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

// 文件到内存的映射
int (*mmap) (struct file *, struct vm_area_struct *);

// 打开文件
int (*open) (struct inode *, struct file *);

// 关闭文件时减少 f_count 计数
int (*flush) (struct file *);

// 释放 file 对象
int (*release) (struct inode *, struct file *);

// 文件在缓冲区的数据写回磁盘
int (*fsync) (struct file *, struct dentry *, int datasync);

...
};

文件描述符是用来描述打开的文件的。每一个进程用一个 files_struct 结构来记录文件描述符的使用情况,即一个进程可以有多个文件描述符,因为一个进程可以打开多个文件。而通过 dup()、dup2() 和 fcntl() 两个文件描述符可以指向同一个打开的文件,数组的两个元素可能指向同一个文件对象。每个进程都有 自己的根目录和当前工作目录 ,内核使用 struct fs_struct 来记录这些信息,其定义为:

struct fs_struct
{
atomic_t count; // 表示共享同一 fs_struct 表进程数目
rwlock_t lock;
int umask; // 为新创建的文件设置初始文件许可权
struct dentry *root, *pwd, *altroot; // 对目录项的描述
struct vfsmount *rootmnt, *pwdmnt, *altrootmnt; // 目录安装点的描述
};

 

除了根目录和当前工作目录,进程还需要记录自己打开的文件。进程已经打开的所有文件使用struct files_struct来记录,进程描述符的files字段便指向该进程的files_struct结构。它是进程的私有数据,其定义如下:

struct files_struct 
{
atomic_t count; // 共享该表的进程数
rwlock_t file_lock; // 保护以下的所有域
int max_fds; // 当前文件对象的最大数
int max_fdset; // 当前文件描述符的最大数
int next_fd; // 已分配的文件描述符加 1

struct file ** fd; // 指向文件对象指针数据的指针
fd_set *close_on_exec; // 指向指向 exec() 时需要关闭的文件描述符
fd_set *open_fds; // 指向打开的文件描述符的指针

fd_set close_on_exec_init; // 执行 exec() 时需要关闭的文件描述符的初值集合
fd_set open_fds_init; // 文件描述符的初值集合
struct file *fd_array[32]; // 文件对象指针的初始化数组
};

旧版本的内核中, struct files_struct 中有一个 fd字段 ,指向文件对象的指针数组。通常fd指向fd_array,如果进程打开的文件数目多于32个,内核就分配一个新的更大的文件对象的指针数组,并将其地址存放在fd字段中,这个数组所包含的元素数目存放在 max_fds字段 。

新版本的内核将 fd , max_fds 以及其他几个相关字段组织在一起,增加一个新的独立数据结构 struct fdtable ,称为 文件描述符表 ,定义于 include/linux/fdtable.h ,其主要数据结构定义如下所示:

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

四、VFS与文件系统的交互

4.1 文件系统的注册与挂载

在 Linux 系统中,文件系统的注册与挂载是文件系统能够正常工作的重要前提,它们就像是文件系统的 “入场券” 和 “入场仪式”,只有完成了这两个步骤,文件系统才能在 Linux 系统中发挥作用。

文件系统的注册是指将文件系统的相关信息和操作函数告知内核,使内核能够识别和管理该文件系统。在 Linux 内核中,每个文件系统都有一个对应的file_system_type结构体,这个结构体就像是文件系统的 “名片”,包含了文件系统的名称、挂载函数指针、卸载函数指针等重要信息。当内核启动时,或者在加载内核模块时,会调用register_filesystem函数来注册文件系统。这个函数会将file_system_type结构体添加到内核的文件系统链表中,这样内核就知道系统中存在哪些文件系统了。

以 ext4 文件系统为例,在 ext4 文件系统的初始化代码中,会定义一个file_system_type结构体,并将其注册到内核中。这个结构体中的挂载函数指针会指向 ext4 文件系统的挂载函数ext4_mount,当需要挂载 ext4 文件系统时,内核就会调用这个函数来完成挂载操作。

挂载是将文件系统关联到目录树的某个挂载点上,使得文件系统的内容可以通过挂载点进行访问。在 Linux 中,挂载操作通常使用mount命令来完成。mount命令的基本语法是mount [-t 文件系统类型] [-o 选项] 设备文件名 挂载点,其中-t选项用于指定文件系统的类型,-o选项用于指定挂载选项,设备文件名是要挂载的文件系统所在的设备,挂载点是文件系统在目录树中挂载的位置。

当执行mount命令时,系统会进行一系列的操作来完成挂载过程。系统会根据mount命令中指定的文件系统类型,在文件系统链表中查找对应的file_system_type结构体。然后,调用该结构体中的挂载函数来创建超级块(superblock)。超级块是文件系统的关键数据结构,它包含了文件系统的全局信息,如文件系统的大小、块大小、inode 表的位置等。挂载函数会从设备中读取超级块的信息,并将其加载到内存中。接着,挂载函数会初始化根目录的 inode,根目录的 inode 是文件系统中所有文件和目录的起点,通过根目录的 inode 可以访问整个文件系统。挂载函数会将挂载点与文件系统关联起来,使得通过挂载点可以访问文件系统的内容。

例如,当我们执行mount /dev/sda1 /mnt命令时,系统会查找/dev/sda1设备上的文件系统类型,假设是 ext4 文件系统。然后,系统会调用 ext4 文件系统的挂载函数ext4_mount,创建 ext4 文件系统的超级块,并初始化根目录的 inode。最后,将/mnt目录作为挂载点,将 ext4 文件系统挂载到/mnt目录下。这样,我们就可以通过访问/mnt目录来访问/dev/sda1设备上的 ext4 文件系统中的文件了。

4.2 文件操作的流程解析

文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U盘等外部存储设备, 文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为 静态文件 。

当我们调用 open 函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存 (也 把内存中的这份文件数据叫做 动态文件 、内核缓冲区 )。打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件 。

当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了, 数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中 。

因为磁盘、硬盘、U盘等存储设备基本都是Flash块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节), 一个字节的改动也需要将该字节所在的block全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活 ;而 内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活 ,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比磁盘读写快得多。

在Linux系统中,内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如记录进程的状态信息、运行特征等,我们把这个称为 进程控制块(Process control block,PCB) 。

PCB数据结构体中有一个指针指向了 文件描述符表(File descriptors) ,文件描述符表中的每一个元素索引到对应的 文件表(File table) ,文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如 文件状态标志、引用计数、当前文件的读写偏移量以及i-node指针(指向该文件对应的inode)等 ,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:

我们打开文件的过程也就是对 文件表file 的初始化的过程。在打开文件的过程中会将 inode 部分关键信息填充到 file 中,特别是文件操作的函数指针 。在 task_struct 中保存着一个 file类型 的数组,而用户态的文件描述符其实就是数组的下标。这样通过文件描述符就可以很容易到找到 file ,然后通过其中的函数指针访问数据。

在 Linux 系统中,文件操作是通过系统调用实现的,而 VFS 在文件操作中起着关键的桥梁作用,它将用户空间的文件操作请求转换为对底层文件系统的具体操作,就像一个高效的交通枢纽,协调着文件操作的各个环节。

以open系统调用为例,当用户在应用程序中调用open函数来打开一个文件时,VFS 会按照以下流程进行处理:

路径解析:open函数会传入文件的路径名,VFS 首先会对路径名进行解析。它会从根目录开始,根据路径名中的各个部分,依次查找对应的目录项(dentry)。每个目录项都包含了文件名和指向对应 inode 的指针。通过不断地查找目录项,VFS 最终找到目标文件的 dentry,进而获取到目标文件的 inode。

inode 查找与获取:找到目标文件的 dentry 后,VFS 会从 dentry 中获取对应的 inode。inode 是文件的重要元数据结构,它包含了文件的权限、所有者、大小、创建时间等信息,以及指向文件数据块的指针。如果 inode 不在内存中,VFS 会从磁盘上读取 inode 的信息,并将其加载到内存中。

文件对象创建与初始化:获取到 inode 后,VFS 会创建一个文件对象(file),用于表示打开的文件。文件对象中包含了文件的读写位置、访问模式、指向文件所属文件系统的超级块指针以及指向文件操作函数集合的指针等信息。VFS 会根据open函数传入的参数,对文件对象进行初始化,设置文件的访问模式(如只读、只写、读写等)和初始读写位置等。

文件操作函数关联:VFS 会根据文件的类型和所属文件系统,将文件对象与相应的文件操作函数集合关联起来。不同类型的文件(如普通文件、目录、设备文件等)和不同的文件系统,都有各自的文件操作函数集合。这些函数集合定义了对文件的具体操作方法,如读取文件内容、写入文件内容、关闭文件等。对于普通文件,文件操作函数集合中可能包含read、write、lseek等函数;对于目录,可能包含readdir、mkdir、rmdir等函数。通过将文件对象与文件操作函数集合关联起来,VFS 可以根据用户的操作请求,调用相应的函数来完成文件操作。

以read系统调用为例,当用户调用read函数来读取文件内容时,VFS 的工作流程如下:

文件描述符验证:read函数会传入一个文件描述符,VFS 首先会验证这个文件描述符的有效性。文件描述符是一个整数,它是在文件打开时由open系统调用返回的,用于标识一个打开的文件。VFS 会根据文件描述符,找到对应的文件对象。

读取位置确定:找到文件对象后,VFS 会根据文件对象中的当前读写位置,确定从文件的哪个位置开始读取数据。如果用户在调用read函数时指定了读取的偏移量,VFS 会将读写位置调整到指定的偏移量处。

数据读取:VFS 会调用文件对象关联的文件操作函数集合中的read函数,来执行实际的读取操作。这个read函数是由底层文件系统提供的,它会根据文件的类型和存储方式,从文件的数据块中读取数据。对于普通文件,read函数会从磁盘上读取数据块,并将数据复制到用户指定的缓冲区中;对于设备文件,read函数可能会从设备中读取数据。

读取结果返回:读取完成后,VFS 会将读取到的数据长度返回给用户。如果读取过程中发生错误,VFS 会返回相应的错误码,告知用户读取失败的原因。

同样,write系统调用的流程与read类似,只是操作的方向相反。当用户调用write函数来写入文件内容时,VFS 会验证文件描述符的有效性,确定写入的位置,然后调用文件操作函数集合中的write函数,将用户提供的数据写入到文件的数据块中。写入完成后,VFS 会返回实际写入的数据长度或错误码。

五、VFS的优势与应用场景

5.1 无缝支持多种文件系统

VFS 的一大显著优势就是能够无缝支持多种不同类型的文件系统,这使得 Linux 系统在存储管理方面具有极高的灵活性和兼容性。在实际应用中,我们常常会遇到需要同时使用多种文件系统的情况,而 VFS 的存在让这一切变得轻松简单。

假设我们有一台 Linux 服务器,它的系统盘使用的是 EXT4 文件系统,这是一种非常稳定且广泛应用的文件系统,适合存储系统文件和常规数据。而在服务器上,我们还挂载了一块大容量的存储硬盘,为了充分发挥其性能,我们选择使用 XFS 文件系统,因为 XFS 在处理大文件和高并发读写时表现出色。

在 VFS 的统一管理下,用户和应用程序可以在这两个不同的文件系统之间自由切换和操作,无需关心底层文件系统的具体实现细节。例如,用户可以使用cp命令将一个文件从 EXT4 文件系统的分区复制到 XFS 文件系统的分区,就像在同一个文件系统中操作一样简单:

cp /ext4分区路径/文件.txt /xfs分区路径/

 

在这个过程中,cp命令通过 VFS 提供的统一接口来执行文件复制操作。VFS 会根据文件的源路径和目标路径,判断出源文件位于 EXT4 文件系统,目标路径位于 XFS 文件系统。然后,VFS 会分别调用 EXT4 文件系统的读取函数从源文件所在的数据块中读取数据,再调用 XFS 文件系统的写入函数将数据写入到目标文件的数据块中。对于用户来说,他只需要执行一条简单的cp命令,而不需要了解 EXT4 和 XFS 文件系统的内部结构、数据存储方式以及文件操作的具体实现方法。

同样,应用程序在访问不同文件系统中的文件时,也不需要针对不同的文件系统编写不同的代码。以一个用 Python 编写的文件处理程序为例,它可以使用内置的文件操作函数来处理不同文件系统中的文件:

# 读取EXT4文件系统中的文件
with open('/ext4分区路径/文件.txt', 'r') as f:
content = f.read()
print(content)

# 写入XFS文件系统中的文件
with open('/xfs分区路径/新文件.txt', 'w') as f:
f.write('这是写入XFS文件系统的内容')

 

在这个 Python 代码中,无论是读取 EXT4 文件系统中的文件,还是写入 XFS 文件系统中的文件,使用的都是相同的open函数和文件操作方法。这是因为 VFS 为应用程序提供了统一的文件访问接口,应用程序只需要调用这些接口,VFS 就会负责将操作请求转发到相应的文件系统,实现对不同文件系统的透明访问。

5.2 提升系统的灵活性与可扩展性

VFS 的设计理念为 Linux 系统带来了出色的灵活性和可扩展性,使其能够适应不断发展的存储技术和多样化的应用需求。

从新文件系统的加入角度来看,VFS 提供了一套通用的接口和抽象机制,这使得开发新的文件系统变得相对容易。开发者只需要按照 VFS 定义的标准接口来实现文件系统的各种操作函数,就可以将新的文件系统集成到 Linux 内核中。当一个新的文件系统被开发出来后,它可以通过注册机制向 VFS “自我介绍”,并将自己的操作函数指针传递给 VFS。这样,VFS 就能够识别和管理这个新的文件系统,将其纳入到整个文件系统体系中。

例如,随着存储技术的不断发展,出现了一些针对特定应用场景的新型文件系统,如用于高速固态硬盘的 F2FS 文件系统。F2FS 针对闪存设备的特性进行了优化,能够提高固态硬盘的读写性能和使用寿命。开发者在实现 F2FS 文件系统时,按照 VFS 的标准接口,实现了诸如文件的创建、读取、写入、删除,目录的创建、遍历等操作函数。然后,通过 VFS 的注册机制,将 F2FS 文件系统注册到内核中。一旦注册成功,用户就可以像使用其他文件系统一样,将 F2FS 文件系统挂载到系统的某个目录下,并使用标准的文件操作命令和接口来访问其中的文件。

在支持不同存储介质方面,VFS 同样发挥着重要作用。无论是传统的机械硬盘、固态硬盘,还是移动存储设备(如 U 盘、移动硬盘),甚至是网络存储设备(如 NFS、CIFS 共享),VFS 都能够将它们统一管理起来。对于不同的存储介质,VFS 通过与相应的设备驱动程序协作,实现对存储介质的访问和控制。

以网络存储设备为例,当我们将一个 NFS(网络文件系统)共享挂载到 Linux 系统中时,VFS 会与 NFS 客户端驱动程序配合,将网络共享视为本地文件系统的一部分。用户可以像访问本地文件一样访问 NFS 共享中的文件,而无需关心数据是通过网络传输的。VFS 会负责处理网络通信、数据传输以及文件系统操作的转换,使得用户能够获得一致的文件访问体验。同样,对于移动存储设备,当我们插入一个 U 盘时,系统会自动识别并挂载 U 盘上的文件系统(可能是 FAT32、NTFS 等),VFS 会协调 USB 设备驱动程序和相应的文件系统驱动程序,实现对 U 盘文件的读写操作。

这种对不同存储介质的统一支持,使得 Linux 系统能够适应各种复杂的存储环境,无论是在个人电脑、服务器还是嵌入式设备中,都能够灵活地管理和使用不同类型的存储设备。同时,也为用户提供了极大的便利,用户可以在不同的存储介质之间自由地移动和共享文件,而无需担心文件系统和存储设备的差异。

 

   
38 次浏览       3
 
相关文章

CMM之后对CMMI的思考
对软件研发项目管理的深入探讨
软件过程改进
软件过程改进的实现
 
相关文档

软件过程改进框架
软件过程改进的CMM-TSP-PSP模型
过程塑造(小型软件团队过程改进)
软件过程改进:经验和教训
 
相关课程

以"我"为中心的过程改进(iProcess )
iProcess过程改进实践
CMMI体系与实践
基于CMMI标准的软件质量保证

最新活动计划
DeepSeek大模型应用开发实践 3-15[在线]
基于 UML 和EA进行分析设计 2-24[上海]
SysML和EA系统设计与建模 3-27[北京]
MBSE(基于模型的系统工程)2-27[北京]
OpenGauss数据库调优实践 3-11[北京]
UAF架构体系与实践 3-25[北京]
 
 
最新文章
iPerson的过程观:要 过程 or 结果
基于模型的需求管理方法与工具
敏捷产品管理之 Story
敏捷开发需求管理(产品backlog)
Kanban看板管理实践精要
最新课程
基于iProcess的敏捷过程
软件开发过程中的项目管理
持续集成与敏捷开发
敏捷过程实践
敏捷测试-简单而可行
更多...   
成功案例
英特尔 SCRUM-敏捷开发实战
某著名汽车 敏捷开发过程与管理实践
北京 敏捷开发过程与项目管理
东方证券 基于看板的敏捷方法实践
亚信 工作量估算
更多...