Apa yang mencegah stdout / stderr dari interleaving?

13

Katakanlah saya menjalankan beberapa proses:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Saya menjalankan skrip di atas seperti ini:

foobarbaz | cat

sejauh yang saya tahu, ketika salah satu proses menulis ke stdout / stderr, outputnya tidak pernah interleave - setiap baris stdio tampaknya atom. Bagaimana cara kerjanya? Utilitas apa yang mengontrol bagaimana setiap baris adalah atom?

Alexander Mills
sumber
3
Berapa banyak data yang dihasilkan perintah Anda? Coba buat mereka menghasilkan beberapa kilobyte.
Kusalananda
Maksud Anda di mana salah satu perintah menghasilkan beberapa kb sebelum baris baru?
Alexander Mills
Tidak, kira-kira seperti ini: unix.stackexchange.com/a/452762/70524
muru

Jawaban:

22

Mereka melakukan interleave! Anda hanya mencoba semburan keluaran pendek, yang tetap tidak bertele-tele, tetapi dalam praktiknya sulit untuk memastikan bahwa keluaran tertentu tetap tidak berbintik.

Output buffering

Tergantung bagaimana program buffer output mereka. The library stdio bahwa sebagian besar program menggunakan ketika mereka sedang menulis menggunakan buffer untuk membuat output lebih efisien. Alih-alih mengeluarkan data segera setelah program memanggil fungsi pustaka untuk menulis ke file, fungsi menyimpan data ini dalam buffer, dan hanya benar-benar menampilkan data setelah buffer telah terisi. Ini berarti bahwa output dilakukan dalam batch. Lebih tepatnya, ada tiga mode keluaran:

  • Tidak dibangun: data ditulis segera, tanpa menggunakan buffer. Ini bisa lambat jika program menulis hasilnya dalam potongan-potongan kecil, misalnya karakter demi karakter. Ini adalah mode standar untuk kesalahan standar.
  • Sepenuhnya buffered: data hanya ditulis ketika buffer penuh. Ini adalah mode default saat menulis ke pipa atau ke file biasa, kecuali dengan stderr.
  • Line-buffered: data ditulis setelah setiap baris baru, atau ketika buffer penuh. Ini adalah mode default saat menulis ke terminal, kecuali dengan stderr.

Program dapat memprogram ulang setiap file untuk berperilaku berbeda, dan secara eksplisit dapat membersihkan buffer. Buffer memerah secara otomatis ketika suatu program menutup file atau keluar secara normal.

Jika semua program yang menulis ke pipa yang sama baik menggunakan mode buffer-line, atau menggunakan mode unbuffered dan menulis setiap baris dengan satu panggilan ke fungsi output, dan jika baris cukup pendek untuk menulis dalam satu chunk, maka output akan menjadi interleaving seluruh baris. Tetapi jika salah satu program menggunakan mode buffered penuh, atau jika garis terlalu panjang, maka Anda akan melihat garis campuran.

Berikut adalah contoh di mana saya interleave output dari dua program. Saya menggunakan GNU coreutils di Linux; versi berbeda dari utilitas ini mungkin berperilaku berbeda.

  • yes aaaamenulis aaaaselamanya dalam apa yang pada dasarnya setara dengan mode buffer-line. The yesutilitas benar-benar menulis beberapa baris pada satu waktu, tetapi setiap kali memancarkan output, output adalah seluruh nomor baris.
  • echo bbbb; done | grep bmenulis bbbbselamanya dalam mode buffer penuh. Ini menggunakan ukuran buffer 8192, dan setiap baris panjangnya 5 byte. Karena 5 tidak membagi 8192, batas-batas antara penulisan tidak pada batas garis pada umumnya.

Mari kita lemparkan mereka bersama.

$ { yes aaaa & while true; do echo bbbb; done | grep b & } | head -n 999999 | grep -e ab -e ba
bbaaaa
bbbbaaaa
baaaa
bbbaaaa
bbaaaa
bbbaaaa
ab
bbbbaaa

