C/C++读写文件的方式以及踩过的坑
前言
最近需要做一个测试,测试同一个程序在两台不同平台的Linux机器上的运行结果,结果完全一致说明成功。由于数据很大,我选择的测试方案是将数据整个写入到文件中,然后比较两个文件的md5码,若md5码一致,说明运行结果一致,测试通过。
测试的最后一步是将接收到的数据写入文件中,项目是用C语言写的,但通信代码用的是C++写的。对于写入文件的方案可分为使用C语言或者C++。
方案一
方案一是我一开始使用的方法,也是我以前常用的方法。使用C语言标准库中的如下三个函数来进行文件的打开、数据的写入,文件的关闭。
//三个函数的函数原型
FILE *fopen(const char *filename, const char *mode);
size_t fwrite(const void* buffer,size_t size, size_t count, FILE* stream);
int fclose (FILE * stream);
使用起来很顺利,但在两台机器上运行同样的代码后,发现写入的文件数据大小不一致。最开始我以为是UDP通信设置的发送速率太快,在其中一台机器上可能丢包了,但后来调低发送速率后,依旧会出现同样的问题。
后来我发现无论使用哪个测试用例进行测试,两个文件字节数(即运行结果)的差距一直都是4096个字节,查看两个文件的实际内容时,发现也确实是其中一个文件缺少4096字节的内容,前面数据完全一致。刚刚好我们使用的UDP通信时,设置的缓冲区大小也是4096字节,好,我以为我找到了问题的所在:
其中一台机器最后一次通信时未能成功将数据写入文件中!
换句话说,最后一次fwrite()没有成功执行。、可是经过排查,实在找不到一个合理的解释来解释为什么最后一次通信没能将数据写入文件中。我经过比较两个文件的实际内容,发现两个文件差距确实是在最后4096个字节,有一个文件中缺少这么4096字节的数据。但是,,,这4096字节的数据并不是在一个UDP包中的数据,也就是说,在最后几次的数据传送中,有一次的数据在中间直接被截断,截断后的数据没能写入文件中,而并不是我之前分析的说最后一次fwrite()没有成功执行。
我不得不查阅资料来分析fwrite()的工作原理。
fread()、fwrite()等函数是C语言库,其底层实现是通过系统调用read()、write()来实现的。
当我们在使用C语言等编程语言进行编程时,整个系统其实是处于用户态的,而read()、write()等函数是系统调用,执行时需要将操作系统切换至内核态,系统在内核态和用户态之间进行切换需要消耗性能。为了减少性能的消耗,C语言自己封装了fread()、fwrite()等函数。拿fwrite()来说,其工作流程是:
fwrite——> c库缓冲——> fflush——> 内核缓冲——–fsync——> 磁盘
而write()函数的工作流程是:
write——> 内核缓冲——fsync——> 磁盘
也就是说,在调用fwrite()时,系统会自动建立一个应用层进程缓冲区,当应用层缓冲区满之后,使用fflush()将应用层进程缓冲区数据写入内核缓冲区中,当内核缓冲区满之后,调用fsync()将数据真正写入磁盘中。write()函数的话,则没有前面应用层缓冲,后面工作原理是一致的。
有了fwrite()后,每次写文件时,当数据量很大时,就可以等应用层进程缓冲区满之后,将数据一次性写入内核缓冲区中,这样就可以大大减少用户态与内核态之间的切换,提高性能。
回到实际问题中来,使用UDP进行数据传送时,实际实现是将**recvfrom()**系统调用放在一个死循环中,当没有数据进行传送时,recvfrom()就一直处于阻塞态,等待接收数据。然而上次fwrite()调用之后,由于缓冲区未满,需要等待下一次fwrite()调用或者手动调用fllush()来对应用层缓冲区数据写入内核缓冲区。
这就测试程序陷入了一个僵局:应用层缓冲区有数据,需要被写入内核缓冲区,但fflush()或者下一次的fwrite()迟迟不被调用,recvfrom()处于阻塞态,这时如果强制关闭程序所在进程,应用层缓冲区的数据有可能会丢失!!!事实证明,对于第一台Linux机器,进程在关闭前会将应用层缓冲区数据写入内核缓冲区,从而写入磁盘文件中;而对于第二台国产Linux机器,进程关闭前什么也没有做,应用层缓冲区数据就这样丢失了。这也就是为什么第二台机器接收到的数据一直比第一台数据少4096个字节的原因。(可能应用层缓冲区大小刚好为4096...)
方案二
方案二决定使用C++中对于文件读写进行操作的方法了,也就是fstream。由于这几天因为疫情原因,一直待在家,也没有办法测试。等过几天再更新测试结果吧。
emmm刚才在centos下测试,使用fstream时,当程序强行终止时,貌似系统也不会把应用层缓冲区内数据写入内核缓冲区然后再终止程序。不同的Linux系统对这些底层实现都有区别,看最终测试结果了。
结论就是单独使用fstream,程序强行终止时,系统也不会把应用层缓冲区内数据写入内核缓冲区再终止程序。
方案三
当配合使用fwrite()+fflush()或者fstream+flush都可以完整的将应用层缓冲区内容全部写入文件。