ปิดการบัฟเฟอร์ในท่อ


395

ฉันมีสคริปต์ที่เรียกสองคำสั่ง:

long_running_command | print_progress

long_running_commandพิมพ์ความคืบหน้า แต่ฉันไม่มีความสุขกับมัน ฉันใช้print_progressเพื่อทำให้ดีขึ้น (กล่าวคือฉันพิมพ์ความคืบหน้าในบรรทัดเดียว)

ปัญหา: การเชื่อมต่อไปยัง stdout ยังเปิดใช้งานบัฟเฟอร์ 4K เพื่อโปรแกรมการพิมพ์ที่ดีไม่ได้รับอะไรเลย ... ไม่มีอะไร ... ไม่มีอะไร ... ทั้งนั้น ... :)

ฉันจะปิดการใช้งานบัฟเฟอร์ 4K สำหรับlong_running_command(ไม่ฉันไม่มีแหล่งที่มา)


1
ดังนั้นเมื่อคุณรัน long_running_command โดยไม่มี piping คุณสามารถดูความคืบหน้าการอัปเดตได้อย่างถูกต้อง แต่เมื่อ piping มันจะถูกบัฟเฟอร์?

1
ใช่นั่นคือสิ่งที่เกิดขึ้น
Aaron Digulla

20
การไร้ความสามารถในวิธีการควบคุมบัฟเฟอร์อย่างง่ายเป็นปัญหามานานหลายทศวรรษ ตัวอย่างเช่นดู: marc.info/?l=glibc-bug&m=98313957306297&w=4ซึ่งบอกว่า "ฉันไม่สามารถทำสิ่งนี้ได้และนี่คือเสียงปรบมือที่ทำให้ตำแหน่งของฉัน"


1
แท้จริงแล้วมันไม่ใช่ท่อที่ทำให้เกิดความล่าช้าในขณะที่รอข้อมูลที่เพียงพอ ท่อมีกำลังการผลิต แต่ทันทีที่มีข้อมูลใด ๆ ที่เขียนลงในไพพ์ก็พร้อมที่จะอ่านที่ปลายอีกด้านทันที
Sam Watkins

คำตอบ:


254

คุณสามารถใช้unbufferคำสั่ง (ซึ่งเป็นส่วนหนึ่งของexpectแพ็คเกจ) เช่น

unbuffer long_running_command | print_progress

unbufferเชื่อมต่อกับlong_running_commandผ่าน pseudoterminal (pty) ซึ่งทำให้ระบบถือว่าเป็นกระบวนการแบบโต้ตอบดังนั้นจึงไม่ใช้บัฟเฟอร์ 4-kiB ในไปป์ไลน์ที่เป็นสาเหตุของความล่าช้า

สำหรับท่อที่ยาวขึ้นคุณอาจต้องคลายคำสั่งแต่ละคำสั่ง (ยกเว้นคำสั่งสุดท้าย) เช่น

unbuffer x | unbuffer -p y | z

3
ในความเป็นจริงการใช้ pty เพื่อเชื่อมต่อกับกระบวนการโต้ตอบนั้นเป็นเรื่องจริงที่คาดหวังโดยทั่วไป

15
เมื่อไปป์ไลน์การเรียกไปยัง unbuffer คุณควรใช้อาร์กิวเมนต์ -p เพื่อให้ unbuffer อ่านจาก stdin

26
หมายเหตุ: สำหรับระบบเดเบียนจะมีการเรียกexpect_unbufferและอยู่ในexpect-devแพ็คเกจไม่ใช่expectแพ็คเกจ
bdonlan

4
@bdonlan: อย่างน้อยใน Ubuntu (ใช้เดเบียน), expect-devให้ทั้งสองunbufferและexpect_unbuffer(ในอดีตคือการเชื่อมโยงไปยังหลัง) ลิงก์มีให้ตั้งแต่expect 5.44.1.14-1(2009)
jfs

1
หมายเหตุ: บน Ubuntu 14.04.x ​​ระบบมันอยู่ในแพ็คเกจ expect-dev ด้วย
Alexandre Mazel

462

อีกวิธีในการสกินแมวตัวนี้ก็คือการใช้stdbufโปรแกรมซึ่งเป็นส่วนหนึ่งของ GNU Coreutils (FreeBSD ก็มีตัวของมันเอง)

