Matikan buffering in pipe

395

Saya memiliki skrip yang memanggil dua perintah:

long_running_command | print_progress

Hasil long_running_commandcetak mengalami kemajuan tetapi saya tidak senang dengannya. Saya menggunakan print_progressuntuk membuatnya lebih baik (yaitu, saya mencetak kemajuan dalam satu baris).

Masalahnya: Koneksi pipa ke stdout juga mengaktifkan buffer 4K, ke program cetak yang bagus tidak mendapatkan apa-apa ... tidak ada ... tidak ada ... banyak ... :)

Bagaimana saya bisa menonaktifkan buffer 4K untuk long_running_command(tidak, saya tidak punya sumber)?

Aaron Digulla
sumber
1
Jadi, ketika Anda menjalankan long_running_command tanpa pemipaan Anda dapat melihat pembaruan kemajuan dengan benar, tetapi ketika pemipaan mereka mendapatkan buffer?
1
Ya, itulah yang terjadi.
Aaron Digulla
20
Ketidakmampuan untuk cara sederhana mengendalikan buffering telah menjadi masalah selama beberapa dekade. Misalnya, lihat: marc.info/?l=glibc-bug&m=98313957306297&w=4 yang pada dasarnya mengatakan "Saya tidak dapat melakukan ini dan inilah beberapa perangkap untuk membenarkan posisi saya"
1
Sebenarnya stdio bukan pipa yang menyebabkan penundaan sambil menunggu cukup data. Pipa memang memiliki kapasitas, tetapi segera setelah ada data yang ditulis ke pipa, segera siap untuk membaca di ujung lainnya.
Sam Watkins

Jawaban:

254

Anda dapat menggunakan unbufferperintah (yang datang sebagai bagian dari expectpaket), mis

unbuffer long_running_command | print_progress

unbufferterhubung ke long_running_commandmelalui pseudoterminal (pty), yang membuat sistem memperlakukannya sebagai proses interaktif, oleh karena itu tidak menggunakan buffering 4-kiB dalam pipa yang merupakan kemungkinan penyebab keterlambatan.

Untuk saluran pipa yang lebih panjang, Anda mungkin harus membatalkan perintah masing-masing (kecuali yang terakhir), misalnya

unbuffer x | unbuffer -p y | z
Stephen Kitt
sumber
3
Bahkan, penggunaan pty untuk terhubung ke proses interaktif adalah benar dari yang diharapkan secara umum.
15
Saat pipelining panggilan ke unbuffer, Anda harus menggunakan argumen -p sehingga unbuffer membaca dari stdin.
26
Catatan: Pada sistem debian, ini disebut expect_unbufferdan ada dalam expect-devpaket, bukan expectpaket
bdonlan
4
@bdonlan: Setidaknya di Ubuntu (berbasis debian), expect-devmenyediakan keduanya unbufferdan expect_unbuffer(yang pertama adalah symlink ke yang terakhir). Tautan tersedia sejak expect 5.44.1.14-1(2009).
jfs
1
Catatan: Pada sistem Ubuntu 14.04.x, itu juga dalam paket harapan-dev.
Alexandre Mazel
462

Cara lain untuk menguliti kucing ini adalah dengan menggunakan stdbufprogram, yang merupakan bagian dari GNU Coreutils (FreeBSD juga memiliki programnya sendiri).

stdbuf -i0 -o0 -e0 command

Ini mematikan buffering sepenuhnya untuk input, output dan kesalahan. Untuk beberapa aplikasi, buffer garis mungkin lebih cocok untuk alasan kinerja:

stdbuf -oL -eL command

Perhatikan bahwa ini hanya berfungsi untuk stdiobuffering ( printf(), fputs()...) untuk aplikasi yang terhubung secara dinamis, dan hanya jika aplikasi itu tidak menyesuaikan buffering dari stream standarnya sendiri, meskipun itu harus mencakup sebagian besar aplikasi.

