Що заважає stdout / stderr перемежуватися?


14

Скажіть, я запускаю деякі процеси:

#!/usr/bin/env bash

foo &
bar &
baz &

wait;

Я запускаю описаний вище сценарій так:

foobarbaz | cat

наскільки я можу сказати, коли будь-який з процесів пише в stdout / stderr, їх вихід ніколи не перемежовується - кожен рядок stdio здається атомним. Як це працює? Яка утиліта керує тим, як кожен рядок є атомним?


3
Скільки даних видає ваші команди? Спробуйте зробити їх на кілька кілобайт.
Кусалаланда

Ви маєте на увазі, де одна з команд виводить кілька кб перед новим рядком?
Олександр Міллс

Ні, щось подібне: unix.stackexchange.com/a/452762/70524
muru

Відповіді:


23

Вони перемежовуються! Ви спробували лише короткі вибутові вибухи, які залишаються нерозбитими, але на практиці важко гарантувати, що будь-який конкретний вихід залишається нерозбитим.

Вихідний буфер

Це залежить від того, як програми буферують свій вихід. Бібліотека stdio, яку більшість програм використовує під час написання, використовує буфери, щоб зробити результат більш ефективним. Замість того, щоб виводити дані, як тільки програма викликає функцію бібліотеки для запису у файл, функція зберігає ці дані в буфері і виводить їх лише після того, як буфер заповниться. Це означає, що вихід проводиться партіями. Точніше, є три вихідні режими:

  • Небуферовані: дані записуються негайно, без використання буфера. Це може бути повільним, якщо програма записує свій вихід невеликими фрагментами, наприклад, символ за символом. Це стандартний режим для стандартної помилки.
  • Повністю буферизовані: дані записуються лише тоді, коли буфер заповнений. Це режим за замовчуванням під час запису в трубу або звичайний файл, за винятком stderr.
  • Буферні рядки: дані записуються після кожного нового рядка або коли буфер заповнений. Це режим за замовчуванням під час запису в термінал, за винятком stderr.

Програми можуть перепрограмувати кожен файл, щоб він поводився інакше, і може явно промивати буфер. Буфер автоматично змивається, коли програма закриває файл або нормально завершує роботу.

Якщо всі програми, які записують в один і той же канал, або використовують режим буферизованого рядка, або використовують режим незаблокованого і записують кожен рядок одним викликом у вихідну функцію, і якщо рядки досить короткі для запису в один відрізок, то вихід буде переплетенням цілих ліній. Але якщо одна з програм використовує повний буферний режим або якщо лінії занадто довгі, то ви побачите змішані лінії.

Ось приклад, коли я переплітаю вихід з двох програм. Я використовував GNU coreutils в Linux; різні версії цих утиліт можуть поводитися по-різному.

  • yes aaaaпише aaaaназавжди в тому, що по суті еквівалентно режиму буферизації рядків. yesУтиліта на насправді пише кілька рядків у той час, але кожен раз , коли він випускає вихід, вихід є цілим числом рядків.
  • echo bbbb; done | grep bпише bbbbназавжди в повному буферному режимі. Він використовує розмір буфера 8192, а кожен рядок - 5 байт. Оскільки 5 не ділить 8192, межі між записами взагалі не знаходяться на межі лінії.

Давайте розставимо їх разом.

