Sen GNU stdbuf ve birlikte kullanabilir pee
gelen moreutils :
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
popen(3)
o 3 kabuk komut satırları işemek ve daha sonra fread
giriş fwrite
s ve üçü de, 1M kadar tamponlu olacak.
Fikir, en az girdi kadar büyük bir arabellek bulundurmaktır. Bu şekilde, üç komut aynı anda başlatılmış olsa bile, girdiler yalnızca pee
pclose
üç komut sırayla geldiğinde görünecektir .
Her üzerine pclose
, pee
sonlanmasını komut ve bekler tamponunu boşaltır. Bu, bu cmdx
komutlar herhangi bir girdi almadan önce herhangi bir şey çıktılamaya başlamadığı sürece (ve ebeveynleri döndükten sonra çıktı almaya devam edebilecek bir işlemi çatallamadıkça), üç komutun çıktısının araya eklenmiş.
Aslında bu, 3 komutun eşzamanlı olarak başlatılması dezavantajı ile bellekte bir geçici dosya kullanmaya benzer.
Komutları aynı anda başlatmaktan kaçınmak pee
için kabuk işlevi olarak yazabilirsiniz :
pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
Ancak zsh
, NUL karakterli ikili girişler için mermilerin dışındaki mermilerin başarısız olacağını unutmayın.
Bu geçici dosyaların kullanılmasını önler, ancak bu, girdinin tamamının bellekte saklandığı anlamına gelir.
Her durumda, girişi bir yerde, bellekte veya geçici bir dosyada saklamanız gerekir.
Aslında, Unix'in birkaç basit aracın tek bir görevle işbirliği yapma fikrinin sınırını gösterdiğinden oldukça ilginç bir soru.
Burada, görev için işbirliği yapan birkaç araç olmasını istiyoruz:
- bir kaynak komutu (burada
echo
)
- bir dağıtıcı komutu (
tee
)
- Bazı filtre komutları (
cmd1
, cmd2
, cmd3
)
- ve bir toplama komutu (
cat
).
Hepsi aynı anda birlikte çalışabilseler ve mümkün olan en kısa sürede işlemek istedikleri veriler üzerinde sıkı çalışabilirlerse güzel olurlar.
Bir filtre komutu söz konusu olduğunda kolaydır:
src | tee | cmd1 | cat
Tüm komutlar eşzamanlı olarak çalıştırılır cmd1
, src
en kısa sürede verileri karıştırmaya başlar .
Şimdi, üç filtre komutuyla, hala aynı şeyi yapabiliriz: onları aynı anda başlatın ve borularla bağlayın:
┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
Hangi adlandırılmış borular ile nispeten kolay yapabiliriz :
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'
(yukarıda } 3<&0
olduğu gerçeğini geçici olarak &
yönlendirmeleri stdin
gelen /dev/null
, kullandığımız <>
diğer ucuna kadar bloğuna boru açıklığı (önlemek için cat
de açtı))
Veya adlandırılmış borulardan kaçınmak için, zsh
coproc ile biraz daha acı verici :
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'
Şimdi soru şu: Tüm programlar başlatıldıktan ve bağlandıktan sonra veri akışı olacak mı?
İki karşıtlığımız var:
tee
tüm çıkışlarını aynı hızda besler, böylece yalnızca en yavaş çıkış borusu hızında veri gönderebilir.
cat
sadece ikinci borudan okumaya başlayacaktır (yukarıdaki çizimde boru 6) ilk veri (5) 'ten tüm veriler okunduğunda.
Bunun anlamı, veriler bitene kadar boru 6'da cmd1
akmayacaktır. Ve tr b B
yukarıdaki gibi, bu da verilerin boru 3'te de akmayacağı anlamına gelebilir, bu da 3 tee
, en yavaş hızda beslendiği için 2, 3 veya 4 borularının hiçbirinde akmayacağı anlamına gelir .
Uygulamada bu borular boş olmayan bir boyuta sahiptir, bu nedenle bazı veriler geçmeyi başarabilir ve en azından sistemimde aşağıdakilere kadar çalışabilirim:
yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
Bunun ötesinde,
yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
Bu durumda olduğumuz bir kilitlenme var:
┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
3 ve 6 numaralı boruları doldurduk (her biri 64KB). tee
fazladan baytı okudu, besledi cmd1
ama
- şimdi boru 3'e yazılmasını engelledi çünkü
cmd2
boşaltmayı bekliyor
cmd2
Boşaltılamıyor çünkü 6. boruya yazılmasını engelliyor, cat
boşaltmayı bekliyor
cat
Boşaltılamıyor çünkü 5. boruda daha fazla girdi kalmayana kadar bekliyor.
cmd1
cat
daha fazla girdi olmadığını söyleyemez çünkü daha fazla girdi beklemektedir tee
.
- ve daha fazla girdi
tee
olmadığını söyleyemem cmd1
çünkü engellenmiş ... vb.
Bir bağımlılık döngümüz ve dolayısıyla bir kilitlenme var.
Şimdi, çözüm nedir? Daha büyük borular 3 ve 4 (tüm src
çıktıları içerecek kadar büyük ) bunu yapardı. Bunu, örneğin 1G'ye kadar veriyi beklemek ve okumak için araya ve nereye yerleştirerek pv -qB 1G
yapabiliriz . Bu iki anlama geliyor:tee
cmd2/3
pv
cmd2
cmd3
- potansiyel olarak çok fazla bellek kullanıyor ve dahası
- bu 3 komutun hepsinin birlikte çalışmasını sağlayamaz çünkü
cmd2
gerçekte sadece cmd1 tamamlandığında verileri işlemeye başlar.
İkinci soruna bir çözüm, boru 6 ve 7'yi de daha büyük yapmak olacaktır. Bunu varsaymak cmd2
ve cmd3
tükettikleri kadar çıktı üretmek, daha fazla bellek tüketmez.
Verilerin çoğaltılmasından kaçınmanın tek yolu (ilk problemde), dağıtıcının kendisinde veri tutmayı uygulamaktır, yani tee
verileri en hızlı çıktı hızında besleyebilen (veriyi beslemek için verileri tutmak) kendi hızlarında yavaşlar). Gerçekten önemsiz değil.
Sonuç olarak, programlama yapmadan makul bir şekilde elde edebileceğimiz en iyi şey muhtemelen (Zsh sözdizimi) gibi bir şeydir:
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