a3nm
sumber
6
"unbuffer" harus diinstal di Ubuntu, yang ada di dalam paket: berharap-dev yang 2MB ...
lepe
2
Ini berfungsi dengan baik pada instalasi raspbian default untuk menghapus log logging. Saya menemukan sudo stdbuff … commandkarya meskipun stdbuff … sudo commandtidak.
natevw
20
@qdii stdbuftidak berfungsi tee, karena teemenimpa default yang ditetapkan oleh stdbuf. Lihat halaman manual stdbuf.
ceving
5
@ lepe Anehnya, unbuffer memiliki dependensi pada x11 dan tcl / tk, yang berarti ia benar-benar membutuhkan> 80 MB jika Anda menginstalnya di server tanpa mereka.
jpatokal
10
@qdii stdbufmenggunakan LD_PRELOADmekanisme untuk memasukkan sendiri perpustakaan dimuat secara dinamis libstdbuf.so. Ini berarti bahwa itu tidak akan bekerja dengan jenis-jenis yang dapat dieksekusi: dengan setuid atau kemampuan file diatur, terhubung secara statis, tidak menggunakan libc standar. Dalam kasus ini lebih baik menggunakan solusi dengan unbuffer/ script/ socat. Lihat juga stdbuf dengan setuid / kemampuan .
pabouk
75

Namun cara lain untuk mengaktifkan mode output line-buffering untuk long_running_commandadalah dengan menggunakan scriptperintah yang menjalankan Anda long_running_commanddi terminal semu (pty).

script -q /dev/null long_running_command | print_progress      # FreeBSD, Mac OS X
script -c "long_running_command" /dev/null | print_progress    # Linux
chad
sumber
15
+1 trik yang bagus, karena scriptini adalah perintah lama, itu harus tersedia di semua platform mirip Unix.
Aaron Digulla
5
Anda juga perlu -qdi Linux:script -q -c 'long_running_command' /dev/null | print_progress
jfs
1
Sepertinya skrip dibaca dari stdin, yang membuatnya tidak mungkin dijalankan long_running_commanddi latar belakang, setidaknya ketika dimulai dari terminal interaktif. Untuk mengatasinya, saya dapat mengarahkan ulang stdin /dev/null, karena saya long_running_commandtidak menggunakannya stdin.
haridsv
1
Bahkan berfungsi di Android.
not2qubit
3
Satu kelemahan signifikan: ctrl-z tidak lagi berfungsi (yaitu saya tidak dapat menunda skrip). Ini dapat diperbaiki dengan, misalnya: echo | skrip sudo -c / usr / local / bin / ec2-snapshot-all / dev / null | ts, jika Anda tidak keberatan tidak dapat berinteraksi dengan program ini.
rlpowell
66

Untuk grep, seddan awkAnda dapat memaksa output menjadi buffer line. Anda dapat gunakan:

grep --line-buffered

Paksa output menjadi garis buffered. Secara default, output adalah garis buffered ketika output standar adalah terminal dan memblokir buffer-bijaksana.

sed -u

Buat garis output buffered.

Lihat halaman ini untuk informasi lebih lanjut: http://www.perkin.org.uk/posts/how-to-fix-stdio-buffering.html

yaneku
sumber
51

Jika masalah dengan libc memodifikasi buffering / flushing ketika output tidak masuk ke terminal, Anda harus mencoba socat . Anda dapat membuat aliran dua arah antara hampir semua jenis mekanisme I / O. Salah satunya adalah program bercabang bercakap-cakap dengan pseudo tty.

 socat EXEC:long_running_command,pty,ctty STDIO 

Apa yang dilakukannya adalah

  • buat pseudo tty
  • fork long_running_command dengan sisi slave pty sebagai stdin / stdout
  • buat aliran dua arah antara sisi master pty dan alamat kedua (ini dia STDIO)

Jika ini memberi Anda output yang sama dengan long_running_command, maka Anda dapat melanjutkan dengan pipa.

Sunting: Wow Tidak melihat jawaban unbuffer! Yah, socat adalah alat yang hebat, jadi saya mungkin meninggalkan jawaban ini

shodanex
sumber
1
... dan saya tidak tahu tentang socat - sepertinya netcat mungkin lebih dari itu. ;) Terima kasih dan +1.
3
Saya akan gunakan di socat -u exec:long_running_command,pty,end-close -sini
Stéphane Chazelas
20

