引入
同CPU、内存一样,磁盘和文件系统的管理,也会是操作系统最核心的功能。
- 磁盘为系统提供了最基本的持久化存储
- 文件系统则在磁盘的基础上,提供了一个用来管理文件的树结构
那么,磁盘和文件系统是怎么工作的呢?又有哪些指标也可以衡量他们的性能呢?
索引节点和目录项
文件系统,本身是对存储设备上的文件,进行组织管理的机制。组织方式的不同,就会形成不同的文件系统。
在Linux中,一切皆文件。不仅普通的文件和目录,就连块设备、套接字、管道等,也都要通过统一的文件系统来管理。
为了方便管理,Linux文件系统为每个文件都分配了两个数据结构,索引节点(index node)和⽬录项(directory entry)。它们主要用来记录文件的元信息和目录结构。
- 索引节点,简称为inode,
- 用来记录文件的元数据,比如inode编号、文件大小、访问权限、修改日期、数据的位置等。
- 索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。所以,索引节点同样占用磁盘空间
- 目录项,简称为dentry
- 用来记录文件的名字、索引节点指针以及其他目录项的关联关系
- 多个关联的目录项,就构成了文件系统的目录结构
- 不过,不同于索引节点,目录项是由内存维护的一个内存数据结构,所以通常被叫做目录项缓存
换句话说,索引节点是每个文件的唯一标识,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,你可以简单理解为,一个文件可以有多个别名。
举个例子,通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接到同一个文件,所以,他们的索引节点相同。
索引节点和目录项记录了文件的元数据,以及文件间的目录关系,那么具体来说,文件数据到底是怎么存储的呢?是不是直接写到磁盘中就好了呢?
- 实际上,磁盘读写的最小单位是扇区,然而扇区只有512B大小,如果每次都读写这么小的单位,效率一定会很低。
- 所以,文件系统又把连续的扇区组成了逻辑块,然后每次以逻辑块为最小单元,来管理数据。
- 常⻅的逻辑块⼤⼩为4KB,也就是由连续
的8个扇区组成。
注意两点:
- 目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据
- 为了协调慢速磁盘与快速CPU的性能差异,⽂件内容会缓存到⻚缓存Cache中。
- 同样的,这些索引节点也会缓存到内存中,加速文件的访问
- 磁盘在执行文件系统格式化时,会被分成三个存储区域:
- 超级块:存储整个文件系统的状态
- 索引节点区:用来存储索引节点
- 数据块区:用来存储文件数据
虚拟文件系统
目录项、索引节点、逻辑块以及超级块,构成了Linux文件系统的四大基本要素。
不过,为了支持各种不同的文件系统,Linux内核在用户进程和文件系统的中间,有引入了一个抽象层,也就是虚拟文件系统VFS
VFS定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程在内核中的其他子系统,只需要跟VFS提供的统一接口进行交互就可以了,而不需要关心底层各种文件系统的细节。
从上图可以看到,在VFS的下面,Linux支持各种各样的文件系统,比如Ext4、XFS、NFS等等,按照存储位置的不同,这些文件系统可以分为三类:
- 第一类是基于磁盘的文件系统,也就是把数据直接存储在计算机本地挂载的磁盘中。场景的ext4、xfs、overlayfs等,都是这类文件系统
- 第二类是基于内存的文件系统,也就是我们尝试的虚拟文件系统。这类文件系统,不需要任何磁盘分配存储空间,但是会占用内存。我们常用的/proc文件系统,其实就是一种虚拟文件系统;比如/sys也是虚拟文件系统,主要用于向用户空间导出层次化的内核对象
- 第三类是网络文件系统,是用来访问其他计算机数据的文件系统,比如NFS、SMB、iSCSi等
这些文件系统,要先挂载到VFS目录树中的某个子目录(称为挂载点),然后才能访问其他的文件。拿第⼀类,也就是基于
磁盘的⽂件系统为例,在安装系统时,要先挂载⼀个根⽬录(/),在根⽬录下再把其他⽂件系统(⽐如其他的磁盘分区、/proc⽂件统、/sys⽂件系统、NFS等)挂载进来
文件IO
把文件系统挂载到挂载点之后,你就能通过挂载点,再去访问它管理的文件了。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。
- 缓冲IO,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度来访问文件
- 非缓存IO,是指直接通过系统调用来访问文件,不再经过标准库缓存。
注意,这里的“缓冲”,是指标准库内部实现的缓存,比如,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来
无论缓冲IO还是非缓冲IO,它们最终还是要经过系统调用来访问文件。系统调用之后,还会通过页缓存,还减少磁盘的IO操作。
第二,根据是否利用操作系统的页缓存,可以把文件IO分为直接IO和非直接IO
- 直接IO,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件
- 非直接IO:文件读写时,先要经过系统的页缓存,然后再由内核或者额外的系统调用,直接写入磁盘
要想实现直接IO,需要在系统调用中直接O_DIRECT
标志。如果没有设置则默认非直接IO
不过要注意,直接IO、非直接IO,本质上还是跟文件系统交互。如果是在数据库等场景中,你还会看到,跳过文件系统读写磁盘的情况。也就是裸IO
第三,根据应用程序是否阻塞自身运行,把文件IO分为阻塞IO和非阻塞IO
- 所谓阻塞IO,是指应用程序执行IO操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。
- 所谓非阻塞IO,是指应用程序执行IO操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。
比如,访问管道或者网络套接字时,设置O_NONBLOCK标志,就表示用非阻塞方式访问;如果不做任何设置,默认就是阻塞访问。
第四,根据是否等待响应结果,可以把文件IO分为同步和异步IO
- 所谓同步IO,是指应用程序执行IO操作后,要一直等到整个IO完成后,才能获得IO响应
- 所谓异步IO,是指应用程序执行IO操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次IO完成后,响应就会用事件通知的方式,告诉应用程序。
举个例子,在访问管道或者⽹络套接字时,设置了O_ASYNC选项后,相应的I/O就是异步I/O。这样,内核会再通过SIGIO或者
SIGPOLL,来通知进程⽂件是否可读写。
性能观察
容量
对文件系统来说,最常见的一个问题就是空间不足。用df命令就能查看文件系统的磁盘空间使用情况:
$ df /dev/sda1
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda1 30308240 3167020 27124836 11% /
可以看到,根⽂件系统只使⽤了11%的空间。这⾥还要注意,总空间⽤1K-blocks的数量来表示,可以给df加上-h选项,以获得更好的可读性
$ df -h /dev/sda1
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 29G 3.1G 26G 11% /`
不过有时候,明明碰到了空间不足的问题,但是用fd查看磁盘空间后,却发现剩余空间还有很多。为什么呢?
- 因为除了文件数据,索引节点也占空间。可以给df加上-i参数,查看索引节点的使用情况:
$ df -i /dev/sda1
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 3870720 157460 3713260 5% /
- 索引节点的容量(也就是inode个数)是在格式化磁盘时设置好的,一般由格式化工具自动生成。当你发现索引节点空间不足,但磁盘空间充足时,很可能就是过多小文件导致的。
所以,一般来说,删除这些小文件,或者把它们移动到索引节点充足的其他磁盘中,就可以解决这个问题
缓存
可以使用free或者vmstat,来观察页缓存的大小。free输出的cache,是页缓存和可回收stab缓存的和,你可以直接从/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这个⽂件
- ⽐如,运⾏下⾯的命令,就可以得到,所有⽬录项和各种⽂件系统索引节点的缓存情况:
- dentry⾏表示⽬录项缓存
- inode_cache⾏,表示VFS索引节点缓存
- 其余的则是各种⽂件系统的索引节点缓存。
$ 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
- 在实际性能分析中,我们更常使⽤ slabtop ,来找到占⽤内存最多的缓存类型。
- 从下面可以看到,在当前系统中,⽬录项和索引节点占⽤了最多的Slab缓存。
- 不过它们占⽤的内存其实并不⼤,加起来也只有23MB左右。
# 按下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
总结
本节主要讲了Linux文件系统的工作原理:
- 文件系统,是对存储设备上的文件,进行组织管理的一种机制。为了支持各种不同的文件系统,Linux在各种文件系统实现上,抽象了一层虚拟文件系统(VFS)
- VFS定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,就只需要跟VFS提供的统一接口进行交互
- 为了降低慢磁盘对性能的影响,文件系统又通过页缓存、目录项缓存以及索引节点缓存,缓和磁盘延迟对应用程序的影响
⽬录项是表示⽬录之间的树状关系,⽽⽂件名则会存储到数据部分。
⽬录项是⼀个缓存,不是持久化存储。⽬录也是⼀个⽂件,这个特殊⽂件保存了该⽬录的所有⽂件名与inode的对应关系