Linux I/O 原理与分析
- I/O 性能指标
- 文件系统
- 存储空间容量、使用量和剩余空间
- 索引节点容量、使用量和剩余量
- 缓存
- 页缓存
- 目录项缓存
- 索引节点缓存
- 物理文件系统缓存
- IOPS(文件 I/O)
- 响应时间(延迟)
- 吞吐量(B/s)
- 磁盘
- 使用率
- IOPS
- 吞吐量(B/s)
- 响应时间(延迟)
- 缓冲区
- 相关因素
- 读写类型(顺序或随机)
- 读写比例
- 读写大小
- 存储类型(RAID 级别、本地或网络等·)
先附上 CPU、内存、磁盘、网络各种操作执行时间开销的直观对比:
操作 | 参考时间 | 对比基本单位 | |
---|---|---|---|
CPU | 执行一般指令 | 0.37ns | 1s |
CPU | 从一级缓存读取数据 | 0.5ns | 1.3s |
CPU | 分支预测错误 | 5ns | 13s |
CPU | 从二级缓存读取数据 | 7ns | 18.2s |
CPU | 加/解互斥锁 | 25ns | 65s |
内存 | 寻址 | 100ns | 4min |
CPU | 上下文切换 | 1500ns | 65min |
网络 | 1Gbps 网络上传输 2KB | 20μs | 14.4h |
固态硬盘 | 随机访问 | 150μs | 4.5d |
内存 | 顺序读取 1MB | 250μs | 7.5d |
网络 | 数据中心网络来回 | 0.5ms | 15d |
固态硬盘 | 顺序读取 1MB | 1ms | 1m |
机械硬盘 | 寻址 | 10ms | 10m |
机械硬盘 | 顺序读取 1MB | 20ms | 20m |
网络 | 世界上任意两地来回(平均) | 150ms | 12.5y |
虚拟服务器 | 重启 | 5s | 300y |
物理服务器 | 重启 | 5min | 25,000y |
文件系统与磁盘
由于磁盘是 块设备,可被划分为不同的 分区,在磁盘或者分区上可再创建 文件系统,并挂载到系统的某个目录中,系统通过挂载目录来读写文件。
Linux 中一切皆文件,因此可通过相同的文件接口来访问磁盘和文件。磁盘读写的原理:磁盘读写的最小单位是扇区(512B),为了提高读写效率,文件系统把连续的扇区组成逻辑块,每次都以逻辑块(常见 4KB,即连续的 8 个扇区)为最小单元来管理数据。其有两种 I/O 方式:
普通文件 :I/O 请求首先经过文件系统,由文件系统与磁盘交互。
块设备文件:跳过文件系统、直接与磁盘交互,即 裸 I/O。
两种 I/O 方式使用的缓存不同,文件系统管理的缓存是 Cache 的一部分,裸磁盘的缓存则是使用 Buffer。
文件系统
Linux 文件系统的四大基本要素:目录项、索引节点、逻辑块、超级块,接下来是具体的解析。
索引节点和目录项
文件系统 是对存储设备上的文件进行组织管理的机制。由于 Linux 中一切皆文件,除了普通的文件和目录,块设备、套接字、管道等也都通过统一的文件系统来管理,并使用两个数据结构记录文件元信息和目录结构:
索引节点(index node,即 inode):用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据位置 等,与文件一一对应,并会被持久化存储到磁盘中(因此也占用磁盘空间)。
目录项(directory entry,即 entry):用来记录 文件名称、索引节点指针 以及 与其他目录项的关联关系(树状结构)。多个关联的目录项构成文件系统的目录结构。目录项是由内核维护的一个内存数据结构,通常也称为 目录项缓存。
索引节点的容量是在格式化磁盘时设定好的,由格式化工具自动生成。当发现索引节点空间不足、但磁盘空间充足时,很可能就是由过多小文件导致,此时应删除小文件或移动到索引节点充足的其它位置。
目录项和索引节点的关系是多对一,一个文件可以有多个别名(比如建立硬链接为文件创建别名)。由于前者是内存缓存数据,后者是磁盘数据,为了协调磁盘与 CPU 的性能差异,文件内容会缓存到页缓存 Cache 中。
磁盘在执行文件系统格式化时,会被分成三个存储区域:
超级块:存储整个文件系统的状态。
索引节点区:存储索引节点。
数据块区:存储文件数据。

