สิ่งที่ป้องกันไม่ให้ stdout / stderr จาก interleaving?


14

ว่าฉันใช้กระบวนการบางอย่าง:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

ฉันรันสคริปต์ด้านบนดังนี้:

foobarbaz | cat

เท่าที่ฉันสามารถบอกได้ว่าเมื่อใดก็ตามที่กระบวนการเขียนไปยัง stdout / stderr ผลลัพธ์ของมันจะไม่แทรกสอด - แต่ละบรรทัดของ stdio นั้นดูเหมือนจะเป็นอะตอม มันทำงานอย่างไร ยูทิลิตี้อะไรควบคุมวิธีแต่ละอะตอมเป็นอะตอม


3
คำสั่งของคุณส่งออกข้อมูลเท่าใด ลองทำให้พวกมันออกมาสองสามกิโลไบต์
Kusalananda

คุณหมายถึงคำสั่งใดคำสั่งหนึ่งออกมาไม่กี่กิโลไบต์ก่อนขึ้นบรรทัดใหม่
Alexander Mills

ไม่บางอย่างเช่นนี้: unix.stackexchange.com/a/452762/70524
muru

คำตอบ:


23

พวกเขาสอดแทรก! คุณลองใช้เอาท์พุตสั้น ๆ ซึ่งยังคง unsplit แต่ในทางปฏิบัติมันยากที่จะรับประกันได้ว่าเอาต์พุตใด ๆ โดยเฉพาะจะยังคง unsplit

บัฟเฟอร์ออก

ขึ้นอยู่กับวิธีที่โปรแกรมบัฟเฟอร์เอาต์พุต ห้องสมุด stdioว่าโปรแกรมส่วนใหญ่ใช้เมื่อพวกเขากำลังเขียนใช้บัฟเฟอร์ที่จะทำให้การส่งออกมีประสิทธิภาพมากขึ้น แทนที่จะส่งออกข้อมูลทันทีที่โปรแกรมเรียกใช้ฟังก์ชันไลบรารีเพื่อเขียนไปยังไฟล์ฟังก์ชันจะเก็บข้อมูลนี้ไว้ในบัฟเฟอร์และจะส่งออกข้อมูลเมื่อบัฟเฟอร์เต็มแล้วเท่านั้น ซึ่งหมายความว่าเอาต์พุตจะถูกทำเป็นแบตช์ แม่นยำยิ่งขึ้นมีโหมดเอาต์พุตสามโหมด:

  • Unbuffered: ข้อมูลถูกเขียนทันทีโดยไม่ต้องใช้บัฟเฟอร์ สิ่งนี้อาจช้าหากโปรแกรมเขียนเอาต์พุตเป็นชิ้นเล็ก ๆ เช่นตัวอักษรต่ออักขระ นี่เป็นโหมดเริ่มต้นสำหรับข้อผิดพลาดมาตรฐาน
  • บัฟเฟอร์เต็ม: ข้อมูลจะถูกเขียนเมื่อบัฟเฟอร์เต็มเท่านั้น นี่เป็นโหมดเริ่มต้นเมื่อเขียนไปที่ไพพ์หรือไฟล์ปกติยกเว้นด้วย stderr
  • Line-buffered: ข้อมูลถูกเขียนหลังแต่ละบรรทัดใหม่หรือเมื่อบัฟเฟอร์เต็ม นี่เป็นโหมดเริ่มต้นเมื่อเขียนไปยังเทอร์มินัลยกเว้น stderr

โปรแกรมสามารถโปรแกรมใหม่แต่ละไฟล์ให้ทำงานแตกต่างกันและสามารถล้างบัฟเฟอร์อย่างชัดเจน บัฟเฟอร์จะถูกล้างโดยอัตโนมัติเมื่อโปรแกรมปิดไฟล์หรือออกจากโปรแกรมตามปกติ

หากโปรแกรมทั้งหมดที่กำลังเขียนไปยังไปป์เดียวกันสามารถใช้โหมด line-buffered หรือใช้โหมด unbuffered และเขียนแต่ละบรรทัดด้วยการเรียกครั้งเดียวไปยังฟังก์ชั่นเอาต์พุตและหากบรรทัดสั้นพอที่จะเขียนในกลุ่มเดียวแล้ว การส่งออกจะเป็น interleaving ของเส้นทั้งหมด แต่ถ้าหนึ่งในโปรแกรมใช้โหมดเต็มบัฟเฟอร์หรือหากบรรทัดยาวเกินไปคุณจะเห็นเส้นผสม

