tail -f /some/log/file | grep thing1 | grep thing2gibi yavaş gelen çıktıyı birden fazla komutla bağlayan pipeline’lar aslında takılmış değildir; ara komut çıktıyı buffer’da biriktirdiği için boş görünebilirgrepve birçok program, stdout’un terminal olup olmadığınıisattyile kontrol eder; terminalse satır buffering, pipe veya dosyaysa yaklaşık 8KB’lık blok buffering kullanırtail,cat,teeçıktı buffering’i yapmayan örneklerdir; ancakgrep --line-buffered,sed -u,tcpdump -l,jq -u,tr -ugibi buffering’i azaltma seçenekleri komuttan komuta değişirCtrl-Cile pipeline’ı keserseniztcpdumpgibi programların buffer’ındaki çıktı kaybolabilir;kill -TERM $PIDile sonlandırırsanız buffer flush edilir ve çıktı görünebilir- Pratik çözümler hızlı biten komutlara geçmek,
grep --line-buffered, tek birawkya da daha karmaşıkgrep,stdbuf,unbuffergibi yöntemlerdir; ancak her birinin çalışma koşulları ve yan etkileri kontrol edilmelidir
Pipe takılmış gibi neden görünür?
- Bir günlük dosyasına satırlar yavaş yavaş eklendiğinde, aşağıdaki pipeline eşleşen sonuçlar olsa bile çıktı göstermeyebilir
tail -f /some/log/file | grep thing1 | grep thing2
- Bunun nedeni pipe’ın kendisi değil, aradaki
grep thing1komutunun sonucu hemen yazmak yerine buffer’da saklamasıdır - Program her seferinde hemen yazarsa sistem çağrıları artacağından, performans için bir miktar veriyi topladıktan sonra pipe’a veya dosyaya yazar
- Bu örnekte
grep thing1, yaklaşık 8KB çıktı birikene kadar bekleyebilir; yavaş bir günlükte bu koşul pratikte hiç gerçekleşmeyebilir
Terminal ve pipe’ta değişen çıktı yöntemi
tail -f file | grep thingiyi çalışır; ancak arkasına ikinci birgrepeklendiğinde çıktı durmuş gibi görünebilirgrepve birçok program, stdout’un terminal olup olmadığınıisattyfonksiyonuyla kontrol eder- stdout bir terminal ise satır buffering kullanır ve satırları hemen çıktı olarak verir
- stdout bir pipe veya dosya ise blok buffering kullanır ve belirli bir boyutun üzerinde veri birikince çıktı verir
- Bu yüzden
grepdoğrudan terminale yazdığında satırlar hemen görünür; ancak bir sonraki komuta giden pipe’a yazdığında görünmeyebilir - Buffer boyutu programdan programa değişir
grepiçin buffering’i libc yönetir ve libc’nin boyutuBUFSIZdeğişkeniyle tanımlanır- glibc’deki tanımın yeri stdio.h dosyasındadır
- Terminale yazarken 8KB’lık çıktı buffer’ı kullanmamak bir fizik kuralı değildir; program isterse böyle uygulanabilir, ancak bu oldukça tuhaf bir davranışa yakın olur
Komuttan komuta değişen buffering davranışı
- Çıktı buffering’inin zor olmasının nedeni, hangi komutun pipe çıktısında buffering yaptığını kullanıcının hatırlamak zorunda olmasıdır
- Çıktı buffering’i yapmayan komut örnekleri şunlardır
tailcattee
- Pipe’a yazarken çıktı buffering’i yapan yaygın komutlar ve bunu azaltma yöntemleri şunlardır
grep:--line-bufferedsed:-uawk:fflush()fonksiyonutcpdump:-ljq:-utr:-ucut: buffering devre dışı bırakılamaz
sortgibi tüm girdiyi aldıktan sonra çalışabilen komutlarda buffering olup olmaması pratikte önemli değildir- Hem Mac OS hem de GNU sürümlerini test etmeye çalıştım; ancak çok fazla varyasyon olduğu için bazı hatalar olabilir
Programlama dillerinin varsayılan çıktısı da buffering yapar
- Bazı programlama dillerindeki varsayılan
printçıktısı da pipe’a yazarken buffering yapar - Dillere göre devre dışı bırakma yöntemleri şöyledir
- C:
setvbuf - Python:
python -u,PYTHONUNBUFFERED=1,sys.stdout.reconfigure(line_buffering=False),print(x, flush=True) - Ruby:
STDOUT.sync = true - Perl:
$| = 1
- C:
- Bu varsayılan davranış, toplu işlemede varsayılan çıktı fonksiyonlarını hızlı yapmak için tasarlanmış gibi görünüyor
- Çıktı yöntemine göre buffering olup olmaması değişebilir
- C++’ta
cout << "hello\n"pipe’a yazarken buffering yapar cout << "hello" << endlçıktıyı flush eder
- C++’ta
Ctrl-C ve dosya yönlendirmesinde oluşan fark
- Aşağıdaki gibi
tcpdumpçıktısınıgrepe bağlayıp-lseçeneğini unutursanız çıktı buffer’da kalabilirsudo tcpdump -ni any port 53 | grep example.com
- İdeal olarak
Ctrl-Cye basıldığındatcpdumpın buffer’ı flush etmesini,grepin arama yapmasını ve eksik çıktının görünmesini bekleyebilirsiniz - Gerçekte programlar sonlanırken
tcpdumpbuffer’ındaki çıktı kaybolur straceile kontrol edildiğindegrep,tcpdumptan önceSIGINTaldığı için,tcpdumpflush etmeye çalışsa bilegrepzaten ölmüş olabilir- Bir geçici çözüm olarak
tcpdumpın PID’sini bulupkill -TERM $PIDçalıştırırsanız,tcpdumpbuffer’ı flush eder ve çıktı görünebilir - Dosya yönlendirmesi de buffering yapar
sudo tcpdump -ni any port 53 > output.txt
- Ancak dosya yönlendirmesinde,
Ctrl-Cnin buffer içeriğini tamamen uçurması sorunundan farklı olarak, deneyime göre program sonlanmadan önce buffer içeriği çoğu zaman dosyaya yazılır - Bu davranışa her zaman güvenilip güvenilemeyeceği kesin değildir
Buffering’den kaçınmanın beş yolu
-
Hızlı biten bir programa geçmek
- Pipe’a yavaş yazılan durumu tamamen önleyip hızlı biten bir komuta geçebilirsiniz
- Örnek şöyledir
cat /some/log/file | grep thing1 | grep thing2 | tail
- Orijinal
tail -fkomutuyla aynı davranış değildir, ancak karmaşık buffering sorunlarından kaçınmanızı sağlar
-
grepin satır-buffer seçeneğini kullanmakgrepte buffering’den kaçınmak için bir bayrak vardır- Örnek şöyledir
tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
-
awkveya daha karmaşık birgrepile birleştirmek- Birden fazla
grepkullanılan durumlar tek birawkile değiştirilebilirtail -f /some/log/file | awk '/thing1/ && /thing2/'
- Ya da daha karmaşık regex kullanan bir
grepyazılabilirtail -f /some/log/file | grep -E 'thing1.*thing2'
awkda buffering yaptığı için, bu yöntemin çalışması içinawkın pipeline’daki son komut olması gerekir
- Birden fazla
-
stdbufkullanmakstdbuf, libc’nin buffering’ini kapatmak içinLD_PRELOADkullanır- Çıktı buffering’ini kapatma örneği şöyledir
tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
LD_PRELOADtabanlı çözümler gibi güvenilirliği sınırlıdır- Statik binary’lerde çalışmaz
- Program libc buffering’i kullanmıyorsa çalışmayabilir
- Mac OS’ta her zaman çalışmaz
- İlgili açıklama olarak Harry Marr’ın How stdbuf works yazısı vardır
-
unbufferkullanmakunbuffer program, program çıktısını TTY’ymiş gibi zorlayarak normal TTY’de olduğu gibi daha az buffering yapılmasını ve renkli çıktı gibi özelliklerin kullanılmasını sağlar- Örnek şöyledir
tail -f /some/log/file | unbuffer grep thing1 | grep thing2
stdbufun aksine her zaman çalışır; ancak istenmeyen yan etkileri olabilir- Örneğin
grep thing1, eşleşen sonuçlara renk ekleyebilir
- Örneğin
unbuffer,expectpaketinin içindedir
Sorunun çoğunlukla ortaya çıktığı durumlar ve ortam değişkeni fikri
- Bu sorun çoğunlukla veriyi pipe’a yavaş yavaş akıtan programlarda ortaya çıkar
- Örnekler şunlardır
tcpdumptail -fkubectl logsbenzeri günlük izleme- Yavaş hesaplamaların çıktısı
- Python’daki
PYTHONUNBUFFEREDgibi buffering’i kapatan standart bir ortam değişkeni olsa iyi olabilir - Bu fikir Mark Dominus’un 2018 tarihli blog yazısı ve devam yazısından geldi
- İsim örneği olarak
NO_COLORgibiNO_BUFFERmümkün olabilir - Tasarımı zordur
- NETBSD’de çok sayıda kontrol sağlayan
STDBUF,STDBUF1vb. ortam değişkenleri vardır - Çoğu geliştirici, görece küçük bir edge case için birden fazla ortam değişkeni uygulamak istemeyebilir
- NETBSD’de çok sayıda kontrol sağlayan
- Çıktı buffer’ını 1 saniye gibi aralıklarla otomatik flush eden programlar olup olmadığını da merak ediyorum; ancak aklıma böyle bir program gelmiyor ve dezavantajları olabilir
Kapsam dışında bırakılanlar
- Satır buffering ile tamamen buffer’sız çıktı arasındaki fark hariç tutuldu
- stderr buffering ile stdout buffering arasındaki fark hariç tutuldu
- Bu içerik yalnızca program içinde oluşan buffering konusunu ele alır
- İşletim sisteminin TTY sürücüsü de bazen az miktarda buffering yapar
- Pipe’a yazma durumunun dışında çıktının flush edilmesi gereken başka nedenler hariç tutuldu
1 yorum
Hacker News yorumları
Tamponlanmış yaklaşım neredeyse her zaman bayt eşiğine ulaşıldığında ya da en az 1 bayt varsa belirli bir süre geçtikten sonra flush eden bir “eşik veya zaman aşımı” yöntemi olmalı
Benzer sorunları çözmek için donanım arayüzlerinde yaygın bir yöntemdir
Bu durumda kullanıcı alanında tamponlama yapan kütüphanenin veriyi ilk kez tampona koyarken uygun bir timer ayarlaması gerekir. Zaman aşımı değeri argüman olarak alınabilir, insanın algısına göre kısa sayılacak 1~100 ms civarında seçilebilir,
{bant genişliği / eşik}ile orantılı tutulabilir ya da sistem çağrısı overhead'inin toplam sürenin %0,1'ini aşmamasını sağlayacak şekilde belirlenebilirBu yöntem yalnızca yazmaya değil okumaya da uygulanır. Toplu okuma veya birleştirilmiş okuma yapılıyorsa benzer bir yöntem gerekir; ancak veri kanalında “bekleyen veriyi” verimli biçimde sorgulamanın veya bildirim almanın bir yolu bulunmalı, bu yüzden daha çok kanal tasarımına bağlıdır. Donanımda interrupt coalescing gibi yöntemler yaygındır
G/Ç hataları yalnızca yazma anında değil, herhangi bir anda oluşabilir; ayrıca yalnızca programın kendi timer koyduğu yerlerde veya sinyalin geldiği noktalarda değil, timer yüzünden çeşitli sistem çağrıları da kesintiye uğrayabilir
Uygulama ve libc ikisi birden timer ayarlarsa karışıklık çıkma olasılığı da var. Güncel kernel timer API'leri eski hatırladığımdan daha iyi göründüğü için bu daha az ilgili olabilir; ama uygulama kritik bölgelerde sinyalleri kısa süreliğine engellerse G/Ç timer'ları da bundan etkilenir
Sinyal işleme zamanı ve biçimi nedeniyle G/Ç yapısı erişimi konusunda daha dikkatli olmak gerekir
io_uring ve kullanıcı alanı timer'ları kullanıldığında çok daha iyi ölçeklenir, ancak çok sayıda hızlı küçük yazmayı desteklemek için yine de hileler gerekir. Örneğin saniyede yaklaşık 1 milyonun üzerine çıkınca timer yönetim maliyeti giderek görünür olmaya başlar; saniyede 100 milyon yazmaya kadar çıkmak için epey sıra dışı teknikler gerekmişti
Sorunun nedeni, etkileşimli olması gereken şeylerle etkileşim varsaymayan bir sözleşmenin karışması. Örneğin
tailtakip çıktısını pipe'a göndermek gibiÇözülmesi gereken gerçek bir sorun olduğunu düşünmüyorum. Donanım benzetmesi yapılacaksa bu, yağmur suyunu biriktiren ve yalnızca dolduğunda taşıyan bir su deposu olur. Hangi örneği kastettiğini bilmiyorum ama bildiğim kadarıyla donanımda zaman tabanlı flush yaygın değil
Önerilen düzeltme sözleşmeyi çok daha karmaşık hale getiriyor
Sorun semantiğin kendisinden çok semantiği bilmemekte
NIX sistemleriyle 20 yılı aşkın süredir uğraşıyor olmama rağmen bunun yaşandığını biliyorum; yine de çıktının neden gelmediğini uzun süre puzzling bulduktan sonra her seferinde ancak hatırlıyorum
“Son yazılar epey uzamaya başladı; tamponlama hakkında 3000 kelimelik bir yazıyı gerçekten okumak isteyen olur mu?” kısmına gelince, şahsen okumak isterim
Uzun yazıların arama optimizasyonu için konmuş dolgu olduğu durumlar da var bence
TLDR ve NTLDR, yani “uzun olsa da okudum” bölümleri koymak da mümkün
Sistem genelinde CPU her boşta kaldığında tüm tamponlar flush edilse güzel olurdu
Tamponlama genel olarak CPU tasarrufu sağlayan bir tekniktir. CPU sonsuz olsaydı tüm tamponlar 1 bayt olurdu. Tampon, veriyi verimlilik için toplayıp toplu işleme yöntemidir
Ama CPU boşta kaldığında “sonra yapılacak iş” kalmamalı. Kernel scheduler boşta kalır kalmaz tüm süreçlere tamponları flush et sinyali göndermeli
O işi yaptıktan sonra bir de tampon flush etmek için sistem çağrısı yaptırmak anlamına geliyor. Kernel'in kullanıcı alanı tamponlarını bilebilmesini sağlayıp boşta kaldığında veriyi doğrudan oradan alacağı bir mekanizma eklenebilir belki
Bu bir ölçüde io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff... gibi bir şey mi acaba
Bu yazı tamponsuz ve satır tamponlama diye iki farklı şeyi karıştırıyor
Tamponsuz çalışma gereksiz yere performansı kötüleştirir ve birden fazla kaynak aynı pipe'a yazıyorsa hatalı çıktı üretebilir. Yeterince uzun satırlar zaten karışacaktır; ancak gerçek dünyadaki çıktı satırlarının çoğu, biçimlendirme/kontrol karakterleri ve yardımcı düzlem karakterleri dahil olsa bile 4096 bayttan kısadır
Satır tamponlama terminalin varsayılanıdır ve pipe'larda da genellikle istenen davranıştır. Her komutu
stdbuf -oL -eLaltında çalıştırmak yeterli. Satır içi güncelleme isteyen nadir programlar zaten manuel flush yapmak zorunda olduğundan burada da doğru çalışırstdbufun gerçekte yaptığı şey şöyle görülebilir:env -i \command -v stdbuf` -oL -eL `command -v env``Bu sorun hakkında daha önce yazmıştım: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
Arabelleğe almayan komutlar konusunda bu uygulamaya bağlı olabilir ya da
catsöz konusuysa yanlış da olabilir. https://pubs.opengroup.org/onlinepubs/9799919799/utilities/c... ve-uya bakılabilir. POSIX’in bunu yönetmek için resmî bir yol koymamış olması büyük bir sıkıntı.Bahsedilmeyen bir şey de girdi arabelleğe alma; şu tür garip sonuçlar üretiyor:
$ seq 5 | { v1=$(head -1); v2=$(head -1); printf '%s=%s\n' v1 "$v1" v2 "$v2"; }v1=1v2=Bu durumda çözüm
stdbuf -i0 head -1kullanmak.socketpairgibi yerlerden okuyan bir sürecin, yazan sürece böyle bir kısıtı dayatabileceğini sanmıyorum.ptrace()gibi ağır hack’ler kullanmak hariç.Pipe arabellek boyutunu ayarlamak mümkün olabilir, ama standart C giriş/çıkışının buna uyması gerektiğine dair bir teamül bilmiyorum.
Her hâlükârda bu durumda
stdbufyardımcı olmuyor gibi:$ ./a | stdbuf -i0 -- cat#include#includeint main(void) {for (;;) {printf("n");usleep(100000);}}Arabelleklerin olmasının iyi nedenleri var. Ekrana çıktı basmak, arabelleğe yazmaya kıyasla nispeten çok yavaş.
Karakterleri tek tek yazdırmak son derece verimsiz.
Bu eski bir sorun ve UART ile uğraşırken sıkça karşımıza çıkar. Olası çözümler birkaç tane: çıktının sonunu yeni satır gibi özel bir karakterle işaretleyen satır tabanlı yöntem, 8KB gibi bir uzunluğa ulaşana kadar bekleyen uzunluk tabanlı yöntem, X milisaniyede bir çıktı veren zaman tabanlı yöntem.
Her yöntemin artıları ve eksileri var; hangisinin en iyi olduğu uygulamaya göre değişir. Yazıda bazı programların arabelleğe alma kullanmadığı söylenen kısmın yanlış olduğunu düşünüyorum. O programlar sadece bariz uzunluk tabanlı yöntemi kullanmıyor.
Satır tabanlı yaklaşım buna örnek; ama hangi karakterin kullanılacağı konusunda uzlaşma gerekir. Genelde bu yeni satırdır.
/dev/nulla bu kadar çok sistem çağrısı yapmak bile performansı ciddi biçimde öldürebilir.Unix’i 35 yılı aşkın süredir kullanıyorum ama bunun nasıl çalıştığını hiçbir zaman tam olarak anlamamıştım.
Birden çok sistem ve bileşene yayılan arabelleğe alma davranışını bütünlüklü şekilde açıklaması hoşuma gitti; kesinlikle bir şeyler öğrendim.
“Pipe’ta Ctrl-C’ye basınca arabellek içeriği kaybolur” kısmına gelince, çoğu programın SIGINT aldığında arabelleği flush edeceğini sanıyorum.
Ancak kabukta böyle çalışması için SIGINT’in yalnızca pipeline’daki ilk programa iletilmesi gerekir; muhtemelen gerçek davranış böyle değildir.
sigintalır, geri kalanlarsigpipealır.