Perché cat x >> x loop?


17

I seguenti comandi bash entrano in un ciclo infinte:

$ echo hi > x
$ cat x >> x

Posso immaginare che catcontinua a leggere xdopo che ha iniziato a scrivere su stdout. Ciò che confonde, tuttavia, è che la mia implementazione di test su Cat mostra comportamenti diversi:

// 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 corro:

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

Lo fa Non loop. Dato il comportamento cate il fatto che sto arrossendo stdoutprima freadviene richiamato di nuovo, mi aspetto che questo codice C continui a leggere e scrivere in un ciclo.

Come sono coerenti questi due comportamenti? Quale meccanismo spiega perché i catcicli si verificano mentre il codice precedente non lo fa?


Fa il ciclo per me. Hai provato a eseguirlo sotto strace / truss? Su quale sistema sei?
Stéphane Chazelas,

Sembra che il gatto BSD abbia questo comportamento e il gatto GNU segnala un errore quando proviamo qualcosa del genere. Questa risposta discute lo stesso e credo che tu stia usando BSD Cat poiché ho GNU Cat e quando testato ha ottenuto l'errore.
Ramesh,

Sto usando Darwin. Mi piace l'idea che cat x >> xcausa un errore; tuttavia, questo comando è suggerito nel libro Unix di Kernighan e Pike come esercizio.
Tyler,

3
catmolto probabilmente usa le chiamate di sistema invece di stdio. Con stdio, il tuo programma potrebbe memorizzare nella cache EOFness. Se inizi con un file di dimensioni superiori a 4096 byte, ottieni un ciclo infinito?
Mark Plotnick,

@MarkPlotnick, sì! Il codice C viene ripetuto quando il file supera i 4k. Grazie, forse questa è l'intera differenza.
Tyler,

Risposte:


12

Su un vecchio sistema di RHEL che ho, /bin/catlo fa senza anello per cat x >> x. catvisualizza il messaggio di errore "cat: x: il file di input è un file di output". Posso ingannare /bin/catin questo modo: cat < x >> x. Quando provo il tuo codice sopra, ottengo il "looping" che descrivi. Ho anche scritto un "gatto" basato sulla chiamata di sistema:

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

Anche questo loop. L'unico buffering qui (a differenza di "mycat" basato su stdio) è ciò che accade nel kernel.

Penso che quello che sta accadendo è che il file descrittore 3 (il risultato di open(av[1])) ha un offset nel file di 0. Archiviato descrittore 1 (stdout) ha un offset di 3, perché la ">>" fa sì che la shell invocazione per fare un lseek()in descrittore di file prima di consegnarlo al catprocesso figlio.

Fare un read()qualsiasi tipo, sia in un buffer stdio, sia in una semplice, fa char buf[]avanzare la posizione del descrittore di file 3. Fare una write()posizione fa avanzare la posizione del descrittore di file 1. Questi due offset sono numeri diversi. A causa del ">>", il descrittore di file 1 ha sempre un offset maggiore o uguale all'offset del descrittore di file 3. Quindi qualsiasi programma "simile a un gatto" eseguirà il ciclo, a meno che non esegua un buffering interno. È possibile, forse anche probabile, che un'implementazione stdio di un FILE *(che è il tipo di simboli stdoute fnel tuo codice) che include il proprio buffer. fread()può effettivamente effettuare una chiamata di sistema read()per riempire il buffer interno fo f. Questo può o non può cambiare nulla all'interno stdout. Chiamata fwrite()sustdoutpuò o meno cambiare qualcosa all'interno di f. Quindi un "gatto" basato su stdio potrebbe non essere in loop. O potrebbe. Difficile dirlo senza leggere un sacco di brutti e brutti codici libc.

Ho fatto un straceRHEL cat- fa solo una serie di read()e write()chiamate di sistema. Ma a catnon deve funzionare in questo modo. Sarebbe possibile al mmap()file di input, quindi fare write(1, mapped_address, input_file_size). Il kernel farebbe tutto il lavoro. Oppure potresti fare una sendfile()chiamata di sistema tra i descrittori dei file di input e output sui sistemi Linux. Si diceva che i vecchi sistemi SunOS 4.x facessero il trucco della mappatura della memoria, ma non so se qualcuno abbia mai fatto un gatto basato su sendfile. In entrambi i casi il "looping" non si verificherebbe, in quanto entrambi write()e sendfile()richiedono un parametro di lunghezza da trasferire.


