Warum schleift Katze x >> x?

17

Die folgenden Bash-Befehle gehen in eine Endlosschleife:

$ echo hi > x
$ cat x >> x

Ich kann mir vorstellen, dass es catweiter liest, xnachdem es angefangen hat, an stdout zu schreiben. Verwirrend ist jedoch, dass meine eigene Testimplementierung von cat ein anderes Verhalten aufweist:

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

Wenn ich renne:

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

Es wird keine Schleife ausgeführt. Angesichts des Verhaltens von catund der Tatsache, dass ich stdoutvorher einen Flush ausführte fread, würde ich erwarten, dass dieser C-Code in einem Zyklus weiter liest und schreibt.

Wie stimmen diese beiden Verhaltensweisen überein? Welcher Mechanismus erklärt, warum catSchleifen ausgeführt werden, während der obige Code dies nicht tut?

Tyler
quelle
Es macht eine Schleife für mich. Haben Sie versucht, es unter Strace / Truss laufen zu lassen? Auf welchem ​​System bist du?
Stéphane Chazelas
Es scheint, dass BSD cat dieses Verhalten hat und GNU cat einen Fehler meldet, wenn wir so etwas versuchen. Diese Antwort bespricht dasselbe und ich glaube, Sie verwenden BSD cat, da ich GNU cat habe und beim Testen den Fehler bekam.
Ramesh
Ich benutze Darwin. Mir gefällt die Idee, cat x >> xdie einen Fehler verursacht. Dieser Befehl wird jedoch in Kernighans und Pikes Unix-Buch als Übung vorgeschlagen.
Tyler
3
catverwendet höchstwahrscheinlich Systemaufrufe anstelle von stdio. Mit stdio kann Ihr Programm EOFness zwischenspeichern. Wenn Sie mit einer Datei beginnen, die größer als 4096 Byte ist, erhalten Sie eine Endlosschleife?
Mark Plotnick
@ MarkPlotnick, ja! Der C-Code wird wiederholt, wenn die Datei größer als 4 KB ist. Danke, vielleicht ist das der ganze Unterschied genau dort.
Tyler

Antworten:

12

Auf einem älteren RHEL System , das ich habe, /bin/cattut nicht Schleife für cat x >> x. catgibt die Fehlermeldung "cat: x: Eingabedatei ist Ausgabedatei" aus. Ich kann täuschen , /bin/catindem Sie diese: cat < x >> x. Wenn ich Ihren obigen Code ausprobiere, erhalte ich die von Ihnen beschriebene "Schleife". Ich habe auch einen Systemaufruf geschrieben, der auf "cat" basiert:

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

Dies ist auch eine Schleife. Die einzige Pufferung hier (im Gegensatz zu stdio-basierten "mycat") ist, was im Kernel vor sich geht.

Ich denke, was passiert ist, dass Dateideskriptor 3 (das Ergebnis von open(av[1])) einen Versatz in die Datei von 0 hat. Abgelegtes Deskriptor 1 (stdout) hat einen Versatz von 3, weil das ">>" die aufrufende Shell veranlasst, ein lseek()auf dem Dateideskriptor, bevor er an den catuntergeordneten Prozess übergeben wird.

Wenn Sie eine read()beliebige Art ausführen, sei es in einem Stdio-Puffer oder in einer Ebene, wird char buf[]die Position des Dateideskriptors vorgerückt. 3. Wenn Sie eine beliebige Art ausführen, wird write()die Position des Dateideskriptors vorgerückt. Aufgrund des ">>" hat Dateideskriptor 1 immer einen Versatz, der größer oder gleich dem Versatz von Dateideskriptor 3 ist. Daher wird jedes "katzenartige" Programm eine Schleife ausführen, es sei denn, es führt eine interne Pufferung durch. Es ist möglich, vielleicht sogar wahrscheinlich, dass eine stdio-Implementierung von a FILE *(das ist der Typ der Symbole stdoutund fin Ihrem Code) einen eigenen Puffer enthält. fread()kann tatsächlich einen Systemaufruf ausführen read(), um den internen Puffer fo zu füllen f. Dies kann oder kann nichts an den Innenseiten von ändern stdout. Ich rufe fwrite()anstdoutkann oder kann nichts innerhalb von ändern f. Eine stdio-basierte "Katze" kann also keine Schleife bilden. Oder es könnte. Schwer zu sagen, ohne viel hässlichen, hässlichen libc-Code durchzulesen.

Ich habe eine straceauf dem RHEL cat- es macht nur eine Abfolge von read()und write()Systemaufrufen. Aber so catmuss man nicht arbeiten. Es wäre dann möglich mmap()die Eingabedatei zu machen write(1, mapped_address, input_file_size). Der Kernel würde die ganze Arbeit machen. sendfile()Auf Linux-Systemen können Sie auch einen Systemaufruf zwischen den Eingabe- und Ausgabedateideskriptoren ausführen. Es wurde gemunkelt, dass alte SunOS 4.x-Systeme den Memory-Mapping-Trick ausführen, aber ich weiß nicht, ob jemals jemand eine sendfile-basierte Katze erstellt hat. In jedem Fall wäre die „Looping“ nicht passieren, da beide write()und sendfile()erfordern eine Länge-zu-Übertragungsparameter.

