Linux文件系统学习

142 阅读8分钟

Linux文件系统是怎么工作的

同CPU、内存一样,磁盘和文件系统的管理,也是os核心功能

  • 磁盘为system提供最基本的持久化存储
  • 文件系统则在磁盘的基础上,提供一个管理文件的树状结构

索引节点和目录项

在Linux中一切皆文件,不仅普通的文件和目录,就连块设备、套接字、管道等都是统一的文件系统来管理。

为了方便管理,Linux文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。

  • 索引节点,简称inode, 用来记录文件的元数据,比如inode编号、文件大小、访问权限、修改日期、数据的位置等。索引阶段和文件一一对应,它跟文件内容一样,都会持久化到磁盘。所以,索引节点同样占磁盘空间
  • 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。

换句话说,索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,可以简单理解为,一个文件可以有多个别名。

索引节点和目录项纪录了文件的元数据,以及文件间的目录关系,那么具体来说,文件数据到底是怎么存储的呢?是不是直接写到磁盘中就好了呢?

实际上,磁盘读写的最小单位是扇区,扇区一把只有512B大小,如果每次读写这么小的单位,效率一定很低。所以,文件系统又把连续的扇区组成了逻辑块,然后每次以逻辑块为最小单元,来管理数据。常见的逻辑块大小为4KB,也就是由连续的8个扇区组成的。

有两点需要注意

  • 目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。在前面的Buffer和Cache原理中,为了协调慢速磁盘和快速CPU的性能差异,文件内容会缓存到页缓存Cache中。这些索引节点自然也会缓存到内存中,加速文件的访问。
  • 磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块,索引节点区和数据块区。其中,
  • 超级块,存储整个文件系统的状态
  • 索引节点区,用来存储索引节点
  • 数据块区,用来存储文件数据

虚拟文件系统

目录项、索引节点、逻辑块以及超级块,构成了 Linux 文件系统的四大基本要素。不过,为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System).

VFS定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟VFS提供的统一接口交互就可以了,而不需要关系底层各种文件系统的实现细节。

一张 Linux 文件系统的架构图,帮你更好地理解系统调用、VFS、缓存、文件系统以及块存储之间的关系。

通过这张图,你可以看到,在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。

  • 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。
  • 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
  • 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。

文件系统I/O

把文件系统挂载挂载点后,就能通过挂载点,再去访问它管理的文件了。VFS提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给application使用。

就拿 cat 命令来说,它首先调用 open() ,打开一个文件;然后调用 read() ,读取文件的内容;最后再调用 write() ,把文件内容输出到控制台的标准输出中

int open(const char *pathname, int flags, mode_t mode); 
ssize_t read(int fd, void *buf, size_t count); 
ssize_t write(int fd, const void *buf, size_t count); 

文件读写方式的各种差异,导致 I/O 的分类多种多样。最常见的有,缓冲与非缓冲 I/O、直接与非直接 I/O、阻塞与非阻塞 I/O、同步与异步 I/O 等。 接下来,我们就详细看这四种分类。

第一种,根据是否利用标准库缓存,可以把文件IO分为缓冲和非缓冲IO

  • 缓冲IO,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。
  • 非缓冲IO,是指直接通过系统调用来访问文件,不再经过标准库缓存。

无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件。而根据上一节内容,我们知道,系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。

第二,根据是否利用os的页缓存,分为直接IO和非直接IO

  • 直接IO,是指跳过os的页缓存,直接与文件系统交互来访问文件。
  • 非直接IO,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。

想要实现直接 I/O,需要你在系统调用中,指定 O_DIRECT 标志。如果没有设置过,默认的是非直接 I/O。

不过要注意,直接 I/O、非直接 I/O,本质上还是和文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/O。

第三,根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O:

  • 所谓阻塞I/O,是指app执行IO操作后,如果没有获得相应,就会阻塞当前线程,自然不能执行其他任务。
  • 所谓非阻塞IO,是指app执行IO操作后,不会阻塞当前的线程,可以继续执行其他的任务,然后通过轮询或者事件通知的形式,获取调用结果。

举个例子,在操作文件时,如果你设置了 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 接口来访问。

小结

文件系统,是对存储设备上的文件,进行组织管理的一种机制。为了支持各类不同的文件系统,Linux 在各种文件系统实现上,抽象了一层虚拟文件系统(VFS)。

VFS 定义了一组所有文件系统都支持的数据结构和标准接口。

这样,用户进程和内核中的其他子系统,就只需要跟 VFS 提供的统一接口进行交互。为了降低慢速磁盘对性能的影响,文件系统又通过页缓存、目录项缓存以及索引节点缓存,缓和磁盘延迟对应用程序的影响。