Por que cat x >> x loop?

17

Os seguintes comandos bash entram em um loop infinte:

$ echo hi > x
$ cat x >> x

Eu acho que isso catcontinua a ser lido xdepois que ele começou a escrever no stdout. O que é confuso, no entanto, é que 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

Ele faz não loop. Dado o comportamento cate o fato de que eu estou liberando para o stdoutanterior freadé chamado novamente, eu esperaria que esse código C continuasse lendo e gravando em um ciclo.

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

Tyler
fonte
Faz um loop para mim. Você já tentou executá-lo sob strace / truss? Em que sistema você está?
Stéphane Chazelas
Parece que o gato BSD tem esse comportamento e o GNU cat relata um erro quando tentamos algo assim. Esta resposta discute o mesmo e acredito que você esteja usando o BSD cat, já que tenho o GNU cat e, quando testado, obteve o erro.
Ramesh
Eu estou usando Darwin. Eu gosto da ideia que cat x >> xcausa um erro; no entanto, esse comando é sugerido no livro Unix de Kernighan e Pike como um exercício.
Tyler
3
catprovavelmente usa chamadas do sistema em vez do stdio. Com o stdio, seu programa pode estar armazenando em cache o EOFness. Se você começar com um arquivo maior que 4096 bytes, obtém um loop infinito?
Mark Plotnick
@MarkPlotnick, yes! O código C faz um loop quando o arquivo tem mais de 4k. Obrigado, talvez essa seja toda a diferença.
Tyler

Respostas:

12

Em um sistema RHEL mais antigo que eu tenho, não/bin/cat faz loop para . dá a mensagem de erro "cat: x: arquivo de entrada é arquivo de saída". I pode enganar , fazendo isso: . Quando tento seu código acima, recebo o "loop" que você descreve. Também escrevi um "gato" baseado em chamada de sistema:cat x >> xcat/bin/catcat < x >> x

#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 dá laços também. O único buffer aqui (ao contrário 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 arquivado 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 catprocesso filho.

Executar read()qualquer tipo, seja em um buffer stdio ou em uma planilha, char buf[]avança a posição do descritor de arquivo 3. Executar a write()avança a posição do descritor de arquivo 1. Esses dois deslocamentos são números diferentes. Por causa do ">>", o descritor de arquivo 1 sempre tem um deslocamento maior ou igual ao deslocamento do descritor de arquivo 3. Portanto, qualquer programa "semelhante a um gato" fará um loop, a menos que faça algum buffer interno. É possível, talvez até provável, que uma implementação stdio de a FILE *(que é o tipo dos símbolos stdoute fno seu código) inclua seu próprio buffer. fread()pode realmente fazer uma chamada do sistema read()para preencher o buffer interno fo f. Isso pode ou não mudar nada no interior de stdout. chamando fwrite()emstdoutpode ou não alterar nada dentro de f. Portanto, um "gato" baseado em stdio pode não ser repetido. Ou pode. Difícil dizer sem ler muitos códigos libc feios e feios.

Eu fiz uma straceno RHEL cat- ele só faz uma sucessão de read()e write()chamadas do sistema. Mas a catnão precisa funcionar dessa maneira. Seria possível para mmap()o arquivo de entrada, então faça write(1, mapped_address, input_file_size). O kernel faria todo o trabalho. Ou você pode fazer uma sendfile()chamada do sistema 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 arquivo de envio. Em ambos os casos, o "loop" não aconteceria, pois ambos write()e sendfile()requerem um parâmetro de comprimento para transferir.

