为什么猫x >> x循环?


17

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

$ echo hi > x
$ cat x >> x

我可以猜到它开始写到stdout之后会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和再次刷新到stdout之前的事实,我fread希望此C代码可以继续在一个周期中进行读写。

这两种行为如何保持一致?什么机制解释了为什么cat上面的代码没有循环?


它确实为我循环。您是否尝试过在strace / truss下运行它?您在什么系统上?
斯特凡Chazelas

似乎BSD cat具有此行为,并且当我们尝试类似的操作时,GNU cat报告错误。这个答案讨论的是相同的,我相信您使用的是BSD cat,因为我有GNU cat,并且在测试时出现错误。
Ramesh 2014年

我正在使用达尔文。我喜欢cat x >> x会导致错误的想法;但是,此命令在Kernighan和Pike的Unix书中建议作为练习。
泰勒2014年

3
cat最有可能使用系统调用而不是stdio。使用stdio,您的程序可能正在缓存EOFness。如果从大于4096字节的文件开始,是否会遇到无限循环?
Mark Plotnick 2014年

@MarkPlotnick,是的!文件超过4k时,C代码会循环。谢谢,也许这就是那里的全部不同。
泰勒2014年

Answers:


12

在旧的RHEL系统我已经有了,/bin/cat确实没有 for循环cat x >> xcat给出错误消息“ 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]))已经偏移到提起描述符1(标准输出)的0文件的3偏移,因为“>>”将导致调用shell做一个lseek()对文件描述符,然后再交给cat子进程。

read()任何类型的,无论是成标准输入输出缓冲器,或纯char buf[]的进步文件描述符3.做的位置write()前进文件描述符1的这两个偏移是不同的号码的位置。由于存在“ >>”,因此文件描述符1的偏移量始终大于或等于文件描述符3的偏移量。因此,除非执行某些内部缓冲操作,否则任何“类似猫的”程序都会循环。这是可能的,甚至是有可能的,即一个stdio实现的FILE *(这是符号的类型stdoutf你的代码中),包括它自己的缓冲区。fread()实际上可以进行系统调用read()来填充内部缓冲区fo f。这可能会或可能不会更改的内部任何内容stdout。调用fwrite()stdout可能会或可能不会更改内的任何内容f。因此,基于stdio的“猫”可能不会循环。否则可能会。如果不通读很多丑陋的libc代码,很难说。

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


谢谢。在达尔文(Darwin)上,该fread呼叫看起来像Mark Plotnick建议的那样缓存了EOF标志。证据:[1]达尔文猫使用阅读而不是恐惧;和[2]达尔文的fread调用__srefill fp->_flags |= __SEOF;在某些情况下会设置。[1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…–
Tyler

1
太棒了-昨天我是第一个赞成它的人。这可能是值得一提的是,对POSIX定义的交换机catcat -u- ü无缓冲
mikeserv 2014年

实际上,>>应该通过使用带有O_APPEND标志的open()来实现,这会使每个写入操作(原子地)写入文件的当前端,而不管读取之前文件描述符的位置如何。例如,此行为是foo >> logfile & bar >> logfile正常工作所必需的-您不能假设自己上次写入结束后的位置仍然是文件的末尾。
hmakholm在Monica's

1

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

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


许多cat实现不缓冲其输出(-u隐含)。这些将永远循环。
斯特凡Chazelas

Solaris 11(SunOS-5.11)似乎没有对小文件使用mmap()(似乎仅对32769字节或更大的文件才使用mmap())。
斯特凡Chazelas

正确的-u通常是默认值。这并不意味着循环,因为实现可以读取整个文件大小,并且只能对该buf进行一次写入。
2015年

猫的Solaris只有当文件大小为>最大mapsize或循环如果初始的FileOffset是= 0!
席利

我在Solaris 11中观察到的情况。如果初始偏移量为!= 0或文件大小介于0和32768之间,它将执行read()循环。甚至对于PiB文件(在稀疏文件上测试)似乎也恢复为read()循环。
斯特凡Chazelas

0

就像Bash陷阱那样您无法在同一管道中读取文件并对其进行写入。

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

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


-1

两者之间存在某种竞争状况xcat(例如coreutils 8.23)的某些实现禁止:

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

如果未检测到,行为显然将取决于实现(缓冲区大小等)。

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


好的操作系统似乎对于具有单个线程并运行相同的读/写命令的单个进程具有确定性的行为。无论如何,这种行为对我来说是确定性的,我主要是在询问差异。
泰勒

@Tyler恕我直言,在这种情况下没有明确的说明,上面的命令没有意义,确定性并不是很重要(除了像这样的错误,这是最好的行为)。这有点像C的i = i++;未定义行为,因此存在差异。
vinc17 2014年

1
不,这里没有竞争条件,行为是明确定义的。但是,它是根据实现定义的,具体取决于文件的相对大小和所使用的缓冲区cat
吉尔斯(Gillles)“所以-别再作恶了” 2014年

@Gilles您在哪里看到行为定义明确/实现定义明确?你能给点参考吗?POSIX cat规范只是说:“如果未指定-u选项,则cat实用程序是否缓冲输出是实现定义的。” 但是,使用缓冲区时,实现不必定义如何使用缓冲区;它可能是不确定的,例如在随机时间冲洗缓冲区。
vinc17 2014年

@ vinc17请在我之前的评论中插入“实践中”。是的,从理论上讲这是可行的,并且符合POSIX,但是没有人这样做。
吉尔斯(Gilles)“所以,别再邪恶了”
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.