stdbuf -i0 -o0 -e0 command

สิ่งนี้จะปิดการบัฟเฟอร์อย่างสมบูรณ์สำหรับอินพุตเอาต์พุตและข้อผิดพลาด สำหรับบางแอปพลิเคชันการบัฟเฟอร์บรรทัดอาจเหมาะสมกว่าด้วยเหตุผลด้านประสิทธิภาพ:

stdbuf -oL -eL command

โปรดทราบว่ามันใช้งานได้กับการstdioบัฟเฟอร์ ( printf(), fputs()... ) สำหรับแอปพลิเคชันที่เชื่อมโยงแบบไดนามิกเท่านั้นและหากแอปพลิเคชันนั้นไม่ได้ปรับการบัฟเฟอร์ของสตรีมมาตรฐานด้วยตัวเองถึงแม้ว่ามันจะครอบคลุมแอพพลิเคชันส่วนใหญ่


6
"unbuffer" จะต้องติดตั้งใน Ubuntu ซึ่งอยู่ในแพ็คเกจ: expect-devซึ่งเป็น 2MB ...
lepe

2
วิธีนี้ใช้งานได้ดีในการติดตั้ง raspbian เริ่มต้นเพื่อการบันทึกแบบ unbuffer ฉันพบว่าsudo stdbuff … commandทำงานได้ แต่stdbuff … sudo commandไม่ได้
natevw

20
@qdii stdbufไม่ได้ทำงานกับteeเพราะเขียนทับค่าเริ่มต้นที่กำหนดโดยtee ดูหน้าคู่มือของstdbuf stdbuf
ceving

5
@lepe Bizreely, unbuffer มีการพึ่งพา x11 และ tcl / tk ซึ่งหมายความว่ามันต้องการจริง> 80 MB หากคุณกำลังติดตั้งบนเซิร์ฟเวอร์ที่ไม่มีพวกเขา
jpatokal

10
@qdii stdbufใช้กลไกในการแทรกห้องสมุดโหลดแบบไดนามิกของตัวเองLD_PRELOAD libstdbuf.soซึ่งหมายความว่ามันจะไม่ทำงานกับไฟล์ประเภทนี้: ด้วย setuid หรือชุดความสามารถของไฟล์, ลิงก์แบบสแตติก, ไม่ได้ใช้ libc มาตรฐาน ในกรณีนี้มันจะดีกว่าที่จะใช้แก้ปัญหาที่มีunbuffer/ /script socatดูยังstdbuf กับ setuid
pabouk

75

อีกวิธีหนึ่งในการเปิดโหมดเอาต์พุตบัฟเฟอร์บรรทัดสำหรับlong_running_commandคือการใช้scriptคำสั่งที่รันของคุณlong_running_commandในเทอร์มินัลหลอก (pty)

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux

15
+1 เคล็ดลับดีเพราะscriptมันเป็นคำสั่งเก่ามันควรจะมีอยู่ในทุกแพลตฟอร์มของ Unix
Aaron Digulla

5
คุณต้องการ-qบน Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs

1
ดูเหมือนว่าสคริปต์อ่านจากstdinซึ่งทำให้เป็นไปไม่ได้ที่จะเรียกใช้long_running_commandในพื้นหลังอย่างน้อยเมื่อเริ่มต้นจาก terminal แบบโต้ตอบ เพื่อแก้ปัญหาผมก็สามารถที่จะเปลี่ยนเส้นทาง stdin จาก/dev/nullตั้งแต่ฉันไม่ได้ใช้long_running_command stdin
haridsv

1
แม้ทำงานบน Android
not2qubit

3
ข้อเสียอย่างหนึ่งที่สำคัญ: ctrl-z ใช้งานไม่ได้อีกต่อไป (เช่นฉันไม่สามารถระงับสคริปต์) สิ่งนี้สามารถแก้ไขได้โดย: echo | สคริปต์ sudo -c / usr / local / bin / ec2-snapshot-all / dev / null | ถ้าคุณไม่รังเกียจที่จะไม่สามารถโต้ตอบกับโปรแกรมได้
rlpowell

66

สำหรับgrep, sedและawkคุณสามารถบังคับให้ส่งออกไปยังบัฟเฟอร์บรรทัด คุณสามารถใช้ได้:

