Linux 文件系统
磁盘和文件系统的关系:
-
磁盘为系统提供了最基本的持久化存储。
-
文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构。
文件系统工作原理
索引节点和目录项
文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式不同,就会形成不同的文件系统。
为了方便管理,Linux 文件系统为每个文件都分配两个数据结构,索引节点(index node)和目录项(directory entry)。它们主要用来记录文件的元信息和目录结构。
-
索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间。
-
目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。
换句话说,索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,你可以简单理解为,一个文件可以有多个别名。
举个例子,通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点相同。
磁盘读写的最小单位是扇区,然而扇区只有 512B 大小,如果每次都读写这么小的单位,效率一定很低。所以,文件系统又把连续的扇区组成了逻辑块,然后每次都以逻辑块为最小单元,来管理数据。常见的逻辑块大小为 4KB,也就是由连续的 8 个扇区组成。
目录项、索引节点、文件数据的关系如下:
-
第一,目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。为了协调慢速磁盘与快速 CPU 的性能差异,文件内容会缓存到页缓存 Cache 中,索引节点也会缓存到内存中,加速文件的访问。
-
第二,磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中:
(1)超级块,存储整个文件系统的状态
(2)索引节点区,用来存储索引节点
(3)数据块区,则用来存储文件数据
虚拟文件系统
为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS(Virtual File System)。
VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互就可以了,而不需要再关心底层各种文件系统的实现细节。
如图在 VFS 的下方,Linux 支持各种各样的文件系统,如 Ext4、XFS、NFS 等等。按照存储位置的不同,这些文件系统可以分为三类。
-
第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等,都是这类文件系统。
-
第二类是基于内存的文件系统,也就是我们常说的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但会占用内存。我们经常用到的 /proc 文件系统,其实就是一种最常见的虚拟文件系统。此外,/sys 文件系统也属于这一类,主要向用户空间导出层次化的内核对象。
-
第三类是网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等。
这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。拿第一类,也就是基于磁盘的文件系统为例,在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(比如其他的磁盘分区、/proc 文件系统、/sys 文件系统、NFS 等)挂载进来。
文件系统 I/O
把文件系统挂载到挂载点后,你就能通过挂载点,再去访问它管理的文件了。VFS 提供了一组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。
文件读写方式的各种差异,导致 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 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。
比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。
同步与异步 I/O
-
同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。
-
异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。
比如,在访问管道或者网络套接字时,设置了 O_ASYNC 选项后,相应的 I/O 就是异步 I/O。这样,内核会再通过 SIGIO 或者 SIGPOLL,来通知进程文件是否可读写。
查看文件系统容量
文件系统和磁盘空间:
# -h表示有更好的可读性
df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 1.7G 0 1.7G 0% /dev
tmpfs 1.7G 24K 1.7G 1% /dev/shm
tmpfs 1.7G 8.4M 1.7G 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 1.7G 0 1.7G 0% /sys/fs/cgroup
/dev/vda1 50G 21G 27G 43% /
/dev/loop0 54M 54M 0 100% /snap/snapd/19361
/dev/loop2 45M 45M 0 100% /snap/certbot/3024
/dev/loop3 64M 64M 0 100% /snap/core20/1950
/dev/loop4 54M 54M 0 100% /snap/snapd/19457
/dev/loop5 64M 64M 0 100% /snap/core20/1974
tmpfs 344M 0 344M 0% /run/user/500
也可以指定目录:
df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 21G 27G 43% /
索引节点的容量
# -h表示有更好的可读性, -i表示查看索引节点
df -hi
Filesystem Inodes IUsed IFree IUse% Mounted on
devtmpfs 427K 411 427K 1% /dev
tmpfs 430K 7 430K 1% /dev/shm
tmpfs 430K 2.5K 427K 1% /run
tmpfs 430K 5 430K 1% /run/lock
tmpfs 430K 18 430K 1% /sys/fs/cgroup
/dev/vda1 3.2M 307K 2.9M 10% /
/dev/loop0 658 658 0 100% /snap/snapd/19361
/dev/loop2 7.5K 7.5K 0 100% /snap/certbot/3024
/dev/loop3 12K 12K 0 100% /snap/core20/1950
/dev/loop4 658 658 0 100% /snap/snapd/19457
/dev/loop5 12K 12K 0 100% /snap/core20/1974
tmpfs 430K 18 430K 1% /run/user/500
当发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题。
目录项和索引节点缓存
sudo 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>
isofs_inode_cache 108 168 664 12 2 : tunables 0 0 0 : slabdata 14 14 0
rpc_inode_cache 46 46 704 23 4 : tunables 0 0 0 : slabdata 2 2 0
mqueue_inode_cache 17 17 960 17 4 : tunables 0 0 0 : slabdata 1 1 0
fuse_inode 8752 9272 832 19 4 : tunables 0 0 0 : slabdata 488 488 0
ecryptfs_inode_cache 0 0 1024 16 4 : tunables 0 0 0 : slabdata 0 0 0
fat_inode_cache 0 0 752 21 4 : tunables 0 0 0 : slabdata 0 0 0
squashfs_inode_cache 9463 9706 704 23 4 : tunables 0 0 0 : slabdata 422 422 0
ext4_inode_cache 114164 143837 1096 29 8 : tunables 0 0 0 : slabdata 33741 33741 0
hugetlbfs_inode_cache 100 100 632 25 4 : tunables 0 0 0 : slabdata 4 4 0
sock_inode_cache 2578 2599 704 23 4 : tunables 0 0 0 : slabdata 113 113 0
shmem_inode_cache 3618 4224 720 22 4 : tunables 0 0 0 : slabdata 192 192 0
proc_inode_cache 8984 13846 688 23 4 : tunables 0 0 0 : slabdata 602 602 0
inode_cache 27039 28925 616 13 2 : tunables 0 0 0 : slabdata 2225 2225 0
dentry 104015 130956 192 21 1 : tunables 0 0 0 : slabdata 6236 6236 0
dentry 行表示目录项缓存,inode_cache 行,表示 VFS 索引节点缓存,其余的则是各种文件系统的索引节点缓存。
也可以用 slabtop 来查看:
# 按下c按照缓存大小排序,按下a按照活跃对象数排序
sudo slabtop
Active / Total Objects (% used) : 1035480 / 1363157 (76.0%)
Active / Total Slabs (% used) : 72678 / 72678 (100.0%)
Active / Total Caches (% used) : 86 / 112 (76.8%)
Active / Total Size (% used) : 296260.45K / 370719.97K (79.9%)
Minimum / Average / Maximum Object : 0.01K / 0.27K / 8.00K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
143566 113865 0% 1.07K 33694 29 1078208K ext4_inode_cache
443586 267139 0% 0.10K 11374 39 45496K buffer_head
131082 104401 0% 0.19K 6242 21 24968K dentry
34692 27457 0% 0.57K 2478 14 19824K radix_tree_node
134190 112894 0% 0.13K 4473 30 17892K kernfs_node_cache
28925 27039 0% 0.60K 2225 13 17800K inode_cache
55566 55437 0% 0.19K 2646 21 10584K kmalloc-192
13846 9010 0% 0.67K 602 23 9632K proc_inode_cache
9272 8752 0% 0.81K 488 19 7808K fuse_inode
9706 9463 0% 0.69K 422 23 6752K squashfs_inode_cache
720 557 0% 7.56K 180 4 5760K task_struct
23902 23376 0% 0.20K 1258 19 5032K vm_area_struct
可以看到我的系统中 ext4_inode_cache 用了最多的 Slab 缓存,超过1G
磁盘I/O工作原理
磁盘
常见磁盘可以分为两类:机械磁盘和固态磁盘。
-
机械磁盘(Hard Disk Driver),缩写为 HDD,由盘片和读写磁头组成。在读写数据前,需要移动读写磁头,定位到数据所在的磁道,然后才能访问数据。显然,如果 I/O 请求刚好连续,那就不需要磁道寻址,速度更快,这其实就是我们熟悉的连续 I/O 的工作原理。与之相对应的,当然就是随机 I/O,它需要不停地移动磁头,来定位数据位置,所以读写速度就会比较慢。
-
固态磁盘(Solid State Disk),缩写为 SSD,由固态电子元器件组成。固态磁盘不需要磁道寻址,所以,不管是连续 I/O,还是随机 I/O 的性能,都比机械磁盘要好得多。
无论机械磁盘,还是固态磁盘,相同磁盘的随机 I/O 都要比连续 I/O 慢很多。对固态磁盘来说,虽然它的随机性能比机械硬盘好很多,但同样存在“先擦除再写入”的限制。随机读写会导致大量的垃圾回收,所以相对应的,随机 I/O 的性能比起连续 I/O 来,也还是差了很多。
此外,连续 I/O 还可以通过预读的方式,来减少 I/O 请求的次数,这也是其性能优异的一个原因。很多性能优化的方案,也都会从这个角度出发,来优化 I/O 性能。
此外,机械磁盘和固态磁盘还分别有一个最小的读写单位。
-
机械磁盘的最小读写单位是扇区,一般大小为 512 字节。
-
固态磁盘的最小读写单位是页,通常大小是 4KB、8KB 等。
按照接口来分类,比如可以把硬盘分为 IDE(Integrated Drive Electronics)、SCSI(Small Computer System Interface) 、SAS(Serial Attached SCSI) 、SATA(Serial ATA) 、FC(Fibre Channel) 等。
不同的接口,往往分配不同的设备名称。比如, IDE 设备会分配一个 hd 前缀的设备名,SCSI 和 SATA 设备会分配一个 sd 前缀的设备名。如果是多块同类型的磁盘,就会按照 a、b、c 等的字母顺序来编号。
其实在 Linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
通用块层
跟虚拟文件系统 VFS 类似,为了减小不同块设备的差异带来的影响,Linux 通过一个统一的通用块层,来管理各种不同的块设备。
通用块层,其实是处在文件系统和磁盘驱动中间的一个块设备抽象层。它主要有两个功能 。
-
第一个功能跟虚拟文件系统的功能类似。向上,为文件系统和应用程序,提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
-
第二个功能,通用块层还会给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率。
其中,对 I/O 请求排序的过程,也就是我们熟悉的 I/O 调度。事实上,Linux 内核支持四种 I/O 调度算法,分别是 NONE、NOOP、CFQ 以及 DeadLine。
-
第一种 NONE ,更确切来说,并不能算 I/O 调度算法。因为它完全不使用任何 I/O 调度器,对文件系统和应用程序的 I/O 其实不做任何处理,常用在虚拟机中(此时磁盘 I/O 调度完全由物理机负责)。
-
第二种 NOOP ,是最简单的一种 I/O 调度算法。它实际上是一个先入先出的队列,只做一些最基本的请求合并,常用于 SSD 磁盘。
-
第三种 CFQ(Completely Fair Scheduler),也被称为完全公平调度器,是现在很多发行版的默认 I/O 调度器,它为每个进程维护了一个 I/O 调度队列,并按照时间片来均匀分布每个进程的 I/O 请求。类似于进程 CPU 调度,CFQ 还支持进程 I/O 的优先级调度,所以它适用于运行大量进程的系统,像是桌面环境、多媒体应用等。
-
最后一种 DeadLine 调度算法,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理。DeadLine 调度算法,多用在 I/O 压力比较重的场景,比如数据库等。
I/O 栈
我们可以把 Linux 存储系统的 I/O 栈,由上到下分为三个层次,分别是文件系统层、通用块层和设备层。这三个 I/O 层的关系如下图所示,这其实也是 Linux 存储系统的 I/O 栈全景图。
根据这张 I/O 栈的全景图,我们可以更清楚地理解,存储系统 I/O 的工作原理。
-
文件系统层,包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据。
-
通用块层,包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。
-
设备层,包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作。
存储系统的 I/O ,通常是整个系统中最慢的一环。所以, Linux 通过多种缓存机制来优化 I/O 效率。
比方说,为了优化文件访问的性能,会使用页缓存、索引节点缓存、目录项缓存等多种缓存机制,以减少对下层块设备的直接调用。同样,为了优化块设备的访问效率,会使用缓冲区,来缓存块设备的数据。