Mengapa cat x >> x loop?

17

Perintah bash berikut masuk ke loop infinte:

$ echo hi > x
$ cat x >> x

Saya bisa menebak bahwa catterus membaca dari xsetelah mulai menulis ke stdout. Yang membingungkan, bagaimanapun, implementasi tes saya sendiri terhadap kucing menunjukkan perilaku yang berbeda:

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

Jika saya menjalankan:

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

Itu benar tidak berulang. Mengingat perilaku catdan fakta yang ingin saya sampaikan stdoutsebelumnya freaddipanggil lagi, saya berharap kode C ini terus membaca dan menulis dalam satu siklus.

Bagaimana kedua perilaku ini konsisten? Mekanisme apa yang menjelaskan mengapacat loop sementara kode di atas tidak?

Tyler
sumber
Itu loop untuk saya. Sudahkah Anda mencoba menjalankannya di bawah strace / truss? Sistem apa yang Anda pakai?
Stéphane Chazelas
Tampaknya kucing BSD memiliki perilaku ini dan kucing GNU melaporkan kesalahan ketika kami mencoba sesuatu seperti ini. Jawaban ini membahas hal yang sama dan saya percaya Anda menggunakan kucing BSD karena saya memiliki kucing GNU dan ketika diuji mendapatkan kesalahan.
Ramesh
Saya menggunakan Darwin. Saya suka ide yang cat x >> xmenyebabkan kesalahan; Namun, perintah ini disarankan dalam buku Kernighan dan Unix Pike sebagai latihan.
Tyler
3
catkemungkinan besar menggunakan panggilan sistem bukan stdio. Dengan stdio, program Anda mungkin caching EOFness. Jika Anda memulai dengan file yang lebih besar dari 4096 byte, apakah Anda mendapatkan loop tak terbatas?
Mark Plotnick
@MarkPlotnick, ya! Kode C berputar ketika file lebih dari 4k. Terima kasih, mungkin itulah perbedaan keseluruhan di sana.
Tyler

Jawaban:

12

Pada sistem RHEL tua saya punya, /bin/cattidak tidak loop untuk cat x >> x. catmemberikan pesan kesalahan "cat: x: file input adalah file output". Aku bisa menipu /bin/catdengan melakukan hal ini: cat < x >> x. Ketika saya mencoba kode Anda di atas, saya mendapatkan "perulangan" yang Anda jelaskan. Saya juga menulis system call berbasis "kucing":

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

Loop ini juga. Satu-satunya buffering di sini (tidak seperti untuk "mycat" berbasis stdio) adalah apa yang terjadi di kernel.

Saya pikir apa yang terjadi adalah file deskriptor 3 (hasil open(av[1])) memiliki offset ke file 0. Deskrip deskriptor 1 (stdout) memiliki offset 3, karena ">>" menyebabkan shell yang dipanggil untuk melakukanlseek() pada deskriptor file sebelum menyerahkannya ke catproses anak.

Melakukan read()apa pun, apakah menjadi buffer stdio, atau polos char buf[]memajukan posisi deskriptor file 3. Melakukan write()memajukan posisi deskriptor file 1. Kedua offset tersebut adalah angka yang berbeda. Karena ">>", deskriptor file 1 selalu memiliki offset lebih besar dari atau sama dengan offset deskriptor file 3. Jadi setiap program "seperti kucing" akan diulang, kecuali program tersebut melakukan buffering internal. Mungkin saja, bahkan mungkin, bahwa implementasi stdio dari FILE *(yang merupakan jenis simbol stdoutdan fdalam kode Anda) yang menyertakan buffernya sendiri. fread()sebenarnya dapat melakukan panggilan sistem read()untuk mengisi buffer internal untuk mungkin atau mungkin tidak mengubah apa pun di dalamnyaf . Ini mungkin atau mungkin tidak mengubah apa pun di bagian dalam stdout. memanggil fwrite()distdoutf . Jadi "kucing" berbasis stdio mungkin tidak berulang. Atau mungkin. Sulit dikatakan tanpa membaca banyak kode libc yang jelek dan jelek.

Saya melakukan stracepada RHEL cat- itu hanya melakukan suksesi read()dan write()panggilan sistem. Tetapi cattidak harus bekerja dengan cara ini. Itu mungkin untuk mmap()file input, kemudian lakukan write(1, mapped_address, input_file_size). Kernel akan melakukan semua pekerjaan. Atau Anda bisa melakukan sendfile()panggilan sistem antara deskriptor file input dan output pada sistem Linux. Sistem SunOS 4.x lama dikabarkan melakukan trik pemetaan memori, tapi saya tidak tahu apakah ada yang pernah menggunakan kucing berbasis sendfile. Dalam kedua kasus, "perulangan" tidak akan terjadi, karena keduanya write()dan sendfile()memerlukan parameter panjang-untuk-transfer.