grep --line-buffered

บังคับให้เอาต์พุตเป็นบัฟเฟอร์บรรทัด โดยค่าเริ่มต้นเอาต์พุตจะถูกบัฟเฟอร์บรรทัดเมื่อเอาต์พุตมาตรฐานเป็นเทอร์มินัลและบล็อกบัฟเฟอร์อื่นที่ฉลาด

sed -u

ทำให้บัฟเฟอร์บรรทัดเอาต์พุต

ดูหน้านี้สำหรับข้อมูลเพิ่มเติม: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html


51

ถ้ามันเป็นปัญหากับ libc ปรับเปลี่ยนบัฟเฟอร์ของ / ล้างเมื่อการส่งออกไม่ได้ไปไปยังสถานีที่คุณควรลองsocat คุณสามารถสร้างกระแสข้อมูลแบบสองทิศทางระหว่างกลไก I / O เกือบทุกชนิด หนึ่งในนั้นคือโปรแกรมที่แยกกันพูดกับ tty หลอก

 socat EXEC:long_running_command,pty,ctty STDIO 

มันคืออะไร

  • สร้าง tty หลอก
  • fork long_running_command พร้อมกับด้านทาสของ pty เป็น stdin / stdout
  • สร้างกระแสแบบสองทิศทางระหว่างด้านต้นแบบของ pty และที่อยู่ที่สอง (นี่คือ STDIO)

หากสิ่งนี้ให้ผลเช่นเดียวกับlong_running_commandคุณคุณสามารถไปป์ต่อได้

แก้ไข: ว้าวไม่เห็นคำตอบ unbuffer! socat เป็นเครื่องมือที่ดีอยู่แล้วดังนั้นฉันอาจจะทิ้งคำตอบนี้ไว้


1
... และฉันไม่รู้เกี่ยวกับ socat - ดูเหมือนว่า netcat อาจจะมากกว่านั้น ;) ขอบคุณและ +1

3
ฉันจะใช้socat -u exec:long_running_command,pty,end-close -ที่นี่
Stéphane Chazelas

20

คุณสามารถใช้ได้

long_running_command 1>&2 |& print_progress

ปัญหาคือ libc จะ line-buffer เมื่อ stdout ไปที่หน้าจอและ full-buffer เมื่อ stdout เป็นไฟล์ แต่ไม่มีบัฟเฟอร์สำหรับ stderr

ฉันไม่คิดว่ามันเป็นปัญหาของไพพ์บัฟเฟอร์ แต่ทั้งหมดเกี่ยวกับนโยบายบัฟเฟอร์ของ libc


คุณถูก; คำถามของฉันยังคงอยู่: ฉันจะมีผลต่อนโยบายบัฟเฟอร์ของ libc โดยไม่ต้องคอมไพล์ใหม่ได้อย่างไร
Aaron Digulla

@ StéphaneChazelas fd1 จะเปลี่ยนเส้นทางไปยัง stderr
Wang HongQin

@ StéphaneChazelasฉันไม่เข้าใจประเด็นของคุณ โปรดทำแบบทดสอบใช้งานได้
วัง HongQin

3
ตกลงสิ่งที่เกิดขึ้นคือเมื่อทั้งคู่zsh(ที่|&มาจากดัดแปลงจาก csh) และbashเมื่อคุณทำcmd1 >&2 |& cmd2ทั้ง fd 1 และ 2 จะเชื่อมต่อกับ stdout ด้านนอก ดังนั้นมันจึงทำงานเพื่อป้องกันการบัฟเฟอร์เมื่อ stdout ด้านนอกนั้นเป็นเทอร์มินัล แต่เนื่องจากเอาต์พุตไม่ผ่านไปป์ (ดังนั้นจึงprint_progressไม่พิมพ์อะไรเลย) ดังนั้นมันจึงเหมือนกับlong_running_command & print_progress(ยกเว้น print_progress stdin เป็นไพพ์ที่ไม่มีตัวเขียน) คุณสามารถตรวจสอบด้วยเมื่อเทียบกับls -l /proc/self/fd >&2 |& cat ls -l /proc/self/fd |& cat
Stéphane Chazelas

