Vyhnite sa týmto problémom obmedzením spúšťania Bash skriptov naraz


Kľúčové poznatky

  • Uistite sa, že je spustená iba jedna inštancia vášho skriptu pomocou pgrep, lsof alebo flock, aby ste predišli problémom so súbežnosťou.
  • Jednoducho implementujte kontroly na samočinné ukončenie skriptu, ak sa zistia iné spustené inštancie.
  • Využitím príkazov exec a env môže príkaz flock toto všetko dosiahnuť pomocou jedného riadku kódu.

Niektoré skripty pre Linux majú takúto réžiu vykonávania, je potrebné zabrániť spusteniu niekoľkých inštancií naraz. Našťastie existuje niekoľko spôsobov, ako to dosiahnuť vo svojich vlastných skriptoch Bash.

Niekedy Raz stačí

Niektoré skripty by sa nemali spúšťať, ak je stále spustená predchádzajúca inštancia tohto skriptu. Ak váš skript spotrebováva nadmerný čas CPU a RAM alebo generuje veľkú šírku pásma siete alebo drvenie disku, je rozumné obmedziť jeho vykonávanie na jednu inštanciu naraz.

Nie sú to však len zdroje, ktoré musia bežať izolovane. Ak váš skript upravuje súbory, môže dôjsť ku konfliktu medzi dvoma (alebo viacerými) inštanciami skriptu, pretože bojujú o prístup k súborom. Aktualizácie sa môžu stratiť alebo súbor môže byť poškodený.

Jednou z techník, ako sa vyhnúť týmto problémovým scenárom, je nechať skript skontrolovať, či nie sú spustené žiadne iné jeho verzie. Ak zistí akékoľvek ďalšie spustené kópie, skript sa sám ukončí.

Ďalšou technikou je vytvoriť skript takým spôsobom, že sa pri spustení uzamkne, čím sa zabráni spusteniu akýchkoľvek iných kópií.

Pozrieme sa na dva príklady prvej techniky a potom sa pozrieme na jeden spôsob, ako urobiť druhú.

Použitie pgrep na zabránenie súbežnosti

Príkaz pgrep prehľadáva procesy, ktoré sú spustené na počítači so systémom Linux, a vracia ID procesu procesov, ktoré zodpovedajú vzoru vyhľadávania.

Mám skript s názvom loop.sh. Obsahuje cyklus for, ktorý vypíše opakovanie cyklu a potom sa na sekundu uspí. Robí to desaťkrát.

#!/bin/bash
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Nastavil som dve spustené inštancie a potom som použil pgrep na vyhľadávanie podľa názvu.

pgrep loop.sh

Vyhľadá dve inštancie a nahlási ich ID procesov. Môžeme pridať voľbu -c (count), aby pgrep vrátil počet inštancií.

pregp -c loop.sh

Tento počet inštancií môžeme použiť v našom skripte. Ak je hodnota vrátená pgrep väčšia ako jedna, musí byť spustených viac ako jedna inštancia a náš skript sa zatvorí.

Vytvoríme skript, ktorý používa túto techniku. Nazvime to pgrep-solo.sh.

Porovnanie if testuje, či je číslo vrátené pgrep väčšie ako jedna. Ak áno, skript sa ukončí.

# count the instances of this script 
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi

Ak je číslo vrátené pgrep jedna, skript môže pokračovať. Tu je úplný skript.