Bruce Ediger
fonte
Obrigado. Em Darwin, parece que a freadchamada armazenou em cache um sinalizador EOF, como sugeriu Mark Plotnick. Evidência: [1] o gato de Darwin usa leitura, não medo; e [2] o medo de Darwin chama __srefill, que ocorre fp->_flags |= __SEOF;em alguns casos. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler
1
Isso é incrível - eu fui o primeiro a votar ontem. Ele pode valer a pena mencionar que a única chave POSIX-definido para caté cat -u- u para unbuffered .
mikeserv 11/09/14
Na verdade, >>deve ser implementado chamando open () com o O_APPENDsinalizador, o que faz com que cada operação de gravação grave (atomicamente) no final atual do arquivo, independentemente da posição do descritor de arquivo antes da leitura. Esse comportamento é necessário para foo >> logfile & bar >> logfilefuncionar corretamente, por exemplo - você não pode assumir que a posição após o final da sua última gravação ainda é o final do arquivo.
hmakholm deixou Monica em 05/04
1

Uma implementação moderna de gato (sunos-4.0 1988) usa mmap () para mapear o arquivo inteiro e depois chama 1x write () para esse espaço. Essa implementação não será executada enquanto a memória virtual permitir mapear o arquivo inteiro.

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

esperto
fonte
Muitas catimplementações não armazenam em buffer sua saída ( -uimplícita). Aqueles sempre serão repetidos.
Stéphane Chazelas
O Solaris 11 (SunOS-5.11) não parece estar usando mmap () para arquivos pequenos (parece recorrer a ele apenas para arquivos com 32769 bytes de tamanho ou mais).
Stéphane Chazelas
-U correto é geralmente o padrão. Isso não implica um loop, pois uma implementação pode ler todo o tamanho do arquivo e fazer apenas uma gravação com esse buf.
schily
Solaris gato lacetes unicamente se o tamanho do arquivo é> mapsize max ou se o fileoffset inicial é = 0.!
Schily
O que observo com o Solaris 11. Ele executa um loop read () se o deslocamento inicial for! = 0 ou se o tamanho do arquivo estiver entre 0 e 32768. Acima disso, ele mapeia () 8MiB grandes regiões do arquivo por vez e nunca parece reverter para loops read () mesmo para arquivos PiB (testados em arquivos esparsos).
Stéphane Chazelas
0

Conforme escrito nas armadilhas do Bash , você não pode ler um arquivo e gravá-lo no mesmo pipeline.

Dependendo do que o seu pipeline faz, o arquivo pode ter um atrito (para 0 bytes ou possivelmente para um número de bytes igual ao tamanho do buffer de pipeline do sistema operacional) ou pode crescer até preencher o espaço em disco disponível ou atingir a limitação do tamanho do arquivo do seu sistema operacional, sua cota etc.

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

MatthewRock
fonte
-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 isso não for detectado, o comportamento obviamente dependerá da implementação (tamanho do buffer, etc.).

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

vinc17
fonte
Parece que um bom sistema operacional terá comportamento determinístico para um único processo com um único thread executando os mesmos comandos de leitura / gravação. De qualquer forma, o comportamento é determinístico para mim, e estou perguntando principalmente sobre a discrepância.
Tyler
@ Tyler IMHO, sem especificação clara sobre esse caso, o comando acima não faz sentido e o determinismo não é realmente importante (exceto um erro como aqui, que é o melhor comportamento). Isso é um pouco como o i = i++;comportamento indefinido de C , daí a discrepância.
precisa saber é o seguinte
1
Não, não há condição de corrida aqui, o comportamento é bem definido. No entanto, é definido pela implementação, dependendo do tamanho relativo do arquivo e do buffer usado por cat.
Gilles 'SO- stop be evil'
@Gilles Onde você vê que o comportamento é bem definido / definido pela implementação? Você pode dar alguma referência? A especificação cat POSIX apenas diz: "É definido pela implementação se o utilitário cat armazena em buffer se a opção -u não for especificada". No entanto, quando um buffer é usado, a implementação não precisa definir como ele é usado; pode ser não determinístico, por exemplo, com um buffer liberado aleatoriamente.
precisa saber é
@ vinc17 Por favor, insira “na prática” no meu comentário anterior. Sim, isso é teoricamente possível e compatível com POSIX, mas ninguém faz isso.
Gilles 'SO- stop be evil' (