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

3

我已经创建了一个函数来复制文件:读取 -> 缓存 -> 写入。我尝试多次增加缓存大小,查看它对复制文件所需时间的影响(约为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]
  1. 为什么第一次运行后似乎无法复制?(根据时间推算?)
  2. 我是否正确使用了free?(我尝试在循环中移动它,但它不运行)
  3. 我是否将缓冲区适当地传递给了copy函数?

谢谢!

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

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()错误。

编辑2:这个复制版本是否可以?

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

1
你为什么认为可以使用%ld格式说明符打印clock_t - Eugene Sh.
4
  1. 可能性很大,第一次(缓慢的)读取将文件加载到内核缓冲池缓存中,剩余的运行不需要再读取它。
  2. 是的。如果您使用malloc(),则必须将其放在循环内部,但是如果您使用realloc(),则不需要。
  3. 是的,大部分情况下是这样的。可能main()函数中的buffer_size应该是size_t类型,copy()函数中的taille也应该是size_t类型。 但是您正在滥用copy()函数中的缓冲区。
- Jonathan Leffler
2
'while ((t = read(in, &buffer, sizeof taille))> 0)'...... '&buffer'? 'buffer'是一个函数参数,您正在获取其地址以用作文件缓冲区...... - Martin James
2
你在主循环的第一次迭代中删除了输出文件(此后删除失败),但你没有重新打开文件描述符。这意味着你现在正在写入一个匿名文件,每个测试迭代都会增长。你应该在写完输出文件后关闭并重新打开它。另外,你没有倒回输入文件,因此第二个及以后的循环处理的数据为空;当循环开始时,读指针位于EOF。这比我第一条评论中提到的“缓存”效果更重要。缓存是真实存在的,但你的“未复制任何字节”的问题更为严重。 - Jonathan Leffler
1
在使用O_CREAT标志与open时,应指定permissionint fichier2 = open("grosfichier_copy", O_WRONLY|O_CREAT,0664); - Achal
显示剩余6条评论
4个回答

4

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

# 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左右。此后,性能稳定并且很快(最后几行似乎在不到1秒的时间内出现,但我没有特别关注)。我会添加备用的墙上时间测量方法,使用clock_gettime()(如果不可用,则使用gettimeofday())来测量每个周期的时间。一开始,由于单字节复制的缓慢进展,我感到有些担心,但第二个终端窗口证实正在进行复制,只是非常缓慢!


3

如评论中所指出,这段代码是错误的:

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

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


3
Why doesn't it seem to copy after the file run? (according to the timing?)
有很多可能性。首先,您的代码存在问题。您似乎没有倒带或重新打开文件进行复制。第一次迭代后,您已经到达文件末尾,因此剩余的迭代将复制0字节。
其次,需要考虑操作系统因素。特别是,通用操作系统会维护最近使用的磁盘内容的内存缓存。这意味着第一次读取文件时,必须从磁盘中提取它,但在随后的情况下,它可能已经在RAM中。
Am I using free appropriately? (I tried moving it in the loop, but it doesn't run)
是的。如果块足够大,realloc将重用同一内存块;否则,它将malloc一个新块,复制旧块并释放旧块。因此,不要尝试realloc已经释放的块。
Am I passing the buffer appropriately to the function copy?
是的,但是您没有在函数copy()中适当使用它,正如您收到的注释所详细说明的那样。copy()中的一些问题包括:
  • buffer 已经是一个 char*,因此不要取它的地址传递给 read()
  • taillebuffer 的长度,因此直接将其传递给 read。传递 sizeof taille 会传递变量本身的大小,而不是其内容。
  • write 不一定需要一次性写入缓冲区中的所有字节。在这种情况下,它将返回一个短计数(对于磁盘文件来说可能不是问题)。
  • write 还可以返回 -1 表示错误。您需要处理该错误。

在您的主程序中也存在问题。

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

remove()是Unix unlink()的标准C变体;它用于删除文件。 - Jonathan Leffler
@JonathanLeffler 哇,我不知道那个。每天都能学到新东西,谢谢。 - JeremyP
感谢您详细的回答,Jeremy!是的,remove()函数只是删除文件。我的新版本的copy代码(在edit2中发布)没问题吧?它不会打印错误信息。如果我尝试@JonathanLeffler建议的那个带有write(...) != t的代码,那么就会打印出大量的perror信息。 - Jonath P
@JonathP 你的新copy和@JonathanLeffler的不同之处在于,他会报告短计数的错误。即如果write返回一个小于t的正数计数。你需要调查为什么会这样。 - JeremyP

2

这个帖子已经有一段时间没有更新了,但我想补充一下Andrew Henle的帖子。

为了更好地了解复制文件所需的实际时间,可以在无限循环退出后并在copy()返回之前添加fsync(2)fsync(2)将确保系统缓冲区中的所有数据已发送到底层存储设备。 但是,请注意,大多数磁盘驱动器都具有可以缓冲写入的内置缓存,这再次掩盖了写入介质所需的实际时间。

我编写的绝大多数代码都用于安全关键系统。如果这些系统发生故障,可能会造成严重的伤害或死亡,或者造成严重的环境损害。这样的系统可以在现代飞机、核电站、医疗设备和汽车计算机等领域找到。

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

我完全理解Andrew写的内容。目的很明确。它简洁明了。没有任何问题。这是一个很好的建议。

但是(这里是“但是”),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 假设您有一个简单的程序,需要执行大量次数的操作。将数据从一个缓冲区复制到另一个缓冲区是一个完美的例子。
    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

当执行到最后一个case时,会评估while条件,如果为真,则继续在do..while的顶部执行。它不会重新执行switch

优点是生成的机器代码是一系列连续的指令,因此执行较少的跳转,更充分地利用流水线架构。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接