Seperti yang Anda lihat, ya kadang-kadang terputus grep dan sebaliknya. Hanya sekitar 0,001% dari jalur yang terputus, tetapi itu terjadi. Outputnya diacak sehingga jumlah interupsi akan bervariasi, tetapi saya melihat setidaknya beberapa interupsi setiap waktu. Akan ada fraksi yang lebih tinggi dari garis terputus jika garis lebih panjang, karena kemungkinan gangguan meningkat ketika jumlah garis per buffer berkurang.

Ada beberapa cara untuk menyesuaikan buffering output . Yang utama adalah:

  • Matikan buffering dalam program yang menggunakan perpustakaan stdio tanpa mengubah pengaturan default dengan program yang stdbuf -o0ditemukan di GNU coreutils dan beberapa sistem lain seperti FreeBSD. Anda juga dapat beralih ke buffer line stdbuf -oL.
  • Beralih ke buffering line dengan mengarahkan output program melalui terminal yang dibuat hanya untuk tujuan ini unbuffer. Beberapa program mungkin berperilaku berbeda dengan cara lain, misalnya grepmenggunakan warna secara default jika outputnya adalah terminal.
  • Konfigurasikan program, misalnya dengan meneruskan --line-bufferedke GNU grep.

Mari kita lihat cuplikan di atas lagi, kali ini dengan garis penyangga di kedua sisi.

{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & } | head -n 999999 | grep -e ab -e ba
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb
abbbb

Jadi kali ini ya tidak pernah terputus grep, tapi kadang-kadang grep terputus ya. Saya akan datang ke mengapa nanti.

Interleaving pipa

Selama setiap program menghasilkan satu baris pada satu waktu, dan garis-garisnya cukup pendek, garis-garis output akan dipisahkan dengan rapi. Tapi ada batas berapa lama garis ini bisa bekerja. Pipa itu sendiri memiliki buffer transfer. Ketika suatu program menghasilkan ke suatu pipa, data disalin dari program penulis ke buffer transfer pipa, dan kemudian dari buffer transfer pipa ke program pembaca. (Setidaknya secara konseptual - kernel terkadang dapat mengoptimalkan ini menjadi satu salinan.)

Jika ada lebih banyak data untuk disalin daripada pas di buffer transfer pipa, maka kernel menyalin satu bufferful pada suatu waktu. Jika banyak program menulis ke pipa yang sama, dan program pertama yang dipilih kernel ingin menulis lebih dari satu bufferful, maka tidak ada jaminan bahwa kernel akan memilih program yang sama lagi untuk kedua kalinya. Sebagai contoh, jika P adalah ukuran buffer, fooingin menulis 2 * P byte dan baringin menulis 3 byte, maka satu kemungkinan interleaving adalah P byte dari foo, kemudian 3 byte dari bar, dan P byte dari foo.

Kembali ke contoh yes + grep di atas, pada sistem saya, yes aaaakebetulan menulis baris sebanyak yang dapat ditampung dalam buffer 8192 byte dalam sekali jalan. Karena ada 5 byte untuk ditulis (4 karakter yang dapat dicetak dan baris baru), itu artinya ia menulis 8190 byte setiap waktu. Ukuran buffer pipa adalah 4096 byte. Oleh karena itu dimungkinkan untuk mendapatkan 4.096 byte dari yes, kemudian beberapa output dari grep, dan kemudian sisa penulisan dari yes (8190 - 4096 = 4094 byte). 4096 byte menyisakan ruang untuk 819 baris dengan aaaadan satu-satunya a. Oleh karena itu satu baris dengan satu-satunya ini adiikuti oleh satu tulis dari grep, memberikan satu baris dengan abbbb.

Jika Anda ingin melihat detail dari apa yang terjadi, maka getconf PIPE_BUF .akan memberi tahu Anda ukuran buffer pipa pada sistem Anda, dan Anda dapat melihat daftar lengkap panggilan sistem yang dibuat oleh setiap program dengan

strace -s9999 -f -o line_buffered.strace sh -c '{ stdbuf -oL yes aaaa & while true; do echo bbbb; done | grep --line-buffered b & }' | head -n 999999 | grep -e ab -e ba

