更改缓冲区大小以在 C 中复制文件

Posted

技术标签:

【中文标题】更改缓冲区大小以在 C 中复制文件【英文标题】:Changing buffer size to copy file in C 【发布时间】:2018-06-24 05:52:39 【问题描述】:

我创建了一个创建文件副本的函数:读取 --> 缓冲区 --> 写入。我正在尝试多次增加缓冲区大小,看看是否会影响复制文件所需的时间(大约 50Mb)

# include <assert.h>
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <sys/types.h>
# include <sys/stat.h>
# include <sys/wait.h>
# include <string.h>
# include <fcntl.h>
# include <time.h>
// Copy the file referred to by in to out 
void copy (int in, int out, char *buffer, long long taille) 
  int t;

  while ((t = read(in, &buffer, sizeof taille))> 0)
    write (out, &buffer, t);


  if (t < 0)
    perror("read");


int main()

  clock_t timing;  //to time 
  int buffer_size = 1;
  char * buffer = NULL;

  // allocating memory for the buffer
  buffer = malloc(sizeof(char)*buffer_size);
  // test mémoire
  if (!buffer) 
    perror("malloc ini");
    exit(1);
  

  // temporary buffer to be able to increase the siwe of the buffer 
  char * temp_buffer = NULL;

  // opening the files
  int fichier1 = open("grosfichier",O_RDONLY);
  int fichier2 = open("grosfichier_copy", O_WRONLY|O_CREAT);

  for (int i=0; buffer_size <= 1048576; i++)

    temp_buffer = realloc(buffer, buffer_size * sizeof(char));
    if(!temp_buffer) 
      perror("malloc temp_buffer");
      exit(1);
    

    buffer = temp_buffer;

    timing = clock();
    copy(fichier1,fichier2, buffer, buffer_size); //recopie l'entree std dans la sortie std
    timing = clock() - timing;

    printf("%d, buffer size = %d, time : %ld\n", i, buffer_size, timing);
    remove("grosfichier_copie");

    buffer_size *= 2;
  
  // free(temp_buffer);
  free(buffer);
  close(fichier1);
  close(fichier2);

  return 0;

代码运行并复制文件,但计时的东西不能正常工作

0, buffer size = 1, time : 6298363
1, buffer size = 2, time : 1
2, buffer size = 4, time : 1
3, buffer size = 8, time : 1
4, buffer size = 16, time : 1
5, buffer size = 32, time : 1
6, buffer size = 64, time : 1
7, buffer size = 128, time : 1
8, buffer size = 256, time : 1
9, buffer size = 512, time : 1
10, buffer size = 1024, time : 1
11, buffer size = 2048, time : 1
12, buffer size = 4096, time : 1
13, buffer size = 8192, time : 1
14, buffer size = 16384, time : 1
15, buffer size = 32768, time : 0
16, buffer size = 65536, time : 1
17, buffer size = 131072, time : 4
18, buffer size = 262144, time : 1
19, buffer size = 524288, time : 2
20, buffer size = 1048576, time : 2
[Finished in 6.5s]
    为什么文件运行后似乎没有复制? (根据时间?) 我是否正确使用了免费? (我尝试在循环中移动它,但它没有运行) 我是否将缓冲区适当地传递给函数副本?

谢谢!

EDIT1:感谢您的所有 cmets!我已经纠正了有关在循环中打开和关闭文件、适当使用缓冲区以及建议的变量类型的主要缺陷。我得到的结果更合乎逻辑:

0, buffer size = 1, time : 8069679
1, buffer size = 2, time : 4082421
2, buffer size = 4, time : 2041673
3, buffer size = 8, time : 1020645
4, buffer size = 16, time : 514176
...

但我一直在努力正确处理 write() 错误。

Edit2:这个版本的副本好吗?

void copy (int in, int out, char *buffer, size_t taille) 
  ssize_t t;

  while ((t = read(in, buffer, taille))> 0)
    if (write (out, buffer, t)<0)
      perror("error writing");
    
  

  if (t < 0)
    perror("read");

【问题讨论】:

为什么你认为你可以用%ld格式说明符打印clock_t 1.第一次(慢速)传递将文件加载到内核缓冲池缓存中的可能性是,其余运行不必读取它。 2. 是的。如果您使用malloc(),则需要将其放入循环中,但如果您使用realloc(),则不需要。 3. 是的——主要是。大概,main() 中的buffer_size 应该是size_t,而copy() 中的taille 也应该是size_t。不过,您正在滥用 copy() 中的缓冲区。 'while ((t = read(in, &buffer, sizeof taille))> 0)'......'&buffer'? 'buffer' 是一个函数参数,您将其地址用作文件缓冲区...... 您在 main 循环的第一次迭代中删除了输出文件(此后删除失败),但您没有重新打开文件描述符。这意味着您现在正在写入一个匿名文件,该文件在每次测试迭代中都在增长。您应该在写入后关闭并重新打开输出文件。此外,您不会倒带输入文件,因此第二个和后续循环不处理任何数据;循环开始时,读指针位于 EOF。这比我在第一条评论中提到的“缓存”效果重要得多。缓存是真实的;但是您的“不复制字节”问题更为严重。 在使用 openO_CREAT 标志时应指定 permissionint fichier2 = open("grosfichier_copy", O_WRONLY|O_CREAT,0664); 【参考方案1】:

这是我修改后的代码版本,解决了我在 cmets 中提出的大部分问题,以及其他人提出的大部分问题。

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <fcntl.h>
# include <time.h>

size_t copy(int in, int out, char *buffer, size_t taille);

size_t copy(int in, int out, char *buffer, size_t taille)

    ssize_t t;
    ssize_t bytes = 0;

    while ((t = read(in, buffer, taille)) > 0)
    
        if (write(out, buffer, t) != t)
            return 0;
        bytes += t;
    

    if (t < 0)
        perror("read");
    return bytes;


int main(void)

    clock_t timing;
    int buffer_size = 1;
    char *buffer = malloc(sizeof(char) * buffer_size);

    if (!buffer)
    
        perror("malloc ini");
        exit(1);
    

    int fichier1 = open("grosfichier", O_RDONLY);
    if (fichier1 < 0)
    
        perror("grosfichier");
        exit(1);
    

    for (int i = 0; buffer_size <= 1048576; i++)
    
        lseek(fichier1, 0L, SEEK_SET);
        char *temp_buffer = realloc(buffer, buffer_size * sizeof(char));
        if (!temp_buffer)
        
            perror("malloc temp_buffer");
            exit(1);
        
        int fichier2 = open("grosfichier_copy", O_WRONLY | O_CREAT, 0644);
        if (fichier2 < 0)
        
            perror("open copy file");
            exit(1);
        

        buffer = temp_buffer;

        timing = clock();
        size_t copied = copy(fichier1, fichier2, buffer, buffer_size);
        timing = clock() - timing;

        printf("%d, buffer size = %9d, time : %8ld (copied %zu bytes)\n",
               i, buffer_size, timing, copied);
        close(fichier2);
        remove("grosfichier_copie");

        buffer_size *= 2;
    
    free(buffer);
    close(fichier1);

    return 0;

当我运行它时(使用两个计时命令给出时间),我得到:

2018-01-15 08:00:27 [PID 43372] copy43
0, buffer size =         1, time : 278480098 (copied 50000000 bytes)
1, buffer size =         2, time : 106462932 (copied 50000000 bytes)
2, buffer size =         4, time : 53933508 (copied 50000000 bytes)
3, buffer size =         8, time : 27316467 (copied 50000000 bytes)
4, buffer size =        16, time : 13451731 (copied 50000000 bytes)
5, buffer size =        32, time :  6697516 (copied 50000000 bytes)
6, buffer size =        64, time :  3459170 (copied 50000000 bytes)
7, buffer size =       128, time :  1683163 (copied 50000000 bytes)
8, buffer size =       256, time :   882365 (copied 50000000 bytes)
9, buffer size =       512, time :   457335 (copied 50000000 bytes)
10, buffer size =      1024, time :   240605 (copied 50000000 bytes)
11, buffer size =      2048, time :   126771 (copied 50000000 bytes)
12, buffer size =      4096, time :    70834 (copied 50000000 bytes)
13, buffer size =      8192, time :    46279 (copied 50000000 bytes)
14, buffer size =     16384, time :    35227 (copied 50000000 bytes)
15, buffer size =     32768, time :    27996 (copied 50000000 bytes)
16, buffer size =     65536, time :    28486 (copied 50000000 bytes)
17, buffer size =    131072, time :    24203 (copied 50000000 bytes)
18, buffer size =    262144, time :    26015 (copied 50000000 bytes)
19, buffer size =    524288, time :    19484 (copied 50000000 bytes)
20, buffer size =   1048576, time :    28851 (copied 50000000 bytes)
2018-01-15 08:08:47 [PID 43372; status 0x0000]  -  8m 19s