虚拟文件系统(VFS)
即为了支持不同的文件系统,Linux 内核在用户进程和文件系统之间引入的抽象层。
VFS 定义一组所有文件系统都支持的数据结构和标准接口。用户进程和内核中的其他子系统与 VFS 提供的统一接口进行交互,而不需要再关心底层文件系统的实现细节。
VFS 支持的各种文件系统可分为三类:
磁盘文件系统:把数据直接存储在计算机本地挂载的磁盘中,如 Ext4、XFS、OverlayFS 等。
内存文件系统:即虚拟文件系统,不需要磁盘分配存储空间、但会占用内存。比如
/proc
、/sys
(向用户空间导出层次化的内核对象)。网络文件系统:用于访问其他计算机数据的文件系统,如 NFS、SMB、iSCSI 等。
文件系统要先挂载到 VFS 目录树中的某个子目录(挂载点)才能访问其中的文件。比如在安装系统时,要先挂载一个根目录(/),在根目录下再把其他文件系统(其他的磁盘分区、/proc、/sys、NFS 等)挂载进来。
Page Cache
已知应用程序通过系统调用向内核发起 I/O 操作,在进入块 I/O 层之前有以下几种情况:
Memory-Mapped I/O:通过内存映射把数据写入 Page Cache。
Buffered I/O:由 VFS 处理系统调用,把数据写入 Page Cache。
Direct I/O:由 VFS 处理系统调用,不经过 Page Cache 直接写入块 I/O 层。
因此 Page Cache 是内核态内存,不属于用户。
文件系统 I/O
文件系统被挂载后,就可以通过挂载点访问其管理的文件。VFS 有标准的文件访问接口,以系统调用的方式提供给应用程序使用。比如 cat
命令,就是依次调用 open()
、read()
、write()
函数来执行:
1 | int open(const char *pathname, int flags, mode_t mode); |
以读取磁盘文件内容、通过网卡发送的场景为例。涉及以下几个环节:
1 | [磁盘文件] -> [PageCache](内核态)-> [用户缓冲区](用户态)-> Socket 缓冲区(内核态)-> [网卡] |
其中从内核态到用户态再到内核态涉及 2 次拷贝,再加上读磁盘和写网卡一共 4 次拷贝。
由于用户进程缓冲区空间有限(比如 32KB),要发送一个大文件(假设 320MB)就需要 320MB / 32KB 共 10,000 次才能完成,即 40,000 次上下文切换。
直接与非直接 I/O
直接 I/O:跳过操作系统页缓存,直接与文件系统交互访问文件。
非直接 I/O:文件读写时经过系统页缓存,再由内核或额外的系统调用真正写入磁盘。
在系统调用中指定 O_DIRECT 标志即实现直接 I/O(默认非直接 I/O)。直接 I/O、非直接 I/O,本质上还是和文件系统交互;在数据库等场景中也有裸 I/O,即跳过文件系统读写磁盘的情况。
零拷贝:属于非直接 I/O,其特点是无需经过用户缓冲区(用户态),即只有 2 次上下文切换,3 次内存拷贝;如果网卡支持 SG-DMA 技术,则无需 Socket 缓冲区,可以进一步优化为 2 次上下文切换和 2 次内存拷贝。
1 | [磁盘文件] -> [Page Cache] -> Socket 缓冲区 -> [网卡] |
零拷贝有以下特点:
开发者无需关心 Socket 缓冲区大小,把发送字节数设为文件未发送字节数(比如 320MB),则一次性经网卡发出的数据量为 Socket 缓冲区大小(比如 1.4MB),上下文切换次数和内存拷贝次数约为 2 * 320MB / 1.4MB 共 457 次,拷贝数据量也只有 640MB(耗费 CPU 资源更少)。
利用 Page Cache:Page Cahce 基于 LRU 缓存最近访问的数据,效率远高于访问磁盘。再加上预读出更多数据,在淘汰出 Page Cache 前就被进程访问,发挥更大收益。
使用零拷贝不允许进程在发送操作前对文件内容进行加工(比如压缩)。
当文件太大,文件中某一部分内容被再次访问的概率非常低,却耗费 CPU 或 DMA 的多一次拷贝;而且大文件快速把 Page Cache 占满,其它热点小文件就无法利用 Page Cache。
直接 I/O 的应用场景:
由应用程序实现磁盘文件缓存,避免 Page Cache 额外的性能消耗(比如 MySQL)。
高并发下传输大文件,本身难以命中 Page Cache、带来额外的内存拷贝、挤占小文件 I/O 的内存,使用 直接 I/O 更合理。
除此之外,直接 I/O 无法享用基于 Page Cache 的 磁盘预读 和 I/O 合并(即内核 I/O 调度试图在 Page Cache 中缓存尽量多的连续 I/O,在合并成大 I/O 后再一次发给磁盘以减少寻址操作)带来的性能提升,因此应用场景很有限。
阻塞与非阻塞 I/O
阻塞 / 非阻塞针对的是 I/O 调用者(即应用程序)。
阻塞 I/O:应用程序执行 I/O 操作后如没有获得响应就会阻塞当前线程,不能执行其他任务。
非阻塞 I/O:应用程序执行 I/O 操作后不会阻塞当前的线程,可以继续执行其它任务,随后再通过轮询或者事件通知的形式获取调用结果。
比如访问管道或者网络套接字时,设置 O_NONBLOCK 标志就表示用非阻塞方式访问(默认阻塞,即 send()
操作的线程阻塞,无法去做其它事)。
如果使用 epoll,系统会告诉该套接字的状态,因此可以用非阻塞的方式使用。当这个套接字不可写时,可以转去做其它事,比如读写其它套接字。
同步与异步 I/O
同步 / 异步针对的是 I/O 执行者(即系统)。
同步 I/O:应用程序发起 I/O 操作后要等到整个 I/O 完成后,才能获得 I/O 响应。
异步 I/O:应用程序发起 I/O 操作后不等待完成,I/O 完成后响应由内核以事件通知应用程序。
操作文件时设置 O_DSYNC(等文件数据写入磁盘后才能返回)或 O_SYNC(在前者基础上要求文件元数据也要写入磁盘后才能返回)标志就代表同步 I/O。比如访问管道或者网络套接字时设置了 O_ASYNC 选项,相应的 I/O 就是异步 I/O,内核会再通过 SIGIO 或者 SIGPOLL,来通知进程文件是否可读写。
异步 I/O 解决了阻塞问题,但其实现上存在无法利用 PageCache 的缺陷,只支持直接 I/O。
1 | [磁盘文件] -> [用户缓冲区](用户态)-> Socket 缓冲区(内核态)-> [网卡] |
缓冲与非缓冲 I/O
缓冲指标准库内部实现的缓存,由于不论是否被标准库缓存,最终还是经过系统调用来访问文件,系统调用后再通过页缓存来减少磁盘的 I/O 操作。
缓冲 I/O:利用标准库缓存加速文件访问,标准库内部再通过系统调度访问文件。
非缓冲 I/O:直接通过系统调用来访问文件,不经过标准库缓存。
比如很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存。
如何监控?
使用 df
查看文件系统磁盘空间、索引节点使用情况:
1 | df -h /dev/sda1 |
通过 /proc/meminfo
查看缓存大小:
1 | cat /proc/meminfo | grep -E "SReclaimable|Cached" |
内核使用 Slab 机制管理目录项和索引节点的缓存。/proc/meminfo
只给出了 Slab 的整体大小,查看 /proc/slabinfo
可查看 Slab 缓存具体情况(所有目录项和各种文件系统索引节点的缓存):
1 | cat /proc/slabinfo | grep -E '^#|dentry|inode' |
使用 slabtop
查看目录项和索引节点占用的 Slab 缓存。
1 | slabtop |
磁盘 I/O
磁盘是可以持久化存储的设备,根据存储介质分类:
机械磁盘(Hard Disk Driver,HDD):最小读写单位为扇区(一般 512Bytes)。主要由盘片和读写磁头组成,数据就存储在盘片的环状磁道中。在读写数据前需要移动读写磁头,定位到数据所在的磁道才能访问数据。如果 I/O 请求刚好连续则不需要磁道寻址,且可以通过预读的方式减少 I/O 次数获得最佳性能。与之相对随机 I/O 需要更多的磁头寻道和盘片旋转,读写速度较慢。
固态磁盘 (Solid State Disk,SSD):最小读写单位为页(一般 4KB、8KB)。由固态电子元器件组成,不需要磁道寻址,连续 I/O 和随机 I/O 的性能都比机械磁盘好。但同样存在“先擦除再写入”的限制,随机读写导致大量的垃圾回收,所以随机 I/O 的性能比起连续 I/O 来差很多。
根据接口来分类: 不同的接口往往分配不同的设备名称。IDE(Integrated Drive Electronics,前缀 hd)、SCSI(Small Computer System Interface,前缀 sd) 、SAS(Serial Attached SCSI,前缀 sd) 、SATA(Serial ATA,前缀 sd) 、FC(Fibre Channel) 等。多块同类型的磁盘会按照 a、b、c 等的字母顺序来编号。
根据架构分类:把磁盘接入服务器后,按照不同的使用方式可以把它们划分为多种不同的架构。
直接作为独立磁盘设备来使用,在磁盘上划分不同的逻辑分区(/dev/sda1)。
把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列(Redundant Array of Independent Disks,RAID),以提高读写性能或提供冗余。
把磁盘组合成网络存储集群,再通过 NFS、SMB、iSCSI 等网络存储协议暴露给服务器使用。
在 Linux 中磁盘以块为单位读写数据(块设备),并支持随机读写。每个块设备在驱动程序中被赋予主设备号(用来区分设备类型)和次设备号(给多个同类设备编号)。
通用块层
即在文件系统和磁盘驱动中间的一个块设备抽象层。
与虚拟文件系统的功能类似。向上为文件系统和应用程序提供访问块设备的标准接口;向下把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序。
I/O 调度:给文件系统和应用程序发来的 I/O 请求排队,并通过重新排序、请求合并等方式提高磁盘读写的效率。
I/O 调度算法:
NONE:对文件系统和应用程序的 I/O 其实不做任何处理,常用在虚拟机中(此时磁盘 I/O 调度完全由物理机负责)。
NOOP:一个先入先出的队列,只做一些最基本的请求合并,常用于 SSD 磁盘。
CFQ:完全公平调度器(Completely Fair Scheduler),通常是默认的 I/O 调度器。为每个进程维护了一个 I/O 调度队列,按照时间片均匀分布每个进程的 I/O 请求;另外还支持进程 I/O 的优先级调度,适用于运行大量进程的系统。
DeadLine:分别为读、写请求创建 I/O 队列,提高机械磁盘的吞吐量,并确保达到最终期限的请求被优先处理。多用在 I/O 压力比较重的场景,比如数据库等。
I/O 栈
存储系统 I/O 的工作原理:
文件系统层:虚拟文件系统和物理文件系统。为上层应用程序提供标准的文件访问接口;对下通过通用块层来存储和管理磁盘数据。
通用块层:包括块设备 I/O 队列和 I/O 调度器。会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层。
设备层:包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作。
存储系统的 I/O 是整个系统中最慢的部分,Linux 通过多种缓存机制来优化 I/O 效率:
优化文件访问:使用页缓存、索引节点缓存、目录项缓存等多种缓存机制减少对下层块设备的直接调用。
优化块设备访问:使用缓冲区来缓存块设备的数据。

