文件系统和磁盘的区别

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

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

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

通用块层

和 VFS 类似,为了减少不同设备的差异带来的影响,Linux 通过统一的通用块(块 I/O 层),管理不同的块设备。

块设备层是处在文件系统和磁盘驱动中间的一个块设备抽象层,主要功能是:

  • 向上为文件系统和应用程序提供访问块设备的标准接口;向下,把各种异构的磁盘块设备抽象为统一的块设备,提供统一框架管理这些设备的驱动程序
  • 通用块层还会给文件系统和应用程序发来 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率

对 I/O 请求排序的过程就是 I/0 调度,Linux 支持的四种 I/O 调度算法,分别是 NONE、NOOP、CFQ 以及 DeadLine:

  • NONE,不适用任何调度,对 I/O 不做任何处理(常用在虚拟机,此时磁盘 I/O 完全由物理机复杂)
  • NOOP,先入先出调度(常用在 SSD)
  • CFQ(Completely Fair Schedule)完全公平调度器,很多 Linux 发行版的默认调度器,它为每个进程维护了一个 I/O 调度队列,按照时间片来均匀分配每个进程的 I/O 请求;还支持优先级调度,适用于大量进程的系统(如桌面、多媒体应用等)
  • DeadLine 调度算法,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保打到最终期限(deadline)的请求被优化处理,多用在 I/O 压力比较重的场景,比如数据库等

I/O 栈

示意图

根据这张 I/O 栈的全景图,可以看出存储系统 I/O 的工作原理

  • 文件系统层,包括虚拟文件系统和其他文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过快层,来存储和管理磁盘资源
  • 通用块层,包括块设备 I/O 队列和 I/O 调度器。他会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发给下一级的设备层
  • 设备层,包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作

存储系统的 I/O,通常是整个系统最慢的一环。

Linux 通过多种缓存机制来优化 I/O 效率。为了优化文件访问的性能,会使用页缓存、索引节点缓存等多种机制,以减少对下层块设配的直接调用。同样,为了优化块设备,会使用缓冲区,来缓存块设备的数据。

磁盘性能指标

使用率、饱和度、IOPS、吞吐量以及响应时间五个指标,是磁盘性能的基本指标

  • 使用率,是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈
  • 饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求
  • IOPS(Input/Output Per Second),是指每秒的 I/O 请求数
  • 吞吐量,是指每秒 I/O 请求大小
  • 响应时间,是指 I/O 请求从发出到收到响应的间隔时间

注意:

  1. 使用率只考虑有没有 I/O,而不考虑 I/O 的大小。换句话说,当使用率是 100% 的时候,磁盘依然有可能接受到新的 I/O 请求
  2. 随即读写多(如数据库、大量小文件)的情况下主要关注 IOPS,而顺序读写(如流媒体)的情况下,主要关注吞吐量

在为应用程序的服务器选型时,要先对磁盘的 I/O 性能进行基准测试,以便可以准确评估,磁盘性能是否可以满足应用程序的需求

磁盘 I/O 观测

使用 iostat 观测每块磁盘的使用情况,提供了每个磁盘的使用率、IOPS、吞吐量等各种常见的性能指标,这些指标实际上来自 /proc/diskstats

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ iostat -dx 1
Linux 3.10.0-1160.76.1.el7.x86_64 (idc16) 20250616日 _x86_64_ (40 CPU)

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 0.69 1.56 25.12 62.56 270.97 25.01 0.27 10.01 0.84 10.58 1.18 3.14
sdd 0.00 0.27 0.17 1.33 10.66 42.08 70.12 0.01 8.48 6.95 8.68 0.26 0.04
sdc 0.00 0.61 0.27 9.80 13.02 193.43 41.03 0.04 3.52 0.82 3.59 0.06 0.06
sde 0.00 0.22 0.21 1.38 11.79 43.80 70.07 0.01 8.43 7.23 8.61 0.26 0.04
sdf 0.00 0.24 0.07 1.29 13.02 18.95 47.09 0.00 3.29 17.06 2.56 0.20 0.03
sdh 0.00 0.26 0.01 1.32 1.49 19.72 31.78 0.00 1.70 9.23 1.63 0.09 0.01
sdg 0.00 0.23 0.05 1.30 11.18 21.33 48.10 0.01 4.07 24.12 3.28 0.21 0.03
sdi 0.00 0.28 0.04 1.67 9.65 19.50 33.96 0.00 1.86 25.33 1.26 0.15 0.03
sdj 0.00 0.24 0.36 2.35 21.61 247.35 198.58 0.14 51.46 4.47 58.60 0.49 0.13
sdb 0.00 0.46 1.66 8.00 180.35 210.13 80.81 0.08 8.74 2.84 9.97 0.20 0.19
dm-0 0.00 0.00 0.85 18.11 38.11 144.32 19.24 0.17 8.82 0.56 9.21 1.14 2.16
dm-1 0.00 0.00 0.00 0.00 0.00 0.00 49.16 0.00 11.10 11.10 0.00 9.62 0.00
scd0 0.00 0.00 0.00 0.00 0.00 0.00 80.31 0.00 813.35 813.35 0.00 809.27 0.00
dm-2 0.00 0.00 0.00 0.21 0.04 16.16 155.78 0.02 99.54 2.63 100.60 1.79 0.04
dm-3 0.00 0.00 0.68 4.54 22.80 72.63 36.54 0.07 14.35 1.04 16.35 1.31 0.69
dm-4 0.00 0.00 0.02 2.93 0.38 37.73 25.90 0.03 10.70 4.72 10.73 1.32 0.39

各个指标解读如下

iostat 指标解读图

注意:

  • %util,就是我们前面提到的磁盘 I/O 使用率
  • r/s + w/s,就是 IOPS
  • rkB/s + wkB/s,就是吞吐量
  • r_await+w_await,就是响应时间

在观测指标时,可以结合请求的大小(rareq-sz 和 wareq-sz)一起分析。

进程 I/O 观测

pidstat 可以实时查看某个进程的 I/O 情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ pidstat -d 1
Linux 3.10.0-957.el7.x86_64 (xxx) 06/17/2025 _x86_64_ (8 CPU)

07:12:18 PM UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
07:12:19 PM 27 362006 0.00 594.17 590.29 mysqld
07:12:19 PM 0 362323 0.00 3.88 3.88 docker-containe
07:12:19 PM 0 362403 0.00 7.77 0.00 kundb-meta-serv
07:12:19 PM 0 362518 0.00 3.88 0.00 java
07:12:19 PM 65534 363123 0.00 3.88 0.00 prometheus
07:12:19 PM 0 372218 0.00 7.77 0.00 java
07:12:19 PM 0 383196 0.00 19.42 0.00 dockerd
07:12:19 PM 0 389626 0.00 23.30 0.00 kube-apiserver
07:12:19 PM 0 389731 0.00 62.14 0.00 etcd
07:12:19 PM 0 391157 0.00 11.65 0.00 kubelet
07:12:19 PM 0 760300 0.00 11.65 0.00 kundb-meta-serv

指标如下:

  • 用户 ID(UID)和进程 ID(PID)
  • 每秒读取的数据大小(kB_rd/s),单位是 KB
  • 每秒发出的写请求数据大小(kB_wr/s),单位是 KB
  • 每秒取消的写请求数据大小(KB_ccwr/s),单位是 KB