本文通过一个文件拷贝程序的三个不同实现,来说明标准库fread/fwrite、系统调用read/write在缓冲机制上的不同。系统调用没有缓冲(这里不考虑内核缓冲),拿write来说,它的原型int write(int fd, char *buf, size)。write将buf处的size个字节立即写入文件描述符fd指名的文件,而不经过任何缓冲。而C标准库中的f系列函数(fwrite/fread/fgetc/fputc)在FILE结构中内部维护了一个缓冲区(大小是多少?),通常情况下只有当这个缓冲区被写满时才会调用write将其真正地写入文件,fflush(FILE* stream)会强制将缓冲区内容写出。
首先是一个使用系统调用,逐字节的将文件51.tar.gz(113M)拷贝到51.tar.gz.bak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> int main (int argc, char **argv) { const int N = 1; //~const int N = 4096; char buf[N]; int fdin = open("51.tar.gz", O_RDONLY); int fdout = open("51.tar.gz.bak", O_WRONLY|O_CREAT|O_TRUNC, S_IRWXU); if (fdout == -1 || fdin == -1) { fprintf(stderr, "open failure\n"); exit(1); } int count; while (count = read(fdin, buf, sizeof(buf))) { write(fdout, buf, count); } close(fdout); close(fdin); return 0; } |
所用时间:
real 16m26.801s user 0m28.686s sys 15m50.243s
第二个程序(改变const int N的大小),还是利用系统调用,但却是4K为单位拷贝,用时
real 0m0.470s user 0m0.012s sys 0m0.460s
第三个程序,使用fgetc/fputc来逐字节拷贝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | int main (int argc, char **argv) { int c; //~ fgetc()返回int FILE* fdin = fopen("51.tar.gz", "r"); FILE* fdout = fopen("51.tar.gz.bak", "w"); if (!fdout || !fdin) { fprintf(stderr, "open failure\n"); exit(1); } while ((c = fgetc(fdin)) != EOF) { fputc(c, fdout); } fclose(fdout); fclose(fdin); return 0; } |
用时
real 0m4.602s user 0m4.192s sys 0m0.408s
简要的分析
第一个程序用时十多分钟!我们知道系统调用要陷入内核,比较耗时,113M的文件大约需要1亿次调用read和write,反复频繁地陷入内核,当然会非常慢。
第二个程序拷贝文件用了不到半秒钟,它一次从源文件中读取4K,然后再一次写入目标文件,系统调用次数不到3万次,大大缩减了陷入内核过程所占用的时间,拷贝过程就快了很多。
再说第三个程序,由于标准库为进程维护着一个自己缓冲区,除非显式刷新,否则只有当缓冲区被写满时才会调用write将缓冲数据写出,因此系统调用的次数也很少。为什么比第二个程序慢呢?一来,标准的缓冲区可能不是4K,更重要的是fgetc/fputc是一个函数,会被调用上亿次,虽说在用户空间执行,但毕竟是函数调用,还是会占用相当一部分的时间。
如果有兴趣,你可以用fread/fwrite来重写这个拷贝程序,相比第三个程序,性能肯定还会有不小的提升。
小疑问
fgetc的原型:int fgetc(FILE* stream). 上面第三个程序中,如果把int c换成char c就会出问题,文件被拷贝128字节就over了~~~这是咋回事?
仔细分析一下,
1 2 3 4 | while ((c = fgetc(fdin)) != EOF) { fputc(c, fdout); } |
fgetc()返回的是int,付给char c时会转换为char(singed or unsigned? ),就会有数据截断,又由于EOF会被多数编译器定义为int型的-1,因此char c在与int EOF比较时会被提升为int。在这两次转换过程中,fgetc原本的返回值就有可能被篡改。也就有可能出现非EOF被解释为EOF的情况:
| int fgetc() | signed char | converted int fgetc() |
|---|---|---|
| 00 xx xx FF | FF | FF FF FF FF |
| int fgetc() | unsigned char | converted int fgetc() |
|---|---|---|
| 00 xx xx FF | FF | 00 00 00 FF |
可以看出,在signed char的情况下,转型后的非EOF有可能会被错当作EOF。
此外,还有,int getc(FILE*)/int getchar(), int fputc(int, FILE*)/int putc(FILE*)/int putchar(int)等,用时谨慎。
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!
Be the first to comment on this entry.