文件系统和磁盘的区别

磁盘是一个存储设备(确切来说是块设备),可以划分为不同的磁盘分区。而在磁盘或者磁盘分区上,还可以在创建文件系统,并挂载到系统的某个目录。这样,系统就可以通过这个挂载目录,来读写文件。

换句话说,磁盘是存储数据的块设备,也是文件系统的载体。所以,文件系统确实还是要通过磁盘,来保证数据的持久化存储。

Linux 中一切皆文件。可以通过相同的文件接口,来访问磁盘和文件(比如 open、read、write、close 等)

  • 通常说的“文件”,是指普通文件
  • 磁盘和分区,是指块设备文件

在读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责,来与磁盘进行交互。而读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的“裸 I/O”。文件系统管理的缓存,是 Cache 的一部分;而裸磁盘的缓存,用的正是 Buffer。

裸磁盘,也称为原始磁盘,是一种未被任何文件系统(如NTFS、FAT32)格式化或管理的磁盘。换句话说,它是直接与磁盘硬件交互,而不通过操作系统的文件系统层进行访问。

缓存 I/O 与直接 I/O(裸磁盘 I/O )的对比

特性缓存I/O(文件系统)直接I/O(裸磁盘)
数据流磁盘 → 内核缓冲区 → 应用程序地址空间磁盘 → 直接应用程序地址空间
缓存使用使用文件系统管理的Cache使用磁盘的Buffer
性能适合常规文件操作,减少磁盘读写次数适合高性能场景,如数据库,减少文件系统开销
应用场景普通文件读写,系统默认方式虚拟化、数据库优化、低级别磁盘操作
优点保护系统安全,减少直接磁盘访问风险降低数据复制开销,提高I/O效率
缺点数据复制开销高,CPU和内存占用多需要应用程序管理缓存,可能增加复杂性

Linux 文件系统如何工作

索引节点和目录项

  • 索引节点(inode),和文件一一对应,存储在磁盘中,记录文件的元数据
  • 目录项(dentry),记录文件的名字、索引节点以及其他目录项的关联关系

举例说明,为文件创建的硬链接,会对应不同的目录项,他们都连接到同一个文件,索引节点相同。

磁盘的最小单位是扇区,文件系统将连续的扇区组成逻辑块,以逻辑块为最小单位,来读写磁盘数据。常见的逻辑块 4KB,由连续的 8 个扇区组成。

示意图

磁盘在执行文件系统格式化时,分为三个区域:超级块、索引节点和数据块:

  • 超级块:整个文件系统的状态
  • 索引节点区:存储索引节点
  • 数据块区:存储文件数据

虚拟文件系统

示意图

文件系统分类:

  • 基于磁盘的文件系统:常见的 ext4、XFS、OverlayFS 等,都是这类文件系统
  • 基于内存的文件系统:常说的虚拟文件系统,不需要磁盘空间,但是占用内存。比如,/proc 和 /sys
  • 网络文件系统:用于访问其他计算机的文件系统,比如 NFS、SMB、ISCSI 等

注意:这些文件系统,要先挂载到 VFS 目录树中的某个子目录(称为挂载点),然后才能访问其中的文件。

文件系统 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 完成后,才能获得 I/O 响应
  • 异步 I/O:应用程序不用等待 I/O 完成,会继续执行,等到 I/O 执行完成,会以事件的方式通知应用程序

设置 O_SYNC 或者 O_DSYNC,代表同步 I/O。如果是 O_DSYNC,要等到文件数据写入磁盘之后,才能返回,如果是 O_SYNC,是在 O_DSYNC 的基础上,要求文件元数据写入磁盘,才返回。

设置 O_ASYNC,代表异步 I/O,系统会再通过 SIGIO 或者 SIFPOLL 通知进程。

性能观测

容量

df 命令查看磁盘空间

1
2
3
4
5
6
7
8
$ df -h /dev/vdb
Filesystem Size Used Avail Use% Mounted on
/dev/vdb 400G 95G 306G 24% /var/lib/docker

# 查看索引节点所占的空间
$ df -i /dev/vdb
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vdb 209715200 546727 209168473 1% /var/lib/docker

当索引节点空间不足,但是索引空间充足时,可能是过多小文件导致的。解决方法一般是删除这些小文件,或者移动到索引节点充足的其他磁盘区。

