Как этот скрипт гарантирует, что работает только один его экземпляр?

22

19 августа 2013 года Рэндал Л. Шварц опубликовал этот сценарий оболочки, который должен был гарантировать, что в Linux «работает только один экземпляр []], без условий гонки или необходимости очистки файлов блокировки»:

#!/bin/sh
# randal_l_schwartz_001.sh
(
    if ! flock -n -x 0
    then
        echo "$$ cannot get flock"
        exit 0
    fi
    echo "$$ start"
    sleep 10 # for testing.  put the real task here
    echo "$$ end"
) < $0

Кажется, работает как рекламируется:

$ ./randal_l_schwartz_001.sh & ./randal_l_schwartz_001.sh
[1] 11863
11863 start
11864 cannot get flock
$ 11863 end

[1]+  Done                    ./randal_l_schwartz_001.sh
$

Вот что я понимаю:

  • Сценарий перенаправляет ( <) копию своего собственного содержимого (т. $0Е. Из ) в STDIN (т. Е. Файловый дескриптор 0) подоболочки.
  • Внутри подоболочки сценарий пытается получить неблокирующую монопольную блокировку ( flock -n -x) для файлового дескриптора 0.
    • Если эта попытка не удалась, подоболочка завершается (как и основной сценарий, так как ему больше нечего делать).
    • Если попытка вместо этого удалась, подоболочка запускает нужную задачу.

Вот мои вопросы:

  • Почему сценарий должен перенаправлять в файловый дескриптор, унаследованный подоболочкой, копию своего собственного содержимого, а не, скажем, содержимого какого-либо другого файла? (Я попытался перенаправить из другого файла и запустить снова, как описано выше, и порядок выполнения изменился: неосновная задача получила блокировку до фоновой. Поэтому, возможно, использование собственного содержимого файла позволяет избежать условий гонки; но как?)
  • Зачем скрипту нужно перенаправить в дескриптор файла, унаследованный подоболочкой, копию содержимого файла?
  • Почему удержание монопольной блокировки дескриптора файла 0в одной оболочке не позволяет копии того же скрипта, работающей в другой оболочке, получить монопольную блокировку дескриптора файла 0? Не снаряды имеют свои собственные, отдельные копии стандартных файловых дескрипторов ( 0, 1и 2, т.е. STDIN, STDOUT и STDERR)?
sampablokuper
источник
Каков был ваш точный процесс тестирования, когда вы пытались экспериментом перенаправить из другого файла?
Freiheit
1
Я думаю, что вы можете сослаться на эту ссылку. stackoverflow.com/questions/185451/…
Деб Пайкар

Ответы:

22

Почему сценарий должен перенаправлять в файловый дескриптор, унаследованный подоболочкой, копию своего собственного содержимого, а не, скажем, содержимого какого-либо другого файла?

Вы можете использовать любой файл, если все копии скрипта используют один и тот же. Использование $0просто привязывает блокировку к самому сценарию: если вы копируете сценарий и модифицируете его для какого-либо другого использования, вам не нужно придумывать новое имя для файла блокировки. Это удобно

Если скрипт вызывается с помощью символической ссылки, блокировка выполняется для самого файла, а не для ссылки.

(Конечно, если какой-то процесс запускает сценарий и присваивает ему в качестве нулевого аргумента выдуманное значение вместо фактического пути, то это нарушается. Но это редко делается.)

(Я попытался использовать другой файл и перезапустить, как указано выше, и порядок выполнения изменился)

Вы уверены, что это из-за используемого файла, а не просто случайного отклонения? Как и в случае с конвейером, на самом деле нет способа быть уверенным в том, в каком порядке запускаются команды cmd1 & cmd. В основном это зависит от планировщика ОС. Я получаю случайные изменения в моей системе.

Зачем скрипту нужно перенаправить в дескриптор файла, унаследованный подоболочкой, копию содержимого файла?

Похоже, что это так, что сама оболочка содержит копию описания файла, содержащего блокировку, а не просто flockутилиту, удерживающую ее. Блокировка, созданная с помощью flock(2), снимается, когда дескрипторы файлов, имеющие ее, закрываются.

flockимеет два режима: либо для блокировки на основе имени файла, и для запуска внешней команды (в этом случае flockсодержится требуемый дескриптор открытого файла), либо для извлечения дескриптора файла извне, поэтому внешний процесс отвечает за удержание Это.

Обратите внимание, что содержимое файла здесь не имеет значения, и копии не сделаны. Перенаправление на подоболочку не копирует никаких данных в себя, оно просто открывает дескриптор файла.

Почему удержание монопольной блокировки дескриптора файла 0 в одной оболочке не позволяет копии того же скрипта, работающей в другой оболочке, получить монопольную блокировку дескриптора файла 0? Разве оболочки не имеют своих собственных отдельных копий стандартных файловых дескрипторов (0, 1 и 2, то есть STDIN, STDOUT и STDERR)?

Да, но блокировка файла , а не дескриптор файла. Только один открытый экземпляр файла может одновременно удерживать блокировку.


Я думаю, что вы должны быть в состоянии сделать то же самое без вложенной оболочки, используя, execчтобы открыть дескриптор файла блокировки:

$ cat lock.sh
#!/bin/sh

exec 9< "$0"

if ! flock -n -x 9; then
    echo "$$/$1 cannot get flock" 
    exit 0
fi

echo "$$/$1 got the lock"
sleep 2
echo "$$/$1 exit"