Bruce Ediger
sumber
Terima kasih. Di Darwin, sepertinya freadpanggilan itu menembolok bendera EOF seperti yang disarankan Mark Plotnick. Bukti: [1] Kucing Darwin menggunakan membaca, bukan ketakutan; dan [2] Panggilan menakutkan Darwin __srefill yang diatur fp->_flags |= __SEOF;dalam beberapa kasus. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler
1
Ini luar biasa - saya adalah yang pertama mengunggahnya kemarin. Ini mungkin layak disebut bahwa hanya beralih POSIX-ditetapkan untuk catadalah cat -u- u untuk unbuffered .
mikeserv
Sebenarnya, >>harus diimplementasikan dengan memanggil open () dengan O_APPENDflag, yang menyebabkan setiap operasi penulisan (secara atomis) menulis ke akhir file saat ini, tidak peduli apa posisi deskriptor file sebelum membaca. Perilaku ini diperlukan untuk foo >> logfile & bar >> logfilebekerja dengan benar, misalnya - Anda tidak dapat mengasumsikan bahwa posisi setelah akhir tulisan terakhir Anda sendiri masih merupakan akhir dari file.
hmakholm tersisa Monica
1

Implementasi kucing modern (sunos-4.0 1988) menggunakan mmap () untuk memetakan seluruh file dan kemudian memanggil 1x tulis () untuk ruang ini. Implementasi seperti itu tidak akan berulang selama memori virtual memungkinkan untuk memetakan seluruh file.

Untuk implementasi lain tergantung pada apakah file lebih besar dari buffer I / O.

schily
sumber
Banyak catimplementasi tidak buffer output mereka ( -utersirat). Itu akan selalu berulang.
Stéphane Chazelas
Solaris 11 (SunOS-5.11) tampaknya tidak menggunakan mmap () untuk file kecil (tampaknya hanya menggunakan file 32769 bytes yang besar atau lebih tinggi).
Stéphane Chazelas
Benar -u biasanya default. Ini tidak menyiratkan loop sebagai implementasi dapat membaca seluruh filesize dan hanya menulis satu dengan buf itu.
schily
Solaris cat hanya loop jika filesize> max mapsize atau jika fileoffset awal! = 0.
schily
Apa yang saya amati dengan Solaris 11. Itu loop baca () jika offset awal adalah! = 0 atau jika filesise antara 0 dan 32768. Di atas itu, ia mmaps () 8MiB wilayah besar file pada suatu waktu dan tidak pernah tampaknya kembali untuk membaca () loop bahkan untuk file PiB (diuji pada file jarang).
Stéphane Chazelas
0

Seperti yang ditulis dalam perangkap Bash , Anda tidak dapat membaca dari file dan menulisnya di pipa yang sama.

Bergantung pada apa yang dilakukan oleh pipeline Anda, file tersebut akan musnah (hingga 0 byte, atau mungkin sejumlah byte sama dengan ukuran buffer pipeline sistem operasi Anda), atau dapat tumbuh hingga mengisi ruang disk yang tersedia, atau mencapai batasan ukuran file sistem operasi Anda, atau kuota Anda, dll.

Solusinya adalah dengan menggunakan editor teks, atau variabel sementara.

MatthewRock
sumber
-1

Anda memiliki semacam kondisi balapan di antara keduanya x. Beberapa implementasi cat(mis. Coreutils 8.23) melarang bahwa:

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

Jika ini tidak terdeteksi, perilaku jelas akan tergantung pada implementasinya (ukuran buffer, dll.).

Dalam kode Anda, Anda bisa mencoba menambahkan clearerr(f);setelah fflush, jika berikutnya freadakan mengembalikan kesalahan jika indikator akhir file diatur.

vinc17
sumber
Tampaknya OS yang baik akan memiliki perilaku deterministik untuk proses tunggal dengan utas tunggal menjalankan perintah baca / tulis yang sama. Bagaimanapun, perilaku itu deterministik bagi saya, dan saya terutama bertanya tentang perbedaan itu.
Tyler
@ Tyler IMHO, tanpa spesifikasi yang jelas tentang kasus ini, perintah di atas tidak masuk akal, dan determinisme tidak terlalu penting (kecuali kesalahan seperti di sini, yang merupakan perilaku terbaik). Ini sedikit seperti i = i++;perilaku C yang tidak terdefinisi, karenanya perbedaan.
vinc17
1
Tidak, tidak ada kondisi ras di sini, perilakunya didefinisikan dengan baik. Namun itu ditentukan implementasi, tergantung pada ukuran relatif file dan buffer yang digunakan oleh cat.
Gilles 'SO- stop being evil'
@Gilles Di mana Anda melihat bahwa perilaku tersebut terdefinisi dengan baik / implementasi didefinisikan? Bisakah Anda memberikan referensi? Spesifikasi cat POSIX hanya mengatakan: "Ini adalah implementasi-ditentukan apakah utilitas cat buffer output jika opsi -u tidak ditentukan." Namun, ketika buffer digunakan, implementasi tidak harus menentukan bagaimana buffer digunakan; itu mungkin non-deterministik, misalnya dengan buffer memerah secara acak.
vinc17
@ vinc17 Silakan masukkan "dalam praktek" di komentar saya sebelumnya. Ya, secara teori itu mungkin dan sesuai dengan POSIX, tetapi tidak ada yang melakukannya.
Gilles 'SO- stop being evil'