คุณสามารถใช้การรวมกันของ GNU stdbuf และpee
จากmoreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
pee popen(3)
s บรรทัดคำสั่งเชลล์ทั้งสามจากนั้นfread
ป้อนข้อมูลและfwrite
ทั้งสามบรรทัดซึ่งจะถูกบัฟเฟอร์สูงถึง 1M
แนวคิดคือมีบัฟเฟอร์อย่างน้อยใหญ่เท่ากับอินพุต วิธีนี้แม้ว่าคำสั่งทั้งสามจะเริ่มขึ้นในเวลาเดียวกัน แต่ก็จะเห็นเฉพาะอินพุตที่เข้ามาเมื่อpee
pclose
คำสั่งทั้งสามนั้นเรียงตามลำดับ
เมื่อแต่ละpclose
, pee
วูบวาบบัฟเฟอร์คำสั่งและรอสำหรับการยกเลิกของมัน สิ่งนี้รับประกันได้ว่าตราบใดที่cmdx
คำสั่งเหล่านั้นไม่ได้เริ่มต้นแสดงผลอะไรก่อนที่พวกเขาจะได้รับอินพุตใด ๆ (และอย่าแยกกระบวนการที่อาจส่งออกต่อไปหลังจากที่ผู้ปกครองส่งคืน) ผลลัพธ์ของคำสั่งทั้งสามจะไม่ บรรณนิทัศน์
ผลก็เหมือนกับการใช้ไฟล์ temp ในหน่วยความจำโดยมีข้อเสียคือคำสั่ง 3 คำสั่งจะเริ่มพร้อมกัน
เพื่อหลีกเลี่ยงการเริ่มต้นคำสั่งพร้อมกันคุณสามารถเขียนpee
เป็นฟังก์ชั่นเปลือก:
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
แต่ระวังว่าเชลล์อื่น ๆ นอกจากzsh
จะล้มเหลวสำหรับอินพุตไบนารีที่มีอักขระ NUL
หลีกเลี่ยงการใช้ไฟล์ชั่วคราว แต่นั่นหมายถึงอินพุตทั้งหมดถูกเก็บไว้ในหน่วยความจำ
ไม่ว่าในกรณีใดคุณจะต้องเก็บอินพุตไว้ในหน่วยความจำหรือไฟล์ชั่วคราว
จริงๆแล้วมันเป็นคำถามที่น่าสนใจเพราะมันแสดงให้เราเห็นถึงขีด จำกัด ของแนวคิดของ Unix ที่มีเครื่องมือง่ายๆหลายอย่างที่ให้ความร่วมมือกับงานเดียว
ที่นี่เราต้องการมีเครื่องมือหลายอย่างที่ให้ความร่วมมือกับงาน:
- คำสั่ง source (ที่นี่
echo
)
- คำสั่งโปรแกรมเลือกจ่ายงาน (
tee
)
- คำสั่งบางตัวกรอง (
cmd1
, cmd2
, cmd3
)
- และคำสั่งการรวม (
cat
)
มันจะดีถ้าพวกเขาสามารถทำงานร่วมกันในเวลาเดียวกันและทำงานอย่างหนักกับข้อมูลที่พวกเขาตั้งใจจะประมวลผลทันทีที่พร้อมใช้งาน
ในกรณีของคำสั่งตัวกรองเดียวมันง่าย:
src | tee | cmd1 | cat
คำสั่งทั้งหมดจะทำงานพร้อมกันcmd1
เริ่มเคี้ยวข้อมูลจากsrc
ทันทีที่พร้อมใช้งาน
ตอนนี้ด้วยคำสั่งตัวกรองสามคำเรายังคงสามารถทำเช่นเดิม: เริ่มต้นพร้อมกันและเชื่อมต่อพวกเขาด้วยไพพ์:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
ซึ่งเราสามารถทำได้ค่อนข้างง่ายกับท่อชื่อ :
pee() (
mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
{ tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
eval "$1 < tee-cmd1 1<> cmd1-cat &"
eval "$2 < tee-cmd2 1<> cmd2-cat &"
eval "$3 < tee-cmd3 1<> cmd3-cat &"
exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
(เหนือ} 3<&0
คือเพื่อหลีกเลี่ยงความจริงที่&
เปลี่ยนเส้นทางstdin
จาก/dev/null
และเราใช้<>
เพื่อหลีกเลี่ยงการเปิดท่อเพื่อปิดกั้นจนกว่าจะสิ้นสุดอีก ( cat
) ได้เปิดเช่นกัน)
หรือเพื่อหลีกเลี่ยงการตั้งชื่อไปป์อีกเล็กน้อยด้วยzsh
coproc:
pee() (
n=0 ci= co= is=() os=()
for cmd do
eval "coproc $cmd $ci $co"
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
ตอนนี้คำถามคือเมื่อโปรแกรมทั้งหมดเริ่มต้นและเชื่อมต่อแล้วการไหลของข้อมูลจะเป็นอย่างไร
เรามีข้อ จำกัด สองข้อ:
tee
ฟีดเอาท์พุททั้งหมดในอัตราเดียวกันดังนั้นมันสามารถส่งข้อมูลในอัตราของท่อส่งออกที่ช้าที่สุดเท่านั้น
cat
จะเริ่มอ่านจากไปป์ที่สองเท่านั้น (ไพพ์ 6 ในรูปวาดข้างบน) เมื่อข้อมูลทั้งหมดถูกอ่านจากครั้งแรก (5)
นั่นหมายความว่าข้อมูลจะไม่ไหลในท่อ 6 จนกว่าจะcmd1
เสร็จสิ้น และเช่นเดียวกับในกรณีtr b B
ข้างต้นนั่นอาจหมายความว่าข้อมูลจะไม่ไหลในไพพ์ 3 ซึ่งหมายความว่าจะไม่ไหลในไพพ์ 2, 3 หรือ 4 เนื่องจากtee
ฟีดที่อัตราช้าที่สุดของทั้ง 3
ในทางปฏิบัติไพพ์เหล่านี้มีขนาดที่ไม่เป็นศูนย์ดังนั้นข้อมูลบางอย่างจะจัดการเพื่อให้ผ่านและในระบบของฉันอย่างน้อยฉันก็สามารถทำให้มันทำงานได้ถึง:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
นอกเหนือจากนั้นด้วย
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
เรามีการหยุดชะงักโดยที่เราอยู่ในสถานการณ์นี้:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
เราได้ใส่ท่อ 3 และ 6 (แต่ละ 64kiB) tee
ได้อ่านไบต์พิเศษนั้นมันได้ป้อนเข้าไปcmd1
แต่
- ตอนนี้มันถูกบล็อกการเขียนบนไพพ์ 3 เนื่องจากมันกำลังรอ
cmd2
ให้มันว่างเปล่า
cmd2
ไม่สามารถว่างเปล่าเพราะมันถูกบล็อกเขียนในท่อ 6 รอcat
เพื่อมันว่างเปล่า
cat
ไม่สามารถล้างข้อมูลได้เนื่องจากกำลังรอจนกว่าจะไม่มีอินพุตเพิ่มเติมในไพพ์ 5
cmd1
ไม่สามารถบอกได้ว่ามีการป้อนข้อมูลไม่มากเพราะมันกำลังรอตัวเองสำหรับการป้อนข้อมูลเพิ่มเติมจากcat
tee
- และ
tee
ไม่สามารถบอกได้ว่าcmd1
ไม่มีข้อมูลป้อนเข้าเพิ่มเติมเนื่องจากถูกบล็อก ... และต่อไป
เรามีห่วงพึ่งพาและทำให้การหยุดชะงัก
ตอนนี้ทางออกคืออะไร ท่อที่ใหญ่กว่า 3 และ 4 (ใหญ่พอที่จะมีsrc
เอาท์พุททั้งหมด) จะทำเช่นนั้น เราสามารถทำเช่นนั้นได้โดยการแทรกpv -qB 1G
ระหว่างtee
และcmd2/3
ตำแหน่งที่pv
สามารถเก็บข้อมูลได้มากถึง 1G ที่รอcmd2
และcmd3
อ่าน นั่นหมายความว่าสองสิ่งแม้ว่า:
- ที่ใช้หน่วยความจำจำนวนมากและยิ่งกว่านั้นก็คือการทำซ้ำ
- นั่นคือความล้มเหลวในการมีคำสั่งทั้ง 3 คำที่ให้ความร่วมมือเพราะ
cmd2
ในความเป็นจริงแล้วจะเริ่มดำเนินการกับข้อมูลเมื่อ cmd1 เสร็จสิ้นเท่านั้น
วิธีแก้ไขปัญหาที่สองคือทำให้ท่อ 6 และ 7 ใหญ่ขึ้นเช่นกัน สมมติว่าcmd2
และcmd3
สร้างเอาต์พุตมากที่สุดเท่าที่ใช้งานซึ่งจะไม่ใช้หน่วยความจำมากขึ้น
วิธีเดียวที่จะหลีกเลี่ยงการทำซ้ำข้อมูล (ในปัญหาแรก) จะใช้การเก็บรักษาข้อมูลในตัวแจกจ่ายที่ใช้การเปลี่ยนแปลงtee
ที่สามารถฟีดข้อมูลในอัตราการส่งออกที่เร็วที่สุด (การเก็บข้อมูลเพื่อป้อนข้อมูล ช้าลงตามความเร็วของตัวเอง) ไม่น่ารำคาญจริงๆ
ดังนั้นในที่สุดสิ่งที่ดีที่สุดที่เราสามารถรับได้โดยไม่ต้องเขียนโปรแกรมอาจเป็นสิ่งที่ต้องการ (ไวยากรณ์ Zsh):
max_hold=1G
pee() (
n=0 ci= co= is=() os=()
for cmd do
if ((n)); then
eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
else
eval "coproc $cmd $ci $co"
fi
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c