与CPU和内存一样,磁盘和文件系统的管理也是操作系统的核心功能。磁盘为系统提供了最基本的持久化存储。文件系统提供了一种以磁盘为基础管理文件的树状结构。那么,磁盘和文件系统是如何工作的呢?哪些指标可以衡量他们的表现?索引节点和目录项文件系统本身是一种组织和管理存储设备上文件的机制。不同的组织方式会形成不同的文件系统。我们必须记住的最重要的事情是在Linux中一切都是文件。不仅是普通的文件和目录,还有块设备、socket、管道等都必须通过统一的文件系统进行管理。Linux文件系统为了便于管理,为每个文件分配了两个数据结构,索引节点(indexnode)和目录项(directoryentry)。它们主要用来记录文件的元信息和目录结构。索引节点,简称inode,用于记录文件的元数据,如inode号、文件大小、访问权限、修改日期、数据位置等。索引之间存在一一对应关系节点和文件,它将像文件内容一样持久化存储在磁盘上。所以请记住,inode也会占用磁盘空间。目录项,简称dentry,用于记录文件名、索引节点指针以及与其他目录项的关系。多个关联的目录条目构成了文件系统的目录结构。但与inode不同的是,目录项是由内核维护的内存数据结构,因此常被称为目录项缓存。换句话说,索引节点是每个文件的唯一符号,目录项维护文件系统的树结构。目录条目和索引节点之间的关系是多对一的。你可以简单的理解为一个文件可以有多个别名。例如,通过硬链接为文件创建的别名对应于不同的目录项,但这些目录项本质上仍然链接到同一个文件,因此它们的索引节点是相同的。索引节点和目录项记录了文件的元数据以及文件之间的目录关系,那么具体来说,文件数据是如何存储的呢?不应该直接写入磁盘吗?其实磁盘读写的最小单位是扇区,但一个扇区只有512B大小。如果每次读写这么小的单元,效率肯定很低。因此,文件系统将连续的扇区组成逻辑块,然后每次都以逻辑块为最小单位来管理数据。常见的逻辑块大小为4KB,由8个连续的扇区组成。为了帮助我们理解目录项、索引节点和文件数据之间的关系,画了一张示意图。我们可以参考这张图来回忆刚才说的内容,把知识和细节串联起来。不过这里有两点需要我们注意:第一,目录项本身就是一个内存缓存,而索引节点是存储在磁盘上的数据。在之前的Buffer和Cache原理中,我提到为了协调慢速磁盘和快速CPU的性能差异,文件内容会被缓存在页面缓存Cache中。那么,我们也应该想到,这些索引节点自然会缓存在内存中,以加快文件访问速度。其次,当磁盘被文件系统格式化后,会被分成三个存储区,超级块、索引节点区和数据块区。其中,超级块存储了整个文件系统的状态。索引节点区用于存放索引节点。数据块区用于存放文件数据。虚拟文件系统目录项、索引节点、逻辑块和超级块构成了Linux文件系统的四大基本要素。但是,为了支持各种文件系统,Linux内核在用户进程和文件系统之间引入了一个抽象层,这就是虚拟文件系统VFS(VirtualFileSystem)。VFS定义了一套所有文件系统都支持的数据结构和标准接口。这样,内核中的用户进程和其他子系统只需要与VFS提供的统一接口进行交互,而无需关心各种底层文件系统的实现细节。这里,下图是Linux文件系统的架构图,可以帮助我们更好的理解系统调用、VFS、缓存、文件系统和块存储之间的关系。从这张图可以看出,在VFS下,Linux支持各种文件系统,如Ext4、XFS、NFS等。根据存储位置的不同,这些文件系统可以分为三类。第一种是基于磁盘的文件系统,它将数据直接存储在安装在计算机本地的磁盘上。常见的Ext4、XFS、OverlayFS等都是这样的文件系统。第二种是基于内存的文件系统,也就是我们常说的虚拟文件系统。这种文件系统不需要任何磁盘来分配存储空间,但会占用内存。我们经常使用的/proc文件系统其实是最常见的虚拟文件系统之一。另外,/sys文件系统也属于这一类,主要是将层次化的内核对象导出到用户空间。第三类是网络文件系统,即用于访问其他计算机数据的文件系统,如NFS、SMB、iSCSI等。这些文件系统首先要挂载到网络中的一个子目录(称为挂载点)可以访问其中的文件之前的VFS目录树。以第一种,即基于磁盘的文件系统为例。安装系统时,必须先挂载一个根目录(/),然后在该根目录下安装其他文件系统(如其他磁盘分区、/proc文件)。系统、/sys文件系统、NFS等)。文件系统I/O将文件系统挂载到挂载点后,就可以通过挂载点访问其管理的文件。VFS提供了一套标准的文件访问接口。这些接口以系统调用的形式提供给应用程序。以cat命令为例,它首先调用open()打开一个文件;然后调用read()读取文件内容;最后调用write()将文件内容输出到控制台的标准输出:inopen(constchar*pathname,intflags,mode_tmode);ssize_tread(intfd,void*buf,size_tcount);ssize_twrite(intfd,constvoid*buf,size_tcount);文件读写方式的各种不同导致了各种类型的I/O。最常见的有缓冲和非缓冲I/O、直接和间接I/O、阻塞和非阻塞I/O、同步和异步I/O等。接下来,我们将详细了解这四类。首先,根据是否使用标准库缓存,文件I/O可以分为缓冲I/O和非缓冲I/O。BufferedI/O是指使用标准库缓存来加速文件访问,标准库内部通过系统调度来访问文件。UnbufferedI/O是指直接通过系统调用访问文件,而不经过标准库缓存。注意,这里所说的“缓冲区”指的是标准库内部实现的缓存。比如你可能看到很多程序遇到换行的时候真的输出,而换行之前的内容其实是标准库临时缓存的。不管是bufferedI/O还是unbufferedI/O,访问文件最终还是要经过系统调用。我们知道在系统调用之后,会使用pagecache来减少磁盘I/O操作。其次,根据是否使用操作系统的pagecache,文件I/O可以分为直接I/O和间接I/O。直接I/O是指跳过操作系统的pagecache,直接与文件系统交互访问文件。间接I/O正好相反。当一个文件被读取或写入时,它必须首先通过系统的页面缓存,然后由内核或额外的系统调用实际写入磁盘。要实现直接I/O,需要在系统调用中指定O_DIRECT标志。如果未设置,则默认为间接I/O。但是,需要注意的是,直接I/O和非直接I/O本质上都是与文件系统交互的。如果是在数据库等场景下,你也会看到跳过文件系统读写磁盘的情况,也就是我们通常所说的rawI/O。第三,根据应用程序是否阻塞自身操作,文件I/O可以分为阻塞I/O和非阻塞I/O。所谓阻塞I/O,就是应用程序在执行了一次I/O操作后,如果没有响应,就会阻塞当前线程,自然无法执行其他任务。所谓非阻塞I/O是指应用程序执行一次I/O操作后,不会阻塞当前线程,可以继续执行其他任务,然后通过轮询或事件获取调用结果通知。例如访问管道或网络套接字时,设置O_NONBLOCK标志,表示以非阻塞方式访问;如果您不设置任何内容,则默认为阻止访问。第四,根据是否等待响应结果,文件I/O可以分为同步I/O和异步I/O。所谓同步I/O是指应用程序在执行完I/O操作后,必须等到整个I/O完成后才能得到I/O响应。所谓异步I/O是指应用程序执行I/O操作后,不需要等待完成和完成后的响应,而是继续执行。本次I/O完成后,响应会以事件通知的形式告诉应用程序。例如,在对文件进行操作时,如果设置了O_SYNC或O_DSYNC标志,则代表同步I/O。如果设置了O_DSYNC,则只有文件数据写入磁盘后才能返回;而O_SYNC是在O_DSYNC的基础上,要求在返回前将文件元数据写入磁盘。再比如访问管道或网络套接字时,设置O_ASYNC选项后,对应的I/O为异步I/O。这样,内核会通过SIGIO或SIGPOLL通知进程文件是否可读或可写。我们可能已经发现,这里的很多概念经常出现在网络编程中。例如,非阻塞I/O通常与select/poll一起用于网络套接字的I/O。现在我们应该也能理解“Linux中的一切都是文件”的深刻含义了。无论是普通文件和块设备,还是网络套接字和管道等,都是通过统一的VFS接口访问的。性能观察接下来,打开一个终端并使用SSH登录到服务器。下面我们一起探讨一下如何观察文件系统的性能。容量文件系统最常见的问题之一是空间不足。当然你可能知道可以使用df命令查看文件系统的磁盘空间使用情况。例如:df/dev/vda1filesystem1K-blockusedavailable%mountpoint/dev/vda1104846316282280447661827227%/如您所见,我的根文件系统只使用了27%的空间。还要注意这里总空间是以1K-fast数量表示的,可以在df中加上-h选项来提高可读性:df-h/dev/vda1filesystemcapacityusedavailableavailableused%Mountpoint/dev/vda1100G27G74G27%/但是有的时候,明明遇到空间不足的问题,但是用df查看磁盘空间后,发现还有很多剩余空间。这里发生了什么?事实上,除了文件数据,索引节点也占用磁盘空间。可以在df命令中加入-i参数查看索引节点的使用情况,如下所示:df-h-i/dev/vda1filesystemInodeused(I)available(I)used(I)%mountpoint/dev/vda150M162K50M1%/index节点的容量(即inode个数)是在格式化磁盘时设置的,一般由格式化工具自动生成。当发现索引节点空间不足,但磁盘空间足够时,很可能是小文件太多造成的。所以,一般情况下,删除这些小文件,或者将它们移动到其他有足够inode的磁盘上,就可以解决问题。缓存可以使用free或者vmstat来观察页面缓存的大小。free输出的Cache是??pagecache和reclaimableSlabcache的总和。你可以直接从/proc/meminfo中获取它们的大小:cat/proc/meminfo|grep-E"SReclaimable|Cached"Cached:2014100kBSwapCached:5316kBSReclaimable:216128kB说到这里,如何观察文件系统中的目录条目和inode缓存?实际上,内核使用Slab机制来管理目录项和inode的缓存。/proc/meminfo只给出了Slab的整体大小。对于每种类型的Slab缓存,您需要检查文件/proc/slabinfo。例如,通过运行以下命令,您可以获取所有目录条目和各种文件系统inode的缓存状态:#name
