Як цей скрипт забезпечує те, що працює лише один екземпляр?


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)?

Яким був ваш тестовий процес, коли ви намагалися експеримент перенаправити з іншого файлу?
Фрайхейт

1
Я думаю, ви можете посилатися на це посилання. stackoverflow.com/questions/185451 / ...
Деб Paikar

Відповіді:


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

1
Використання { }замість ( )також буде спрацьовувати і уникати передплати.
Р ..

Далі в коментарях до публікації G + хтось там також запропонував приблизно той самий метод, використовуючи exec.
David Z

@R .., о, звичайно. Але це все ще некрасиво з додатковими дужками навколо фактичного сценарію.
ilkkachu

9

Пасмо файл додається до файлу, через опис файлу . На високому рівні послідовність операцій в одному екземплярі сценарію:

  1. Відкрийте файл, до якого додається замок ("файл блокування").
  2. Візьміть замок у файлі блокування.
  3. Робіть речі.
  4. Закрийте файл блокування. Це звільняє замок, який додається до опису файлу, створеного відкриттям файлу.

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

Відкриття файлу створює опис файлу . Це об’єкт ядра, який не має великої прямої видимості в інтерфейсах програмування. Ви отримуєте доступ до опису файлу опосередковано через дескриптори файлів, але зазвичай ви вважаєте, що це доступ до файлу (читання або запис його вмісту чи метаданих). Блокування - це один з атрибутів, які є властивістю опису файлу, а не файлу чи дескриптора.

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

Ось як послідовність операцій вище впливає на опис файлу.

  1. Перенаправлення <$0відкриває файл скрипту в підрозділі, створюючи опис файлу. На цьому етапі є один дескриптор файлу, приєднаний до опису: дескриптор номер 0 в нижній частині.
  2. Підрозділ викликає flockі чекає його виходу. Поки flock працює, до опису додаються два дескриптори: число 0 в нижній частині корпусу і число 0 в процесі flock. Коли 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
@Mark Існує гонка на замок, але це не умова гонки. Стан гонки , коли вибір часу може дозволити що - щось погане має статися, наприклад, два процеси знаходяться в одній і тій же критичної секції одночасно. Не знаючи, який процес увійде в критичну секцію, очікується недетермінізм, це не умова гонки.
Жил "ТАК - перестань бути злим"

1
Щойно FYI, посилання в "описі файлів" вказує на індексну сторінку специфікацій 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 &
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.