为什么 cat x >> x 循环?

为什么 cat x >> x 循环?

以下 bash 命令进入无限循环:

$ echo hi > x
$ cat x >> x

我可以猜测,在开始写入标准输出后,它cat会继续读取。x然而,令人困惑的是,我自己的 cat 测试实现表现出不同的行为:

// mycat.c
#include <stdio.h>

int main(int argc, char **argv) {
  FILE *f = fopen(argv[1], "rb");
  char buf[4096];
  int num_read;
  while ((num_read = fread(buf, 1, 4096, f))) {
    fwrite(buf, 1, num_read, stdout);
    fflush(stdout);
  }

  return 0;
}

如果我运行:

$ make mycat
$ echo hi > x
$ ./mycat x >> x

确实如此不是环形。考虑到 的行为以及我正在再次调用之前cat刷新的事实,我希望此 C 代码能够继续在一个循环中进行读写。stdoutfread

这两种行为如何一致?什么机制解释了为什么cat会出现循环而上面的代码却不会?

答案1

在我拥有的较旧的 RHEL 系统上,/bin/cat确实如此不是循环cat x >> x.cat给出错误消息“cat:x:输入文件是输出文件”。我可以/bin/cat通过这样做来愚弄:cat < x >> x。当我尝试上面的代码时,我得到了你描述的“循环”。我还写了一个基于系统调用的“cat”:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int
main(int ac, char **av)
{
        char buf[4906];
        int fd, cc;
        fd = open(av[1], O_RDONLY);
        while ((cc = read(fd, buf, sizeof(buf))) > 0)
                if (cc > 0) write(1, buf, cc);
        close(fd);
        return 0;
}

这也循环。这里唯一的缓冲(与基于 stdio 的“mycat”不同)是内核中发生的事情。

我认为发生的情况是文件描述符 3( 的结果open(av[1]))在文件中的偏移量为 0。文件描述符 1(stdout)的偏移量为 3,因为“>>”导致调用 shelllseek()在文件描述符,然后将其交给cat子进程。

执行read()任何类型的 a 操作,无论是进入 stdio 缓冲区还是普通操作,都会使char buf[]文件描述符 3 的位置前进。执行 a 操作会使write()文件描述符 1 的位置前进。这两个偏移量是不同的数字。由于“>>”,文件描述符 1 的偏移量始终大于或等于文件描述符 3 的偏移量。因此任何“类似猫”的程序都会循环,除非它进行一些内部缓冲。有可能,甚至很可能, a 的 stdio 实现(这是代码中的FILE *符号类型stdout)包含其自己的缓冲区。实际上可能会执行系统调用来填充内部缓冲区 fo 。这可能会也可能不会改变 内部的任何内容。调用可能会也可能不会改变 内部的任何内容。因此基于 stdio 的“cat”可能不会循环。或者可能是这样。如果不阅读大量丑陋的 libc 代码,就很难说。ffread()read()fstdoutfwrite()stdoutf

strace在 RHEL 上做了一个cat- 它只是执行一系列的系统read()调用write()。但 acat不一定要这样工作。可以mmap()输入文件,然后执行write(1, mapped_address, input_file_size).内核将完成所有工作。或者您可以sendfile()在 Linux 系统上的输入和输出文件描述符之间进行系统调用。据传旧的 SunOS 4.x 系统会执行内存映射技巧,但我不知道是否有人做过基于 sendfile 的猫。在任何一种情况下,“循环”都不会发生,因为 和 都write()需要sendfile()传输长度参数。

答案2

现代 cat 实现 (sunos-4.0 1988) 使用 mmap() 映射整个文件,然后为该空间调用 1x write()。只要虚拟内存允许映射整个文件,这样的实现就不会循环。

对于其他实现,这取决于文件是否大于 I/O 缓冲区。

答案3

正如所写重击陷阱,您无法在同一管道中读取文件并写入文件。

根据管道的作用,文件可能会被破坏(到 0 字节,或者可能到等于操作系统管道缓冲区大小的字节数),或者它可能会增长,直到填满可用磁盘空间,或达到您的操作系统的文件大小限制或配额等。

解决方案是使用文本编辑器或临时变量。

答案4

两者之间存在某种竞争条件x。某些实现cat(例如 coreutils 8.23)禁止:

$ cat x >> x
cat: x: input file is output file

如果没有检测到这一点,则行为显然取决于实现(缓冲区大小等)。

clearerr(f);在您的代码中,您可以尝试在后面添加fflush,以防fread如果设置了文件结束指示符,下一个将返回错误。

编辑:该行为最终被奥斯汀小组讨论,允许失败。

相关内容