Чому існує умова гонки
Дві сторони труби виконані паралельно, не одна за одною. Існує дуже простий спосіб продемонструвати це: запустити
time sleep 1 | sleep 1
Це займає одну секунду, а не дві.
Оболонка запускає два дочірні процеси і чекає, поки вони завершаться. Ці два процеси виконуються паралельно: єдина причина, чому один з них синхронізується з другим, це коли йому потрібно чекати іншого. Найпоширеніша точка синхронізації - це коли права частина блокує очікування для зчитування даних на її стандартному вході і стає розблокованою, коли ліва сторона записує більше даних. Зворотне може трапитися і тоді, коли права рука повільно читає дані, а ліва блокує в процесі запису, поки права частина не зчитує більше даних (в самій трубі є буфер, яким керує ядро, але воно має невеликий максимальний розмір).
Щоб спостерігати за точкою синхронізації, дотримуйтесь наступних команд ( sh -x
друкує кожну команду під час її виконання):
time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'
Грайте з варіаціями, поки вам не сподобається те, що ви спостерігаєте.
Дано складену команду
cat tmp | head -1 > tmp
лівий процес виконує наступні дії (я перераховував лише кроки, які стосуються мого пояснення):
- Виконати зовнішню програму
cat
з аргументом tmp
.
- Відкрити
tmp
для читання.
- Поки він не дійшов до кінця файлу, прочитайте фрагмент з файлу та запишіть його на стандартний вихід.
Правий процес робить наступне:
- Перенаправити стандартний висновок на
tmp
, обрізання файлу в процесі.
- Виконати зовнішню програму
head
з аргументом -1
.
- Прочитайте один рядок зі стандартного вводу та запишіть його на стандартний вихід.
Єдиний момент синхронізації полягає в тому, що правий-3 чекає, поки лівий-3 обробить один повний рядок. Немає синхронізації між лівою-2 та правою-1, тому вони можуть відбуватися в будь-якому порядку. Який порядок вони відбуваються, не передбачувано: це залежить від архітектури процесора, оболонки, ядра, від того, які ядра мають бути заплановані, від того, що перериває процесор, отриманий за цей час тощо.
Як змінити поведінку
Ви не можете змінити поведінку, змінивши системне налаштування. Комп’ютер робить те, що ви йому наказали робити. Ви сказали йому, щоб усікати tmp
і читати з tmp
паралельно, так це робить дві речі паралельно.
Гаразд, є одне «системне налаштування», яке ви можете змінити: ви можете замінити /bin/bash
іншою програмою, яка не є баш. Я сподіваюся, що це само собою зрозуміло, що це не дуже гарна ідея.
Якщо ви хочете, щоб усікання відбулося перед лівою частиною труби, вам потрібно поставити її поза трубопроводу, наприклад:
{ cat tmp | head -1; } >tmp
або
( exec >tmp; cat tmp | head -1 )
Я не маю поняття, чому ви хочете цього хотіти. Який сенс читати з файлу, який ви знаєте порожнім?
І навпаки, якщо ви хочете, щоб перенаправлення виводу (включаючи усікання) відбулося після cat
закінчення читання, вам потрібно або повністю забуферувати дані в пам'яті, наприклад
line=$(cat tmp | head -1)
printf %s "$line" >tmp
або запишіть у інший файл, а потім перемістіть його на місце. Зазвичай це надійний спосіб робити речі за сценаріями і має перевагу в тому, що файл пишеться повністю, перш ніж він буде видно через оригінальне ім'я.
cat tmp | head -1 >new && mv new tmp
Колекція moreutils включає в себе програму, яка робить саме це, що називається sponge
.
cat tmp | head -1 | sponge tmp
Як визначити проблему автоматично
Якщо вашою метою було взяти неправильно написані сценарії та автоматично визначити, де вони ламаються, то, вибачте, життя не так просто. Аналіз виконання не може надійно знайти проблему, оскільки іноді cat
закінчується читання, перш ніж відбудеться усічення. Статичний аналіз в принципі може це зробити; спрощений приклад у вашому запитанні потрапляє в Shellcheck , але він може не сприймати подібну проблему в більш складному сценарії.