磁盘性能指标
衡量磁盘性能需要考虑:
使用率:磁盘处理 I/O 的时间百分比。过高的使用率(80%+)通常意味着磁盘 I/O 存在性能瓶颈。只考虑是否有 I/O,不考虑 I/O 大小。
饱和度:磁盘处理 I/O 的繁忙程度。过高的饱和度意味着磁盘存在严重的性能瓶颈(100% 时磁盘无法接受新的 I/O 请求),大量顺序读写场景下更能反映系统性能(比如多媒体)。
IOPS(Input/Output Per Second):每秒的 I/O 请求数,大量随机读写场景下更能反映系统性能(比如数据库、大量小文件)。
吞吐量:每秒的 I/O 请求大小。
响应时间:I/O 请求从发出到收到响应的间隔时间。
以上指标可使用工具 fio
结合具体应用场景(顺序读/写,随机读/写)进行基准测试来评估。
饱和度通常没有简单的观测方法,但可以把观测到的,平均请求队列长度或者读写请求完成的等待时间,与基准测试的结果(比如通过 fio)进行对比,综合评估磁盘的饱和情况。
如何监控?
使用 iostat
命令查看每个磁盘的使用率、IOPS、吞吐量等(数据源于 /proc/diskstats
):
1 | -d -x 表示显示所有磁盘 I/O 的指标 |