#!/bin/bash
echo "Starting."
# count the instances of this script 
if [ $(pgrep -c pgrep-solo.sh) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Skopírujte si to do svojho obľúbeného editora a uložte ako pgrep-solo.sh. Potom ho urobte spustiteľným pomocou chmod.

chmod +x pgrep-loop.sh

Keď to beží, vyzerá to takto.

./pgrep-solo.sh

Ale ak sa ho pokúsim spustiť s inou kópiou, ktorá už beží v inom okne terminálu, zistí to a ukončí sa.

./pgrep-solo.sh

Použitie lsof na zabránenie súbežnosti

Veľmi podobnú vec môžeme urobiť s príkazom lsof.

Ak pridáme voľbu -t (stručné), lsof vypíše ID procesov.

lsof -t loop.sh

Výstup z lsof môžeme priviesť do wc. Voľba -l (riadky) počíta počet riadkov, ktorý je v tomto scenári rovnaký ako počet ID procesov.

lsof -t loop.sh | wc -l

Môžeme to použiť ako základ testu v porovnaní if v našom skripte.

Uložte túto verziu ako lsof-solo.sh.

#!/bin/bash
echo "Starting."
# count the instances of this script 
if [ $(lsof -t "$0" | wc -l) -gt 1 ]; then
 echo "Another instance of $0 is running. Stopping."
 exit 1
fi
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Použite chmod, aby bol spustiteľný.

chmod +x lsof-solo.sh

Teraz, keď je skript lsof-solo.sh spustený v inom okne terminálu, nemôžeme spustiť druhú kópiu.

./lsof-solo.sh

Metóda pgrep vyžaduje iba jedno volanie externého programu (pgrep), metóda lsof vyžaduje dve (lsof a wc). Ale výhodou metódy lsof oproti metóde pgrep je, že v porovnaní if môžete použiť premennú $0. Toto obsahuje názov skriptu.

Znamená to, že skript môžete premenovať a stále bude fungovať. Nemusíte si pamätať, že treba upraviť porovnávací riadok if a vložiť nový názov skriptu. Premenná $0 obsahuje znak „./“ na začiatku názvu skriptu (napríklad ./lsof-solo.sh) a pgrep sa to nepáči.

Použitie kŕdľa na zabránenie súbežnosti

Naša tretia technika používa príkaz flock, ktorý je určený na nastavenie zámkov súborov a adresárov v rámci skriptov. Kým je uzamknutý, žiadny iný proces nemôže pristupovať k uzamknutému zdroju.

Táto metóda vyžaduje pridanie jedného riadku do hornej časti skriptu.

[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :

Čoskoro tieto hieroglyfy dekódujeme. Zatiaľ si len overme, či to funguje. Uložte si to ako flock-solo.sh.

#!/bin/bash
[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
echo "Starting."
# we're cleared for take off
for (( i=1; i<=10; i+=1 ))
do
 echo "Loop:" $i
 sleep 1
done
exit 0

Samozrejme, musíme ho urobiť spustiteľným.

chmod +x flock-solo.sh

Spustil som skript v jednom terminálovom okne a potom som sa ho pokúsil spustiť znova v inom terminálovom okne.

./flock-solo
./flock-solo
./flock-solo

Nemôžem spustiť skript, kým sa nedokončí inštancia v druhom okne terminálu.

Poďme si vybrať líniu, ktorá robí kúzlo. Jadrom je príkaz stáda.

flock -en "$0" "$0" "$@"

Príkaz flock sa používa na uzamknutie súboru alebo adresára a potom na spustenie príkazu. Možnosti, ktoré používame, sú -e (exkluzívne) a -n (neblokujúce).

Exkluzívna možnosť znamená, že ak sa nám podarí súbor uzamknúť, nikto iný k nemu nebude mať prístup. Možnosť neblokovania znamená, že ak sa nám nepodarí získať zámok, okamžite prestaneme skúšať. Chvíľu to neskúšame, hneď sa elegantne ukloníme.

Prvých $0 označuje súbor, ktorý chceme uzamknúť. Táto premenná obsahuje názov aktuálneho skriptu.

Druhý $0 je príkaz, ktorý chceme spustiť, ak sa nám podarí získať zámok. Opäť prechádzame v mene tohto skriptu. Pretože zámok uzamkne všetkých okrem od nás, môžeme spustiť súbor skriptu.

Parametre môžeme odovzdať príkazu, ktorý sa spustí. Používame $@ na odovzdanie akýchkoľvek parametrov príkazového riadka, ktoré boli odovzdané tomuto skriptu, novému vyvolaniu skriptu, ktorý bude spustený kŕdeľ.

Takže máme tento skript, ktorý uzamkne súbor skriptu a potom spustí ďalšiu inštanciu. To je takmer to, čo chceme, ale je tu problém. Po dokončení druhej inštancie bude spracovanie prvého skriptu pokračovať. Ako však uvidíte, máme v rukáve ďalší trik, ako sa o to postarať.

Používame premennú prostredia, ktorú nazývame GEEKLOCK, aby sme naznačili, či spustený skript musí použiť zámok alebo nie. Ak sa skript spustil a nie je zamknutý, musíte ho použiť. Ak bol skript spustený a je zamknutý, nemusí robiť nič, môže sa len spustiť. So spusteným skriptom a uzamknutím nie je možné spustiť žiadne ďalšie inštancie skriptu.

[ "${GEEKLOCK}" != "$0" ] 

Tento test sa prekladá ako ‚vrátiť hodnotu true, ak premenná prostredia GEEKLOCK nie je nastavená na názov skriptu.‘ Test je spojený so zvyškom príkazu pomocou && (a) a || (alebo). Časť && sa vykoná, ak test vráti hodnotu true a znak || sekcia sa vykoná, ak test vráti hodnotu false.

env GEEKLOCK="$0"

Príkaz env sa používa na spustenie iných príkazov v upravených prostrediach. Naše prostredie upravujeme vytvorením premennej prostredia GEEKLOCK a jej nastavením na názov skriptu. Príkaz, ktorý sa spustí env, je príkaz flock a príkaz flock spustí druhú inštanciu skriptu.

Druhá inštancia skriptu vykoná kontrolu, aby zistila, či premenná prostredia GEEKLOCK neexistuje, ale zistí, že áno. || vykoná sa sekcia príkazu, ktorá neobsahuje nič okrem dvojbodky „:“, čo je vlastne príkaz, ktorý nič nerobí. Cesta vykonávania potom prebieha cez zvyšok skriptu.

Stále však máme problém s tým, že prvý skript pokračuje vo svojom vlastnom spracovaní, keď sa druhý skript skončí. Riešením je príkaz exec. Toto spustí ďalšie príkazy nahradením volajúceho procesu novo spusteným procesom.

exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" 

Takže celá postupnosť je:

  • Skript sa spustí a nemôže nájsť premennú prostredia. Klauzula && sa vykoná.
  • exec spustí env a nahradí pôvodný proces skriptu novým procesom env.
  • Proces env vytvorí premennú prostredia a spustí flock.
  • flock uzamkne súbor skriptu a spustí novú inštanciu skriptu, ktorá zistí premennú prostredia, spustí || klauzulu a scenár je schopný dotiahnuť do konca.
  • Keďže pôvodný skript bol nahradený procesom env, už nie je prítomný a nemôže pokračovať vo svojom vykonávaní, keď sa druhý skript skončí.
  • Pretože je súbor skriptu počas spustenia zamknutý, iné inštancie nemožno spustiť, kým skript spustený flockom neprestane bežať a neuvoľní zámok.

Môže to znieť ako zápletka Inception, ale funguje to nádherne. Ten jeden riadok určite zaboduje.

Kvôli prehľadnosti je to zámok na súbore skriptu, ktorý bráni spusteniu iných inštancií, nie detekcia premennej prostredia. Premenná prostredia iba povie spustenému skriptu, aby buď nastavil zámok, alebo že zámok je už na svojom mieste.

Zamknúť a načítať

Zabezpečiť, aby sa naraz spustila iba jedna inštancia skriptu, je jednoduchšie, než by ste čakali. Všetky tri tieto techniky fungujú. Hoci je to najspletitejšie v prevádzke, kŕdeľ jednoradových je najjednoduchšie vložiť do akéhokoľvek skriptu.