Как заставить чтение и запись одного и того же файла в одном и том же конвейере всегда «терпеть неудачу»?

9

Скажем, у меня есть следующий скрипт:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

В ключевой строке я читаю и пишу один и тот же файл, tmpкоторый иногда не удается.

(Я читал, что это из-за условий гонки, потому что процессы в конвейере выполняются параллельно, что я не понимаю, почему - каждому headнужно брать данные из предыдущего, не так ли? Это НЕ мой главный вопрос, но вы можете ответить и на него.)

Когда я запускаю скрипт, он выводит около 200 строк. Есть ли способ заставить этот скрипт выводить всегда 0 строк (поэтому перенаправление ввода / вывода tmpвсегда готовится первым и поэтому данные всегда уничтожаются)? Чтобы было понятно, я имею в виду изменение системных настроек, а не этот скрипт.

Спасибо за ваши идеи.

karlosss
источник

Ответы:

2

Ответ Жиля объясняет состояние гонки. Я просто собираюсь ответить на эту часть:

Есть ли способ заставить этот скрипт выводить всегда 0 строк (поэтому перенаправление ввода / вывода на tmp всегда готовится первым и поэтому данные всегда уничтожаются)? Чтобы было понятно, я имею в виду изменение настроек системы

IDK, если инструмент для этого уже существует, но у меня есть идея, как его можно реализовать. (Но обратите внимание, что это не всегда будет 0 строк, просто полезный тестер, который легко ловит простые гонки, подобные этой, и некоторые более сложные гонки. См. Комментарий @Gilles .) Это не гарантирует, что скрипт безопасен , но может быть полезным инструментом в тестировании, аналогично тестированию многопоточной программы на разных процессорах, включая слабо упорядоченные не x86-процессоры, такие как ARM.

Вы бы запустить его как racechecker bash foo.sh

Используйте тот же системный вызов трассировки / перехват объектов , которые strace -fи ltrace -fиспользовать для крепления к каждому дочернему процессу. (В Linux это тот же ptraceсистемный вызов, который используется GDB и другими отладчиками для установки точек останова, одного шага и изменения памяти / регистров другого процесса.)

Инструмент openи openatсистемные вызовы: когда любой процесс , работающий в рамках этого инструмента делает системный вызов (или ) с , сном, может быть , 1/2 или 1 секунду. Пусть другие системные вызовы (особенно те, в том числе ) выполняются без задержки.open(2)openatO_RDONLYopenO_TRUNC

Это должно позволить автору выиграть гонку почти во всех условиях гонки, если только нагрузка на систему не была высокой или это было сложное состояние гонки, когда усечение не происходило до некоторого другого чтения. Так что случайное изменение того, какие open()s (и, возможно, read()s или записи) задерживаются , увеличит мощность обнаружения этого инструмента, но, конечно, без тестирования в течение бесконечного количества времени с имитатором задержки, который в конечном итоге охватит все возможные ситуации, с которыми вы можете столкнуться в в реальном мире, вы не можете быть уверены, что ваши сценарии свободны от гонок, если вы не прочитаете их внимательно и докажете, что это не так.


Вы, вероятно , нужно, чтобы белый список (не задержка open) для файлов /usr/binи /usr/libпоэтому процесс-старте не принимает навсегда. (Динамическое связывание во время выполнения имеет отношение к open()нескольким файлам (посмотрите strace -eopen /bin/trueили /bin/lsкогда-нибудь), хотя, если родительская оболочка сама выполняет усечение, это будет хорошо. Но для этого инструмента все равно будет хорошо не делать сценарии чрезмерно медленными).

Или, может быть, белый список каждого файла, который вызывающий процесс не имеет разрешения на обрезание. т.е. процесс трассировки может сделать access(2)системный вызов, прежде чем фактически приостановить процесс, который хотел open()файл.


racecheckerСамо по себе должно быть написано на C, а не в оболочке, но, возможно, может использовать straceкод в качестве отправной точки и может не потребовать много работы для реализации.

Вы можете получить ту же функциональность с файловой системой FUSE . Вероятно, есть пример FUSE чистой сквозной файловой системы, так что вы можете добавить в open()функцию проверки , которые переводят ее в спящий режим только для чтения, но позволяют усечению происходить сразу же.