Anda dapat gunakan

long_running_command 1>&2 |& print_progress

Masalahnya adalah bahwa libc akan buffer-line ketika stdout ke layar, dan buffer penuh ketika stdout ke file. Tapi tanpa buffer untuk stderr.

Saya tidak berpikir itu masalah dengan penyangga pipa, ini semua tentang kebijakan penyangga libc.

Wang HongQin
sumber
Kamu benar; pertanyaan saya masih: Bagaimana saya bisa mempengaruhi kebijakan buffer libc tanpa kompilasi ulang?
Aaron Digulla
@ StéphaneChazelas fd1 akan dialihkan ke stderr
Wang HongQin
@ StéphaneChazelas saya tidak mengerti maksud Anda. tolong lakukan tes, itu berhasil
Wang HongQin
3
OK, apa yang terjadi adalah bahwa dengan keduanya zsh(dari mana |&berasal dari diadaptasi dari csh) dan bash, ketika Anda melakukannya cmd1 >&2 |& cmd2, kedua fd 1 dan 2 terhubung ke stdout luar. Jadi ia berfungsi mencegah buffering ketika stdout luar itu adalah terminal, tetapi hanya karena output tidak melalui pipa (jadi print_progresstidak mencetak apa-apa). Jadi itu sama dengan long_running_command & print_progress(kecuali bahwa print_progress stdin adalah pipa yang tidak memiliki penulis). Anda dapat memverifikasi dengan ls -l /proc/self/fd >&2 |& catdibandingkan dengan ls -l /proc/self/fd |& cat.
Stéphane Chazelas
3
Itu karena |&kependekan dari 2>&1 |, secara harfiah. Begitu cmd1 |& cmd2juga cmd1 1>&2 2>&1 | cmd2. Jadi, kedua fd 1 dan 2 akhirnya terhubung ke stderr asli, dan tidak ada yang tersisa menulis ke pipa. ( s/outer stdout/outer stderr/gdalam komentar saya sebelumnya).
Stéphane Chazelas
11

Dulu kasusnya, dan mungkin masih demikian, bahwa ketika output standar ditulis ke terminal, itu adalah garis buffered secara default - ketika baris baru ditulis, garis tersebut ditulis ke terminal. Ketika output standar dikirim ke pipa, itu sepenuhnya buffered - sehingga data hanya dikirim ke proses berikutnya dalam pipa ketika buffer I / O standar diisi.

Itulah sumber masalahnya. Saya tidak yakin apakah ada banyak yang dapat Anda lakukan untuk memperbaikinya tanpa mengubah program menulis ke dalam pipa. Anda bisa menggunakan setvbuf()fungsi dengan _IOLBFflag untuk tanpa syarat dimasukkan stdoutke mode buffered line. Tapi saya tidak melihat cara mudah untuk menerapkannya pada sebuah program. Atau program dapat melakukannya fflush()pada titik yang sesuai (setelah setiap baris output), tetapi komentar yang sama berlaku.

Saya mengira bahwa jika Anda mengganti pipa dengan pseudo-terminal, maka perpustakaan I / O standar akan berpikir output adalah terminal (karena itu adalah jenis terminal) dan akan menyangga buffer secara otomatis. Namun, itu adalah cara yang rumit untuk berurusan dengan berbagai hal.

Jonathan Leffler
sumber
7

Saya tahu ini adalah pertanyaan lama dan sudah memiliki banyak jawaban, tetapi jika Anda ingin menghindari masalah buffer, coba saja sesuatu seperti:

stdbuf -oL tail -f /var/log/messages | tee -a /home/your_user_here/logs.txt

Ini akan menampilkan log waktu nyata dan juga menyimpannya ke dalam logs.txtfile dan buffer tidak akan lagi mempengaruhi tail -fperintah.

Marin Nedea
sumber
4
Ini terlihat seperti jawaban kedua: - /
Aaron Digulla
2
stdbuf termasuk dalam gnu coreutils (saya diverifikasi pada versi terbaru 8.25). diverifikasi ini berfungsi di linux tertanam.
zhaorufei
Dari dokumentasi stdbuf, NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does for example) then that will override corresponding changes by 'stdbuf'.
shrewmouse
6

