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 三种方案怎么选?
我画了张图,帮你理清思路:
30.5 避坑指南
我曾经在某个项目里,用 mmap 映射了一个不断增长的文件。结果文件写完了,映射区域没更新,读到的全是旧数据。后来才意识到,mmap 映射的是映射那一刻的文件大小,文件增长后需要重新 mmap。
还有一次,我图省事把 MAP_SHARED 写成了 MAP_PRIVATE。结果修改了内存,文件纹丝不动。嗯,这个坑踩得值,从此再没犯过。
总结一下:
- 小文件(<几十MB):用标准库
fread/fwrite,设置合适缓冲区 - 大文件(>几十MB):用
mmap,注意文件增长问题 - 高频小IO:攒一批再处理,别一次一调
- 永远记得检查返回值,
mmap失败返回MAP_FAILED
说白了,性能优化就是「空间换时间」和「批量处理」这两个思路。你想想看,系统调用再快,也快不过内存操作。能少进内核就少进,能一次搞定就别分多次。做到这几点,你的文件IO性能基本就稳了。