real    8m19.351s
user    1m21.231s
sys 6m52.312s

如您所见,1 字节的复制非常糟糕,复制数据需要大约 4 分钟的挂钟时间。使用 2 个字节将其减半; 4 字节再次减半,改进一直持续到大约 32 KiB。在那之后,性能稳定且快速(最后几行似乎每行都在不到一秒的时间内出现,但我没有密切注意)。我会使用clock_gettime()(或gettimeofday(),如果不可用)来为每个周期计时。起初我担心单字节复制缺乏进展,但第二个终端窗口确认副本正在增长,但是太慢了!

【讨论】:

【参考方案2】:

为什么文件运行后似乎没有复制? (根据时间?)

很多可能性。首先,您的代码存在问题。您似乎没有倒带或重新打开要复制的文件。在第一次迭代之后,你在文件的末尾,所以剩余的迭代复制了 0 个字节。

其次,需要考虑操作系统因素。特别是,通用操作系统维护最近使用的磁盘内容的内存缓存。这意味着您第一次读取文件时,必须将其从磁盘中取出,但在随后的情况下,它可能已经在 RAM 中。

我是否正确使用了免费? (我尝试在循环中移动它,但它没有运行)

是的。如果它足够大,Realloc 将重用相同的内存块,或者它会 malloc 一个新块,复制旧块并释放旧块。所以永远不要尝试重新分配你已经释放的块。

我是否将缓冲区适当地传递给函数副本?

是的,但是您没有在函数copy() 中正确使用它,正如您收到的 cmets 所详述的那样。 copy() 中的一些问题是:

buffer 已经是 char*,所以不要将其地址传递给 read()。 taillebuffer 的长度,所以直接传递给read。 Passingf sizeof taille 传递变量本身的大小,而不是其内容。 write 不一定需要一口气将缓冲区中的所有字节都写入。在这种情况下,它将返回一个简短的计数(不太可能是磁盘文件的问题)。 write 也可以返回 -1 来表示错误。您需要处理该错误。

在你的主程序中也有问题。

如上所述:您要么需要关闭并重新打开输入文件,要么在每次循环迭代时将其倒回到开头。 remove 不会按照您的想法做,它只是删除目录条目并减少文件的引用计数。该文件只有在其引用计数达到零时才会物理消失。当您仍然有一个打开的文件描述符时,它不会达到零。因此,您还需要关闭并重新打开输出文件,否则您将继续附加到一个匿名文件,该文件将在您的进程退出时自动删除。 我以前没有发现的一个:你应该将taillebuffer_size 声明为size_t,因为这是reallocread(和write)的参数的正确大小类型。但是,t 应该是 ssize_t(有符号大小),因为它可以返回 -1 或读取/写入的字节数。

【讨论】:

remove() 是 Unix unlink() 的标准 C 变体;它会删除一个文件。 @JonathanLeffler 哇。我不知道也不知道。你每天学习新的东西。谢谢。 感谢 Jeremy 的详细回答!是的,remove() 只是删除文件。我在edit2中发布的新版本副本好吗?它不会打印错误。如果我尝试使用 @JonathanLeffler 建议的 write(...) != t,那么我会打印大量错误。 @JonathP 你的新copy 和@JonathanLeffler 之间的区别在于他报告了一个简短的错误。即如果write 返回小于t 的正数。您需要调查原因。【参考方案3】:

如 cmets 中所述,此代码是错误的:

void copy (int in, int out, char *buffer, long long taille) 
  int t;

  while ((t = read(in, &buffer, sizeof taille))> 0)
    write (out, &buffer, t);


  if (t < 0)
    perror("read");

首先,一个小问题:read()write() 返回 ssize_t,而不是 int

其次,您忽略了来自write() 的返回值,因此您永远不知道写入了多少。这可能是代码中的问题,也可能不是问题,但例如,您不会从已填满的文件系统中检测到失败的副本。

现在,解决真正的问题。

read(in, &buffer, sizeof taille)

&amp;buffer 是错误的。 bufferchar * - 内存中的变量,包含 char 缓冲区的地址。这告诉read() 将从in 文件描述符读取的数据放入buffer 指针变量本身占用的内存中,而不是buffer 指针变量中保存的地址所指的实际内存中。你只需要buffer

sizeof taille 也是错误的。这就是 taille 变量本身的大小 - 作为 long long,它可能是 8 个字节。

如果您尝试复制整个文件:

