本文约2300字,最近好几款型号都要降内存,导致OTA升级模块因内存不足需要去调整写文件的策略,基于此本文再来梳理一次Linux下写文件确保数据落入物理盘的常规操作,但应用层全部合规操作,是否一定能确保数据写入物理盘呢?答案很显然不是,可参考之前遇到过的一次应用层操作合规依然写Flash失败的案例《【项目实战】SPI Nor Flash下OTA升级概率失败导致系统成砖问题的排查和解决方法》,应用层合规操作,到内核驱动,还得看驱动是否完全没有问题,否则还是可能数据不能完全写入物理盘(Flash),本文仅从Linux用户层来分析落盘的合规操作。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。
C 标准库文件操作存在用户态库缓冲区 + 内核页缓存两层缓存,很多开发者分不清 fflush、fsync、fclose 的作用,写出冗余代码、重复关闭文件、数据未落盘等问题。
本文结合缓冲模式分场景给出标准代码范例,同时梳理高频坑点,适用于日志、关键配置、持久化数据等需要保证数据落地磁盘的开发场景。
基础概念回顾
用户态缓冲区: FILE*标准库自带,fwrite优先写入此处,由 C 库管理。内核页缓存:操作系统内核缓存,数据进入这里≠写入物理磁盘。 真正落盘:数据从内核页缓存刷入物理硬盘,断电不丢失。 fflush:刷用户态缓冲区到内核缓存,不保证物理落盘。fsync:刷内核页缓存到物理磁盘,实现强制落盘。fclose:内部隐式调用fflush,但不会调用 fsync。
这是最常用的业务场景:使用 fopen 默认缓冲,写入关键数据(交易、日志、配置),要求执行完毕数据必须落地磁盘。
默认开启库缓冲 → 数据先存在用户态缓存;调用 fsync 前必须先执行 fflush,否则用户态残留数据无法落盘。
#include<stdio.h>#include<unistd.h>#include<string.h>// 默认缓冲 + 强制落盘 标准实现intWriteFileWithSync(constchar *file_path, constvoid *data, size_t len){if (NULL == file_path || NULL == data || 0 == len) {return-1; } FILE *fp = fopen(file_path, "ab+");if (NULL == fp) {return-1; }int ret = -1;size_t write_len = fwrite(data, 1, len, fp);// 校验写入结果if (write_len != len) {goto END; }// 1. 关键:默认缓冲必须先 fflush,清空用户态缓存到内核if (0 != fflush(fp)) {goto END; }// 2. 获取文件描述符,调用 fsync 强制刷物理磁盘int fd = fileno(fp);if (-1 != fsync(fd)) { ret = 0; // 全部流程成功 }END:// 统一入口关闭文件,避免二次 close fclose(fp);return ret;}fopen 打开文件,使用系统默认全缓冲;fwrite 写入数据到用户态库缓冲区;fflush:用户态缓存 → 内核页缓存;fsync:内核页缓存 → 物理磁盘(真正落盘);fclose 释放资源。手动通过 setvbuf 设置无缓冲模式,fwrite 数据会直接进入内核页缓存,不再经过用户态缓冲区。
无缓冲模式下,fflush 完全无效、冗余,可直接省略,仅需 fsync 完成落盘。
#include<stdio.h>#include<unistd.h>#include<string.h>// 无缓冲模式 + 强制落盘 标准实现intWriteFileNoBufferSync(constchar *file_path, constvoid *data, size_t len){if (NULL == file_path || NULL == data || 0 == len) {return-1; } FILE *fp = fopen(file_path, "ab+");if (NULL == fp) {return-1; }// 关闭标准库用户态缓冲区 setvbuf(fp, NULL, _IONBF, 0);int ret = -1;size_t write_len = fwrite(data, 1, len, fp);if (write_len != len) {goto END; }// 无缓冲:跳过 fflush,直接 fsync 落盘int fd = fileno(fp);if (-1 != fsync(fd)) { ret = 0; }END: fclose(fp);return ret;}fopen + setvbuf(_IONBF):关闭用户态缓冲;fwrite 数据直接写入内核页缓存;fflush,直接调用 fsync 刷物理磁盘;// 错误写法:无缓冲 + 多余 fflushsetvbuf(fp, NULL, _IONBF, 0);fwrite(buf, 1, len, fp);if (fflush(fp) != 0) // 完全无用,多余调用{ fclose(fp);return-1;}fsync(fileno(fp));fclose(fp);_IONBF 代表无用户态缓冲,fflush 没有数据可刷,调用无任何作用,只会增加 IO 开销。
直接删除fflush相关判断与调用。
// 高危错误:二次 fcloseFILE *fp = fopen("test.txt", "ab+");fwrite(buf, 1, len, fp);if (fflush(fp) != 0){ fclose(fp); // 分支内关闭return-1;}fsync(fileno(fp));fclose(fp); // 再次关闭!二次 closereturn0;FILE* 被重复关闭属于 C 标准未定义行为,会引发野指针、程序崩溃、内存泄漏。
使用goto统一资源释放入口,整段代码只保留一处 fclose。
// 严重错误:默认缓冲,跳过 fflushFILE *fp = fopen("test.txt", "ab+");fwrite(buf, 1, len, fp);// 直接 fsync,用户态缓存数据未进入内核,无法落盘fsync(fileno(fp));fclose(fp);数据还停留在用户态缓冲区,fsync只能操作内核缓存,无法读取用户态数据,断电后数据直接丢失。
默认缓冲必须遵循:fwrite → fflush → fsync。
很多人认为 fclose 会把数据刷到磁盘,这是典型误区。
fclose 只会隐式执行 fflush(数据进入内核缓存);适用区分:
fwrite + fclose 即可;fsync。✅ 需要调用
FILE* 使用默认缓冲,且后续要执行 fsync 强制落盘;❌ 不需要调用
setvbuf(_IONBF) 设置无缓冲;fclose(fclose 内部自动 fflush)。✅ 需要调用
❌ 不需要调用
fdatasync:功能类似 fsync,仅同步文件数据、不同步文件元数据,性能更高,对纯数据文件可优先使用;fileno:非标准 C 库函数,Linux/UNIX 系支持,跨平台场景需谨慎使用;fwrite,规则保持不变。📌 总结一句话:默认缓冲先
fflush再fsync,无缓冲直接fsync,资源释放只关一次fclose。
往期文章(欢迎订阅技术分享栏目全部文章):

这里是女程序员的笔记本
15年+嵌入式软件工程师兼二胎宝妈
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助