当前位置: 首页 > 科技观察

Linux虚拟文件系统详解

时间:2023-03-18 22:05:21 科技观察

虚拟文件系统是一种神奇的抽象,使得“一切皆文件”的理念在Linux中成为可能。什么是文件系统?根据早期Linux贡献者和作者RobertLove的说法,“文件系统是按照特定结构对数据进行分层存储。”然而,这种描述同样适用于VFAT(虚拟文件分配表)、Git和Cassandra(一种NoSQL数据库)。那么如何区分文件系统呢?文件系统基本概念Linux内核要求文件系统必须是一个实体,它还必须在持久化对象上实现open()、read()和write()方法,并且这些实体需要有与之关联的名称。从面向对象编程的角度看,内核把通用的文件系统看作是一个抽象接口,这三个函数是“虚”的,没有默认定义。因此,内核的默认文件系统实现称为虚拟文件系统(VFS)。如果我们可以open()、read()和write()它就是一个文件,如这个控制台会话所示。VFS是类Unix系统中著名的“一切皆文件”概念的基础。让我们看看它有多奇怪,上面的小演示显示了字符设备/dev/console实际工作。该图显示了虚拟电传打字机控制台(tty)上的交互式Bash会话。将字符串发送到虚拟控制台设备会使其显示在虚拟屏幕上。VFS具有更奇怪的特性。例如,它可以在其中寻址。熟悉的ext4、NFS、/proc等文件系统都在名为file_operations的C语言数据结构中提供了三大函数的定义。此外,各个文件系统以熟悉的面向对象的方式扩展和覆盖VFS功能。正如RobertLove指出的那样,VFS的抽象允许Linux用户轻松地将文件复制到(或从)外部操作系统或抽象实体(如管道),而无需担心其内部数据格式。在用户空间端,通过系统调用,进程可以使用其中一个文件系统方法read()从文件复制到内核的数据结构中,然后使用另一个文件系统方法write()输出数据。属于VFS原语的函数定义本身可以在内核源代码的fs/*.c文件中找到,fs/的子??目录包含特定的文件系统。内核还包含类似文件系统的实体,例如cgroups、/dev和tmpfs,它们在引导过程的早期需要,因此定义在内核的init/子目录中。注意cgroup、/dev、tmpfs并没有调用file_operations三大函数,而是直接读写内存。下图粗略地说明了用户空间如何访问通常安装在Linux系统上的各种类型的文件系统。图中未显示的管道、dmesg和POSIX时钟等结构也实现了structfile_operations,它们的访问也通过VFS层进行。用户空间如何访问各种类型的文件系统VFS是位于系统调用和特定文件操作(例如ext4和procfs)的实现之间的“填充层”。然后file_operations函数可以与特定于设备的驱动程序或内存访问器进行通信。tmpfs、devtmpfs和cgroup不使用file_operations而是直接访问内存。VFS的存在促进了代码重用,因为与文件系统相关的基本方法不需要由每种文件系统类型重新实现。代码重用是被广泛接受的软件工程最佳实践!唉,但是如果重用的代码引入了严重的错误,那么所有继承公共方法的实现都会受到影响。/tmp:一个小提示找出系统上存在哪个VFS的简单方法是键入mount|grep-v标准|grep-v:/,在大多数计算机上,这将列出所有不驻留的磁盘,而它也不是NFS的挂载文件系统。列出的VFS挂载之一必须是/tmp,对吧?每个人都知道将/tmp放在物理存储设备上是疯狂的!图片:https://tinyurl.com/ybomxyfo为什么不建议将/tmp留在存储设备上?因为/tmp中的文件是临时的(!),并且存储设备比内存慢,所以创建了一个像tmpfs这样的文件系统。此外,与内存相比,物理设备由于频繁写入而更容易磨损。***,/tmp中的文件可能包含敏感信息,因此让它们在每次重新启动时消失是一项功能。不幸的是,某些Linux发行版的安装脚本仍然默认在存储设备上创建/tmp。如果您的系统发生这种情况,请不要绝望。只需按照一直优秀的ArchWiki上的简单说明来解决问题,记住分配给tmpfs的内存不能用于其他目的。换句话说,包含大文件的巨大tmpfs可能会导致系统内存不足并崩溃。另一个提示:编辑/etc/fstab文件时,一定要以换行符结尾,否则系统将无法启动。(猜猜我是怎么知道的。)/proc和/sys除了/tmp,大多数Linux用户最熟悉的VFS是/proc和/sys。(/dev依赖于共享内存并且没有file_operations结构)。为什么有两个?让我们看一些更多的细节。procfs为用户空间提供了内核及其控制的进程的瞬时状态的快照。在/proc中,内核发布有关它提供的设施的信息,例如中断、虚拟内存和调度程序。此外,/proc/sys是存储可以通过sysctl命令配置的设置的地方,可以从用户空间访问。/proc/目录中报告了各个进程的状态和统计信息。/proc/meminfo是一个空文件,但仍然包含有价值的信息。/proc文件的行为说明VFS可能不同于磁盘上的文件系统。一方面,/proc/meminfo包含可以通过命令free显示的信息。另一方面,它仍然是空的!为何如此?这种情况让人想起1985年康奈尔大学物理学家N.DavidMermin写的一篇名为《没有人看见月亮的情况吗?现实和量子理论》的论文。事实是当进程向/proc请求数据时,内核会收集有关内存的统计信息,而当没有人查看它时,/proc中的文件实际上什么都没有。正如Mermin所说,“这是一个基本的量子学说,一般来说,测量不会揭示被测属性的预先存在的值。”(关于卫星的问题的答案留作练习。)当没有进程访问它们时,/proc中的文件为空。(来源)procfs的空文件很有意义,因为那里可用的信息是动态的。sysfs的情况不同。让我们比较一下/proc和/sys中非空文件的数量。procfs只有一个非空文件,导出的内核配置,这是一个例外,因为它只需要在每次启动时生成一次。另一方面,/sys有许多较大的文件,其中大部分由一页内存组成。通常,一个sysfs文件只包含一个数字或字符串,这与通过读取文件(如/proc/meminfo)生成的信息表形成鲜明对比。sysfs的目的是将内核称为“kobjects”的可读和可写属性暴露给用户空间。kobject的唯一目的是引用计数:当对kobject的最后一个引用被删除时,系统会回收与其关联的资源。但是,/sys构成了内核著名的“到用户空间的稳定ABI”,其中大部分在任何情况下都无法“破解”。但这并不意味着sysfs中的文件是静态的,这与易失性对象的引用计数相反。内核的稳定ABI限制了/sys中可能存在的内容,而不是任何给定时刻实际存在的内容。列出sysfs中文件的权限,以了解如何设置或读取设备、模块、文件系统等的可配置、可调参数。逻辑强调了procfs也是内核稳定ABI的一部分的结论,即使内核的文档没有明确这么说。sysfs中的文件准确地描述了实体的每一个属性,并且可以是可读的、可写的或两者兼而有之。文件中的“0”表示SSD是不可移动的存储设备。使用eBPF和bcc工具窥视VFS了解内核如何管理sysfs文件的最简单方法是观察它的运行情况,而在ARM64或x86_64上观察它的最简单方法是使用eBPF。eBPF(extendedBerkeleyPacketFilter)由运行在内核中的虚拟机组成,特权用户可以从命令行查询。内核源码告诉读者内核能做什么;在启动的系统上运行eBPF工具将显示内核实际做了什么。令人高兴的是,使用bcc工具开始使用eBPF非常容易,这些工具在主要Linux发行版的软件包中可用,并且由BrendanGregg详细记录。bcc工具是带有小型嵌入式C代码片段的Python脚本,这意味着熟悉这两种语言的任何人都可以轻松修改它们。据目前统计,bcc/tools中有80个Python脚本,这使得系统管理员或开发人员很可能能够找到与她/他的需求相关的现有脚本。要了解VFS在正在运行的系统上的表现,请尝试一个简单的vfscount或vfsstat脚本,它会看到每秒对vfs_open()及其相关事件的数十次调用。vfsstat.py是一个带有嵌入式C代码片段的Python脚本,它只对VFS函数调用进行计数。作为一个重要的例子,让我们看看当一个U盘插入正在运行的系统时sysfs中会发生什么。观察当使用eBPF插入USB记忆棒时/sys中会发生什么,简单和复杂的示例。在上面的第一个简单示例中,只要sysfs_create_files()命令运行,trace.pybcc工具脚本就会打印出一条消息。我们看到sysfs_create_files()由kworker线程启动以响应USB记忆棒插入事件,但它创建了哪些文件?第二个示例说明了eBPF的强大功能。在这里,trace.py正在打印内核回溯(-K选项)和sysfs_create_files()创建的文件的名称。单引号中的片段是一些C源代码,包括一个易于识别的格式字符串,并且提供的Python脚本引入了LLVM即时编译器(JIT)以在内核虚拟机中编译和执行它。必须在第二个命令中复制完整的sysfs_create_files()函数签名,以便格式字符串可以引用其中一个参数。此C片段中的错误导致可识别的C编译器错误。例如,如果省略-I参数,则结果为“无法编译BPF文本”。熟悉C或Python的开发人员会发现bcc工具易于扩展和修改。插入U盘后,内核回溯显示PID7711是一个kworker线程,它在sysfs中创建了一个名为events的文件。与sysfs_remove_files()的对应调用显示,移除U盘导致事件文件被删除,与引用计数的思想一致。在USB记忆棒插入(未显示)期间观察eBPF中的sysfs_create_link()表明创建了不少于48个符号链接。无论如何,事件文件的目的是什么?使用cscope查找函数__device_add_disk()表明它调用了disk_add_events()并且可以将“mediachange”或“ejectrequest”写入文件。在这里,内核的块层通知用户空间该“磁盘”的出现和消失。考虑一下这种检查USB记忆棒插入工作原理的方法与尝试仅从源头找出过程相比有多快。只读根文件系统使嵌入式设备成为可能事实上,没有人会通过拔下电源插头来关闭服务器或桌面系统。为什么?因为物理存储设备上挂载的文件系统可能有待处理的(未完成的)写入,并且记录其状态的数据结构可能与写入存储的内容不同步。发生这种情况时,系统所有者将不得不等待fsck文件系统恢复工具在下次启动时完成运行,在最坏的情况下,实际上会丢失数据。然而,狂热者会听说许多物联网和嵌入式设备(例如路由器、恒温器和汽车)现在运行Linux。这些设备中有许多几乎没有用户界面,也没有办法彻底“卸载”它们。想一想用没电的电池启动汽车,其中运行Linux的主机设备的电源不断循环打开和关闭。当引擎最终开始运行时,系统如何在没有长时间fsck的情况下启动?答案是嵌入式设备依赖于一个只读的根文件系统(简称ro-rootfs)。ro-rootfs是嵌入式系统通常不需要fsck的原因。来源:https://tinyurl.com/yxoauoubro-rootfs提供了许多优势,尽管这些优势不如耐用性那么明显。一是如果Linux进程不可写,则恶意软件也无法写入/usr或/lib。另一个原因是,一个基本不可变的文件系统对于远程设备的现场支持至关重要,因为支持人员在理论上拥有与现场相同的本地系统。也许最重要(也是最微妙)的优势是ro-rootfs迫使开发人员在项目的设计阶段决定哪些系统对象是不可变的。处理ro-rootfs通常会很不方便甚至很痛苦,就像编程语言中的常量变量一样,但好处很容易抵消这种额外的开销。对于嵌入式开发人员来说,创建一个只读的根文件系统确实需要一些额外的工作,这就是VFS的用武之地。Linux要求/var中的文件是可写的,此外,许多运行在嵌入式系统上的流行应用程序试图为创建点文件$HOME中的配置。将配置文件放在主目录中的一种解决方案通常是预先生成它们并将它们构建到rootfs中。对于/var,一种方法是将其挂载到单独的可写分区,而/本身以只读方式挂载。使用绑定或覆盖安装是另一种流行的选择。绑定和覆盖挂载及其在容器中的使用运行manmount是了解绑定挂载和覆盖挂载的最佳方式,这种方式可以让嵌入式开发人员和系统管理员在一个路径位置创建文件系统,然后将其提供给应用程序作为另一条路。对于嵌入式系统,这意味着文件可以存储在/var中的不可写闪存设备上,但在启动时tmpfs中的路径被覆盖或绑定安装在/var路径上,以便应用程序可以随意编写它们的那里的内容。/var中的更改将在下次启动时消失。覆盖挂载提供了tmpfs和底层文件系统之间的联合,允许直接修改ro-rootfs中的现有文件,而绑定挂载可以使新的空tmpfs目录在ro-rootfs路径中显示为可写。虽然覆盖文件系统是一种合适的文件系统类型,但绑定挂载是由VFS命名空间工具实现的。鉴于覆盖挂载和绑定挂载的描述,没有人应该对它们在Linux容器中大量使用感到惊讶。让我们看看当我们通过运行bcc的mountsnoop工具启动带有systemd-nspawn的容器时会发生什么:当mountsnoop.py运行时,system-nspawn调用启动容器。让我们看看会发生什么:在容器“启动”期间运行mountsnoop表明容器运行时严重依赖绑定挂载。(只显示了冗长输出的开头)这里,systemd-nspawn将主机的procfs和sysfs中的选定文件提供给位于其rootfs中路径的容器。除了在绑定挂载时设置MS_BIND标志外,挂载系统调用的其他一些标志用于确定主机命名空间与容器中更改之间的关系。例如,绑定挂载可以将/proc和/sys中的更改传播到容器,也可以隐藏它们,具体取决于调用。总结了解Linux内部机制似乎是一项不可能完成的任务,因为内核本身包含大量代码,此外还有Linux用户空间应用程序和C库(如glibc)中的系统调用接口。取得进展的一种方法是阅读内核子系统的源代码,重点了解面向用户空间的系统调用和标头以及主要的内核内部接口,这里以file_operations表为例。file_operations使“一切皆文件”真正起作用,因此掌握它们特别有益。***fs/目录中的内核C源文件构成了虚拟文件系统的实现,虚拟文件??系统是一个垫片层,支持广泛且相对简单的流行文件系统和存储设备的互操作性。通过Linux命名空间绑定挂载和覆盖挂载是VFS的魔力,它使容器和只读根文件系统成为可能。结合源代码研究,eBPF内核工具及其bcc接口使探测内核比以往任何时候都容易。