Saya tidak berpikir masalahnya dengan pipa. Sepertinya proses lama Anda tidak cukup sering menyiram buffernya sendiri. Mengubah ukuran buffer pipa akan menjadi hack untuk memperbaikinya, tetapi saya tidak berpikir itu mungkin tanpa membangun kembali kernel - sesuatu yang Anda tidak ingin lakukan sebagai hack, karena mungkin aversley mempengaruhi banyak proses lainnya.


sumber
18
Akar penyebabnya adalah libc beralih ke buffer 4k jika stdout bukan tty.
Aaron Digulla
5
Ini sangat menarik ! karena pipa tidak menyebabkan penyangga. Mereka menyediakan buffering, tetapi jika Anda membaca dari sebuah pipa, Anda mendapatkan data apa pun yang tersedia, Anda tidak perlu menunggu buffer di dalam pipa. Jadi pelakunya akan menjadi buffering stdio dalam aplikasi.
3

Menurut posting ini di sini , Anda dapat mencoba mengurangi ulimit pipa menjadi satu blok 512-byte tunggal. Ini tentu saja tidak akan mematikan buffering, tapi yah, 512 byte lebih kecil dari 4K: 3

RAKK
sumber
3

Dalam nada yang mirip dengan jawaban chad , Anda dapat menulis skrip kecil seperti ini:

# save as ~/bin/scriptee, or so
script -q /dev/null sh -c 'exec cat > /dev/null'

Kemudian gunakan scripteeperintah ini sebagai pengganti tee.

my-long-running-command | scriptee

Sayangnya, sepertinya saya tidak bisa mendapatkan versi seperti itu untuk bekerja dengan sempurna di Linux, jadi sepertinya terbatas pada unix gaya BSD.

Di Linux, ini sudah dekat, tetapi Anda tidak mendapatkan kembali prompt ketika selesai (sampai Anda menekan enter, dll) ...

script -q -c 'cat > /proc/self/fd/1' /dev/null
jwd
sumber
Mengapa itu berhasil? Apakah "script" mematikan buffering?
Aaron Digulla
@ Harun Digulla: scriptmengemulasi terminal, jadi ya, saya percaya itu mematikan buffering. Itu juga menggemakan kembali setiap karakter yang dikirim kepadanya - itulah sebabnya catdikirim ke /dev/nulldalam contoh. Sejauh menyangkut program yang berjalan di dalamnya script, ia berbicara pada sesi interaktif. Saya percaya ini mirip dengan expectdalam hal ini, tetapi scriptkemungkinan merupakan bagian dari sistem basis Anda.
jwd
Alasan saya menggunakan teeadalah untuk mengirim salinan aliran ke file. Di mana file ditentukan scriptee?
Bruno Bronosky
@BrunoBronosky: Anda benar, ini adalah nama yang buruk untuk program ini. Itu tidak benar-benar melakukan operasi 'tee'. Itu hanya menonaktifkan buffering output, sesuai pertanyaan awal. Mungkin itu harus disebut "scriptcat" (meskipun itu tidak melakukan penggabungan baik ...). Apapun, Anda dapat mengganti catperintah dengan tee myfile.txt, dan Anda harus mendapatkan efek yang Anda inginkan.
jwd
2

Saya menemukan solusi cerdas ini: (echo -e "cmd 1\ncmd 2" && cat) | ./shell_executable

Ini caranya. catakan membaca input tambahan (sampai EOF) dan meneruskannya ke pipa setelah echoargumennya dimasukkan ke dalam aliran input shell_executable.

jaggedsoft
sumber
2
Sebenarnya, cattidak melihat output dari echo; Anda hanya menjalankan dua perintah dalam subkulit dan output keduanya dikirim ke pipa. Perintah kedua di subshell ('cat') dibaca dari induk / stdin luar, itu sebabnya ia bekerja.
Aaron Digulla
0

Menurut ini , ukuran buffer pipa tampaknya diatur dalam kernel dan akan meminta Anda untuk mengkompilasi ulang kernel Anda untuk diubah.


sumber
7
Saya percaya itu adalah buffer yang berbeda.
Samuel Edwin Ward