Вони перемежовуються! Ви спробували лише короткі вибутові вибухи, які залишаються нерозбитими, але на практиці важко гарантувати, що будь-який конкретний вихід залишається нерозбитим.
Вихідний буфер
Це залежить від того, як програми буферують свій вихід. Бібліотека 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 робить це за замовчуванням.