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执行成功。然后我们看一下strace.log内容
从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日志查看:
这次可以看到,代码里依然没有调用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(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经测试也是一样的情况,感兴趣的可以自行测试。