IO性能优化:缓冲IO与直接IO,异步IO,mmap与sendfile

IO性能,说白了就是程序跟磁盘打交道快不快。我见过太多项目,CPU算得飞快,内存也够大,结果一读写文件就卡成PPT。嗯,今天咱们就把IO这块彻底聊透。

一、缓冲IO vs 直接IO

先问个问题:你写文件时,数据真的立刻写到磁盘上了吗?

答案是——不一定。这取决于你用哪种IO方式。

缓冲IO(Buffered IO)

这是最常用的方式。你调用fwrite()read()时,数据先到内核的页缓存(Page Cache),然后内核再找机会刷到磁盘。

核心特点:

  • 数据走两次拷贝:磁盘→内核缓存→用户空间
  • 读小文件时命中缓存,速度极快
  • 写操作是异步的(先写缓存,后刷盘)

我个人习惯,处理日志文件、配置文件这类小IO时,直接用缓冲IO。省心,性能也够。

// 缓冲IO示例
FILE *fp = fopen("data.log", "w");
fprintf(fp, "这是一条日志\n");
fclose(fp);  // 此时数据可能还在缓存里

注意:程序崩溃时,未刷盘的数据会丢失。我曾经在线上环境遇到过,日志写到一半进程挂了,结果排查问题时发现最后几条日志没了。从那以后,关键数据我会手动调用fflush()fsync()

直接IO(Direct IO)

直接IO跳过页缓存,数据直接从磁盘到用户空间。你想想看,这适合什么场景?

  • 数据库自己管理缓存(如MySQL的InnoDB)
  • 大文件顺序读写,不需要内核帮你缓存
  • 对数据持久性要求极高
// 直接IO示例(Linux)
int fd = open("data.bin", O_RDWR | O_DIRECT);
// 注意:缓冲区需要对齐到512字节
char *buf = aligned_alloc(512, 4096);
read(fd, buf, 4096);

避坑指南:直接IO要求缓冲区地址、偏移量、大小都按磁盘扇区对齐(通常是512字节)。我曾经没注意对齐,结果read()返回-EINVAL,排查了半天。嗯,血的教训。

二、异步IO(AIO)

同步IO的问题在于——你发起一个读请求,线程就卡在那等结果。如果IO很慢,CPU就闲着。

异步IO的思路是:你告诉内核「我要读这个文件,读完了通知我」,然后你继续干别的活。

Linux AIO vs POSIX AIO

特性 Linux AIO(libaio) POSIX AIO(aio.h)
实现方式 内核原生支持 用户态线程模拟
性能 高(适合高并发) 一般(有线程开销)
使用复杂度 较高 较低
典型场景 数据库、存储引擎 通用异步IO
// Linux AIO 示例(简化)
io_context_t ctx;
io_setup(128, &ctx);  // 创建上下文

struct iocb cb;
io_prep_pread(&cb, fd, buf, size, offset);

struct iocb *cbs[] = {&cb};
io_submit(ctx, 1, cbs);  // 提交请求

// 干别的活...

struct io_event events[1];
io_getevents(ctx, 1, 1, events, NULL);  // 等待完成

我的经验:异步IO真正发挥威力是在高并发场景。比如一个缓存服务器,同时处理上千个IO请求。如果每个请求都开一个线程,光线程切换就能把CPU吃满。用AIO,几个线程就能搞定。

三、mmap与sendfile

这两个是零拷贝技术的代表。什么叫零拷贝?就是数据从磁盘到网络,中间不经过用户空间。

mmap:内存映射文件

mmap把文件映射到进程的虚拟地址空间。你读写内存,就是在读写文件。省去了read()/write()的数据拷贝。

// mmap示例
int fd = open("data.bin", O_RDONLY);
size_t len = 4096;
char *addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);

// 直接像访问数组一样读文件
printf("%c\n", addr[0]);

munmap(addr, len);
close(fd);

适用场景:频繁读写大文件的同一区域。比如数据库的缓冲池、索引文件。我做过一个KV存储,用mmap映射数据文件,查询时直接内存访问,比read()快了一个数量级。

注意:mmap映射的文件大小不能超过虚拟地址空间。32位系统上,单个映射通常不能超过2GB。另外,频繁的msync()也会带来性能开销。

sendfile:零拷贝传输

sendfile用于在两个文件描述符之间直接传输数据,典型场景是——从磁盘读文件,然后发送到网络socket。

// sendfile示例
int in_fd = open("bigfile.dat", O_RDONLY);
int out_fd = accept(listen_fd, ...);

off_t offset = 0;
size_t count = 1024 * 1024;  // 1MB
ssize_t sent = sendfile(out_fd, in_fd, &offset, count);

传统方式需要:磁盘→内核缓存→用户空间→socket缓存→网卡。一共4次拷贝。

sendfile只需要:磁盘→内核缓存→网卡。只有2次拷贝。

性能对比:我测试过,传输1GB文件,传统方式耗时约3.2秒,sendfile只要1.8秒。CPU占用率从45%降到了12%。

四、知识体系总览

下面这张图,把今天讲的内容串起来了:

IO性能优化知识体系 IO性能优化 缓冲IO vs 直接IO 缓冲IO:页缓存 直接IO:跳过缓存 异步IO(AIO) Linux AIO(libaio) POSIX AIO(aio.h) 零拷贝技术 mmap:内存映射 sendfile:零拷贝传输 核心原则 小文件/随机IO → 缓冲IO 大文件/高并发 → 直接IO + 异步IO + 零拷贝

五、如何选择?

我总结了一个简单的决策思路:

  1. 文件小、访问频繁 → 缓冲IO。让内核帮你缓存,省心省力。
  2. 文件大、顺序读写 → 直接IO。避免缓存污染,减少内存占用。
  3. 高并发、IO密集 → 异步IO。别让线程闲着等IO。
  4. 文件传输、网络服务 → sendfile。零拷贝,性能翻倍。
  5. 随机访问、共享内存 → mmap。像操作内存一样操作文件。

最后说一句:没有银弹。每种技术都有适用场景。我见过有人对1KB的小文件用直接IO,结果性能反而更差。也见过有人用mmap映射100GB文件,32位系统直接OOM。嗯,选对工具,比用好工具更重要。


公众号:蓝海资料掘金营,微信deep3321