ทำไม cat x >> x loop


17

คำสั่ง bash ต่อไปนี้จะเข้าสู่วง infinte:

$ echo hi > x
$ cat x >> x

ฉันเดาได้ว่าcatอ่านต่อไปxหลังจากเริ่มเขียนไปยัง stdout แล้ว อย่างไรก็ตามสิ่งที่น่าสับสนคือการทดสอบการใช้งานแมวของฉันแสดงพฤติกรรมที่แตกต่าง:

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

ถ้าฉันวิ่ง:

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

มันไม่วนซ้ำ รับพฤติกรรมของcatและความจริงที่ว่าฉันเคยถูกลบล้างไปstdoutก่อนหน้าfreadนี้อีกครั้งฉันคาดว่ารหัส C นี้จะอ่านและเขียนต่อไปในรอบ

พฤติกรรมทั้งสองนี้สอดคล้องกันอย่างไร กลไกอะไรอธิบายว่าทำไมcatลูปในขณะที่โค้ดด้านบนไม่ได้


มันห่วงสำหรับฉัน คุณลองใช้งานภายใต้ strace / truss หรือไม่ คุณใช้ระบบอะไร
Stéphane Chazelas

ดูเหมือนว่า BSD cat มีพฤติกรรมนี้และ GNU cat รายงานข้อผิดพลาดเมื่อเราลองทำสิ่งนี้ คำตอบนี้พูดถึงสิ่งเดียวกันและฉันเชื่อว่าคุณกำลังใช้ BSD cat ตั้งแต่ฉันมี GNU cat และเมื่อทดสอบได้รับข้อผิดพลาด
Ramesh

ฉันใช้ดาร์วิน ฉันชอบความคิดที่cat x >> xทำให้เกิดข้อผิดพลาด แม้กระนั้นคำสั่งนี้แนะนำในหนังสือของยูนิกซ์ Kernighan และหอกเป็นแบบฝึกหัด
ไทเลอร์

3
catส่วนใหญ่ใช้การโทรของระบบแทน stdio ด้วย stdio โปรแกรมของคุณอาจแคช EOFness หากคุณเริ่มด้วยไฟล์ที่มีขนาดใหญ่กว่า 4096 ไบต์คุณจะได้วนซ้ำไม่สิ้นสุดหรือไม่?
Mark Plotnick

@ MarkPlotnick ใช่! รหัส C วนซ้ำเมื่อไฟล์มีขนาดเกิน 4k ขอบคุณบางทีนั่นอาจเป็นความแตกต่างทั้งหมด
ไทเลอร์

คำตอบ:


12

บนระบบเก่า RHEL ฉันมี, /bin/catไม่ได้cat x >> xห่วงสำหรับ catให้ข้อความแสดงข้อผิดพลาด "cat: x: ไฟล์อินพุตเป็นไฟล์เอาต์พุต" ฉันสามารถหลอกโดยการทำเช่นนี้:/bin/cat cat < x >> xเมื่อฉันลองโค้ดของคุณด้านบนฉันจะได้ "ลูป" ที่คุณอธิบาย ฉันยังเขียน "cat" ตามสายระบบ:

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

ลูปนี้เช่นกัน บัฟเฟอร์เดียวที่นี่ (ซึ่งแตกต่างจาก stdio ตาม "mycat") คือสิ่งที่เกิดขึ้นในเคอร์เนล

ฉันคิดว่าสิ่งที่เกิดขึ้นคือ file descriptor 3 (ผลลัพธ์ของopen(av[1])) มี offset เป็นไฟล์ 0 Filed descriptor 1 (stdout) มี offset ของ 3 เพราะ ">>" ทำให้เชลล์ที่อ้างถึงทำlseek()บน file descriptor ก่อนส่งไปยังcatกระบวนการ child

การread()เรียงลำดับใด ๆ ไม่ว่าจะเป็นบัฟเฟอร์ stdio หรือธรรมดาจะchar buf[]เลื่อนตำแหน่งของไฟล์ descriptor 3 การทำwrite()ตำแหน่งของไฟล์ descriptor ขั้นสูง 1 การออฟเซ็ตทั้งสองนั้นเป็นตัวเลขที่แตกต่างกัน เนื่องจาก ">>" file descriptor 1 มักจะมี offset มากกว่าหรือเท่ากับ offset ของ file descriptor 3 ดังนั้นโปรแกรม "cat-like" ใด ๆ ก็ตามจะวนซ้ำจนกว่าจะมีการบัฟเฟอร์ภายใน อาจเป็นไปได้หรืออาจเป็นไปได้ว่าการใช้ stdio ของFILE *(ซึ่งเป็นประเภทของสัญลักษณ์stdoutและfในรหัสของคุณ) ที่มีบัฟเฟอร์ของตัวเอง fread()จริงอาจจะเรียกระบบที่จะเติมบัฟเฟอร์ภายในสำหรับread()fstdoutนี้อาจหรือไม่อาจมีการเปลี่ยนแปลงอะไรในอวัยวะภายในของCalling fwrite()บนstdoutfหรืออาจจะไม่เปลี่ยนอะไรภายในของ ดังนั้น "cat" ที่ใช้ stdio อาจไม่วนซ้ำ หรืออาจ ยากที่จะพูดโดยไม่ต้องอ่านรหัส libc ที่น่าเกลียดและน่าเกลียดมาก