3
นั่นเป็นเพราะ|&สั้นสำหรับ2>&1 |ตัวอักษร ดังนั้นเป็นcmd1 |& cmd2 cmd1 1>&2 2>&1 | cmd2ดังนั้นทั้ง fd 1 และ 2 จะเชื่อมต่อกับ stderr ดั้งเดิมและไม่มีอะไรเหลือให้เขียนลงไปในท่อ ( s/outer stdout/outer stderr/gในความคิดเห็นก่อนหน้าของฉัน)
Stéphane Chazelas

11

มันเคยเป็นกรณีและอาจยังคงเป็นกรณีที่เมื่อออกมาตรฐานเขียนไปยังสถานีมันเป็นบรรทัดบัฟเฟอร์ตามค่าเริ่มต้น - เมื่อมีการเขียนขึ้นบรรทัดใหม่บรรทัดจะถูกเขียนไปยังสถานี เมื่อเอาต์พุตมาตรฐานถูกส่งไปยังไพพ์มันจะถูกบัฟเฟอร์อย่างสมบูรณ์ดังนั้นข้อมูลจะถูกส่งไปยังกระบวนการถัดไปในไพพ์ไลน์เมื่อบัฟเฟอร์ I / O มาตรฐานเต็มเท่านั้น

นั่นคือสาเหตุของปัญหา ฉันไม่แน่ใจว่าคุณสามารถแก้ไขได้โดยไม่แก้ไขโปรแกรมที่เขียนลงในไพพ์หรือไม่ คุณสามารถใช้setvbuf()ฟังก์ชั่นที่มีการ_IOLBFตั้งค่าสถานะเพื่อใส่stdoutในโหมดบัฟเฟอร์บรรทัดโดยไม่มีเงื่อนไข แต่ฉันไม่เห็นวิธีที่ง่ายในการบังคับใช้กับโปรแกรม หรือโปรแกรมสามารถทำfflush()ที่จุดที่เหมาะสม (หลังจากแต่ละบรรทัดของเอาต์พุต) แต่ใช้ความคิดเห็นเดียวกัน

ฉันคิดว่าถ้าคุณเปลี่ยนไพพ์ด้วยเทอร์มินัลหลอกแล้วไลบรารี่ I / O มาตรฐานจะคิดว่าเอาต์พุตเป็นเทอร์มินัล (เพราะเป็นเทอร์มินัลชนิดหนึ่ง) และจะบัฟเฟอร์บรรทัดโดยอัตโนมัติ นั่นเป็นวิธีที่ซับซ้อนในการจัดการกับสิ่งต่าง ๆ แม้ว่า


7

ฉันรู้ว่านี่เป็นคำถามเก่าและมีคำตอบมากมายอยู่แล้ว แต่หากคุณต้องการหลีกเลี่ยงปัญหาบัฟเฟอร์ลองทำสิ่งต่อไปนี้:

stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

สิ่งนี้จะแสดงผลแบบเรียลไทม์บันทึกและบันทึกลงในlogs.txtไฟล์และบัฟเฟอร์จะไม่ส่งผลกระทบต่อtail -fคำสั่งอีกต่อไป


4
ดูเหมือนคำตอบที่สอง: - /
Aaron Digulla

2
stdbuf รวมอยู่ใน gnu coreutils (ฉันตรวจสอบแล้วในเวอร์ชั่นล่าสุด 8.25) ตรวจสอบนี้ใช้งานได้บน linux ฝังตัว
zhaorufei

จากเอกสารของ stdbuf NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
shrewmouse

6

ฉันไม่คิดว่าปัญหาเกิดขึ้นกับไพพ์ ดูเหมือนว่ากระบวนการที่ใช้เวลานานของคุณไม่ได้ล้างบัฟเฟอร์ของตัวเองบ่อยครั้งเพียงพอ การเปลี่ยนขนาดบัฟเฟอร์ของไปป์อาจเป็นการแฮ็กที่จะปัดเศษ แต่ฉันไม่คิดว่าจะเป็นไปได้หากไม่ได้สร้างเคอร์เนลใหม่ - สิ่งที่คุณไม่ต้องการทำเป็นแฮ็กเพราะอาจมีผลต่อกระบวนการอื่น ๆ มากมาย


18
สาเหตุหลักคือ libc นั้นสลับไปที่การบัฟเฟอร์ 4k หาก stdout ไม่ใช่ tty
Aaron Digulla

