30、性能优化:减少系统调用、使用大缓冲区、mmap内存映射文件

文件IO的性能问题,说白了就是「用户态」和「内核态」之间来回折腾的成本。我早年做嵌入式日志系统时,就吃过这个亏——每秒几万条日志,每条都调一次 write(),结果CPU全耗在上下文切换上了。今天咱们就聊聊三个最实用的优化手段。

30.1 减少系统调用——别让内核太忙

每次 read()write() 都是一次系统调用。系统调用意味着什么?用户态切内核态,再切回来。一次两次无所谓,但高频场景下,这个开销就非常可观了。

我见过最夸张的案例:某同学写了个网络抓包工具,每次收到一个包就 write() 一次磁盘。结果抓包速度还没磁盘写入快,丢包率高达30%。后来改成批量写入,问题直接解决。

核心原则:能一次干完的活,别分十次干。

举个例子,你要写1000个整数到文件:

// 糟糕的做法:每次写一个整数
for (int i = 0; i < 1000; i++) {
    write(fd, &data[i], sizeof(int));  // 1000次系统调用
}

// 好的做法:攒一批再写
write(fd, data, 1000 * sizeof(int));  // 1次系统调用

你想想看,1000次系统调用和1次系统调用,差距有多大?实测下来,后者速度能快几十倍。

30.2 使用大缓冲区——给数据找个「中转站」

减少系统调用最直接的办法,就是用缓冲区。C标准库的 fread()/fwrite() 就是这么干的——它们内部维护了一个缓冲区,攒够了才真正调用系统函数。

我个人习惯,做文件IO时优先用标准库函数,而不是裸调 read()/write()。除非有特殊需求,比如需要直接控制磁盘写入时机。

缓冲区大小 写入100MB耗时(约) 系统调用次数
1字节 > 10秒 1亿次
4KB(默认页大小) ~0.5秒 25600次
1MB ~0.2秒 100次

看到没?缓冲区从1字节涨到4KB,性能提升20倍以上。再往上提,收益就递减了。我一般选4KB~64KB,具体看场景。

小技巧:可以用 setvbuf() 手动调整标准库的缓冲区大小。比如 setvbuf(fp, NULL, _IOFBF, 65536) 设置64KB全缓冲。

30.3 mmap内存映射文件——把文件当内存用

这个就厉害了。mmap() 把磁盘文件直接映射到进程的虚拟地址空间。你读写这块内存,操作系统自动帮你同步到磁盘。说白了,就是「文件即内存,内存即文件」。

我曾在项目里用 mmap 处理一个2GB的配置文件。如果用传统 read(),得先分配2GB内存,再读进来,内存压力很大。用 mmap 呢?只映射,不实际加载,按需缺页。内存占用几乎为零,速度还快。

#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int fd = open("data.bin", O_RDWR);
if (fd == -1) {
    perror("open");
    return -1;
}

// 获取文件大小
off_t size = lseek(fd, 0, SEEK_END);

// 映射整个文件
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
    perror("mmap");
    close(fd);
    return -1;
}

// 现在可以直接读写这块内存
int *data = (int *)addr;
data[0] = 42;  // 直接修改文件内容

// 用完记得解除映射
munmap(addr, size);
close(fd);

注意:mmap 不是银弹。小文件用 read() 反而更快,因为 mmap 有页对齐、缺页中断等开销。我一般文件超过几十MB才考虑用 mmap

30.4 三种方案怎么选?

我画了张图,帮你理清思路:

文件IO性能优化方案选择 文件IO场景 文件大小 < 几十MB? 标准库函数 fread/fwrite + 缓冲区 mmap内存映射 按需缺页,零拷贝 高频小IO → 批量写入/读取 核心:减少系统调用次数,让每次IO干更多的活

30.5 避坑指南

我曾经在某个项目里,用 mmap 映射了一个不断增长的文件。结果文件写完了,映射区域没更新,读到的全是旧数据。后来才意识到,mmap 映射的是映射那一刻的文件大小,文件增长后需要重新 mmap

还有一次,我图省事把 MAP_SHARED 写成了 MAP_PRIVATE。结果修改了内存,文件纹丝不动。嗯,这个坑踩得值,从此再没犯过。

总结一下:

  • 小文件(<几十MB):用标准库 fread/fwrite,设置合适缓冲区
  • 大文件(>几十MB):用 mmap,注意文件增长问题
  • 高频小IO:攒一批再处理,别一次一调
  • 永远记得检查返回值,mmap 失败返回 MAP_FAILED

说白了,性能优化就是「空间换时间」和「批量处理」这两个思路。你想想看,系统调用再快,也快不过内存操作。能少进内核就少进,能一次搞定就别分多次。做到这几点,你的文件IO性能基本就稳了。

专注资料整理