ฉันทำstraceบน RHEL cat- มันเป็นการต่อเนื่องread()และการwrite()เรียกของระบบ แต่catไม่ต้องทำงานแบบนี้ มันจะเป็นไปได้ที่จะใส่ไฟล์แล้วทำmmap() write(1, mapped_address, input_file_size)เคอร์เนลจะทำงานทั้งหมด หรือคุณสามารถทำการsendfile()เรียกระบบระหว่างตัวอธิบายไฟล์อินพุตและเอาต์พุตบนระบบ Linux ระบบเก่า SunOS 4.x มีข่าวลือว่าทำเคล็ดลับการจับคู่หน่วยความจำ แต่ฉันไม่รู้ว่ามีใครทำแมวที่ใช้ sendfile หรือไม่ ในทั้งสองกรณี "ลูป" จะไม่เกิดขึ้นเนื่องจากทั้งคู่write()และsendfile()ต้องการพารามิเตอร์แบบยาวต่อการถ่ายโอน


ขอบคุณ บนดาร์วินดูเหมือนว่าการfreadโทรจะเก็บค่าสถานะ EOF ตามที่ได้รับการแนะนำโดย Mark Plotnick หลักฐาน: [1] แมวดาร์วินใช้การอ่านไม่ใช่ fread; และ [2] fread ของดาร์วินเรียก __srefill ซึ่งกำหนดfp->_flags |= __SEOF;ไว้ในบางกรณี [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/
Tyler

1
มันยอดเยี่ยมมาก - ฉันเป็นคนแรกที่โหวตขึ้นเมื่อวานนี้ มันอาจจะมีมูลค่าการกล่าวขวัญว่าเพียงสวิทช์ POSIX ที่กำหนดไว้สำหรับcatเป็นcat -u- ยูสำหรับบัฟเฟอร์
mikeserv

ที่จริงแล้ว>>ควรจะนำมาใช้โดยการเรียก open () ด้วยการO_APPENDตั้งค่าสถานะซึ่งทำให้ทุกการดำเนินการเขียน (atomically) เขียนไปยังจุดสิ้นสุดปัจจุบันของไฟล์ไม่ว่าตำแหน่งของไฟล์ descriptor จะเป็นอย่างไรก่อนการอ่าน พฤติกรรมนี้จำเป็นสำหรับfoo >> logfile & bar >> logfileการทำงานอย่างถูกต้องตัวอย่างเช่นคุณไม่สามารถจะถือว่าตำแหน่งหลังจากสิ้นสุดการเขียนครั้งล่าสุดของคุณยังคงเป็นจุดสิ้นสุดของไฟล์
hmakholm ออกเดินทางจากโมนิก้า

1

การใช้ cat ที่ทันสมัย ​​(sunos-4.0 1988) ใช้ mmap () เพื่อแมปไฟล์ทั้งหมดแล้วเรียก 1x write () สำหรับพื้นที่นี้ การใช้งานดังกล่าวจะไม่วนซ้ำตราบใดที่หน่วยความจำเสมือนอนุญาตให้แมปไฟล์ทั้งหมด

สำหรับการนำไปใช้งานอื่น ๆ นั้นขึ้นอยู่กับว่าไฟล์มีขนาดใหญ่กว่าบัฟเฟอร์ I / O


catการใช้งานจำนวนมากไม่ได้บัฟเฟอร์ผลลัพธ์ของพวกเขา ( -uโดยนัย) สิ่งเหล่านั้นจะวนซ้ำเสมอ
Stéphane Chazelas

Solaris 11 (SunOS-5.11) ดูเหมือนจะไม่ใช้ mmap () สำหรับไฟล์ขนาดเล็ก (ดูเหมือนจะใช้กับไฟล์ 32769 ไบต์ที่มีขนาดใหญ่หรือสูงกว่า)
Stéphane Chazelas

ถูกต้อง -u มักจะเป็นค่าเริ่มต้น นี่ไม่ได้หมายความถึงการวนซ้ำเนื่องจากการใช้งานสามารถอ่านขนาดไฟล์ทั้งหมดและเขียนเพียงหนึ่งครั้งด้วย buf นั้น
schily

Solaris cat ลูปหากขนาดไฟล์คือ> max mapsize หรือหากชุดไฟล์เริ่มต้นคือ! = 0.
schily

สิ่งที่ฉันสังเกตเห็นด้วย Solaris 11 มันจะทำการอ่าน () ลูปถ้าอ็อฟเซ็ตเริ่มต้นคือ! = 0 หรือถ้าไฟล์นั้นมีค่าเป็น 0 และ 32768 เหนือกว่านั้นคือ mmaps () 8MiB พื้นที่ขนาดใหญ่ของไฟล์ในแต่ละครั้ง ดูเหมือนจะย้อนกลับไปอ่าน () ลูปแม้สำหรับไฟล์ PiB (ทดสอบในไฟล์ที่กระจัดกระจาย)
Stéphane Chazelas

0

ตามที่เขียนไว้ในข้อผิดพลาดทุบตี , คุณไม่สามารถอ่านจากไฟล์และเขียนไปในท่อเดียวกัน

ไฟล์อาจถูกอุดตัน (เป็น 0 ไบต์หรืออาจเป็นจำนวนไบต์เท่ากับขนาดของไพพ์ไลน์ของระบบปฏิบัติการของคุณ) หรืออาจขยายจนเต็มพื้นที่ดิสก์ที่มีอยู่หรือไปถึง ข้อ จำกัด ขนาดไฟล์ของระบบปฏิบัติการหรือโควต้าของคุณเป็นต้น

การแก้ปัญหาคือการใช้โปรแกรมแก้ไขข้อความหรือตัวแปรชั่วคราว


-1

xคุณมีชนิดของสภาพการแข่งขันระหว่างทั้งบาง การใช้งานบางอย่างของcat(เช่น coreutils 8.23) ห้ามว่า:

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

หากไม่พบสิ่งนี้พฤติกรรมจะขึ้นอยู่กับการนำไปใช้อย่างชัดเจน (ขนาดบัฟเฟอร์ ฯลฯ )

ในรหัสของคุณคุณสามารถลองเพิ่มclearerr(f);หลังfflushในกรณีที่ต่อไปfreadจะกลับข้อผิดพลาดหากตัวบ่งชี้การสิ้นสุดของไฟล์ถูกตั้งค่า


ดูเหมือนว่าระบบปฏิบัติการที่ดีจะมีพฤติกรรมที่กำหนดไว้สำหรับกระบวนการเดียวโดยมีเธรดเดียวที่รันคำสั่งอ่าน / เขียนเดียวกัน ไม่ว่าในกรณีใดพฤติกรรมนี้กำหนดไว้สำหรับฉันและฉันส่วนใหญ่ถามเกี่ยวกับความคลาดเคลื่อน
ไทเลอร์

@Tyler IMHO โดยไม่มีข้อกำหนดที่ชัดเจนในกรณีนี้คำสั่งดังกล่าวไม่สมเหตุสมผลและการกำหนดไม่ใช่เรื่องสำคัญ (ยกเว้นข้อผิดพลาดเช่นที่นี่ซึ่งเป็นพฤติกรรมที่ดีที่สุด) นี่เป็นi = i++;พฤติกรรมที่ไม่ได้กำหนดของ C ดังนั้นความแตกต่าง
vinc17

1
ไม่ไม่มีสภาพการแข่งขันที่นี่พฤติกรรมมีความชัดเจน catแต่มันเป็นการดำเนินงานที่กำหนดไว้ทั้งนี้ขึ้นอยู่กับขนาดของญาติของไฟล์และบัฟเฟอร์ที่ใช้โดย
Gilles 'หยุดชั่วร้าย'

@Gilles คุณเห็นว่าพฤติกรรมนั้นดี / กำหนดใช้งานได้อย่างไร คุณสามารถให้การอ้างอิงบางส่วนได้หรือไม่? ข้อมูลจำเพาะ cat POSIXเพิ่งบอกว่า: "มีการกำหนดการนำไปใช้งานว่าจะให้บัฟเฟอร์ยูทิลิตี้ cat ส่งออกหากไม่ได้ระบุตัวเลือก -u" อย่างไรก็ตามเมื่อมีการใช้บัฟเฟอร์การใช้งานไม่จำเป็นต้องกำหนดวิธีการใช้งาน มันอาจจะไม่ได้กำหนดไว้เช่นกับบัฟเฟอร์ล้างในเวลาสุ่ม
vinc17

@ vinc17 กรุณาใส่“ ในทางปฏิบัติ” ในความคิดเห็นก่อนหน้าของฉัน ใช่มันเป็นไปได้ในทางทฤษฎีและสอดคล้องกับ POSIX แต่ก็ไม่มีใครทำ
Gilles 'หยุดความชั่วร้าย'
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.