5
นั่นเป็นเรื่องที่น่าสนใจมาก! เพราะท่อไม่ทำให้เกิดบัฟเฟอร์ใด ๆ พวกเขาให้บัฟเฟอร์ แต่ถ้าคุณอ่านจากไปป์คุณจะได้รับข้อมูลใด ๆ ที่มีอยู่คุณไม่ต้องรอบัฟเฟอร์ในไปป์ ดังนั้นผู้กระทำผิดจึงเป็น stdio buffering ในแอปพลิเคชัน


3

ในทำนองเดียวกันกับคำตอบของแช้ดคุณสามารถเขียนสคริปต์เล็กน้อยเช่นนี้:

# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

จากนั้นใช้นี้คำสั่งแทนสำหรับscripteetee

my-long-running-command | scriptee

อนิจจาฉันไม่สามารถรับรุ่นเช่นนั้นทำงานได้อย่างสมบูรณ์แบบใน Linux ดังนั้นดูเหมือน จำกัด ยูนิกซ์สไตล์ BSD

บน Linux นี่ใกล้ แต่คุณจะไม่ได้รับพรอมต์เมื่อมันเสร็จสิ้น (จนกว่าคุณจะกด Enter ฯลฯ ) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null

ทำไมจึงใช้งานได้ "สคริปต์" ปิดการบัฟเฟอร์หรือไม่
Aaron Digulla

@Aaron Digulla: scriptเทอร์มินัลจำลองดังนั้นใช่ฉันเชื่อว่ามันปิดการบัฟเฟอร์ นอกจากนี้ยังสะท้อนกลับอักขระแต่ละตัวที่ส่งไป - ซึ่งเป็นสาเหตุที่catส่งไป/dev/nullในตัวอย่าง เท่าที่โปรแกรมกำลังทำงานอยู่ภายในscriptเกี่ยวข้องมันกำลังพูดคุยกับเซสชันแบบโต้ตอบ ฉันเชื่อว่ามันคล้ายกับexpectในเรื่องนี้ แต่scriptน่าจะเป็นส่วนหนึ่งของระบบฐานของคุณ
jwd

เหตุผลที่ฉันใช้teeคือการส่งสำเนาของกระแสข้อมูลไปยังไฟล์ ไฟล์จะถูกระบุไว้scripteeที่ไหน?
Bruno Bronosky

@BrunoBronosky: ถูกต้องมันเป็นชื่อที่ไม่ดีสำหรับโปรแกรมนี้ มันไม่ได้เป็นการดำเนินการ 'ที' จริงๆ มันเป็นเพียงการปิดการใช้งานบัฟเฟอร์ของผลลัพธ์ตามคำถามเดิม บางทีมันควรจะเรียกว่า "scriptcat" (แม้ว่ามันจะไม่ต่อกันก็ตาม ... ) คุณสามารถแทนที่catคำสั่งด้วยtee myfile.txtและคุณควรได้รับเอฟเฟกต์ที่คุณต้องการ
jwd

2

ฉันพบโซลูชันที่ชาญฉลาดนี้: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

นี่เป็นการหลอกลวง catจะอ่านการป้อนข้อมูลเพิ่มเติม (จนกว่า EOF) และส่งผ่านไปยังท่อว่าหลังจากที่ได้วางข้อโต้แย้งของมันเข้าไปในกระแสของการป้อนข้อมูลechoshell_executable


2
ที่จริงแล้วcatไม่เห็นผลลัพธ์ของecho; คุณเพียงแค่เรียกใช้สองคำสั่งใน subshell และผลลัพธ์ของทั้งสองจะถูกส่งไปยังไปป์ คำสั่งที่สองใน subshell ('cat') อ่านจาก parent / outer stdin นั่นคือสาเหตุที่มันทำงาน
Aaron Digulla

0

ตามนี้ขนาดท่อบัฟเฟอร์ดูเหมือนว่าจะถูกตั้งค่าในเคอร์เนลและจะต้องให้คุณคอมไพล์เคอร์เนลของคุณเพื่อแก้ไข


7
ฉันเชื่อว่าเป็นบัฟเฟอร์ที่แตกต่างกัน
ซามูเอลเอ็ดวินวอร์ด
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.