文件操作性能优化:减少系统调用次数、使用大缓冲区、异步IO简介
做嵌入式开发这些年,我踩过最多的坑就是文件IO性能。你想想看,一个嵌入式设备动不动就要写日志、存配置、记录传感器数据,如果IO效率上不去,整个系统都会被拖慢。今天咱们就聊聊怎么把文件操作跑得更快。
一、性能瓶颈到底在哪?
先看一个典型场景。假设你要往文件里写1000个字节,每次写1个字节。你猜会发生什么?
// 低效写法
for (int i = 0; i < 1000; i++) {
write(fd, buf + i, 1);
}
这段代码会触发1000次系统调用。每次系统调用都要从用户态切换到内核态,保存上下文、检查权限、拷贝数据……开销非常大。我在一个数据采集项目里就吃过这个亏,当时每秒要写几千条记录,结果CPU全耗在系统调用上了,采集任务反而跟不上。
核心结论:系统调用次数越少,IO性能越好。减少系统调用是优化的第一要务。
二、减少系统调用的三板斧
1. 合并读写操作
把多次小数据量的读写合并成一次大数据量的操作。说白了就是攒够了再干。
// 高效写法
char buffer[1024];
// 先填充buffer
write(fd, buffer, 1024); // 一次系统调用搞定
我建议你在设计上层接口时就考虑这一点。比如日志模块,可以先把日志拼成一行,攒够4KB再一次性写入。
2. 使用标准库的缓冲机制
C标准库的fread/fwrite内部已经做了缓冲。默认情况下,它们会维护一个缓冲区,满了才真正调用系统调用。
FILE *fp = fopen("data.bin", "wb");
fwrite(data, 1, 100, fp); // 可能还在缓冲区里
fclose(fp); // 关闭时才会刷到磁盘
嗯,这里要注意:fclose会自动刷新缓冲区。但如果你程序突然崩溃,缓冲区里的数据就丢了。所以关键数据记得手动fflush。
3. 调整缓冲区大小
缓冲区不是越大越好,但太小了肯定不行。我做过测试,缓冲区从1字节增加到4KB,性能提升非常明显。再往上加,收益就递减了。
| 缓冲区大小 | 写入100MB耗时 | 系统调用次数 |
|---|---|---|
| 1字节 | 约12秒 | 1亿次 |
| 1KB | 约0.3秒 | 10万次 |
| 4KB | 约0.08秒 | 2.5万次 |
| 64KB | 约0.06秒 | 1600次 |
从表格能看出来,4KB到64KB的提升已经不明显了。我个人习惯用4KB或8KB,兼顾性能和内存占用。
三、大缓冲区策略
大缓冲区说白了就是「空间换时间」。但怎么用有讲究。
1. 预分配缓冲区
不要在循环里反复malloc/free。一次性分配好,重复使用。
#define BUF_SIZE (64 * 1024) // 64KB
static char buffer[BUF_SIZE];
static int pos = 0;
void buffered_write(int fd, const char *data, int len) {
if (pos + len > BUF_SIZE) {
write(fd, buffer, pos); // 刷出去
pos = 0;
}
memcpy(buffer + pos, data, len);
pos += len;
}
这种模式在嵌入式里很常见。我曾经在一个RTOS项目里用这个方案,把SD卡的写入速度从200KB/s提升到了1.2MB/s。
2. 对齐问题
警告:很多嵌入式存储设备(如SD卡、NAND Flash)要求缓冲区地址对齐。比如SD卡通常要求4字节对齐,有些甚至要求512字节对齐。不对齐的话,驱动层会做额外的拷贝,性能直接打折扣。
我建议你分配缓冲区时用posix_memalign或者自己实现对齐分配函数。
四、异步IO简介
前面讲的都是同步IO——程序发出读写请求后,必须等操作完成才能继续。异步IO就不一样了:你发起请求后,可以继续干别的事,等IO完成了再通知你。
1. 为什么需要异步IO?
你想想看,如果程序一边要处理网络数据,一边要写日志。同步IO的话,写日志时网络数据就堆积了。异步IO可以让你「同时」做这两件事。
2. Linux下的异步IO方式
Linux提供了几种异步IO机制:
- aio系列函数:
aio_read/aio_write,POSIX标准,但实际用起来坑不少。 - io_uring:Linux 5.1引入的新机制,性能非常好,但需要较新内核。
- epoll + 非阻塞IO:虽然不是严格意义上的异步IO,但很多场景下够用了。
我个人更推荐io_uring,前提是你的内核版本够新。它在高并发场景下表现非常出色。
3. 一个简单的异步IO示例
#include <aio.h>
#include <errno.h>
struct aiocb cb;
char buffer[4096];
// 准备异步读
cb.aio_fildes = fd;
cb.aio_buf = buffer;
cb.aio_nbytes = sizeof(buffer);
cb.aio_offset = 0;
if (aio_read(&cb) == -1) {
perror("aio_read");
return -1;
}
// 这里可以做其他事情
do_other_work();
// 检查是否完成
while (aio_error(&cb) == EINPROGRESS) {
// 等待或做其他事
}
// 获取结果
ssize_t ret = aio_return(&cb);
提示:异步IO的编程模型比同步复杂得多。我建议你先用同步+大缓冲区的方式优化,如果性能还不够,再考虑异步IO。不要一开始就上异步,容易把自己绕进去。
五、知识体系总览
下面这张图总结了文件操作性能优化的核心思路:
六、避坑指南
最后分享几个我踩过的坑:
- 不要迷信大缓冲区:我曾经把缓冲区设到1MB,结果内存碎片严重,系统反而变慢了。4KB到64KB是比较合理的范围。
- 注意文件系统差异:FAT32和ext4的IO特性完全不同。我在一个项目里用FAT32,频繁小文件写入导致文件系统碎片化严重,后来改成ext4才解决。
- 异步IO的坑:aio系列函数在glibc的实现里,底层还是用线程池模拟的,并不是真正的异步。io_uring才是真异步,但需要内核支持。
嗯,文件操作性能优化就聊到这里。记住一个原则:减少系统调用次数,合理使用缓冲区。大部分场景下,这两招就够用了。
公众号:蓝海资料掘金营,微信deep3321