对大文件读写操作时谨慎使用fseek/lseek

fseek/lseek在某些情况会产生read系统调用?

在测试某厂家的云存储产品的性能时,发现一个比较诡异的问题,即在将视频流数据写入磁盘的过程中,监测到了大量的读操作(read系统调用),每个操作文件较大,有几百兆,大量的读操作会一定程度上降低写入的性能。但是在经过代码排查后,确定在写入数据的过程中是没有出现fread、read调用的,那么问题来了,read调用从何而来?

由于从创建文件到关闭,中间除了fwrite之外还有fseek和fteel操作,当时将目标锁定在这两个标准函数上。

(记:在一次gdb调试dump文件时,无意中在一个线程的堆栈里发现lseek系统调用之后接着系统调用read,此时也印证了前面的猜想,极有可能是fseek最终导致了read读操作。)

通过下面的测试来证明上述猜测:

这里需要用到strace命令,strace是一个可用于诊断、调试和教学的Linux用户空间跟踪器。我们用它来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。

(1)test_strace.cpp:

fseek(fp, 100, SEEK_CUR);//参数3是SEEK_CUR

#include <iostream>
#include <string.h>
#include <errno.h>
#include <stdio.h>
using namespace std;
int main(int argc, char* argv[])
{
		int ret = -1;
		size_t filelen = 0;

		if(argc < 2)
		{
				cerr << "please input filename\n";
				return -1;
		}
		const char* filename = argv[1];
		FILE* fp = fopen(filename, "rb+");
		if(fp)
		{
				ret = fseek(fp, 100, SEEK_CUR);
				if(ret == 0)
				{
						cout << "seek success" << endl;
				}
				fclose(fp);
		}else{
				cerr << "fopen error, errno is " << errno << " : " << strerror(errno) << endl; 
		}

		return 0;
}

[root@localhost ~]# g++ test_strace.cpp

生成a.out可执行文件,然后使用strace运行:

strace信息输出strace.log,把test_strace.cpp文件当作参数来进行fseek测试。

对大文件读写操作时谨慎使用fseek/lseek

从输出结果来看fseek执行成功。然后我们看一下strace.log内容

对大文件读写操作时谨慎使用fseek/lseek

从55行可以看到代码执行了fopen后,调用了系统函数open(),接下来执行lseek(),从参数2看,和代码fseek()传入的一致,但是接下来并没有调用read,后面也正常调用close关闭文件然后程序退出。


那是否和fseek的参数3有关系,进一步测试,修改fseek传入的参数3类型:

(2)test_strace.cpp:

fseek(fp, 100, SEEK_SET);//参数3是SEEK_SET

将上面代码fseek的参数3修改为SEEK_SET

#include <iostream>
#include <string.h>
#include <errno.h>
#include <stdio.h>
using namespace std;
int main(int argc, char* argv[])
{
		int ret = -1;
		size_t filelen = 0;

		if(argc < 2)
		{
				cerr << "please input filename\n";
				return -1;
		}
		const char* filename = argv[1];
		FILE* fp = fopen(filename, "rb+");
		if(fp)
		{
				ret = fseek(fp, 100, SEEK_SET);
				if(ret == 0)
				{
						cout << "seek success" << endl;
				}
				fclose(fp);
		}else{
				cerr << "fopen error, errno is " << errno << " : " << strerror(errno) << endl; 
		}

		return 0;
}

重新编译,然后同样使用strace运行,执行成功后打开strace.log日志查看:

对大文件读写操作时谨慎使用fseek/lseek

这次可以看到,代码里依然没有调用fread或者read函数,但是strace却检测到了read系统调用,而且是在lseek(3, 0, SEEK_SET)之后,write(1, "seek success\n", 13)之前(这里的write是cout产生),并且read的内容正好是fseek要偏移的内容,可以确定此时的read系统调用是fseek引起。


接下来测fseek的参数3是SEEK_END时的情况:

(3)test_strace.cpp:

fseek(fp, 100, SEEK_END);//参数3是SEEK_END

将上面代码fseek的参数3修改为SEEK_END

#include <iostream>
#include <string.h>
#include <errno.h>
#include <stdio.h>
using namespace std;
int main(int argc, char* argv[])
{
		int ret = -1;
		size_t filelen = 0;

		if(argc < 2)
		{
				cerr << "please input filename\n";
				return -1;
		}
		const char* filename = argv[1];
		FILE* fp = fopen(filename, "rb+");
		if(fp)
		{
				ret = fseek(fp, 0, SEEK_END);
				if(ret == 0)
				{
						cout << "seek success" << endl;
				}
				fclose(fp);
		}else{
				cerr << "fopen error, errno is " << errno << " : " << strerror(errno) << endl; 
		}

		return 0;
}

修改后然后重新编译,使用strace执行,执行成功后打开strace.log查看:

对大文件读写操作时谨慎使用fseek/lseek

可以看到,代码里fseek(fp, 0, SEEK_END);是偏移到文件末尾,strace.log记录到系统调用lseek()后,接下来调用read,并且read的内容即fseek偏移的内容,可以确定此时的read系统调用是由fseek引起。

结论:

结合上面的测试,可以发现:

fseek标准函数,在参数3为SEEK_CUR时,不会产生read操作;参数3为SEEK_SET、SEEK_END时都会产生read操作,且读的数据大小就是fseek要偏移的大小。

存储服务器,一般都是24小时在频繁的进行大量的文件操作,某些业务场景对性能要求极高,若较多地使用了fseek/lseek,且偏移的量越大,对性能的影响越明显,可以修改存储文件结构,尽量顺序写入,即使要用fseek/lseek,也尽量想办法避免使用参数SEEK_SET、SEEK_END。

注:上述结论是使用标准函数fseek进行测试得出,实际lseek经测试也是一样的情况,感兴趣的可以自行测试。

上一篇:fseek函数会刷新C缓冲区中的数据吗?


下一篇:linux – 关于文件搜索位置的问题