Bruce Ediger
quelle
Vielen Dank. Auf Darwin sieht es so aus, als hätte der freadAnruf eine EOF-Flagge zwischengespeichert, wie Mark Plotnick vorgeschlagen hatte. Beweise: [1] Darwin-Katze verwendet Read, nicht Fread; und [2] Darwins Stirnrunzeln ruft __srefill auf, was fp->_flags |= __SEOF;in einigen Fällen einstellt . [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler
1
Das ist großartig - ich war der erste, der gestern darüber gestimmt hat. Es könnte erwähnenswert, dass die nur POSIX definierten Schalter für catist cat -u- u für ungepufferte .
mikeserv
Eigentlich >>sollte es implementiert werden, indem open () mit dem O_APPENDFlag aufgerufen wird, was bewirkt, dass jede Schreiboperation (atomar) an das aktuelle Ende der Datei schreibt, unabhängig davon, an welcher Position sich der Dateideskriptor vor dem Lesen befand. Dieses Verhalten ist erforderlich foo >> logfile & bar >> logfile, um beispielsweise ordnungsgemäß zu funktionieren. Sie können es sich nicht leisten, davon auszugehen, dass die Position nach dem Ende Ihres letzten Schreibvorgangs immer noch das Ende der Datei ist.
Henning Makholm
1

Eine moderne Katzenimplementierung (sunos-4.0 1988) verwendet mmap (), um die gesamte Datei abzubilden, und ruft dann 1x write () für diesen Bereich auf. Eine solche Implementierung führt keine Schleife aus, solange der virtuelle Speicher die Zuordnung der gesamten Datei ermöglicht.

Bei anderen Implementierungen hängt es davon ab, ob die Datei größer als der E / A-Puffer ist.

schily
quelle
Viele catImplementierungen puffern ihre Ausgabe nicht ( -uimpliziert). Diese werden immer wiederholt.
Stéphane Chazelas
Solaris 11 (SunOS-5.11) verwendet anscheinend mmap () nicht für kleine Dateien (scheint nur für Dateien mit einer Größe von 32769 Bytes oder mehr darauf zurückzugreifen).
Stéphane Chazelas
Richtig -u ist normalerweise die Standardeinstellung. Dies bedeutet keine Schleife, da eine Implementierung die gesamte Dateigröße lesen und mit diesem Puffer nur einen Schreibvorgang ausführen kann.
Schily
Solaris cat wird nur wiederholt, wenn die Dateigröße> max. Kartengröße ist oder wenn der anfängliche Dateiversatz! = 0 ist.
Schily
Was ich mit Solaris 11 beobachte. Es wird eine read () - Schleife ausgeführt, wenn der anfängliche Versatz! = 0 ist oder wenn die Dateigröße zwischen 0 und 32768 liegt scheinen zu read () - Schleifen zurückzukehren, auch für PiB-Dateien (getestet mit spärlichen Dateien).
Stéphane Chazelas
0

Wie in Bash-Fallstricke geschrieben , können Sie nicht aus einer Datei lesen und in derselben Pipeline darauf schreiben.

Je nachdem, was Ihre Pipeline tut, ist die Datei möglicherweise überlastet (auf 0 Bytes oder möglicherweise auf eine Anzahl von Bytes, die der Größe des Pipeline-Puffers Ihres Betriebssystems entspricht) oder wächst, bis sie den verfügbaren Speicherplatz ausfüllt oder erreicht Die Dateigrößenbeschränkung Ihres Betriebssystems oder Ihr Kontingent usw.

Die Lösung besteht darin, entweder einen Texteditor oder eine temporäre Variable zu verwenden.

MatthewRock
quelle
-1

Sie haben eine Art Rennbedingung zwischen beiden x. Einige Implementierungen von cat(zB coreutils 8.23) verbieten Folgendes:

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

Wird dies nicht erkannt, hängt das Verhalten natürlich von der Implementierung ab (Puffergröße usw.).

In Ihrem Code können Sie versuchen, ein clearerr(f);nach dem einzufügen fflush, falls das nächste freadeinen Fehler zurückgibt, wenn das Kennzeichen für das Dateiende gesetzt ist.

vinc17
quelle
Es scheint, dass ein gutes Betriebssystem ein deterministisches Verhalten für einen einzelnen Prozess mit einem einzelnen Thread aufweist, in dem dieselben Lese- / Schreibbefehle ausgeführt werden. In jedem Fall ist das Verhalten für mich deterministisch und ich frage hauptsächlich nach der Diskrepanz.
Tyler
@ Tyler IMHO, ohne klare Angabe zu diesem Fall ist der obige Befehl nicht sinnvoll, und Determinismus ist nicht wirklich wichtig (mit Ausnahme eines Fehlers wie hier, der das beste Verhalten ist). Dies ist ein bisschen wie das i = i++;undefinierte Verhalten von C , daher die Diskrepanz.
Vinc17
1
Nein, hier gibt es keine Rennbedingung, das Verhalten ist klar definiert. Es ist jedoch abhängig von der relativen Größe der Datei und dem von verwendeten Puffer implementierungsdefiniert cat.
Gilles 'SO- hör auf böse zu sein'
@ Gilles Wo sehen Sie, dass das Verhalten klar definiert / implementierungsdefiniert ist? Können Sie einen Hinweis geben? Die POSIX-Katzenspezifikation sagt nur: "Es ist implementierungsdefiniert, ob das Dienstprogramm cat die Ausgabe puffert, wenn die Option -u nicht angegeben ist." Wenn jedoch ein Puffer verwendet wird, muss die Implementierung nicht definieren, wie er verwendet wird. Es kann nicht deterministisch sein, z. B. wenn ein Puffer zu einer zufälligen Zeit geleert wird.
Vinc17
@ vinc17 Bitte füge "in der Praxis" in meinen vorherigen Kommentar ein. Ja, das ist theoretisch möglich und POSIX-konform, aber niemand tut es.
Gilles 'SO- hör auf böse zu sein'