Bagaimana menjamin interleaving garis bersih

Jika panjang garis lebih kecil dari ukuran penyangga pipa, maka penyangga garis menjamin bahwa tidak akan ada garis campuran dalam output.

Jika panjang garis bisa lebih besar, tidak ada cara untuk menghindari pencampuran sembarang ketika beberapa program menulis ke pipa yang sama. Untuk memastikan pemisahan, Anda perlu membuat setiap program menulis ke pipa yang berbeda, dan menggunakan program untuk menggabungkan garis. Sebagai contoh, GNU Parallel melakukan ini secara default.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
menarik, jadi apa mungkin cara yang baik untuk memastikan bahwa semua baris ditulis catsecara atom, sehingga proses kucing menerima seluruh baris dari foo / bar / baz tetapi tidak setengah garis dari satu dan setengah garis dari yang lain, dll. Apakah ada yang bisa saya lakukan dengan skrip bash?
Alexander Mills
1
Kedengarannya ini berlaku untuk kasus saya juga di mana saya memiliki ratusan file dan awkdiproduksi dua (atau lebih) jalur output untuk ID yang sama dengan find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' tetapi dengan find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'benar hanya menghasilkan satu baris untuk setiap ID.
αғsнιη
Untuk mencegah interleaving, saya bisa melakukannya dengan dalam pemrograman env seperti Node.js, tetapi dengan bash / shell, tidak yakin bagaimana melakukannya.
Alexander Mills
1
@ JoL Ini karena buffer pipa terisi. Saya tahu saya harus menulis bagian kedua dari cerita ... Selesai.
Gilles 'SANGAT berhenti menjadi jahat'
1
@OlegzandrDenman TLDR menambahkan: mereka melakukan interleave. Alasannya rumit.
Gilles 'SANGAT berhenti menjadi jahat'
1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P telah melihat ini:

GNU xargs mendukung menjalankan banyak pekerjaan secara paralel. -P n dimana n adalah jumlah pekerjaan yang harus dijalankan secara paralel.

seq 100 | xargs -n1 -P10 echo "$a" | grep 5
seq 100 | xargs -n1 -P10 echo "$a" > myoutput.txt

Ini akan berfungsi dengan baik untuk banyak situasi tetapi memiliki kelemahan tipuan: Jika $ a berisi lebih dari ~ 1000 karakter, gema mungkin bukan atomik (ini dapat dibagi menjadi beberapa panggilan tulis () panggilan), dan ada risiko bahwa dua baris akan dicampur.

$ perl -e 'print "a"x2000, "\n"' > foo
$ strace -e write bash -c 'read -r foo < foo; echo "$foo"' >/dev/null
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 1008) = 1008
write(1, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 993) = 993
+++ exited with 0 +++

Jelas masalah yang sama muncul jika ada beberapa panggilan untuk echo atau printf:

slowprint() {
  printf 'Start-%s ' "$1"
  sleep "$1"
  printf '%s-End\n' "$1"
}
export -f slowprint
seq 10 | xargs -n1 -I {} -P4 bash -c "slowprint {}"
# Compare to no parallelization
seq 10 | xargs -n1 -I {} bash -c "slowprint {}"
# Be sure to see the warnings in the next Pitfall!

Output dari pekerjaan paralel dicampur bersama, karena setiap pekerjaan terdiri dari dua (atau lebih) panggilan tulis () terpisah.

Jika Anda membutuhkan output yang tidak dicampur, oleh karena itu disarankan untuk menggunakan alat yang menjamin output akan diserialisasi (seperti GNU Parallel).

Ole Tange
sumber
Bagian itu salah. xargs echotidak memanggil echo bash builtin, tetapi echoutilitas dari $PATH. Lagi pula saya tidak bisa mereproduksi perilaku bash echo dengan bash 4.4. Di Linux, menulis ke sebuah pipa (bukan / dev / null) yang lebih besar dari 4K tidak dijamin atom.
Stéphane Chazelas