Grazie. Su Darwin, sembra che la freadchiamata abbia memorizzato nella cache una bandiera EOF come suggerito da Mark Plotnick. Prova: [1] il gatto Darwin usa read, non fread; e [2] la fread di Darwin chiama __srefill che fp->_flags |= __SEOF;in alcuni casi si imposta . [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler,

1
È fantastico - sono stato il primo a votarlo ieri. Esso potrebbe essere la pena ricordare che l' unico interruttore POSIX definito per catè cat -u- u per buffer .
Mikeserv,

In realtà, >>dovrebbe essere implementato chiamando open () con il O_APPENDflag, il che fa sì che ogni operazione di scrittura scriva (atomicamente) alla fine corrente del file, indipendentemente dalla posizione del descrittore di file prima della lettura. Questo comportamento è necessario per foo >> logfile & bar >> logfilefunzionare correttamente, ad esempio: non puoi permetterti di supporre che la posizione dopo la fine della tua ultima scrittura sia ancora la fine del file.
Hmakholm ha lasciato Monica il

1

Una moderna implementazione cat (sunos-4.0 1988) usa mmap () per mappare l'intero file e quindi chiama 1x write () per questo spazio. Tale implementazione non eseguirà il ciclo finché la memoria virtuale consente di mappare l'intero file.

Per altre implementazioni dipende dal fatto che il file sia più grande del buffer I / O.


Molte catimplementazioni non bufferizzano il loro output ( -uimplicito). Quelli saranno sempre in loop.
Stéphane Chazelas,

Solaris 11 (SunOS-5.11) non sembra utilizzare mmap () per file di piccole dimensioni (sembra ricorrere ad esso solo per file di dimensioni pari o superiori a 32769 byte).
Stéphane Chazelas,

Corretto -u è di solito il valore predefinito. Ciò non implica un ciclo poiché un'implementazione può leggere l'intera dimensione del file e fare solo una scrittura con quel buf.
schily

Il gatto Solaris esegue il loop solo se la dimensione del file è> dimensione massima della mappa o se la dimensione iniziale del file è! = 0.
schily

Cosa osservo con Solaris 11. Fa un ciclo read () se l'offset iniziale è! = 0 o se la dimensione del file è compresa tra 0 e 32768. Inoltre, mmaps () 8MiB di grandi aree del file alla volta e mai sembra tornare ai cicli read () anche per i file PiB (testati su file sparsi).
Stéphane Chazelas,

0

Come scritto nelle insidie ​​di Bash , non è possibile leggere da un file e scriverlo nella stessa pipeline.

A seconda di ciò che fa la pipeline, il file può essere bloccato (a 0 byte o eventualmente a un numero di byte uguale alla dimensione del buffer della pipeline del sistema operativo) oppure può crescere fino a riempire lo spazio disponibile su disco o raggiungere la limitazione della dimensione del file del sistema operativo o la quota, ecc.

La soluzione è utilizzare l'editor di testo o una variabile temporanea.


-1

Hai una sorta di condizione di razza tra entrambi x. Alcune implementazioni di cat(ad esempio coreutils 8.23) vietano che:

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

Se questo non viene rilevato, il comportamento dipenderà ovviamente dall'implementazione (dimensione del buffer, ecc.).

Nel tuo codice, potresti provare ad aggiungere un clearerr(f);dopo il fflush, nel caso in cui il prossimo freadrestituisca un errore se è impostato l'indicatore di fine file.


Sembra che un buon sistema operativo avrà un comportamento deterministico per un singolo processo con un singolo thread che esegue gli stessi comandi di lettura / scrittura. In ogni caso, il comportamento è deterministico per me e chiedo principalmente la discrepanza.
Tyler,

@Tyler IMHO, senza una chiara specifica in questo caso, il comando sopra non ha senso e il determinismo non è davvero importante (tranne un errore come qui, che è il miglior comportamento). Questo è un po 'come il i = i++;comportamento indefinito di C , quindi la discrepanza.
vinc17,

1
No, non ci sono condizioni di gara qui, il comportamento è ben definito. Tuttavia, è definito dall'implementazione, a seconda della dimensione relativa del file e del buffer utilizzato da cat.
Gilles 'SO- smetti di essere malvagio' l'

@Gilles Dove vedi che il comportamento è ben definito / definito dall'implementazione? Puoi darci qualche riferimento? La specifica cat di POSIX dice semplicemente: "È definito dall'implementazione se l'utility cat esegue il buffering se l'opzione -u non è specificata." Tuttavia, quando viene utilizzato un buffer, l'implementazione non deve definire il modo in cui viene utilizzato; può essere non deterministico, ad esempio con un buffer scaricato a tempo casuale.
vinc17,

@ vinc17 Inserire "in pratica" nel mio commento precedente. Sì, è teoricamente possibile e conforme a POSIX, ma nessuno lo fa.
Gilles 'SO- smetti di essere malvagio' l'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.