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

Changing buffer size to copy file in C

我创建了一个创建文件副本的函数:读取 --> 缓冲区 --> 写入。我试图多次增加缓冲区大小,看看是否会影响复制文件所需的时间(大约 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;
}

代码 运行s 并复制了文件,但计时部分无法正常工作

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]
  1. 为什么在第一个 运行 之后似乎没有复制? (根据时间?)
  2. 我是否适当地使用了免费? (我试着在循环中移动它,但它没有 运行)
  3. 我是否将缓冲区适当地传递给函数副本?

谢谢!

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

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");
}

Why doesn't it seem to copy after the file run? (according to the timing?)

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

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

Am I using free appropriately? (I tried moving it in the loop, but it doesn't run)

是的。如果内存块足够大,realloc 将重用同一个内存块,或者分配一个新块,复制旧块并释放旧块。因此,永远不要尝试重新分配您已经释放的块。

Am I passing the buffer appropriately to the function copy?

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

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

你的主程序也有问题。

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

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

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()returnssize_t,而不是int

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

现在,针对真正的问题。

read(in, &buffer, sizeof taille)

&buffer 是错误的。 buffer 是一个 char * - 内存中包含 char 缓冲区地址的变量。这告诉 read() 将从 in 文件描述符读取的数据放在 buffer 指针变量本身占用的内存中,而不是地址在 [=] 中保存的实际内存19=]指针变量所指。你只需要 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
        }
    }
 }

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

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

# 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(),如果不可用的话)放置替代的挂钟计时来为每个周期计时。起初我担心单字节复制缺乏进展,但第二个终端 window 确认副本正在增长,但是太慢了!

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

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

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

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

我完全理解 Andrew 写的内容。意图很明确。它很简洁。这没什么不对的。这是一个很好的建议。

But(这里是“但是”),for 中的条件乍一看似乎是无限的: </p> <blockquote> <p>for ( ;; ) { ... }</p> </blockquote>

为什么这很重要?源代码验证器会将此标记为无限循环。然后你的绩效评估结果很糟糕,你没有得到预期的加薪,你的妻子生你的气,申请离婚,带走你所有的一切,并与你的离婚律师一起离开。 这就是为什么它很重要。

我想推荐一个替代结构: </p> <pre><code>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 年代中期。这是通过避免偏离顺序指令的执行来优化流水线架构的使用的努力。

假设您有一个简单的例程,必须多次执行一些操作。将数据从一个缓冲区复制到另一个缓冲区就是一个很好的例子。 </p> <pre><code> char *srcp, *dstp; // source and destination pointers int count; // number of bytes to copy (must be > 0) ... while (count--) { *dstp++ = *srcp++; } ...

很简单。对吗?

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

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

<pre><code> 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.

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

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