Linux 文件系统原理

Linux 下一切皆文件。

索引节点和目录项

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

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

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

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

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

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

索引节点和目录项纪录了文件的元数据,以及文件间的目录关系.

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

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

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

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

虚拟文件系统

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

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

理解系统调用、VFS、缓存、文件系统以及块存储之间的关系:

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

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

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

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

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

文件系统 I/O

把文件系统挂载到挂载点后,你就能通过挂载点,再去访问它管理的文件了。VFS 提供了一-组标准的文件访问接口。这些接口以系统调用的方式,提供给应用程序使用。

就拿cat命令来说,它首先调用open(),打开一个文件;然后调用read(),读取文件的内容;最后再调用write(),把文件内容输出到控制台的标准输出中:

1
2
3
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。

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

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

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

第二,根据是否利用操作系统的页缓存,可以把文件I/O0分为直接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/0操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。

  • 所谓非阻塞I/O, 是指应用程序执行I/O操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。

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

第四,根据是否等待响应结果,可以把文件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 中。

缓存

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@linux:~# 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 94 94 688 47 8 : tunables 0 0 0 : slabdata 2 2 0
mqueue_inode_cache 34 34 960 34 8 : tunables 0 0 0 : slabdata 1 1 0
fuse_inode 0 0 832 39 8 : tunables 0 0 0 : slabdata 0 0 0
ecryptfs_inode_cache 0 0 1024 32 8 : tunables 0 0 0 : slabdata 0 0 0
fat_inode_cache 0 0 744 44 8 : tunables 0 0 0 : slabdata 0 0 0
squashfs_inode_cache 0 0 704 46 8 : tunables 0 0 0 : slabdata 0 0 0
ext4_inode_cache 17811 18330 1088 30 8 : tunables 0 0 0 : slabdata 611 611 0
hugetlbfs_inode_cache 52 52 624 52 8 : tunables 0 0 0 : slabdata 1 1 0
sock_inode_cache 1610 1610 704 46 8 : tunables 0 0 0 : slabdata 35 35 0
shmem_inode_cache 3456 4094 712 46 8 : tunables 0 0 0 : slabdata 89 89 0
proc_inode_cache 22250 22944 680 48 8 : tunables 0 0 0 : slabdata 478 478 0
inode_cache 22917 24327 608 53 8 : tunables 0 0 0 : slabdata 459 459 0
dentry 66224 70728 192 42 2 : tunables 0 0 0 : slabdata 1684 1684 0

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# slabtop  # 按c 按照缓存大小排序,按a 按照活跃对象排序

Active / Total Objects (% used) : 353223 / 365205 (96.7%)
Active / Total Slabs (% used) : 7710 / 7710 (100.0%)
Active / Total Caches (% used) : 75 / 114 (65.8%)
Active / Total Size (% used) : 103736.12K / 108805.45K (95.3%)
Minimum / Average / Maximum Object : 0.01K / 0.30K / 8.00K

OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
70728 66224 0% 0.19K 1684 42 13472K dentry
57600 57600 100% 0.13K 960 60 7680K kernfs_node_cache
28743 28743 100% 0.10K 737 39 2948K buffer_head
24327 22917 0% 0.59K 459 53 14688K inode_cache
22944 22250 0% 0.66K 478 48 15296K proc_inode_cache
18330 17811 0% 1.06K 611 30 19552K ext4_inode_cache
15912 15912 100% 0.04K 156 102 624K ext4_extent_status
11008 11008 100% 0.03K 86 128 344K kmalloc-32
10944 10944 100% 0.06K 171 64 684K pid
9632 9350 0% 0.50K 301 32 4816K kmalloc-512
9009 8811 0% 0.20K 231 39 1848K vm_area_struct
8960 8608 0% 0.06K 140 64 560K kmalloc-64
7168 7168 100% 0.01K 14 512 56K kmalloc-8
6624 5715 0% 0.25K 207 32 1656K filp
6144 6144 100% 0.02K 24 256 96K kmalloc-16
5880 4378 0% 0.57K 105 56 3360K radix_tree_node
5750 5750 100% 0.09K 125 46 500K anon_vma
4578 4578 100% 0.19K 109 42 872K cred_jar
4094 3456 0% 0.70K 89 46 2848K shmem_inode_cache
3400 3400 100% 0.05K 40 85 160K ftrace_event_field
3360 3360 100% 0.09K 80 42 320K kmalloc-96
2982 2982 100% 0.19K 71 42 568K kmalloc-192
2816 2371 0% 0.25K 88 32 704K kmalloc-256
2128 2128 100% 0.07K 38 56 152K Acpi-Operand
1792 1772 0% 1.00K 56 32 1792K kmalloc-1024
1610 1610 100% 0.69K 35 46 1120K sock_inode_cache
1460 1168 0% 0.05K 20 73 80K mbcache
1380 1380 100% 0.09K 30 46 120K trace_event_file
1360 1360 100% 0.02K 8 170 32K lsm_file_cache
1326 1326 100% 0.04K 13 102 52K Acpi-Namespace
1152 1152 100% 0.06K 18 64 72K kmem_cache_node
1134 1134 100% 0.38K 27 42 432K kmem_cache
1024 1024 100% 0.12K 32 32 128K kmalloc-128
928 928 100% 2.00K 58 16 1856K kmalloc-2048
896 896 100% 0.07K 16 56 64K eventpoll_pwq
816 680 0% 0.12K 24 34 96K jbd2_journal_head
782 782 100% 0.69K 17 46 544K files_cache
672 672 100% 1.00K 21 32 672K signal_cache
640 640 100% 0.12K 20 32 80K eventpoll_epi
420 348 0% 5.69K 84 5 2688K task_struct
420 420 100% 0.38K 10 42 160K mnt_cache
390 390 100% 2.06K 26 15 832K sighand_cache
345 345 100% 2.06K 23 15 736K mm_struct

问题

以下常用的find 搜索方式会不会影响系统缓存?

1
find / -name ls

实验:

1
2
3
4
5
6
7
8
root@linux:~# echo 3 > /proc/sys/vm/drop_caches ;sync
root@linux:~# find / -name ls
/usr/lib/klibc/bin/ls
/bin/ls
/var/lib/docker/overlay2/b720c3b3b6d8c9b5043f2a84687546e5370495209f04cf72d1129b59c6c6de8c/diff/bin/ls
/var/lib/docker/overlay2/a8967911e4f5504993174d9beb3212fdd64c173dfa1d01adf96255cb01d5c9f0/diff/bin/ls
/var/lib/docker/overlay2/c3560098e9aa20adc9df744ee722dd6b90d8c871dd14b9a68e01f98fcdd9d53a/diff/bin/ls
root@linux:~#

结果slabtop 观察,发现影响 ext4_inode_cache、proc_inode_cache、inode_cache、dentry

1
2
3
4
5
6
7
8
9
10
11
Active / Total Objects (% used)    : 335663 / 352629 (95.2%)
Active / Total Slabs (% used) : 7444 / 7444 (100.0%)
Active / Total Caches (% used) : 75 / 114 (65.8%)
Active / Total Size (% used) : 99769.48K / 105041.85K (95.0%)
Minimum / Average / Maximum Object : 0.01K / 0.30K / 8.00K

OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
17220 16731 0% 1.06K 574 30 18368K ext4_inode_cache
23424 22928 0% 0.66K 488 48 15616K proc_inode_cache
22631 21262 0% 0.59K 427 53 13664K inode_cache
69048 61225 0% 0.19K 1644 42 13152K dentry

再次执行 find命令,观察slabtop 发现无任何变化