- Pipeline yang menghubungkan output yang masuk perlahan melalui beberapa perintah, seperti
tail -f /some/log/file | grep thing1 | grep thing2, sebenarnya tidak macet; ia bisa tampak kosong karena perintah di tengah menumpuk output di buffer grepdan banyak program memeriksa apakah stdout adalah terminal denganisatty; jika terminal, mereka memakai line buffering, sedangkan jika pipe atau file, mereka memakai block buffering sekitar 8KBtail,cat, danteeadalah contoh yang tidak melakukan buffering output, tetapi opsi untuk mengurangi buffering berbeda-beda di tiap perintah, sepertigrep --line-buffered,sed -u,tcpdump -l,jq -u,tr -u- Jika pipeline diputus dengan
Ctrl-C, output yang ada di buffer program sepertitcpdumpbisa hilang; jika dihentikan dengankill -TERM $PID, buffer bisa di-flush sehingga output terlihat - Solusi praktis mencakup mengganti dengan perintah yang cepat selesai,
grep --line-buffered, satuawkataugrepyang lebih kompleks,stdbuf,unbuffer, tetapi masing-masing perlu diperiksa kondisi kerja dan efek sampingnya
Mengapa pipe tampak seperti macet
- Ketika baris ditambahkan perlahan ke file log, pipeline berikut bisa tidak menampilkan output meskipun ada hasil yang cocok
tail -f /some/log/file | grep thing1 | grep thing2
- Penyebabnya bukan pipe itu sendiri, melainkan
grep thing1di tengah tidak langsung menulis hasilnya dan justru menyimpannya di buffer - Jika program langsung menulis setiap kali, jumlah system call meningkat, jadi demi performa ia mengumpulkan sejumlah data terlebih dahulu sebelum menulis ke pipe atau file
- Dalam contoh ini,
grep thing1bisa menunggu hingga output sekitar 8KB terkumpul, dan pada log yang lambat kondisi itu bisa saja praktis tidak pernah tercapai
Cara output berbeda antara terminal dan pipe
tail -f file | grep thingberjalan baik, tetapi jika menambahkangrepkedua di belakangnya, output bisa tampak berhentigrepdan banyak program memeriksa apakah stdout adalah terminal dengan fungsiisatty- Jika stdout adalah terminal, mereka memakai line buffering dan langsung mengeluarkan output per baris
- Jika stdout adalah pipe atau file, mereka memakai block buffering dan baru mengeluarkan output saat data sudah terkumpul di atas ukuran tertentu
- Karena itu, jika
grepmenulis langsung ke terminal, baris langsung terlihat, tetapi jika menulis ke pipe yang diteruskan ke perintah berikutnya, output bisa tidak terlihat - Ukuran buffer berbeda-beda di tiap program
- Pada
grep, buffering ditangani oleh libc, dan ukuran libc didefinisikan oleh variabelBUFSIZ - Lokasi definisi glibc ada di stdio.h
- Pada
- Tidak memakai buffer output 8KB saat menulis ke terminal bukanlah hukum fisika; program bisa saja mengimplementasikannya jika mau, tetapi itu akan terasa sebagai perilaku yang sangat aneh
Perilaku buffering berbeda di tiap perintah
- Alasan buffering output merepotkan adalah pengguna harus mengingat perintah mana yang melakukan buffering saat output-nya berupa pipe
- Contoh perintah yang tidak melakukan buffering output adalah sebagai berikut
tailcattee
- Perintah umum yang melakukan buffering output saat menulis ke pipe, beserta cara menguranginya, adalah sebagai berikut
grep:--line-bufferedsed:-uawk: fungsifflush()tcpdump:-ljq:-utr:-ucut: tidak bisa menonaktifkan buffering
- Untuk perintah seperti
sortyang baru bisa bekerja setelah menerima seluruh input, apakah buffering terjadi atau tidak secara praktis tidak terlalu penting - Penulis mencoba menguji Mac OS dan versi GNU, tetapi karena ada banyak variasi, beberapa kesalahan mungkin saja ada
Output default bahasa pemrograman juga melakukan buffering
- Output
printdefault di beberapa bahasa pemrograman juga melakukan buffering saat menulis ke pipe - Cara menonaktifkannya per bahasa adalah sebagai berikut
- C:
setvbuf - Python:
python -u,PYTHONUNBUFFERED=1,sys.stdout.reconfigure(line_buffering=False),print(x, flush=True) - Ruby:
STDOUT.sync = true - Perl:
$| = 1
- C:
- Perilaku default seperti ini tampaknya dirancang agar fungsi output default cepat untuk pemrosesan batch
- Ada atau tidaknya buffering bisa berbeda tergantung cara output dilakukan
- Di C++,
cout << "hello\n"melakukan buffering saat menulis ke pipe cout << "hello" << endlakan melakukan flush output
- Di C++,
Perbedaan yang muncul pada Ctrl-C dan redirect file
- Jika output
tcpdumpdihubungkan kegrepseperti berikut dan-lterlewat, output bisa tertahan di buffersudo tcpdump -ni any port 53 | grep example.com
- Idealnya, saat menekan
Ctrl-C, kita bisa berharaptcpdumpmelakukan flush buffer dangrepmencari sehingga output yang terlewat terlihat - Pada kenyataannya, saat program-program berhenti, output yang ada di buffer
tcpdumphilang - Saat diperiksa dengan
strace,grepmenerimaSIGINTlebih dulu daripadatcpdump, sehingga meskipuntcpdumpmencoba melakukan flush,grepmungkin sudah mati - Sebagai jalan pintas, jika mencari PID
tcpdumplalu menjalankankill -TERM $PID,tcpdumpbisa melakukan flush buffer sehingga output terlihat - Redirect file juga melakukan buffering
sudo tcpdump -ni any port 53 > output.txt
- Namun berbeda dari masalah
Ctrl-Cyang bisa membuang seluruh isi buffer, pada redirect file, berdasarkan pengalaman, isi buffer sering ditulis ke file sebelum program berhenti - Belum jelas apakah perilaku ini selalu bisa diandalkan
Lima cara menghindari buffering
-
Mengganti dengan program yang cepat selesai
- Hindari situasi menulis perlahan ke pipe, dan ubah menjadi perintah yang cepat selesai jika memungkinkan
- Contohnya sebagai berikut
cat /some/log/file | grep thing1 | grep thing2 | tail
- Ini tidak sama persis dengan perintah
tail -fsemula, tetapi bisa menghindari masalah buffering yang rumit
-
Menggunakan opsi line buffering pada
grepgrepmemiliki flag untuk menghindari buffering- Contohnya sebagai berikut
tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
-
Menggabungkan dengan
awkataugrepyang lebih kompleks- Situasi yang memakai beberapa
grepbisa diganti dengan satuawktail -f /some/log/file | awk '/thing1/ && /thing2/'
- Atau bisa ditulis dengan
grepmemakai regex yang lebih komplekstail -f /some/log/file | grep -E 'thing1.*thing2'
- Karena
awkjuga melakukan buffering, agar cara ini bekerja,awkharus menjadi perintah terakhir dalam pipeline
- Situasi yang memakai beberapa
-
Menggunakan
stdbufstdbufmemakaiLD_PRELOADuntuk mematikan buffering libc- Contoh mematikan buffering output adalah sebagai berikut
tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2
- Seperti solusi berbasis
LD_PRELOAD, keandalannya terbatas- Tidak bekerja pada binary statis
- Bisa tidak bekerja jika program tidak memakai buffering libc
- Tidak selalu bekerja di Mac OS
- Penjelasan terkait ada di How stdbuf works oleh Harry Marr
-
Menggunakan
unbufferunbuffer programmemaksa output program seolah-olah berupa TTY, sehingga buffering berkurang seperti pada TTY biasa dan fitur seperti output berwarna digunakan- Contohnya sebagai berikut
tail -f /some/log/file | unbuffer grep thing1 | grep thing2
- Berbeda dari
stdbuf, ini selalu bekerja, tetapi bisa memiliki efek samping yang tidak diinginkan- Misalnya,
grep thing1bisa menambahkan warna pada hasil yang cocok
- Misalnya,
unbufferada di dalam paketexpect
Situasi yang paling sering memunculkan masalah dan ide variabel lingkungan
- Masalah seperti ini terutama muncul pada program yang mengalirkan data ke pipe secara perlahan
- Contohnya sebagai berikut
tcpdumptail -f- Pemantauan log seperti
kubectl logs - Output dari komputasi yang lambat
- Akan berguna jika ada variabel lingkungan standar untuk mematikan buffering, seperti
PYTHONUNBUFFEREDdi Python - Ide ini berasal dari tulisan blog Mark Dominus tahun 2018 dan tulisan lanjutannya
- Sebagai contoh nama, mungkin bisa memakai
NO_BUFFERsepertiNO_COLOR - Desainnya sulit
- NETBSD memiliki variabel lingkungan seperti
STDBUF,STDBUF1, dan lain-lain yang menyediakan banyak kontrol - Kebanyakan developer mungkin tidak ingin mengimplementasikan beberapa variabel lingkungan untuk edge case yang relatif kecil
- NETBSD memiliki variabel lingkungan seperti
- Penulis juga penasaran apakah ada program yang otomatis melakukan flush buffer output secara berkala, misalnya setiap 1 detik, tetapi tidak terpikir ada program seperti itu dan bisa saja ada kekurangannya
Ruang lingkup yang tidak dibahas
- Perbedaan antara line buffering dan output yang sepenuhnya tanpa buffer dikecualikan
- Perbedaan antara buffering stderr dan buffering stdout dikecualikan
- Pembahasan ini hanya mencakup buffering yang terjadi di dalam program
- Driver TTY milik sistem operasi juga kadang melakukan sedikit buffering
- Alasan lain untuk melakukan flush output selain saat menulis ke pipe dikecualikan
1 komentar
Komentar Hacker News
Pendekatan yang dibuffer hampir selalu seharusnya memakai pola “ambang batas atau timeout”: flush saat mencapai ambang batas byte, atau setelah waktu tertentu berlalu jika ada setidaknya 1 byte
Ini adalah pola yang umum di antarmuka hardware untuk menyelesaikan masalah serupa
Dalam kasus ini, library yang melakukan buffering di user space harus menyetel timer yang sesuai saat pertama kali memasukkan data ke buffer. Nilai timeout bisa diterima sebagai argumen, ditetapkan sekitar 1–100 ms yang terasa singkat bagi manusia, dibuat sebanding dengan
{bandwidth / ambang batas}, atau ditetapkan agar overhead system call tidak melebihi 0,1% dari total waktuPola ini berlaku bukan hanya untuk write, tetapi juga untuk read. Jika melakukan read secara batch atau read yang digabungkan, pendekatan serupa diperlukan, tetapi channel data harus punya cara untuk menanyakan atau menerima notifikasi “data yang menunggu” secara efisien, sehingga lebih bergantung pada desain channel. Di hardware, pola seperti interrupt coalescing umum digunakan
Error I/O bisa terjadi bukan hanya saat write, tetapi kapan saja, dan berbagai system call bisa terinterupsi oleh timer, bukan hanya di tempat program memasang timer sendiri atau tempat sinyal tiba
Jika aplikasi dan libc sama-sama menyetel timer, kemungkinan besar akan membingungkan. API timer kernel modern tampaknya lebih baik daripada yang saya ingat dulu, jadi ini mungkin kurang relevan, tetapi jika aplikasi memblokir sinyal sebentar di bagian penting, timer I/O juga akan terpengaruh
Karena waktu dan cara penanganan sinyal, akses ke struktur I/O harus dilakukan dengan lebih hati-hati
Dengan io_uring dan timer user space, skalanya jauh lebih baik, tetapi untuk mendukung banyak write kecil yang cepat, tetap diperlukan trik. Misalnya, setelah melewati sekitar 1 juta kali per detik, biaya manajemen timer mulai terlihat, dan untuk mencapai 100 juta write per detik diperlukan teknik yang cukup tidak biasa
Sumber masalahnya adalah tercampurnya sesuatu yang seharusnya interaktif dengan kontrak yang tidak mengasumsikan interaksi. Contohnya adalah saat output pelacakan
taildikirim lewat pipeSaya rasa tidak ada masalah nyata yang perlu diselesaikan. Jika memakai analogi hardware, ini seperti tangki air yang menampung air hujan dan hanya memindahkannya saat penuh. Saya tidak tahu contoh apa yang Anda maksud, tetapi sejauh yang saya tahu, flush berbasis waktu tidak umum di hardware
Perbaikan yang diusulkan membuat kontraknya jauh lebih rumit
Masalahnya bukan pada semantiknya sendiri, melainkan pada ketidaktahuan terhadap semantik tersebut
Meski sudah menangani sistem NIX lebih dari 20 tahun, saya tahu hal seperti ini terjadi, tetapi setiap kali saya baru teringat setelah lama bingung kenapa output tidak muncul
Mengenai bagian “tulisan-tulisan belakangan ini cukup panjang, memangnya ada yang benar-benar ingin membaca tulisan 3000 kata tentang buffering?”, secara pribadi saya ingin membacanya
Menurut saya ada juga tulisan panjang yang isinya tambalan tak perlu demi optimasi mesin pencari
Bisa juga dengan menyediakan bagian TLDR dan NTLDR, yaitu “panjang, tetapi sudah dibaca”
Saya berharap setiap kali CPU seluruh sistem berada dalam keadaan idle, semua buffer di-flush
Buffering pada dasarnya adalah teknik untuk menghemat CPU. Jika CPU tak terbatas, semua buffer akan berukuran 1 byte. Buffer mengumpulkan data untuk diproses sekaligus demi efisiensi
Namun saat CPU menjadi idle, tidak boleh ada “pekerjaan untuk nanti” yang tersisa. Begitu scheduler kernel masuk keadaan idle, ia seharusnya mengirim sinyal ke semua proses untuk mem-flush buffer
Itu berarti setelah semua pekerjaan itu dilakukan, proses masih harus melakukan system call lagi untuk flush buffer. Mungkin bisa ditambahkan mekanisme agar kernel mengetahui buffer user space dan mengambil langsung dari sana saat idle
Saya bertanya-tanya apakah ini kurang lebih seperti io_uring https://man7.org/linux/man-pages/man3/io_uring_register_buff...
Tulisan ini mencampuradukkan dua hal berbeda: tanpa buffer dan line buffering
Tanpa buffer membuat performa buruk tanpa perlu, dan jika beberapa sumber menulis ke pipe yang sama, bisa menghasilkan output yang keliru. Baris yang cukup panjang memang tetap akan tercampur, tetapi sebagian besar baris output di dunia nyata, bahkan termasuk karakter format/kontrol dan karakter supplementary planes, lebih pendek dari 4096 byte
Line buffering adalah default pada terminal, dan biasanya juga perilaku yang diinginkan pada pipe. Cukup jalankan tiap perintah di bawah
stdbuf -oL -eL. Program langka yang ingin memperbarui isi di dalam satu baris memang sudah harus melakukan flush manual, jadi di sini pun akan bekerja dengan benarHal yang sebenarnya dilakukan
stdbufbisa dilihat seperti ini:env -i \command -v stdbuf` -oL -eL `command -v env``Saya pernah menulis tentang masalah ini sebelumnya: https://world-playground-deceit.net/blog/2024/09/bourne_shel...
Untuk perintah yang tidak melakukan buffering, itu bergantung pada implementasi, atau dalam kasus
catmungkin saja salah. Lihat https://pubs.opengroup.org/onlinepubs/9799919799/utilities/c... dan-u. Sangat menyakitkan bahwa POSIX tidak memasukkan cara resmi untuk mengelola hal ini.Yang tidak disebutkan adalah buffering input, dan ini menghasilkan hasil aneh seperti ini:
$ seq 5 | { v1=$(head -1); v2=$(head -1); printf '%s=%s\n' v1 "$v1" v2 "$v2"; }v1=1v2=Dalam kasus ini solusinya adalah memakai
stdbuf -i0 head -1.socketpairbisa memaksakan batasan semacam itu pada proses yang menulis. Kecuali kalau memakai hack berat sepertiptrace().Mungkin saja ukuran buffer pipe bisa disetel, tetapi saya tidak tahu ada konvensi bahwa I/O C standar harus mengikuti itu.
Bagaimanapun, dalam kasus ini
stdbuftampaknya tidak membantu:$ ./a | stdbuf -i0 -- cat#include#includeint main(void) {for (;;) {printf("n");usleep(100000);}}Ada alasan bagus mengapa buffer itu ada. Mencetak output ke layar relatif sangat lambat dibandingkan menulis ke buffer.
Mengeluarkan karakter satu per satu sangat tidak efisien.
Ini masalah lama, dan sering ditemui saat menangani UART. Ada beberapa kemungkinan solusi: pendekatan berbasis baris yang menandai akhir output dengan karakter khusus seperti baris baru, pendekatan berbasis panjang yang menunggu sampai mencapai panjang seperti 8KB, atau pendekatan berbasis waktu yang mengeluarkan output setiap X milidetik.
Masing-masing punya kelebihan dan kekurangan, dan mana yang optimal bergantung pada aplikasinya. Menurut saya bagian di tulisan yang mengatakan program-program tertentu tidak memakai buffering itu salah. Program-program itu hanya tidak memakai pendekatan berbasis panjang yang eksplisit.
Pendekatan berbasis baris adalah salah satu cara seperti itu, tetapi perlu kesepakatan tentang karakter apa yang dipakai. Biasanya baris baru.
/dev/nullsaja bisa sangat membunuh performa.Saya sudah memakai Unix selama lebih dari 35 tahun, tetapi tidak pernah benar-benar memahami bagaimana ini bekerja.
Senang ada penjelasan menyeluruh tentang perilaku buffering di berbagai sistem dan komponen, dan saya jelas belajar sesuatu.
Mengenai bagian “jika menekan Ctrl-C di pipe, isi buffer akan hilang”, saya kira sebagian besar program akan mem-flush buffer saat SIGINT.
Namun agar shell berperilaku seperti itu, SIGINT harus diteruskan hanya ke program pertama dalam pipeline, dan kemungkinan perilaku sebenarnya bukan begitu.
sigintdan sisanya menerimasigpipe