使用 pidstat
观察进程 I/O 情况:
1 | pidstat -d 1 |
其每列的含义:
用户 ID(UID)和进程 ID(PID) 。
每秒读取的数据大小(kB_rd/s) ,单位 KB。
每秒发出的写请求数据大小(kB_wr/s) ,单位 KB。
每秒取消的写请求数据大小(kB_ccwr/s) ,单位 KB。
块 I/O 延迟(iodelay),包括等待同步块 I/O 和换入块 I/O 结束的时间,单位时钟周期。
使用 iotop
根据 I/O 大小对进程进行排序:
1 | iotop |
优化总结
应用优化
大文件异步 I/O 和直接 I/O,小文件零拷贝(文件大小的阈值可灵活配置,参考 Nginx 的 directio 指令)。
文件系统优化
根据实际负载场景选择最适合的文件系统。比如相比于 ext4(Ubuntu 默认),xfs(CentOS 默认)支持更大的磁盘分区(>16TB)和更大的文件数量,但缺点在于无法收缩,而 ext4 则可以。
优化文件系统的配置选项。包括文件系统的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback)、挂载选项(如 noatime)等。比如使用
tune2fs
可以调整文件系统的特性(也可以查看文件系统超级块的内容)。 而通过/etc/fstab
或mount
命令行参数,可以调整文件系统的日志模式和挂载选项等。优化文件系统的缓存。比如可以优化 pdflush 脏页的刷新频率(设置 dirty_expire_centisecs 和 dirty_writeback_centisecs)以及脏页的限额(调整 dirty_background_ratio 和 dirty_ratio 等)。还可以优化内核回收目录项缓存和索引节点缓存的倾向,即调整 vfs_cache_pressure(
/proc/sys/vm/vfs_cache_pressure
,默认值 100),数值越大表示越容易回收。使用内存文件系统 tmpfs 以获得更好的 I/O 性能 。tmpfs 把数据直接保存在内存中,而不是磁盘中。比如
/dev/shm
就是大多数 Linux 默认配置的一个内存文件系统,大小默认为总内存的一半,在不需要持久化时建议使用。
磁盘优化
换用性能更好的磁盘,比如用 SSD 替代 HDD。
使用 RAID 把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列。可以提高数据的可靠性和数据的访问性能。
针对磁盘和应用程序 I/O 模式的特征,选择最适合的 I/O 调度算法。比如 SSD 和虚拟机中的磁盘通常用 noop 调度算法。而数据库应用更推荐使用 deadline 算法。
对应用程序的数据进行磁盘级别的隔离。比如可以为日志、数据库等 I/O 压力比较重的应用,配置单独的磁盘。
在顺序读比较多的场景中可以增大磁盘的预读数据,比如调整 /dev/sdb 的预读大小:
调整选项
/sys/block/sdb/queue/read_ahead_kb
,默认大小是 128 KB,单位为 KB。使用
blockdev
工具设置,比如blockdev --setra 8192 /dev/sdb
,单位是 512B(0.5KB),其数值总是 read_ahead_kb 两倍。
优化内核块设备 I/O 的选项。比如可以适当增大磁盘队列长度
/sys/block/sdb/queue/nr_requests
,以提升磁盘的吞吐量(也会导致 I/O 延迟增大)。发现性能急剧下降时还需要确认磁盘本身是否出现硬件错误:使用
dmesg
查看是否有硬件 I/O 故障的日志;使用badblocks
、``smartctl等工具检测磁盘硬件问题,或用 e2fsck 等来检测文件系统的错误;如果发现问题可以使用
fsck` 等工具来修复。


参考
分析案例可参考: