Почему существует состояние гонки?
Две стороны трубы выполняются параллельно, а не одна за другой. Есть очень простой способ продемонстрировать это: запустить
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
.
- Прочитайте одну строку из стандартного ввода и запишите ее в стандартный вывод.
Единственная точка синхронизации состоит в том, что right-3 ожидает, пока left-3 обработает одну полную строку. Синхронизация между left-2 и right-1 отсутствует, поэтому они могут происходить в любом порядке. В каком порядке они происходят, непредсказуемо: это зависит от архитектуры ЦП, от оболочки, от ядра, на каких ядрах планируются процессы, какие прерывания получает ЦП в это время и т. Д.
Как изменить поведение
Вы не можете изменить поведение, изменив настройки системы. Компьютер делает то, что вы говорите. Вы сказали ему обрезать tmp
и читать tmp
параллельно, поэтому он выполняет две вещи параллельно.
Хорошо, есть один «системный параметр», который вы можете изменить: вы можете заменить /bin/bash
его другой программой, отличной от 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 поймал упрощенный пример в вашем вопросе , но он может не уловить подобную проблему в более сложном скрипте.