11.3 文件系统与 IO 模型 (阻塞 / 非阻塞 / 异步)

一句话

文件系统把 “路径” 翻译成 “块”,中间隔着一层 page cache;IO 模型解决的是 “等待数据时进程在干什么”——阻塞、非阻塞、多路复用、信号驱动、异步,五种选择决定了你能撑多少并发。

从 write 到磁盘:四层

层 做什么 关键点
VFS 统一接口(inode/dentry/file) 路径解析有缓存(dcache)
具体文件系统 ext4/xfs/btrfs:块分配、日志 journal 影响写放大
page cache 缓存文件页 write 默认只写缓存
块层 + 设备 调度、合并、下发 SSD/云盘决定真实延迟

所以 write() 返回成功 ≠ 落盘;要落盘必须 fsync()(或 O_SYNC)。

五种 IO 模型

模型 等待数据 等待就绪 典型调用 适合
阻塞 进程睡 进程睡 read 简单、低并发
非阻塞 轮询 轮询 read + O_NONBLOCK 极少单独使用
多路复用 睡在 select/poll/epoll 睡 epoll_wait 高并发网络(见 11.4 epoll 与 IO 多路复用)
信号驱动 睡 信号回调 SIGIO 罕见
异步 完全不阻塞 内核完成后通知 io_uring / POSIX AIO 高吞吐存储、减少 syscall

关键区别:前四种在 “数据从内核拷到用户态” 这一步仍是同步的;只有真异步(io_uring)把这一步也交给内核。

缓冲 vs 直接 IO

方式 路径 优点 缺点
缓冲 IO(默认) 用户缓冲 ↔ page cache ↔ 设备 快、可合并、预读 双份内存;掉电丢缓存
O_DIRECT 用户缓冲 ↔ 设备 绕过 cache,可预测 对齐要求、失去预读、通常更慢
mmap 用户地址空间映射 page cache 零拷贝读、随机访问方便 缺页开销、文件截断即 SIGBUS

落到数字:五种模型的阻塞点

模型 等待数据 拷到用户态 单线程可管连接
阻塞 IO 进程睡 同步 1
非阻塞 轮询(浪费 CPU) 同步 1
epoll 内核事件表 同步 万级
信号驱动 信号 同步 少用
io_uring 内核队列 内核完成 万级 + 低 syscall
strace -c -p <pid>            # 按 syscall 统计耗时占比:先看 read/write 还是 futex
iostat -x 1 3                 # 设备侧:await 高说明排队,%util 高说明饱和

反模式

反模式 后果
每条记录 fsync IOPS 被打满(每次都是设备级等待)
以为 write 成功就落盘 掉电丢数据
用 O_DIRECT “优化” 小随机读 失去预读与合并,通常更慢
单线程阻塞 IO 做高并发 一个慢请求拖住所有连接
忽略 io_uring 的内核版本要求 老内核上回退到同步,性能不达预期

三条铁律

🔑 write 进缓存,fsync 才落盘;可靠性要求决定 fsync 频率。

🔑 并发靠 “等待方式”:能睡在 epoll 上,就不要每连接一个线程。

🔑 O_DIRECT 是特殊工具:数据库/自缓存系统用,普通服务别用。

自检

Q1 服务 “写日志很慢”,可能是什么原因?

▸ 答案:大概率是每条日志都 fsync(或用了 O_SYNC):每次写都要等设备确认,机械盘/网络盘上直接变成 ms 级。改成 “写内存缓冲 + 批量刷盘”(如每 100ms 或每 N 条 fsync 一次),吞吐能提升一到两个数量级。

Q2 为什么 write() 返回成功,进程崩溃后数据却丢了?

▸ 答案:因为默认是缓冲写:数据只进了 page cache,尚未落盘;进程崩溃不影响 page cache(数据还在),但机器掉电就会丢。要保证 “崩溃后可恢复”,必须在关键点 fsync(或依赖 WAL 的 fsync 策略)。

Q3 高并发网络服务为什么用 epoll 而不是 “每连接一线程”?

▸ 答案:因为线程是昂贵资源:每线程有栈、上下文切换、TLB/cache 失效。epoll 让一个线程管理上万连接,只在就绪时处理,把 “等待” 的成本从线程换成内核事件表(见 11.4 epoll 与 IO 多路复用)。

Q4 io_uring 相比 epoll 解决了什么?

▸ 答案:epoll 解决 “等就绪”,但数据拷贝与提交仍是同步 syscall;io_uring 用共享内存环形队列提交/回收请求,支持真正的异步读写(含文件 IO),大幅减少 syscall 与上下文切换。代价是内核版本要求(5.1+,生产建议 5.10+)与编程复杂度。

Q5 mmap 读大文件一定比 read 快吗?

▸ 答案:不一定。mmap 省掉了用户态拷贝,随机访问友好,但会引入缺页开销;顺序读用 read + 预读往往更快。mmap 还有个坑:文件被截断后访问映射区会触发 SIGBUS,需要处理信号或避免共享可变文件。

Q6 iostat 的 %util 接近 100% 一定说明磁盘是瓶颈吗?

▸ 答案:不一定。%util 表示 “设备有未完成请求的时间占比”,在支持并行的 NVMe 上,100% 也可能还有余量;真正的判据是 await(排队延迟)与应用的 P99 是否同步恶化。机械盘上 %util 100% 通常确实饱和。

关联