缓存

可以使用 free 或者 vmstat,观察页缓存的大小;也可以查看 /proc/meminfo

1
2
3
4
$ cat /proc/meminfo | grep -E "SReclaimable|Cached"
Cached: 3987272 kB
SwapCached: 109532 kB
SReclaimable: 4095228 kB

内核使用 slab 机制,管理目录项和索引节点的缓存,/proc/meminfo 给出了整体的 slab 大小,/proc/slabinfo 可以查看每一种 slab 的缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@pudding-160 ~]# 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>
fuse_inode 189 189 768 21 4 : tunables 0 0 0 : slabdata 9 9 0
ovl_inode 11534 15600 680 24 4 : tunables 0 0 0 : slabdata 650 650 0
xfs_inode 175054 175372 960 34 8 : tunables 0 0 0 : slabdata 5158 5158 0
mqueue_inode_cache 288 288 896 36 8 : tunables 0 0 0 : slabdata 8 8 0
hugetlbfs_inode_cache 52 52 608 26 4 : tunables 0 0 0 : slabdata 2 2 0
sock_inode_cache 2255 2475 640 25 4 : tunables 0 0 0 : slabdata 99 99 0
shmem_inode_cache 6783 7056 680 24 4 : tunables 0 0 0 : slabdata 294 294 0
proc_inode_cache 28816 29688 656 24 4 : tunables 0 0 0 : slabdata 1237 1237 0
inode_cache 34463 35208 592 27 4 : tunables 0 0 0 : slabdata 1304 1304 0
dentry 20014134 20014134 192 21 1 : tunables 0 0 0 : slabdata 953054 953054 0
selinux_inode_security 15708 15708 40 102 1 : tunables 0 0 0 : slabdata 154 154 0

其中 dentry 代表目录项缓存,inode_cache 代表 VFS 索引节点缓存,其他的就是各种文件系统的索引节点缓存。

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

示例如下:可以看到,目录项占用了最多的 Slab 缓存,大约 3.91 G

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 按下c按照缓存大小排序,按下a按照活跃对象数排序 
$ slabtop
Active / Total Objects (% used) : 22757396 / 23018218 (98.9%)
Active / Total Slabs (% used) : 1067320 / 1067320 (100.0%)
Active / Total Caches (% used) : 72 / 103 (69.9%)
Active / Total Size (% used) : 4462258.28K / 4545420.86K (98.2%)
Minimum / Average / Maximum Object : 0.01K / 0.20K / 8.00K

OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
21574896 21574735 99% 0.19K 1027376 21 4109504K dentry
355914 286236 80% 0.10K 9126 39 36504K buffer_head
223872 205323 91% 0.06K 3498 64 13992K kmalloc-64
175372 175125 99% 0.94K 5158 34 165056K xfs_inode
57600 57529 99% 0.16K 2400 24 9600K xfs_ili
136528 53170 38% 0.57K 4876 28 78016K radix_tree_node
...

Linux 磁盘 I/O 工作原理

磁盘

根据存储介质,磁盘分为:

  • 机械磁盘,也称为硬盘驱动器(Hard Disk Driver),通常缩写为 HDD。机械磁盘主要有盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前,需要移动读写磁头,定位数据所在的磁道,才能访问数据。如果 I/O 请求刚好连续,就不需要磁道寻址,可以获得最佳性能。这就是连续 I/O 的工作原理。与之对应的是随机 I/O,它需要不停地移动磁头,来定位数据位置,读写速度就会比较慢。
  • 固态磁盘(Silid State Disk),通常缩写为 SSD,由固态电子元件组成。固态磁盘不需要磁盘寻址,不管是连续 I/O,还是随机 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)等。

磁盘介入服务器时,按照不通的使用方式,会划分为不用的架构:

  • 最简单的直接作为独立磁盘设备来使用
  • 将多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列(RAID),提高数据访问的性能,并增强数据存储的可靠性
  • 最后一种,是将磁盘组合成网络存储集群,再通过 NFS、SMB、ISCSI 等网络存储协议,暴露给服务器使用

在 Linux 中,磁盘是作为一个块设备来管理,以块为单位来读写,支持随机读写。每个块设备赋予两个设备号,分别是主、次设备号,主设备号用在驱动程序中,用来区分设备类型;次设备号用来在多个同类设备编号。