Por que o loop cat x x?

16

Os seguintes comandos bash vão para um loop infinito:

$ echo hi > x
$ cat x >> x

Eu posso supor que cat continue lendo x depois começou a escrever para stdout. O que é confuso, no entanto, é que a minha própria implementação de teste de gato exibe um comportamento diferente:

// 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;
}

Se eu correr:

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

Não faz um loop não . Dado o comportamento de cat e o fato de que eu sou flushing para stdout antes de fread ser chamado novamente, eu esperaria que este código C continue lendo e escrevendo em um ciclo.

Como esses dois comportamentos são consistentes? Qual mecanismo explica por que cat faz um loop enquanto o código acima não funciona?

    
por Tyler 10.09.2014 / 22:17

4 respostas

11

Em um sistema RHEL mais antigo eu tenho, /bin/cat não não faz loop para cat x >> x . cat fornece a mensagem de erro "cat: x: o arquivo de entrada é o arquivo de saída". Eu posso enganar /bin/cat fazendo isso: cat < x >> x . Quando eu tento seu código acima, recebo o "looping" que você descreve. Eu também escrevi uma chamada de sistema com base em "gato":

#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;
}

Isso faz um loop também. O único buffer aqui (diferentemente do "mycat" baseado em stdio) é o que acontece no kernel.

Acho que o que está acontecendo é que o descritor de arquivo 3 (o resultado de open(av[1]) ) tem um deslocamento no arquivo de 0. O descritor de arquivo 1 (stdout) tem um deslocamento de 3, porque o "> >" faz com que o shell de chamada faça um lseek() no descritor de arquivo antes de entregá-lo ao processo filho cat .

Fazer um read() de qualquer tipo, seja em um buffer stdio, ou um char buf[] simples avança a posição do descritor de arquivo 3. Fazer um write() avança a posição do descritor de arquivo 1. Esses dois deslocamentos são diferentes números. Por causa do "> >", o descritor de arquivo 1 sempre tem um deslocamento maior ou igual ao deslocamento do descritor de arquivo 3. Assim, qualquer programa "parecido com gato" fará um loop, a menos que faça algum buffer interno. É possível, talvez até provável, que uma implementação stdio de um FILE * (que é o tipo dos símbolos stdout e f em seu código) que inclui seu próprio buffer. fread() pode realmente fazer uma chamada de sistema read() para preencher o buffer interno para f . Isso pode ou não alterar qualquer coisa no interior de stdout . Chamar fwrite() on stdout pode ou não alterar qualquer coisa dentro de f . Portanto, um "gato" baseado em stdio pode não dar um loop. Ou talvez. Difícil dizer sem ler um monte de código feio e feio.

Eu fiz um strace no RHEL cat - ele faz apenas uma sucessão de chamadas de sistema read() e write() . Mas um cat não precisa funcionar dessa maneira. Seria possível mmap() do arquivo de entrada, em seguida, write(1, mapped_address, input_file_size) . O kernel faria todo o trabalho. Ou você poderia fazer uma chamada de sistema sendfile() entre os descritores de arquivo de entrada e saída nos sistemas Linux. Dizia-se que os antigos sistemas SunOS 4.x faziam o truque de mapeamento de memória, mas não sei se alguém já fez um gato baseado em sendfile. Em ambos os casos, o "looping" não aconteceria, já que write() e sendfile() exigem um parâmetro de comprimento para transferência.

    
por 11.09.2014 / 00:11
1

Uma implementação de cat moderna (sunos-4.0 1988) usa mmap () para mapear todo o arquivo e, em seguida, chama 1x write () para este espaço. Tal implementação não irá fazer um loop, desde que a memória virtual permita mapear todo o arquivo.

Para outras implementações, depende se o arquivo é maior que o buffer de E / S.

    
por 25.08.2015 / 20:56
0

Como escrito em armadilhas do Bash , você não pode ler um arquivo e gravar nele no mesmo pipeline.

Depending on what your pipeline does, the file may be clobbered (to 0 bytes, or possibly to a number of bytes equal to the size of your operating system's pipeline buffer), or it may grow until it fills the available disk space, or reaches your operating system's file size limitation, or your quota, etc.

A solução é usar editor de texto ou variável temporária.

    
por 25.08.2015 / 16:45
-1

Você tem algum tipo de condição de corrida entre os dois x . Algumas implementações de cat (por exemplo, coreutils 8.23) proíbem que:

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

Se isto não for detectado, o comportamento obviamente dependerá da implementação (tamanho do buffer, etc.).

Em seu código, você pode tentar adicionar um clearerr(f); após o fflush , caso o próximo fread retorne um erro se o indicador de fim de arquivo estiver definido.

    
por 10.09.2014 / 22:23