นี่คือตัวอย่างที่ฉัน interleave ผลลัพธ์จากสองโปรแกรม ฉันใช้ GNU coreutils บน Linux ยูทิลิตี้รุ่นต่าง ๆ เหล่านี้อาจทำงานต่างกัน

  • yes aaaaเขียนaaaaตลอดไปในสิ่งที่เทียบเท่ากับโหมด line-buffered yesยูทิลิตี้จริงเขียนหลายบรรทัดในเวลา แต่ทุกครั้งที่มันปล่อยออกมาเอาท์พุทเอาท์พุทเป็นจำนวนทั้งหมดของเส้น
  • echo bbbb; done | grep bเขียนbbbbตลอดไปในโหมดเต็มบัฟเฟอร์ มันใช้ขนาดบัฟเฟอร์ 8192 และแต่ละบรรทัดมีความยาว 5 ไบต์ ตั้งแต่ 5 ไม่ได้แบ่ง 8192 ขอบเขตระหว่างการเขียนไม่ได้อยู่ที่ขอบเขตของเส้นโดยทั่วไป

ลองผสมมันเข้าด้วยกัน

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

อย่างที่คุณเห็นใช่บางครั้งขัดจังหวะ grep และในทางกลับกัน มีเพียงประมาณ 0.001% ของบรรทัดที่ถูกขัดจังหวะ แต่มันเกิดขึ้น ผลลัพธ์จะถูกสุ่มดังนั้นจำนวนการขัดจังหวะจะแตกต่างกัน แต่ฉันเห็นการขัดจังหวะอย่างน้อยสองสามครั้งทุกครั้ง จะมีส่วนของเส้นที่ถูกขัดจังหวะมากกว่าถ้าเส้นนั้นยาวกว่าเนื่องจากความน่าจะเป็นของการขัดจังหวะจะเพิ่มขึ้นเมื่อจำนวนบรรทัดต่อบัฟเฟอร์ลดลง

มีหลายวิธีที่จะมีการปรับบัฟเฟอร์เอาท์พุท คนหลักคือ:

  • ปิดการบัฟเฟอร์ในโปรแกรมที่ใช้ไลบรารี stdio โดยไม่เปลี่ยนการตั้งค่าเริ่มต้นด้วยโปรแกรมที่stdbuf -o0พบใน GNU coreutils และระบบอื่น ๆ เช่น FreeBSD stdbuf -oLหรือคุณสามารถสลับไปยังเส้นบัฟเฟอร์ด้วย
  • สลับไปที่การบัฟเฟอร์บรรทัดโดยสั่งเอาต์พุตของโปรแกรมผ่านเทอร์มินัลที่สร้างขึ้นเพื่อจุดประสงค์นี้unbufferเท่านั้น บางโปรแกรมอาจทำงานแตกต่างกันในวิธีอื่นตัวอย่างเช่นgrepใช้สีตามค่าเริ่มต้นหากเอาต์พุตเป็นเทอร์มินัล
  • กำหนดค่าโปรแกรมตัวอย่างเช่นส่งผ่าน--line-bufferedไปยัง GNU grep

ลองดูตัวอย่างด้านบนอีกครั้งคราวนี้มีการบัฟเฟอร์บรรทัดทั้งสองด้าน

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

ดังนั้นครั้งนี้ใช่ไม่เคยขัดจังหวะ grep แต่บางครั้ง grep ขัดจังหวะใช่ ฉันจะมาทำไมในภายหลัง

การแทรกสอดของท่อ

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

หากมีข้อมูลที่จะคัดลอกมากกว่าที่พอดีในบัฟเฟอร์การโอนของไพพ์เคอร์เนลจะคัดลอกหนึ่งบัฟเฟอร์ในแต่ละครั้ง หากมีหลายโปรแกรมกำลังเขียนไปยังไพพ์เดียวกันและโปรแกรมแรกที่เคอร์เนลเลือกต้องการเขียนมากกว่าหนึ่งบัฟเฟอร์จะไม่มีการรับประกันว่าเคอร์เนลจะเลือกโปรแกรมเดียวกันอีกครั้งในครั้งที่สอง ตัวอย่างเช่นถ้าPคือขนาดบัฟเฟอร์fooต้องการที่จะเขียน 2 * Pไบต์และbarต้องการที่จะเขียน 3 ไบต์แล้วหนึ่ง interleaving เป็นไปได้คือPไบต์จากfooนั้น 3 ไบต์จากbarและPfooไบต์จาก

กลับมาที่ตัวอย่าง yes + grep ด้านบนในระบบของฉันyes aaaaเกิดขึ้นกับการเขียนหลายบรรทัดที่สามารถใส่ในบัฟเฟอร์ 8192- ไบต์ในคราวเดียว เนื่องจากมี 5 ไบต์ในการเขียน (4 อักขระที่พิมพ์ได้และขึ้นบรรทัดใหม่) นั่นหมายถึงมันเขียน 8190 ไบต์ทุกครั้ง ขนาดบัฟเฟอร์ของไพพ์คือ 4096 ไบต์ ดังนั้นจึงเป็นไปได้ที่จะได้รับ 4096 ไบต์จากใช่จากนั้นออกบางส่วนจาก grep และจากนั้นส่วนที่เหลือของการเขียนจากใช่ (8190 - 4096 = 4094 ไบต์) 4096 ไบต์ห้องใบสำหรับ 819 เส้นที่มีและคนเดียวaaaa aดังนั้นสอดคล้องกับคนเดียวนี้aตามด้วยหนึ่งเขียนจาก grep abbbbให้สอดคล้องกับ

