你不好奇Linux文件系统是怎么工作的?

开源Linux

共 14385字,需浏览 29分钟

 · 2021-09-03

关注「开源Linux」,选择“设为星标”
回复「学习」,有我为您特别筛选的学习资料~

1前言

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

磁盘为系统提供了最基本的持久化存储。文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构。那么,磁盘和文件系统是怎么工作的呢?又有哪些指标可以衡量它们的性能呢?

2索引节点和目录项

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

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

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

  • 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问 权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会 被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。

  • 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联 关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。

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

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

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

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

为了帮助我们理解目录项、索引节点以及文件数据的关系,画了一张示意图。我们可以对照着这张图,来回忆刚刚讲过的内容,把知识和细节串联起来。

不过,这里有两点需要我们注意:

  • 第一,目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。在前面的 Buffer 和 Cache 原理中,我曾经提到过,为了协调慢速磁盘与快速 CPU 的性能差异,文 件内容会缓存到页缓存 Cache 中。那么,我们也应该想到,这些索引节点自然也会缓存到内存中,加速文件的访问。

  • 第二,磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中:

超级块,存储整个文件系统的状态。

索引节点区,用来存储索引节点。

数据块区,则用来存储文件数据。

3虚拟文件系统

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

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

下图是 Linux 文件系统的架构图,帮我们更好地理解系统调用VFS缓存文件系统以及块存储之间的关系。

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

  • 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。

  • 第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是 一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。

  • 第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、 SMB、iSCSI 等。

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

4文件系统 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 等。接下来,我们就详细看这四种分类。

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

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

  • 非缓冲 I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。

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

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

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

  • 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。

  • 非直接 I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。

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

不过要注意,直接 I/O、非直接 I/O,本质上还是和文件系统交互。如果是在数据库等场景中,还会看到,跳过文件系统读写磁盘的情况,也就是我们通常所说的裸 I/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 完成后,响应会用事件通知的方式,告诉应用程序。

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

5性能观测

接下来,打开一个终端,SSH 登录到服务器上,我们一起来探索,如何观测文件系统的性能。

容量

对文件系统来说,最常见的一个问题就是空间不足。当然,你可能本身就知道,用 df 命 令,就能查看文件系统的磁盘空间使用情况。比如:

df /dev/vda1
文件系统           1K-块         已用         可用        已用%    挂载点
/dev/vda1      104846316     28228044     76618272      27%     /

可以看到,我的根文件系统只使用了 27% 的空间。这里还要注意,总空间用 1K- blocks 的数量来表示,你可以给 df 加上 -h 选项,以获得更好的可读性:

df -h /dev/vda1
 文件系统         容量     已用     可用   已用%   挂载点
/dev/vda1       100G     27G      74G   27%     /

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

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

df -h -i /dev/vda1
文件系统         Inode    已用(I)   可用(I)   已用(I)%  挂载点
/dev/vda1        50M     162K     50M        1%       /

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

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

缓存

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

cat /proc/meminfo | grep -E "SReclaimable|Cached"
Cached:          2014100 kB
SwapCached:         5316 kB
SReclaimable:     216128 kB

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

实际上,内核使用 Slab 机制,管理目录项和索引节点的缓存。/proc/meminfo 只给出了 Slab 的整体大小,具体到每一种 Slab 缓存,还要查看 /proc/slabinfo 这个文件。

比如,运行下面的命令,你就可以得到,所有目录项和各种文件系统索引节点的缓存情况:

# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunab
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             66     66    736   22    4 : tunables    0    0    0 : slabdata      3      3      0
fuse_inode             0      0    832   19    4 : tunables    0    0    0 : slabdata      0      0      0
xfs_inode         100470 110736   1024   16    4 : tunables    0    0    0 : slabdata   6921   6921      0
mqueue_inode_cache     64     64   1024   16    4 : tunables    0    0    0 : slabdata      4      4      0
hugetlbfs_inode_cache     48     48    680   24    4 : tunables    0    0    0 : slabdata      2      2      0
sock_inode_cache    4581   4807    704   23    4 : tunables    0    0    0 : slabdata    209    209      0
shmem_inode_cache   1816   2541    760   21    4 : tunables    0    0    0 : slabdata    121    121      0
proc_inode_cache   10210  13024    728   22    4 : tunables    0    0    0 : slabdata    592    592      0
inode_cache        36498  38832    656   24    4 : tunables    0    0    0 : slabdata   1618   1618      0
dentry            150086 183204    192   21    1 : tunables    0    0    0 : slabdata   8724   8724      0

这个界面中,dentry 行表示目录项缓存,inode_cache 行,表示 VFS 索引节点缓存,其 余的则是各种文件系统的索引节点缓存。

/proc/slabinfo 的列比较多,具体含义可以查询 man slabinfo。在实际性能分析中,我们更常使用 slabtop ,来找到占用内存最多的缓存类型。

比如,下面就是运行 slabtop 得到的结果:

# 按下 c 按照缓存大小排序,按下 a 按照活跃对象数排序 
slabtop
Active / Total Objects (% used)    : 991123 / 1087653 (91.1%)
 Active / Total Slabs (% used)      : 40627 / 40627 (100.0%)
 Active / Total Caches (% used)     : 103 / 138 (74.6%)
 Active / Total Size (% used)       : 329426.37K / 371563.66K (88.7%)
 Minimum / Average / Maximum Object : 0.01K / 0.34K / 8.00K

 OBJS  ACTIVE   USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE    NAME                   
183204 150026   81%    0.19K   8724      21       34896K    dentry
 38832  36498   93%    0.64K   1618      24       25888K    inode_cache
....
  6174   6009   97%    0.19K   294      21       1176K    cred_jar
  6066   5878   96%    0.44K   337      18       2696K    xfrm_dst_cache
  5950   5950   100%   0.02K   35      170      140K     avtab_node
  4807   4581   95%    0.69K   209      23       3344K    sock_inode_cache
  3948   3812   96%    1.12K   141      28       4512K    signal_cache
  3744   3680   98%    0.25K   234      16       936K     skbuff_head_cache
  3634   3634   100%   0.09K   79      46       316K     trace_event_file

从这个结果你可以看到,在我的系统中,目录项(dentry)和索引节点(inode_cache)占用了最多的 Slab 缓存。不 过它们占用的内存其实并不大,加起来也只有 60MB 左右。

6总结

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

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

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

来自:杰哥的IT之旅

往期推荐

开源~新款装机神器!启动 U盘 告别格式化,直接挂载多 ISO

Linux 服务器日常巡检脚本分享

终于有人把Prometheus入门讲明白了

Redis中主、从库宕机如何恢复?

Linux 标准大页和透明大页

文件存储、对象存储、块存储分不清楚?看完就懂了

黑客 Shell 神技:掩盖 Linux 服务器上的操作痕迹

彻底搞懂 Nginx 的五大应用场景

一网打尽大厂MongoDB面试题

存储系统基础知识介绍

深入研究Docker联合文件系统

Linux交换分区要点汇总

如何优雅的在 Linux 下开机自动重启脚本

18个工具分析Linux占用网络带宽大的进程

超全面的 Kubernetes 容器网络技能,运维看后都说好

关注「开源Linux」加星标,提升IT技能


有收获,点个在看 
浏览 2
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报