23-基础篇:Linux文件系统是怎么工作的?





前言

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

磁盘为系统提供了最基本的持久化存储

文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构



索引节点和目录项

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

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

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

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

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

举个例子,通过硬链接为文件创建的别名,就会对应不同的目录项
不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点相同

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

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

目录项、索引节点以及文件数据的关系图

image-20211215144203610

需要注意两点

  1. 目录项本身是一个内存缓存,而索引节点则是存储在磁盘中的数据
    但是为了协调慢速磁盘与快速CPU的性能差异,文件内容会缓存到页缓存Cache中
    所以,索引节点也会缓存到内存中,加速文件的访问
  2. 磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区
    1. 超级块,存储整个文件系统的状态
    2. 索引节点区,用来存储索引节点
    3. 数据块区,则用来存储文件数据


虚拟文件系统

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

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

Linux文件系统的架构图
image-20211215144918774

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

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

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

这些文件系统,要先挂载到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

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

    注意,这里所说的“缓冲”,是指标准库内部实现的缓存
    比方说,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来

    无论缓冲I/O还是非缓冲I/O,它们最终还是要经过系统调用来访问文件
    系统调用后,还会通过页缓存,来减少磁盘的I/O操作


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

    1. 直接I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件
    2. 非直接I/O正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘

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

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


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

    1. 阻塞I/O,是指应用程序执I/O操作后,如果没有获得响应,就会阻塞当前线程,就不能执行其他任务
    2. 非阻塞I/O,是指应用程序执行I/O操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果

    比方说,访问管道或者网络套接字时,设置O_NONBLOCK标志,就表示用非阻塞方式访问
    而如果不做任何设置,默认的就是阻塞访问


  4. 根据是否等待响应结果,可以把文件I/O分为同步和异步I/O

    1. 同步I/O,是指应用程序执行I/O操作后,要一直等到整个I/O完成后,才能获得I/O响应
    2. 异步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,来通知进程文件是否可读写



性能观测

容量

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

[root@local_sa_192-168-1-6 ~]# df /dev/mapper/centos-root
文件系统                    1K-块     已用     可用 已用% 挂载点
/dev/mapper/centos-root 102217728 16777860 85439868   17% /
[root@local_sa_192-168-1-6 ~]# df -h /dev/mapper/centos-root
文件系统                 容量  已用  可用 已用% 挂载点
/dev/mapper/centos-root   98G   17G   82G   17% /

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

除了文件数据,索引节点也占用磁盘空间,可以给df命令加上-i参数,查看索引节点的使用情况

[root@local_sa_192-168-1-6 ~]# df -i /dev/mapper/centos-root
文件系统                   Inode 已用(I)  可用(I) 已用(I)% 挂载点
/dev/mapper/centos-root 51113984  180553 50933431       1% /

索引节点的容量,(也就是Inode个数)是在格式化磁盘时设定好的,一般由格式化工具自动生成
当发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的

一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题



缓存

可以用free或vmstat,来观察页缓存的大小
free输出的Cache,是页缓存和可回收Slab缓存的和,可以从/proc/meminfo ,直接得到它们的大小

[root@local_sa_192-168-1-6 ~]# free
              total        used        free      shared  buff/cache   available
Mem:        4027980      228156     1034612       10184     2765212     3533124
Swap:       8388604         804     8387800

[root@local_sa_192-168-1-6 ~]# cat /proc/meminfo |grep -E "SReclaimable|^Cached"
Cached:            82044 kB
SReclaimable:     107908 kB

文件系统中的目录项和索引节点缓存,又该如何观察呢?

实际上,内核使用Slab机制,管理目录项和索引节点的缓存
/proc/meminfo只给出了Slab的整体大小,具体到每一种Slab缓存,还要查看/proc/slabinfo这个文件
运行下面的命令,就可以得到所有目录项和各种文件系统索引节点的缓存情况

[root@local_sa_192-168-1-6 ~]# 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>
ovl_inode             46     46    704   23    4 : tunables    0    0    0 : slabdata      2      2      0
xfs_inode          16796  16796    960   17    4 : tunables    0    0    0 : slabdata    988    988      0
mqueue_inode_cache     17     17    960   17    4 : tunables    0    0    0 : slabdata      1      1      0
hugetlbfs_inode_cache     12     12    648   12    2 : tunables    0    0    0 : slabdata      1      1      0
sock_inode_cache     437    437    832   19    4 : tunables    0    0    0 : slabdata     23     23      0
proc_inode_cache     828    828    696   23    4 : tunables    0    0    0 : slabdata     36     36      0
shmem_inode_cache    968   1012    744   22    4 : tunables    0    0    0 : slabdata     46     46      0
inode_cache        13230  13325    624   13    2 : tunables    0    0    0 : slabdata   1025   1025      0
dentry             35511  35511    192   21    1 : tunables    0    0    0 : slabdata   1691   1691      0
##
dentry行表示目录项缓存
inode_cache行表示VFS索引节点缓存

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

# 按下c按照缓存大小排序,按下a按照活跃对象数排序
[root@local_sa_192-168-1-6 ~]# slabtop
Active / Total Objects (% used)    : 376464 / 379920 (99.1%)
 Active / Total Slabs (% used)      : 16885 / 16885 (100.0%)
 Active / Total Caches (% used)     : 114 / 158 (72.2%)
 Active / Total Size (% used)       : 127951.42K / 129205.28K (99.0%)
 Minimum / Average / Maximum Object : 0.01K / 0.34K / 8.00K

 OBJS     ACTIVE  USE    OBJ_SIZE  SLABS  OBJ/SLAB  CACHE   SIZE NAME
 130550   130550  100%   0.57K     9325	  14        74600K  radix_tree_node
 16796    16796   100%   0.94K     988	  17        15808K  xfs_inode
 13325    13230   99%    0.61K     1025	  13	      8200K   inode_cache
 35574    35574   100%   0.19K     1694	  21	      6776K   dentry
 ##
 


小结

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

VFS定义了一组所有文件系统都支持的数据结构和标准接口
这样,用户进程和内核中的其他子系统,就只需要跟VFS提供的统一接口进行交互

为了降低慢速磁盘对性能的影响
文件系统又通过页缓存、目录项缓存以及索引节点缓存,缓和磁盘延迟对应用程序的影响


posted @ 2021-12-15 17:02  李成果  阅读(625)  评论(0编辑  收藏  举报