$ { 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

Як бачите, так, інколи переривається греп і навпаки. Перервано лише близько 0,001% ліній, але це сталося. Вихід рандомізований, тому кількість перерв буде змінюватися, але я щоразу бачив принаймні кілька перерв. Була б більша частка перерваних ліній, якби лінії були довші, оскільки ймовірність переривання збільшується зі зменшенням кількості рядків на буфер.

Існує кілька способів налаштування вихідної буферизації . Основні з них:

  • Вимкніть буферизацію в програмах, які використовують бібліотеку stdio, не змінюючи її налаштувань за замовчуванням за допомогою програми, stdbuf -o0знайденої в GNU coreutils та деяких інших системах, таких як FreeBSD. Можна також переключитися на буферизацію ліній за допомогою stdbuf -oL.
  • Перейдіть на буферизацію ліній, направляючи вихід програми через термінал, створений саме для цього unbuffer. Деякі програми можуть поводитися по-різному по-іншому, наприклад, grepвикористовують кольори за замовчуванням, якщо його вихід є терміналом.
  • Налаштуйте програму, наприклад, перейшовши --line-bufferedна GNU grep.

Давайте ще раз побачимо фрагмент, на цей раз з буферизацією ліній з обох сторін.

{ 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

Тож цей раз так ніколи не переривав греп, але греп іноді переривав так. Я прийду до чого пізніше.

Переплетення труб

Поки кожна програма виводить по одному рядку за один раз, а рядки є досить короткими, вихідні лінії будуть акуратно розділені. Але існує обмеження того, як довго можуть тривати лінії для цього. Сама труба має буфер передачі. Коли програма виводить на трубу, дані копіюються з програми запису в буфер передачі труби, а потім пізніше з буфера передачі труби в програму зчитування. (Принаймні концептуально - ядро ​​може іноді оптимізувати це до однієї копії.)

Якщо в буфері передачі труби є більше даних для копіювання, то ядро ​​копіює один буфер одночасно. Якщо кілька програм записують на одну і ту ж трубку, а перша програма, яку вибирає ядро, хоче написати більше одного буфера, то немає гарантії, що ядро ​​вибере ту саму програму ще раз. Наприклад, якщо P - розмір буфера, fooхоче записати 2 * P байтів і barхоче записати 3 байти, то одне можливе переплетення - P байти від foo, потім 3 байти від barі P байти від foo.

Повертаючись до прикладу так + grep вище, в моїй системі yes aaaaтрапляється записати стільки рядків, скільки може вміститися в 8192-байтний буфер за один раз. Оскільки для запису є 5 байтів (4 символи для друку та новий рядок), це означає, що вона пише щодня 8190 байт. Розмір буфера труб становить 4096 байт. Тому можна отримати 4096 байт з так, потім деякий вихід з grep, а потім решта запису з так (8190 - 4096 = 4094 байт). 4096 байт залишає місце для 819 рядків з aaaaі одиноким a. Звідси рядок із цією одинокою, aза якою слідує одне записування з grep, даючи рядок із abbbb.

Якщо ви хочете побачити деталі того, що відбувається, то getconf PIPE_BUF .вам повідомлять розмір буфера труби у вашій системі, і ви можете побачити повний перелік системних викликів, здійснених кожною програмою з

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

Як гарантувати чітке переплетення ліній

Якщо довжини лінії менші за розмір буфера труб, то буферизація рядків гарантує, що у виході не буде жодної змішаної лінії.

Якщо довжина рядків може бути більшою, немає ніякого способу уникнути довільного змішування, коли кілька програм записують на одну трубу. Щоб забезпечити розділення, потрібно змусити кожну програму записувати в іншу трубку і використовувати програму для об'єднання ліній. Наприклад, GNU Parallel робить це за замовчуванням.


цікаво, так що може бути хорошим способом забезпечити, щоб усі рядки були записані catатомно, таким чином, щоб процес кішки отримував цілі рядки або з foo / bar / baz, але не з половини рядка з одного і з половини рядка з іншого тощо. Чи можна щось зробити зі скриптом bash?
Олександр Міллс

1
Звучить, це стосується і мого випадку, коли у мене було сотні файлів, і awkбуло створено дві (або більше) лінії виводу для одного і того ж ідентифікатора, find -type f -name 'myfiles*' -print0 | xargs -0 awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }' але за допомогою find -type f -name 'myfiles*' -print0 | xargs -0 cat| awk '{ seen[$1]= seen[$1] $2} END { for(x in seen) print x, seen[x] }'нього правильно було створено лише одну лінію для кожного ідентифікатора.
αғsnιη

Щоб запобігти будь-якому переплетенню, я можу це зробити з програмуванням env, як Node.js, але з bash / shell, не знаю, як це зробити.
Олександр Міллс

1
@JoL Це пов’язано з заповненням буфера труб. Я знав, що мені доведеться написати другу частину історії ... Готово.
Жил "ТАК - перестань бути злим"

1
@OlegzandrDenman TLDR додав: вони перемежовуються. Причина складна.
перестань бути злим"

1

http://mywiki.wooledge.org/BashPitfalls#Non-atomic_writes_with_xargs_-P вивчив це:

GNU xargs підтримує паралельне виконання декількох завдань. -P n, де n - кількість завдань, які потрібно виконати паралельно.

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

Це буде добре працювати в багатьох ситуаціях, але має оманливий недолік: Якщо $ a містить більше ~ 1000 символів, ехо може бути не атомним (воно може бути розділене на кілька викликів write ()), і є ризик, що два рядки буде змішаним.

$ 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 +++

Очевидно те саме питання виникає, якщо є кілька дзвінків на ехо або 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!

Виходи з паралельних завдань змішуються разом, оскільки кожне завдання складається з двох (або більше) окремих викликів write ().

Якщо вам потрібні результати не змішані, рекомендується використовувати інструмент, який гарантує, що вихід буде серіалізований (наприклад, паралельний GNU).


Цей розділ неправильний. xargs echoне викликає ехо-баш вбудований, а echoутиліту від $PATH. І все одно я не можу відтворити таку поведінку ехо-ехо з баш 4.4. У Linux записи на трубу (не / dev / null), що перевищує 4K, не гарантують, що вони будуть атомними.
Стефан Шазелас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.