未调用fclose关闭文件时append追加模式为何出现逆序写入?
在Ubuntu环境下运行如下C++代码时,遗漏fclose()调用会导致文件写入顺序和预期不符:
#include <fstream> void append(std::string path, int i){ FILE *out = fopen(&path[0], "a"); fprintf(out, "%d\n", i); //fclose(out); } int main(){ for(int i=0; i<3; i++) append("./text.txt",i); return 0; }
- 预期输出(追加模式按调用顺序写入):
0 1 2
- 实际输出(未调用
fclose时):
2 1 0
补上fclose(out)调用后写入顺序即可恢复正常。
这个现象是C标准IO缓冲、进程退出流清理逻辑、O_APPEND打开语义三者共同作用的结果:
C标准IO的默认缓冲规则
对普通磁盘文件,fopen()返回的FILE*流默认采用块缓冲:调用fprintf()写入的内容不会立刻触发系统调用写入磁盘,而是先存放在C库为该流分配的用户态内存缓冲区中,只有缓冲区满、主动调用fflush()/fclose()、或进程退出执行流清理时,缓冲区中的内容才会被真正提交给内核写入文件。泄漏的流不会立刻触发写入
代码每次调用append()都会以追加模式独立打开同一个文件,拿到一个全新的FILE*流,写入对应数字后既不关闭也不冲刷缓冲区,直接返回导致流指针丢失,形成资源泄漏:- i=0时打开的流,缓冲区存着
"0\n" - i=1时打开的流,缓冲区存着
"1\n" - i=2时打开的流,缓冲区存着
"2\n"
三次fopen对应三个独立的流、三个独立的用户态缓冲区,在main函数返回前,没有任何一次写入真正进入内核。
- i=0时打开的流,缓冲区存着
进程退出时的流冲刷顺序
进程从main返回后,glibc会执行退出清理逻辑,遍历所有仍处于打开状态的FILE*流,逐个冲刷缓冲区、关闭文件。在当前Ubuntu使用的glibc实现中,流按打开顺序逆序被冲刷:- 最先冲刷最后打开的、存放
"2\n"的流:触发write()系统调用,由于文件以O_APPEND模式打开,write前会原子性定位到文件末尾(此时文件为空),写入"2\n" - 其次冲刷i=1时打开的、存放
"1\n"的流:同样触发write,定位到当前文件末尾(已经有"2\n"),写入"1\n" - 最后冲刷i=0时打开的、存放
"0\n"的流:write到文件末尾,写入"0\n"
最终文件内容就呈现为逆序的2\n1\n0\n。
- 最先冲刷最后打开的、存放
添加fclose后恢复正常的原因
每次调用fclose()时,会立刻先冲刷当前流的缓冲区,触发write系统调用把内容写入磁盘。三次append按i=0、1、2的顺序执行,fclose的顺序也对应0、1、2,每次写入都追加到当时的文件末尾,自然得到预期的正序内容。
注意:这个逆序结果是glibc特定实现下的表现,不属于C/C++标准规定的行为,泄漏
FILE*指针本身属于未定义行为,换不同C库、不同操作系统可能出现完全不同的结果,生产环境绝对不能遗漏文件关闭操作。
内容的提问来源于stack exchange,提问作者sicheng mao