Питер Кордес
источник
Ваша идея о гоночной проверке на самом деле не работает. Во-первых, проблема в том, что тайм-ауты не являются надежными: однажды другой парень займет больше времени, чем вы ожидаете (это классическая проблема со скриптами сборки или тестирования, которые, кажется, работают некоторое время, а затем терпят неудачу в трудных для отладки способах. когда рабочая нагрузка увеличивается и многие вещи работают параллельно). Но помимо этого, к какому открытию вы собираетесь добавить задержку? Чтобы обнаружить что-нибудь интересное, вам нужно будет сделать много прогонов с разными схемами задержки и сравнить их результаты.
Жиль "ТАК - перестань быть злым"
@ Жиль: Да, любая разумно короткая задержка не гарантирует, что усеченный выиграет гонку (как вы указали, на сильно загруженной машине). Идея заключается в том, что вы используете это для проверки своего сценария несколько раз, а не то, что используете racecheckerвсе время. И, возможно, вы захотите, чтобы время сна было открыто для чтения, чтобы его можно было настраивать для людей на очень сильно загруженных машинах, которые хотят установить его выше, например, на 10 секунд. Или установите его ниже, как 0,1 секунды для длительного или неэффективные сценарии , которые повторно открыть файлы лота .
Питер Кордес
@Gilles: Отличная идея о различных шаблонах задержки, которые могут позволить вам поймать больше гонок, чем просто простые вещи в рамках одного конвейера, которые «должны быть очевидны (если вы знаете, как работают оболочки)», как в случае с OP. Но "что открывается?" любой открытый только для чтения, с белым списком или каким-либо другим способом, чтобы не задерживать запуск процесса.
Питер Кордес
Полагаю, вы думаете о более сложных гонках с фоновыми заданиями, которые не усекаются до завершения какого-либо другого процесса? Да, случайные изменения могут понадобиться, чтобы поймать это. Или, может быть, посмотрите на дерево процессов и задержите «раннее» чтение, чтобы попытаться инвертировать обычный порядок. Вы можете сделать инструмент все более и более сложным, чтобы имитировать все больше и больше возможностей переупорядочения, но в какой-то момент вам все равно придется правильно проектировать свои программы, если вы выполняете многозадачность. Автоматическое тестирование может быть полезно для более простых сценариев, где возможные проблемы более ограничены.
Питер Кордес
Это очень похоже на тестирование многопоточного кода, особенно алгоритмов без блокировки: логическое обоснование того, почему он корректен, очень важно, а также тестирование, потому что вы не можете рассчитывать на тестирование на каком-либо конкретном наборе машин для получения всех переупорядочений, которые могут быть проблемой, если вы не закрыли все лазейки. Но так же, как тестирование на слабо упорядоченной архитектуре, такой как ARM или PowerPC, является хорошей идеей на практике, тестирование скрипта в системе, которая искусственно задерживает вещи, может обнажить некоторые расы, так что это лучше, чем ничего. Вы всегда можете ввести ошибки, которые не поймают!
Питер Кордес
18

Почему существует состояние гонки?

Две стороны трубы выполняются параллельно, а не одна за другой. Есть очень простой способ продемонстрировать это: запустить

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

левый процесс выполняет следующее (я только перечислил шаги, которые имеют отношение к моему объяснению):

  1. Запустите внешнюю программу catс аргументом tmp.
  2. Открыто tmpдля чтения.
  3. Пока он не достиг конца файла, прочитайте фрагмент из файла и запишите его в стандартный вывод.

Правый процесс выполняет следующие действия:

  1. Перенаправить стандартный вывод на tmpусечение файла в процессе.
  2. Запустите внешнюю программу headс аргументом -1.
  3. Прочитайте одну строку из стандартного ввода и запишите ее в стандартный вывод.

Единственная точка синхронизации состоит в том, что 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 поймал упрощенный пример в вашем вопросе , но он может не уловить подобную проблему в более сложном скрипте.

Жиль "ТАК - прекрати быть злым"
источник
Моей целью было определить, хорошо ли написан сценарий или нет. Если скрипт мог уничтожить данные таким образом, я просто хотел, чтобы он уничтожал их каждый раз. Нехорошо слышать, что это почти невозможно. Благодаря вам, теперь я знаю, в чем проблема, и постараюсь придумать решение.
karlosss
@karlosss: Хм, мне интересно, могли бы вы использовать ту же самую систему отслеживания / перехвата системных вызовов, что и strace(например, Linux ptrace), чтобы openсистемные вызовы для чтения (во всех дочерних процессах) спали в течение полсекунды, поэтому при гонке с усечение, усечение почти всегда побеждает.
Питер Кордес
@PeterCordes Я новичок в этом, если вы сможете найти способ добиться этого и записать его в качестве ответа, я приму это.
Карлосс
@PeterCordes Вы не можете гарантировать, что усечение победит с задержкой. Это будет работать большую часть времени, но иногда на сильно загруженной машине ваш скрипт будет более или менее загадочным.
Жиль "ТАК - перестань быть злым"
@ Жиль: Давайте обсудим это под моим ответом.
Питер Кордес