一、 索引节点和目录项

文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。

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

  • 索引节点 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。inode和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中,同样占用磁盘空间(也会缓存到内存中加速文件读取)。
  • 目录项 dentry,用来记录文件的名字、inode 指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。
  • 索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,可以简单理解为,一个文件可以有多个别名。

 

具体来说,文件数据到底是怎么存储的呢?是不是直接写到磁盘中就好了呢?

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

磁盘在执行文件系统格式化时,会被分成三个存储区域:

  • 超级块,存储整个文件系统的状态
  • 索引节点区,用来存储索引节点
  • 数据块区,则用来存储文件数据

目录项、索引节点、逻辑块以及超级块,构成了 Linux 文件系统的四大基本要素。

《Linux性能优化实战》笔记(十四)—— Linux 文件系统是怎么工作的?_缓存

 

二、 虚拟文件系统 VFS

前面说到,对文件的组织管理方式不同,就会形成不同的文件系统,如果每种要分别支持实在过于复杂。因此,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)。

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

《Linux性能优化实战》笔记(十四)—— Linux 文件系统是怎么工作的?_应用程序_02

可以看到,在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。

按照存储位置的不同,这些文件系统可以分为三类

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

这些文件系统,要先mount到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。以基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录,在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys 文
件系统、NFS 等)挂载进来。

 

三、 文件系统 I/O

VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。拿 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 等。 接下来,我们就详细看这四种分类。

1. 缓冲与非缓冲 I/O

根据是否利用标准库缓存,可以把文件 I/O 分为缓冲 I/O 与非缓冲 I/O

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

这里所说的“缓冲”,是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来。无论缓冲 I/O 还是非缓冲 I/O,它们最终还是要经过系统调用来访问文件。而根据上一节内容,我们知道,系统调用后,还会通过页缓存,来减少磁盘的 I/O 操作。

2. 直接与非直接 I/O

根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O。

  • 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。需要在系统调用中指定 O_DIRECT 标志。
  • 非直接 I/O(默认),文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。

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

 

3. 阻塞与非阻塞 I/O

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

  • 阻塞 I/O(默认),是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
  • 非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他任务,随后再通过轮询或者事件通知的形式,获取调用的结果。需要设置 O_NONBLOCK 标志。

 

4. 同步与异步 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,来通知进程文件是否可读写。

 

四、 观测文件系统性能

1. 容量

对文件系统来说,最常见的一个问题就是空间不足。用 df 命令,就能查看文件系统的磁盘空间使用情况。

$ df /dev/sda1
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda1 30308240 3167020 27124836 11% /

有时候,明明你碰到了空间不足的问题,可是用 df 查看磁盘空间后,却发现剩余空间还有很多。这是怎么回事呢?

回忆最前面我们说过,除了文件数据,索引节点也占用磁盘空间。可以 df -i 查看索引节点的使用情况

df -i /dev/sda1
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 3870720 157460 3713260 5% /

索引节点的容量(Inode 个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。一般删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以
解决这个问题。

2. 缓存

在前面 Cache 案例中,我已经介绍过,可以用 free 或 vmstat,来观察页缓存的大小。复习一下,free 输出的 Cache,是页缓存和可回收 Slab 缓存的和,你可以从/proc/meminfo ,直接得到它们的大小:

cat /proc/meminfo | grep -E "SReclaimable|Cached"
Cached: 748316 kB
SwapCached: 0 kB
SReclaimable: 179508 kB

文件系统中的目录项和索引节点缓存,又该如何观察呢?实际上,内核使用 Slab 机制管理目录项和索引节点的缓存,/proc/meminfo 只给出了Slab 的整体大小,具体到每一种 Slab 缓存,还要查看 /proc/slabinfo 这个文件。比如,运行下面的命令,查看所有目录项和各种文件系统索引节点的缓存情况:

cat /proc/slabinfo | grep -E '^#|dentry|inode' 
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> 
xfs_inode              0      0    960   17    4 : tunables    0    0    0 : slabdata      0      0      0 
... 
ext4_inode_cache   32104  34590   1088   15    4 : tunables    0    0    0 : slabdata   2306   2306      0hugetlbfs_inode_cache     13     13    624   13    2 : tunables    0    0    0 : slabdata      1      1      0 
sock_inode_cache    1190   1242    704   23    4 : tunables    0    0    0 : slabdata     54     54      0 
shmem_inode_cache   1622   2139    712   23    4 : tunables    0    0    0 : slabdata     93     93      0 
proc_inode_cache    3560   4080    680   12    2 : tunables    0    0    0 : slabdata    340    340      0 
inode_cache        25172  25818    608   13    2 : tunables    0    0    0 : slabdata   1986   1986      0 
dentry             76050 121296    192   21    1 : tunables    0    0    0 : slabdata   5776   5776      0

这个界面中,dentry 行表示目录项缓存,inode_cache 行,表示 VFS 索引节点缓存,其余的则是各种文件系统的索引节点缓存,具体含义你可以查询 man slabinfo。

在实际性能分析中,我们更常使用 slabtop ,来找到占用内存最多的缓存类型。

# 按下c按照缓存大小排序,按下a按照活跃对象数排序 
$ slabtop 
Active / Total Objects (% used)    : 277970 / 358914 (77.4%) 
Active / Total Slabs (% used)      : 12414 / 12414 (100.0%) 
Active / Total Caches (% used)     : 83 / 135 (61.5%) 
Active / Total Size (% used)       : 57816.88K / 73307.70K (78.9%) 
Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K 

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME 
69804  23094   0%    0.19K   3324       21     13296K dentry 
16380  15854   0%    0.59K   1260       13     10080K inode_cache 
58260  55397   0%    0.13K   1942       30      7768K kernfs_node_cache 
   485    413   0%    5.69K     97        5      3104K task_struct 
  1472   1397   0%    2.00K     92       16      2944K kmalloc-2048

可以看到,在我的系统中,目录项和索引节点占用了最多的 Slab 缓存。不过它们占用的内存其实并不大,加起来也只有 23MB 左右。