void copy( int in, int out, char *buffer, size_t bufsize )

    // why stuff three or four operations into
    // the conditional part of a while()??
    for ( ;; )
    
        ssize_t bytes_read = read( in, buffer, bufsize );
        if ( bytes_read <= 0 )
        
            break;
        

        ssize_t bytes_written = write( out, buffer, bytes_read );
        if ( bytes_written != bytes_read )
        
            // error handling code
        
    
 

就这么简单。困难的部分是任何可能的故障的错误处理。

【讨论】:

【参考方案4】:

这个话题已经有一段时间没有活跃了,但我想添加到 Andrew Henle 的帖子中。

为了更好地了解复制文件所涉及的实时时间,可以在永远循环退出之后和 copy() 返回之前添加一个fsync(2)fsync(2) 将确保系统缓冲区中的所有数据都已发送到底层存储设备。但是请注意,大多数磁盘驱动器都有一个板载缓存,可以缓冲写入,再次掩盖写入媒体所需的实际时间。

我编写的绝大多数代码都是针对安全关键系统的。这些系统如果发生故障,可能会导致严重的伤害或死亡,或严重的环境破坏。此类系统可以在现代飞机、核电站、医疗设备和汽车计算机中找到,仅举几例。

适用于安全关键系统源代码的规则之一是循环必须有一个明确的条件才能跳出循环。通过“清除”,中断条件必须在forwhiledo-while 中表示,而不是在复合语句中的某个位置。

我完全理解安德鲁写的内容。意图很明确。它很简洁。它没有任何问题。这是一个很好的建议。

但是(这里是“但是”),for 中的条件乍一看似乎是无限的:

for (;;) ...

为什么这很重要?源代码验证器会将其标记为无限循环。然后,您的绩效评估受到影响,您没有得到预期的加薪,您的妻子生您的气,提出离婚,拿走您拥有的一切,并与您的离婚律师一起离开。 这就是为什么它很重要。

我想建议一种替代结构:

void copy( int in, int out, char *buffer, size_t bufsize )

    ssize_t bytes_read;

    switch(1) do
    
        ssize_t bytes_written;

        bytes_written = write( out, buffer, bytes_read );
        if ( bytes_written != bytes_read )
        
            // error handling code
        

    default:    // Loop entry point is here.
        bytes_read = read( in, buffer, bufsize );
     while (bytes_read > 0 );

    fsync(out);
 
我第一次遇到像这样的switch-loop 结构是在 80 年代中期。这是通过避免偏离顺序指令的执行来优化流水线架构的使用的努力。

假设您有一个简单的例程,必须多次执行几件事。将数据从一个缓冲区复制到另一个缓冲区就是一个很好的例子。

    char *srcp, *dstp;    // source and destination pointers
    int count;            // number of bytes to copy (must be > 0)
    ...
    while (count--) 
        *dstp++ = *srcp++;
    
    ...

足够简单。对吧?

缺点:循环周围的每次迭代,处理器都必须跳回到循环的开头,这样做会转储预取管道中的任何内容。

使用一种称为“循环展开”的技术,可以对其进行重写以利用管道:

    char *srcp, *dstp;    // source and destination pointers
    int count;            // number of bytes to copy (must be > 0)
    ...
    switch (count % 8) do 
        case 0:  *dstp++ = *srcp++; --count;
        case 7:  *dstp++ = *srcp++; --count;
        case 6:  *dstp++ = *srcp++; --count;
        case 5:  *dstp++ = *srcp++; --count;
        case 4:  *dstp++ = *srcp++; --count;
        case 3:  *dstp++ = *srcp++; --count;
        case 2:  *dstp++ = *srcp++; --count;
        case 1:  *dstp++ = *srcp++; --count;
     while (count > 0);
    ...

跟随它。执行的第一条语句是switch。它采用计数的低三位并跳转到适当的case 标签。每个 case 复制数据,增加指针,减少计数,然后下降到下一个 case

当它到达底部时,将评估 while 条件,如果为真,则在 do..while 的顶部继续执行。它确实重新执行switch

优点是生成的机器代码是一系列较长的顺序指令,因此执行的跳转更少,从而更好地利用了流水线架构。

【讨论】:

以上是关于更改缓冲区大小以在 C 中复制文件的主要内容,如果未能解决你的问题,请参考以下文章

linux下c语言实现多线程文件复制

理想的缓冲区大小是多少? [复制]

在 Windows 上更改文件的大小写? [复制]

在 Windows 上更改文件的大小写? [复制]

复制文件

准确汇总文件大小以在 JProgressBar 中使用