Vyhněte se těmto problémům omezením Bash skriptů, aby se spouštěly pouze jednou
Klíčové věci
- Ujistěte se, že je spuštěna pouze jedna instance vašeho skriptu pomocí pgrep, lsof nebo flock, abyste předešli problémům se souběžností.
- Snadno implementujte kontroly pro samoukončení skriptu, pokud jsou detekovány další spuštěné instance.
- Využitím příkazů exec a env toho může příkaz flock dosáhnout pomocí jednoho řádku kódu.
Některé linuxové skripty mají takovou režii provádění, je třeba zabránit spuštění několika instancí najednou. Naštěstí existuje několik způsobů, jak toho dosáhnout ve svých vlastních Bash skriptech.
Někdy Jednou stačí
Některé skripty by se neměly spouštět, pokud stále běží předchozí instance tohoto skriptu. Pokud váš skript spotřebovává nadměrně čas CPU a RAM nebo generuje velkou šířku pásma sítě nebo drcení disku, je rozumné omezit jeho provádění na jednu instanci najednou.
Ale nejsou to jen prasata zdrojů, která musí běžet izolovaně. Pokud váš skript upravuje soubory, může dojít ke konfliktu mezi dvěma (nebo více) instancemi skriptu, protože bojují o přístup k souborům. Aktualizace mohou být ztraceny nebo soubor může být poškozen.
Jednou z technik, jak se těmto problémovým scénářům vyhnout, je nechat skript zkontrolovat, zda nejsou spuštěny žádné další verze jeho samotného. Pokud skript zjistí další spuštěné kopie, sám se ukončí.
Další technikou je navrhnout skript takovým způsobem, že se při spuštění uzamkne a zabrání spuštění jakýchkoli dalších kopií.
Podíváme se na dva příklady první techniky a pak se podíváme na jeden způsob, jak provést druhou.
Použití pgrep k zabránění souběžnosti
Příkaz pgrep prohledává procesy, které jsou spuštěny na počítači se systémem Linux, a vrací ID procesu procesů, které odpovídají vyhledávacímu vzoru.
Mám skript s názvem loop.sh. Obsahuje cyklus for, který vytiskne iteraci cyklu a poté na sekundu usne. Dělá to desetkrát.
#!/bin/bash
for (( i=1; i<=10; i+=1 ))
do
echo "Loop:" $i
sleep 1
done
exit 0
Spustil jsem dvě instance a pak jsem použil pgrep k vyhledání podle názvu.
pgrep loop.sh
Vyhledá dvě instance a nahlásí jejich ID procesů. Můžeme přidat volbu -c (count), aby pgrep vrátil počet instancí.
pregp -c loop.sh
Tento počet instancí můžeme použít v našem skriptu. Pokud je hodnota vrácená pgrep větší než jedna, musí být spuštěno více než jedna instance a náš skript se ukončí.
Vytvoříme skript, který tuto techniku používá. Budeme to nazývat pgrep-solo.sh.
Porovnání if testuje, zda je číslo vrácené pgrep větší než jedna. Pokud ano, skript se 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
Pokud je číslo vrácené pgrep jedna, skript může pokračovat. Zde je kompletní 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
Zkopírujte to do svého oblíbeného editoru a uložte jako pgrep-solo.sh. Poté jej udělejte spustitelným pomocí chmod.
chmod +x pgrep-loop.sh
Když to běží, vypadá to takto.
./pgrep-solo.sh
Ale pokud se jej pokusím spustit s jinou kopií, která již běží v jiném okně terminálu, zjistí to a ukončí se.
./pgrep-solo.sh
Použití lsof k zabránění souběžnosti
Velmi podobnou věc můžeme udělat s příkazem lsof.
Pokud přidáme volbu -t (stručné), lsof vypíše ID procesů.
lsof -t loop.sh
Můžeme vést výstup z lsof do wc. Volba -l (řádky) počítá počet řádků, který je v tomto scénáři stejný jako počet ID procesů.
lsof -t loop.sh | wc -l
Můžeme to použít jako základ testu v porovnání if v našem skriptu.
Uložte tuto verzi jako 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
K tomu, aby byl spustitelný, použijte chmod.
chmod +x lsof-solo.sh
Nyní, se skriptem lsof-solo.sh spuštěným v jiném okně terminálu, nemůžeme spustit druhou kopii.
./lsof-solo.sh
Metoda pgrep vyžaduje pouze jedno volání externího programu (pgrep), metoda lsof vyžaduje dvě (lsof a wc). Ale výhoda metody lsof oproti metodě pgrep je ta, že v porovnání if můžete použít proměnnou $0. Toto obsahuje název skriptu.
Znamená to, že skript můžete přejmenovat a bude stále fungovat. Nemusíte si pamatovat, že upravíte porovnávací řádek if a vložíte nový název skriptu. Proměnná $0 obsahuje „./“ na začátku názvu skriptu (jako ./lsof-solo.sh) a pgrep to nemá rád.
Použití hejna k zabránění souběžnosti
Naše třetí technika používá příkaz flock, který je určen k nastavení zámků souborů a adresářů ze skriptů. Když je uzamčen, žádný jiný proces nemá k uzamčenému prostředku přístup.
Tato metoda vyžaduje, aby byl na začátek skriptu přidán jeden řádek.
[ "${GEEKLOCK}" != "$0" ] && exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@" || :
Tyto hieroglyfy brzy dekódujeme. Zatím jen zkontrolujme, že to funguje. Uložte tento jako 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
Samozřejmě to musíme udělat spustitelným.
chmod +x flock-solo.sh
Spustil jsem skript v jednom okně terminálu a poté jsem ho zkusil znovu spustit v jiném okně terminálu.
./flock-solo
./flock-solo
./flock-solo
Nemohu spustit skript, dokud nebude dokončena instance v druhém okně terminálu.
Pojďme si vybrat čáru, která dělá kouzlo. Jádrem toho je příkaz hejna.
flock -en "$0" "$0" "$@"
Příkaz flock se používá k uzamčení souboru nebo adresáře a poté ke spuštění příkazu. Možnosti, které používáme, jsou -e (exkluzivní) a -n (neblokující).
Exkluzivní možnost znamená, že pokud se nám podaří soubor uzamknout, nikdo jiný k němu nebude mít přístup. Možnost neblokování znamená, že pokud se nám nepodaří získat zámek, okamžitě přestaneme zkoušet. Nějakou dobu to nezkoušíme, hned se elegantně ukloníme.
První $0 označuje soubor, který chceme zamknout. Tato proměnná obsahuje název aktuálního skriptu.
Druhý $0 je příkaz, který chceme spustit, pokud se nám podaří získat zámek. Opět předáváme jménem tohoto skriptu. Protože zámek zamyká všechny kromě od nás, můžeme spustit soubor skriptu.
Parametry můžeme předat příkazu, který je spuštěn. Pomocí $@ předáváme všechny parametry příkazového řádku, které byly předány tomuto skriptu, novému vyvolání skriptu, který bude spuštěn hejnem.
Máme tedy tento skript, který zamyká soubor skriptu a poté spouští další svou instanci. To je skoro to, co chceme, ale je tu problém. Po dokončení druhé instance bude zpracování prvního skriptu pokračovat. Máme však v rukávu další trik, jak to zajistit, jak uvidíte.
Používáme proměnnou prostředí, kterou nazýváme GEEKLOCK, abychom určili, zda běžící skript musí použít zámek nebo ne. Pokud se skript spustil a není na místě žádný zámek, musí být zámek použit. Pokud byl skript spuštěn a je na místě zámek, nemusí nic dělat, může se pouze spustit. Se spuštěným skriptem a uzamčením nelze spustit žádné další instance skriptu.
[ "${GEEKLOCK}" != "$0" ]
Tento test se překládá jako ‚vrátí hodnotu true, pokud proměnná prostředí GEEKLOCK není nastavena na název skriptu.‘ Test je zřetězen se zbytkem příkazu pomocí && (a) a || (nebo). Část && se provede, pokud test vrátí hodnotu true, a || sekce se provede, pokud test vrátí hodnotu false.
env GEEKLOCK="$0"
Příkaz env se používá ke spouštění dalších příkazů v upravených prostředích. Upravujeme naše prostředí vytvořením proměnné prostředí GEEKLOCK a jejím nastavením na název skriptu. Příkaz, který se spustí env, je příkaz flock a příkaz flock spustí druhou instanci skriptu.
Druhá instance skriptu provede kontrolu, aby zjistila, zda proměnná prostředí GEEKLOCK neexistuje, ale zjistí, že ano. || provede se sekce příkazu, která neobsahuje nic než dvojtečku „:“, což je ve skutečnosti příkaz, který nic nedělá. Cesta provedení pak prochází zbytkem skriptu.
Ale stále tu máme problém s tím, že první skript pokračuje ve svém vlastním zpracování, když druhý skript skončil. Řešením je příkaz exec. Tím se spustí další příkazy nahrazením volajícího procesu nově spuštěným procesem.
exec env GEEKLOCK="$0" flock -en "$0" "$0" "$@"
Takže celá sekvence je:
- Skript se spustí a nemůže najít proměnnou prostředí. Je provedena klauzule &&.
- exec spustí env a nahradí původní proces skriptu novým procesem env.
- Proces env vytvoří proměnnou prostředí a spustí flock.
- flock uzamkne soubor skriptu a spustí novou instanci skriptu, která detekuje proměnnou prostředí, spustí || klauzule a skript je schopen doběhnout do svého závěru.
- Protože byl původní skript nahrazen procesem env, již není přítomen a nemůže pokračovat ve svém provádění, když druhý skript skončí.
- Protože je soubor skriptu při spuštění uzamčen, nelze spustit další instance, dokud skript spouštěný hejnem nepřestane běžet a neuvolní zámek.
Může to znít jako zápletka Inception, ale funguje to nádherně. Ten jeden řádek určitě zasáhne.
Pro srozumitelnost je to zámek na souboru skriptu, který zabraňuje spouštění dalších instancí, nikoli detekci proměnné prostředí. Proměnná prostředí pouze říká spuštěnému skriptu, aby buď nastavil zámek, nebo že zámek je již na svém místě.
Zamknout a načíst
Je snazší, než byste čekali, zajistit, aby se najednou provedla pouze jedna instance skriptu. Všechny tři tyto techniky fungují. I když je provoz nejspletitější, je nejjednodušší použít hejno s jednou vložkou do jakéhokoli skriptu.