本文通过一个文件拷贝程序的三个不同实现,来说明标准库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的情况:

signed char的情况
int fgetc() signed char converted int fgetc()
00 xx xx FF FF FF FF FF FF
unsigned char的情况
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)等,用时谨慎。

Tags: .
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!
Home

Be the first to comment on this entry.

Name(required)
Mail (required),(will not be published)

RFC: Request For Comments. Orz..

Website(recommended)