read、wrte函数与带空洞的文件复制
在UNIX(UNIX Like)环境中我们可以利用read和write这两个函数实现对一个文件的复制操作。不过现实中,我们要复制的文件中可能存在着空洞!如果我们不加处理,直接使用这两个函数进行对文件进行复制,那么拷贝的文件中,原来文件的空洞部分就会被字符0填写。当然有的时候我们并不希望将文件的空洞也一并带人到复制的文件中,所以这种情况我们需要进行一些细节上的处理。《UNIX 环境高级编程》中第四章练习题4.6就是一个这样的题目。
虽然说空洞部分实际上占用大小为0,但是这并不意味着从其它角度来看文件的空洞部分就是真的什么都没有。如果我们使用命令“od”来查看一个带有空洞的文件,我们会发现文件的空洞部分是由字符‘\0’填充的(详见《UNIX环境高级编程》第52页),况且就算我们用vim对一个带有空洞的文件进行编辑我们看到空部分也不是什么都没有的。我们可以认为如果read函数读入的字符中含有‘\0’就可以说明文件中有空洞,此时我们就要采取一些措施,这样就可以保证空洞不会被带人新的文件中。
在网上看到过这样的解决办法,每次读取一个字符,之后判断这个字符是不是‘\0’,如果不是的话直接输入到文件,否则跳过这个字符。这种方法确实可以解决问题,但是这种实现很糟糕。使用这个方法的程序员使用了一个长度为10K的带有空洞的文件进行了测试,毫无疑问测试结果是成功的,但是这样做却频繁的进行系统调用。可以说这个时候IO的速度是奇慢无比的,但由于10K的长度并不大,计算机仍然可以在很短的时间内处理完问题。不过一旦遇到一个体积很大(比如有数百MB)的而且带有空洞的文件那么就会消耗几分钟甚至更长的时间,长时间的等待是人们不愿意看到的。尽管我们不会主动去创建一个体积庞大且带有空洞的文件,但是不代表这样的文件不会在你的系统中存在。所以我们要在上面思路的基础上进行优化,最有效的优化方式就是减少系统调用的次数!建议先去看一看《UNIX环境高级编程》第三章中有关缓冲区大小的选取与文件复制速度快慢的讨论,这个对后面的代码实现非常有帮助,我们需要选择一个相对合适的缓冲区大小,这样才能保证高效的实现。
在这里先选取4096作为缓冲区大小(具体这个值是否合适,可以通过实际测试进行修改与推敲),每次我们先用read函数尝试读入4096个字节的数据。使用read读入数据之后我们就需要判断读入的这段字符中是否含有‘\0’。可以利用一个栈,如果判断读入的字符不是‘\0’那么就将其入栈。最终只需要将栈中的字符一次写入文件即可,这样会明显的降低系统调用的次数从而得到相对较高的效率。
以《UNIX环境高级编程》课后习题4.6为例,我们就一些实现的细节进行一下讨论。虽然我们不需要实现所有的cp命令的功能,但是一些基本问题处理还是需要注意的。这里实现的复制功能是将一个文件(普通文件)复制成为另一个指定名称的文件(这个比cp命令的处理就简单一些了)。首先需要检查主函数接受参数的个数是否正确,接下来还要检查需要复制的文件是否存在,不过要注意使用open函数返回值判断文件是否存在并不靠谱,即使是如“open(filename, O_RDONLY | O_CREAT | O_EXCL)”这样调用,因为无法以只读方式打开文件的时候函数同样会返回-1(当然配合使用errno也会很好的处理问题的)。因此建议使用access函数对文件是否存在和文件的权限进行检查,这样相对更加的可靠。同样在创建文件的时候也要注意严格的检查,否则如果原来那里确实就有一个名字与要创建文件相同的文件而且还不让用户去读写可能就会导致原有的文件被覆盖掉(不管怎么说,这都是一个非常严重的bug)。想一想如果cp命令如果没有考虑上述问题,那么事情就会变得很糟糕。不过就算是经过这样检查文件仍有可能出现创建失败的情况,比如对目录没有写的权限或者路径路径中某个目录不存在,此时返回一个错误并提示用户做出相应的修改即可。
还有一个不可忽视的问题就是新创建的文件权限问题,需要使用umask函数与合适的mod进行配合得到一个相对合理的文件权限。一般来说建议文件权限设置于原文件相同,此时需要使用lstat函数(到底用lstat还是stat需要认真的推敲一下)来获取有关文件的权限信息。如果忘记里指定权限的问题,那么就不能够保证创建出的文件权限是什么样的了。在文件的复制问题上,如果为了数据安全考虑可以调用fsync函数或者是打开要写入的文件时指定O_SYNC,当然这个就会相对的消耗更多的时间。但是这个其实也没有多大关系,因为我们可以将程序丢到后台去运行。最后还是显示的调用close函数来关闭文件,养成一个好的习惯还是比较重要的。
下面的一段实现代码可以作为参考(仅仅是供参考,代码并没有保证一个良好的实现),但是下面的代码实现并不好。因为很明显上文中提到的一些重点并没得到很好的实现,当然我们可以从这个代码找出其存在的问题,并对其可能出现的问题进行分析。这样就可以在自己的代码中避免出现类似的问题。
参考代码:
/*
*filename: mycp.c
*date: 3/24/2013
*/
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define failed 1
#define success 0
#define BUFFSIZE 4096
int check(const int argc);
int checkfilename(const char *filename);
int execute(const char *filename_a, const char *filename_b);
int main(int argc, char *argv[])
{
char *filename_a;
char *filename_b;
if (failed == check(argc))
{
exit(failed);
}
filename_a = argv[1];
filename_b = argv[2];
if (failed == checkfilename(filename_a))
{
exit(failed);
}
if (failed == execute(filename_a, filename_b))
{
exit(failed);
}
exit(success);
}
int check(const int argc)
{
if (3 != argc)
{
printf("Parameter number error!\n");
return failed;
}
return success;
}
int checkfilename(const char *filename)
{
if (access(filename, F_OK) < 0)
{
printf("System can not fine %s\n", filename);
return failed;
}
if (access(filename, R_OK) < 0)
{
printf("Can not read %s\n", filename);
return failed;
}
return success;
}
int execute(const char *filename_a, const char *filename_b)
{
int i;
int n;
int fileflag_a;
int fileflag_b;
char buf[BUFFSIZE];
struct stat mod;
if ((fileflag_a=open(filename_a, O_RDONLY)) < 0)
{
printf("Read error!\n");
return failed;
}
lstat(filename_a, &mod);
umask(0);
if ((fileflag_b=open(filename_b, O_RDWR | O_CREAT | O_EXCL | O_TRUNC, mod.st_mode)) < 0)
{
printf("System can not found the %s\n", filename_b);
return failed;
}
while ((n=read(fileflag_a, buf, BUFFSIZE)) > 0)
{
if (strlen(buf) != n)
{
for (i=0; i<n; ++i)
{
if ('\0' == buf[i])
{
continue;
}
if (write(fileflag_b, buf+i, 1) != 1)
{
printf("Write error!\n");
return failed;
}
}
}
else
{
if (write(fileflag_b, buf, n) != n)
{
printf("Write error!");
return failed;
}
}
}
close(fileflag_a);
close(fileflag_b);
return success;
}
对于上述内容如有考虑不周之处,欢迎各位大牛指点。