$ ./lock.sh bg & ./lock.sh fg ; wait; echo
[1] 11362
11363/fg got the lock
11362/bg cannot get flock
11363/fg exit
[1]+  Done                    ./lock.sh bg
ilkkachu
источник
1
Использование { }вместо ( )будет также работать и избежать подоболочки.
Р.
Далее в комментариях к посту G + кто-то там также предложил примерно тот же метод, используя exec.
Дэвид З
@R .. о, конечно. Но это все еще безобразно с дополнительными скобками вокруг фактического сценария.
ilkkachu
9

Блокировка файла прикрепляется к файлу через описание файла . На высоком уровне последовательность операций в одном экземпляре скрипта:

  1. Откройте файл, к которому прикреплена блокировка («файл блокировки»).
  2. Взять блокировку на файл блокировки.
  3. Делай вещи.
  4. Закройте файл блокировки. Это снимает блокировку, которая прикреплена к описанию файла, созданному путем открытия файла.

Удержание блокировки не позволяет запустить еще одну копию того же сценария, потому что это то, что делают блокировки. Пока в системе существует исключительная блокировка файла, невозможно создать второй экземпляр той же блокировки, даже с помощью другого описания файла.

Открытие файла создает описание файла . Это объект ядра, который не имеет прямой видимости в интерфейсах программирования. Вы обращаетесь к описанию файла косвенно через файловые дескрипторы, но обычно вы думаете о нем как о доступе к файлу (чтение или запись его содержимого или метаданных). Блокировка - это один из атрибутов, которые являются свойством описания файла, а не файла или дескриптора.

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

Вот как последовательность операций выше влияет на описание файла.

  1. Перенаправление <$0открывает файл сценария в подоболочке, создавая описание файла. На данный момент к описанию прикреплен единственный дескриптор файла: дескриптор номер 0 в подоболочке.
  2. Subshell вызывает flockи ждет его выхода. Во время работы flock к описанию прикреплены два дескриптора: номер 0 в подоболочке и номер 0 в процессе стека. Когда flock берет блокировку, это устанавливает свойство описания файла. Если другое описание файла уже имеет блокировку файла, flock не может взять блокировку, так как это эксклюзивная блокировка.
  3. Раковина делает вещи. Поскольку в описании по-прежнему имеется дескриптор открытого файла с описанием с блокировкой, это описание сохраняется и сохраняет свою блокировку, поскольку никто никогда не снимает блокировку.
  4. Подоболочка умирает в закрывающей скобке. Это закрывает последний дескриптор файла в описании файла, который имеет блокировку, поэтому блокировка исчезает в этот момент.

Причина, по которой сценарий использует перенаправление, $0заключается в том, что перенаправление является единственным способом открыть файл в оболочке, а сохранение активного перенаправления является единственным способом сохранить дескриптор файла открытым. Подоболочка никогда не читает со своего стандартного ввода, она просто должна оставаться открытой. На языке, который дает прямой доступ к открытию и закрытию вызова, вы можете использовать

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)

На самом деле вы можете получить ту же последовательность операций в оболочке, если вы делаете перенаправление с помощью execвстроенной функции.

exec <$0
flock -n -x 0
# do stuff
exec <&-

Сценарий может использовать другой файловый дескриптор, если он хочет сохранить доступ к исходному стандартному вводу.

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-

или с ракушкой:

(
  flock -n -x 3
  # do stuff
) 3<$0

Блокировка не должна быть в файле сценария. Это может быть любой файл, который может быть открыт для чтения (поэтому он должен существовать, это должен быть тип файла, который можно прочитать, например обычный файл или именованный канал, но не каталог, и процесс сценария должен иметь разрешение на его чтение). Преимущество файла сценария в том, что он гарантированно присутствует и доступен для чтения (за исключением крайнего случая, когда он был удален извне между моментом вызова сценария и моментом, когда сценарий достигает <$0перенаправления).

Пока это flockудается, а сценарий находится в файловой системе, где блокировки не содержат ошибок (некоторые сетевые файловые системы, такие как NFS, могут содержать ошибки), я не вижу, как использование другого файла блокировки может привести к состоянию гонки. Я подозреваю ошибку манипуляции с вашей стороны.

Жиль "ТАК - перестань быть злым"
источник
Существует условие гонки: вы не можете контролировать, какой экземпляр скрипта получает блокировку. К счастью, практически для всех целей это не имеет значения.
Марк
4
@ Марк Есть гонка к замку, но это не условие гонки. Состояние гонки , когда выбор времени может позволить что - то плохое должно произойти, например, два процесса находятся в одной и той же критической секции одновременно. Не зная, какой процесс войдет в критическую секцию, ожидается недетерминизм, это не условие гонки.
Жиль "ТАК - перестань быть злым"
1
Просто к сведению, ссылка в «описании файла» указывает на индексную страницу спецификаций Open Group, а не на конкретное описание концепции, что, как я думаю, вы намеревались сделать. Или вы также можете связать свой старый ответ здесь также unix.stackexchange.com/a/195164/85039
Сергей Колодяжный
5

Файл, используемый для блокировки, не важен, скрипт использует, $0потому что это файл, который, как известно, существует.

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

Вы можете использовать любой файловый дескриптор, не обязательно 0. Блокировка удерживается для файла, открытого для файлового дескриптора, а не для самого дескриптора.

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &
Кусалананда
источник