Dlaczego pętla cat x >> x?

17

Następujące polecenia bash przechodzą w nieskończoną pętlę:

$ echo hi > x
$ cat x >> x

Mogę zgadywać, że catkontynuuje czytanie xpo rozpoczęciu pisania na standardowe wyjście. Mylące jest jednak to, że moja własna testowa implementacja kota wykazuje inne zachowanie:

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

Jeśli uruchomię:

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

Czyni nie pętla. Ze względu na zachowanie catoraz fakt, że mam do płukania stdoutprzed freadnazywa się ponownie, chciałbym się spodziewać ten kod C do dalszego czytania i pisania w cyklu.

Jak spójne są te dwa zachowania? Jaki mechanizm wyjaśnia, dlaczego catpętle, podczas gdy powyższy kod nie działa?

Tyler
źródło
Zapętla się dla mnie. Próbowałeś uruchomić go pod strace / kratownicą? W jakim systemie jesteś?
Stéphane Chazelas,
Wygląda na to, że BSD cat ma takie zachowanie, a GNU cat zgłasza błąd, gdy próbujemy czegoś takiego. Ta odpowiedź dotyczy tego samego i uważam, że używasz kota BSD, ponieważ mam kota GNU i po przetestowaniu dostałem błąd.
Ramesh
Używam Darwina. Podoba mi się pomysł, który cat x >> xpowoduje błąd; jednak polecenie to jest sugerowane w książce Kernighan and Pike's Unix jako ćwiczenie.
Tyler,
3
catnajprawdopodobniej używa wywołań systemowych zamiast standardowego. W stdio twój program może buforować EOFness. Jeśli zaczynasz od pliku większego niż 4096 bajtów, czy otrzymujesz nieskończoną pętlę?
Mark Plotnick,
@MarkPlotnick, tak! Kod C zapętla się, gdy rozmiar pliku przekracza 4k. Dzięki, może to jest cała różnica tutaj.
Tyler,

Odpowiedzi:

12

Na starszego systemu RHEL mam, /bin/catczy nie pętli dla cat x >> x. catwyświetla komunikat o błędzie „cat: x: plik wejściowy to plik wyjściowy”. Mogę oszukać /bin/catw ten sposób: cat < x >> x. Gdy wypróbuję powyższy kod, pojawia się opisany „zapętlenie”. Napisałem też „cat” oparty na wywołaniach systemowych:

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

To również pętle. Jedyne buforowanie tutaj (w przeciwieństwie do „mycat” na stdio) jest tym, co dzieje się w jądrze.

Myślę, że dzieje się tak, że deskryptor pliku 3 (wynik open(av[1])) ma przesunięcie do pliku 0. Przesunięty deskryptor 1 (stdout) ma przesunięcie 3, ponieważ „>>” powoduje, że powłoka wywołująca wykonuje lseek()polecenie deskryptor pliku przed przekazaniem go do catprocesu potomnego.

Robienie read()dowolnego rodzaju, czy to w buforze standardowym, czy zwykłym, char buf[]przesuwa pozycję deskryptora pliku 3. Robienie write()przesunięcia pozycji deskryptora pliku 1. Te dwa przesunięcia są różnymi liczbami. Z powodu „>>” deskryptor pliku 1 zawsze ma przesunięcie większe lub równe przesunięciu deskryptora pliku 3. Tak więc każdy program „podobny do kota” zapętli się, chyba że wykona jakieś buforowanie wewnętrzne. Możliwe, a może nawet prawdopodobne, że implementacja standardu FILE *(który jest typem symboli stdouti fkodu), która zawiera własny bufor. fread()może faktycznie wykonać wywołanie systemowe, read()aby wypełnić wewnętrzny bufor fo f. To może, ale nie musi, zmienić niczego w środku stdout. dzwoniąc fwrite()nastdoutmoże, ale nie musi, zmieniać niczego w środku f. Dlatego „kot” oparty na standardzie może nie zapętlić się. Lub może. Trudno powiedzieć bez przeczytania dużo brzydkiego, brzydkiego kodu libc.

Zrobiłem stracena RHEL cat- po prostu robi kolejne read()i write()systemowe wywołania. Ale catnie musi tak działać. mmap()Plik wejściowy byłby wtedy możliwy write(1, mapped_address, input_file_size). Jądro wykona całą pracę. Lub możesz wykonać sendfile()wywołanie systemowe między deskryptorami plików wejściowych i wyjściowych w systemach Linux. Mówi się, że stare systemy SunOS 4.x wykonują sztuczkę mapowania pamięci, ale nie wiem, czy ktokolwiek kiedykolwiek stworzył kota opartego na plikach send. W obu przypadkach „zapętlenie” nie stało, jak zarówno write()i sendfile()wymaga parametru długości do transferu.