หากคุณต้องการดูรายละเอียดของสิ่งที่เกิดขึ้นแล้วgetconf PIPE_BUF .จะบอกขนาดของบัฟเฟอร์ท่อในระบบของคุณและคุณสามารถดูรายการการเรียกระบบทั้งหมดที่ทำโดยแต่ละโปรแกรมด้วย

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

วิธีการรับประกัน interleaving เส้นสะอาด

หากความยาวบรรทัดมีขนาดเล็กกว่าขนาดบัฟเฟอร์ของท่อการบัฟเฟอร์บรรทัดรับประกันว่าจะไม่มีบรรทัดผสมในเอาต์พุต

หากความยาวบรรทัดสามารถมากกว่านั้นได้จะไม่มีทางหลีกเลี่ยงการมั่วโดยพลการเมื่อโปรแกรมหลายโปรแกรมกำลังเขียนไปยังไปป์เดียวกัน เพื่อให้แน่ใจว่ามีการแยกคุณต้องทำให้แต่ละโปรแกรมเขียนไปยังไพพ์ที่แตกต่างกันและใช้โปรแกรมเพื่อรวมบรรทัด ตัวอย่างเช่นGNU Parallelทำสิ่งนี้ตามค่าเริ่มต้น


น่าสนใจดังนั้นสิ่งที่อาจเป็นวิธีที่ดีในการตรวจสอบให้แน่ใจว่าทุกบรรทัดถูกเขียนไปยังcatอะตอมเช่นกระบวนการแมวได้รับทั้งบรรทัดจาก foo / bar / baz แต่ไม่ได้ครึ่งบรรทัดจากหนึ่งและครึ่งหนึ่งจากอีกบรรทัดหนึ่งเป็นต้น มีบางสิ่งที่ฉันสามารถทำกับสคริปต์ทุบตีได้หรือไม่
Alexander Mills

1
เสียงนี้ใช้กับกรณีของฉันซึ่งฉันมีไฟล์หลายร้อยไฟล์และawkสร้างเอาต์พุตสองบรรทัด (หรือมากกว่า) สำหรับ ID เดียวกันด้วยfind -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' แต่find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'สร้างได้อย่างถูกต้องเพียงหนึ่งบรรทัดสำหรับทุก ID
αғsнιη

เพื่อป้องกันการแทรกสอดใด ๆ ฉันสามารถทำได้ด้วยในการเขียนโปรแกรม env เช่น Node.js แต่ด้วย bash / shell ไม่แน่ใจว่าจะทำอย่างไร
Alexander Mills

1
@JoL มันเป็นเพราะบัฟเฟอร์ท่อเต็ม ฉันรู้ว่าฉันต้องเขียนตอนที่สองของเรื่อง…เสร็จแล้ว
Gilles 'ดังนั้น - หยุดความชั่วร้าย'

1
@OlegzandrDenman TLDR เพิ่ม: พวกมันสอดแทรก เหตุผลมีความซับซ้อน
Gilles 'หยุดชั่วร้าย'

1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-Pได้ตรวจสอบเรื่องนี้:

GNU xargs รองรับการเรียกใช้งานหลายงานพร้อมกัน -P n โดยที่ n คือจำนวนของงานที่ต้องรันแบบขนาน

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

สิ่งนี้จะใช้ได้ดีในหลาย ๆ สถานการณ์ แต่มีข้อบกพร่องที่หลอกลวง: ถ้า $ a มีมากกว่า ~ 1,000 ตัวอักษรเสียงสะท้อนอาจไม่ใช่อะตอมมิก (อาจแบ่งออกเป็นหลายสายการเขียน ()) และมีความเสี่ยงที่สองบรรทัด จะถูกผสม

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

เห็นได้ชัดว่าปัญหาเดียวกันนี้เกิดขึ้นหากมีการโทรไปยัง echo หรือ printf หลายสาย:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

เอาต์พุตจากงานแบบขนานถูกผสมเข้าด้วยกันเนื่องจากแต่ละงานประกอบด้วยการเรียก write () สองครั้ง (หรือมากกว่า) แยกกัน

หากคุณต้องการเอาท์พุทที่ไม่ได้รวมกันดังนั้นขอแนะนำให้ใช้เครื่องมือที่รับประกันว่าเอาต์พุตจะได้รับการต่อเนื่อง (เช่น GNU Parallel)


ส่วนนั้นผิด xargs echoไม่เรียกเสียงสะท้อนทุบตี builtin แต่ยูทิลิตี้จากecho $PATHและต่อไปฉันไม่สามารถทำซ้ำพฤติกรรมทุบตีดังกล่าวด้วยทุบตี 4.4 บน Linux การเขียนไปที่ไพพ์ (ไม่ใช่ / dev / null) ที่มีขนาดใหญ่กว่า 4K นั้นไม่ได้รับประกันว่าจะเป็นแบบอะตอมมิก
Stéphane Chazelas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.