Bruce Ediger
źródło
Dzięki. Na Darwinie wygląda to tak, jakby freadwezwanie buforowało flagę EOF, jak sugerował Mark Plotnick. Dowody: [1] Kot Darwina używa czytać, a nie bać się; i [2] wywołania fread Darwina __srefill, które ustawiają fp->_flags |= __SEOF;w niektórych przypadkach. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler
1
To jest niesamowite - byłem pierwszym, który głosował wczoraj. To może być warto wspomnieć, że tylko POSIX-zdefiniowany przełącznik catjest cat -u- u dla niebuforowana .
mikeserv
W rzeczywistości >>należy go zaimplementować, wywołując O_APPENDflagę open () , co powoduje każdy operacja zapisu zapisuje (atomowo) na bieżącym końcu pliku, bez względu na to, jaka była pozycja deskryptora pliku przed odczytem. Takie zachowanie jest konieczne na przykład foo >> logfile & bar >> logfiledo prawidłowego działania - nie możesz sobie pozwolić na założenie, że pozycja po zakończeniu ostatniego zapisu jest nadal końcem pliku.
hmakholm opuścił Monikę
1

Nowoczesna implementacja cat (sunos-4.0 1988) używa mmap () do mapowania całego pliku, a następnie wywołuje 1x write () dla tego miejsca. Taka implementacja nie będzie zapętlała się, dopóki pamięć wirtualna pozwoli zmapować cały plik.

W przypadku innych implementacji zależy to od tego, czy plik jest większy niż bufor We / Wy.

schily
źródło
Wiele catimplementacji nie buforuje wyników ( -udomyślnie). Te zawsze będą się zapętlać.
Stéphane Chazelas,
Solaris 11 (SunOS-5.11) nie wydaje się używać mmap () do małych plików (wydaje się, że korzysta z niego tylko w przypadku plików o wielkości 32769 bajtów lub większej).
Stéphane Chazelas,
Prawidłowe -u jest zwykle wartością domyślną. Nie oznacza to pętli, ponieważ implementacja może odczytać cały rozmiar pliku i wykonać tylko jeden zapis z tym bufem.
schily,
Kot Solaris zapętla się tylko wtedy, gdy rozmiar pliku jest> maksymalny rozmiar mapy lub jeśli początkowy limit plików to! = 0.
schily
To, co obserwuję w systemie Solaris 11. Wykonuje pętlę read (), jeśli początkowe przesunięcie wynosi! = 0 lub jeśli rozmiar pliku wynosi od 0 do 32768. Ponad tym, to mmaps () 8MiB dużych obszarów pliku na raz i nigdy wydają się wracać do pętli read () nawet dla plików PiB (testowane na plikach rzadkich).
Stéphane Chazelas,
0

Jak napisano w pułapkach Bash , nie możesz czytać z pliku i pisać do niego w tym samym potoku.

W zależności od tego, co robi Twój potok, plik może być zablokowany (do 0 bajtów lub być może do liczby bajtów równych rozmiarowi bufora potoku systemu operacyjnego) lub może rosnąć, dopóki nie zapełni dostępnego miejsca na dysku lub nie osiągnie ograniczenie rozmiaru pliku systemu operacyjnego lub limit itp.

Rozwiązaniem jest użycie edytora tekstu lub zmiennej tymczasowej.

MatthewRock
źródło
-1

Między nimi jest jakiś warunek wyścigu x. Niektóre implementacje cat(np. Coreutils 8.23) zabraniają:

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

Jeśli nie zostanie to wykryte, zachowanie będzie oczywiście zależeć od implementacji (rozmiar bufora itp.).

W swoim kodzie możesz spróbować dodać znak „ clearerr(f);po” fflush, na wypadek gdyby następny freadzwrócił błąd, jeśli ustawiony jest wskaźnik końca pliku.

vinc17
źródło
Wygląda na to, że dobry system operacyjny będzie zachowywał się deterministycznie dla pojedynczego procesu z jednym wątkiem uruchamiającym te same polecenia odczytu / zapisu. W każdym razie zachowanie jest dla mnie deterministyczne i pytam głównie o rozbieżności.
Tyler,
@Tyler IMHO, bez jasnego sprecyzowania tej sprawy, powyższe polecenie nie ma sensu, a determinizm nie jest tak naprawdę ważny (z wyjątkiem błędu takiego jak tutaj, który jest najlepszym zachowaniem). Jest to trochę jak i = i++;niezdefiniowane zachowanie C , stąd rozbieżność.
vinc17
1
Nie, nie ma tutaj warunków wyścigu, zachowanie jest dobrze określone. Jest on jednak zdefiniowany w zależności od implementacji, w zależności od względnego rozmiaru pliku i używanego bufora cat.
Gilles „SO- przestań być zły”
@Gilles Gdzie widzisz, że zachowanie jest dobrze zdefiniowane / zdefiniowane w realizacji? Czy możesz podać jakieś referencje? W specyfikacji POSIX cat jest po prostu powiedziane: „Zdefiniowane jest to, czy narzędzie cat buforuje dane wyjściowe, jeśli nie podano opcji -u”. Jednak gdy używany jest bufor, implementacja nie musi określać, w jaki sposób jest używany; może być niedeterministyczny, np. z buforem opróżnianym w przypadkowym czasie.
vinc17
@ vinc17 Proszę wstawić „w praktyce” do mojego poprzedniego komentarza. Tak, to teoretycznie możliwe i zgodne z POSIX, ale nikt tego nie robi.
Gilles „SO- przestań być zły”