Bagaimana cara memastikan hanya satu instance dari skrip bash berjalan?

26

Solusi yang tidak memerlukan alat tambahan akan lebih disukai.

Tobias Kienzler
sumber
Bagaimana dengan file kunci?
Marco
@ Marsco Saya menemukan jawaban SO ini menggunakan itu, tetapi seperti yang dinyatakan dalam komentar , ini dapat membuat kondisi balapan
Tobias Kienzler
3
Ini BashFAQ 45 .
jw013
@ jw013 terima kasih! Jadi mungkin sesuatu seperti ln -s my.pid .lockakan mengklaim kunci (diikuti oleh echo $$ > my.pid) dan jika gagal dapat memeriksa apakah PID yang disimpan .lockadalah benar-benar contoh aktif dari skrip
Tobias Kienzler

Jawaban:

18

Hampir seperti jawaban nsg: gunakan direktori kunci . Pembuatan direktori adalah atom di linux dan unix dan * BSD dan banyak OS lainnya.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Anda dapat memasukkan PID dari sh penguncian ke dalam file di direktori kunci untuk tujuan debugging, tetapi jangan jatuh ke dalam jebakan berpikir Anda dapat memeriksa PID tersebut untuk melihat apakah proses penguncian masih dijalankan. Banyak kondisi lomba berada di jalur itu.

Bruce Ediger
sumber
1
Saya akan mempertimbangkan menggunakan PID yang disimpan untuk memeriksa apakah instance penguncian masih hidup. Namun, inilah klaim yang mkdirtidak atom pada NFS (yang tidak berlaku untuk saya, tapi saya kira orang harus menyebutkan bahwa, jika benar)
Tobias Kienzler
Ya, tentu saja gunakan PID yang disimpan untuk melihat apakah proses penguncian masih dijalankan, tetapi jangan mencoba melakukan apa pun selain mencatat pesan. Pekerjaan memeriksa pid yang disimpan, membuat file PID baru, dll, meninggalkan jendela besar untuk balapan.
Bruce Ediger
Ok, seperti yang dinyatakan oleh Ihunath , lockdir kemungkinan besar berada di /tmpmana biasanya tidak dibagikan NFS, sehingga seharusnya tidak masalah.
Tobias Kienzler
Saya akan gunakan rm -rfuntuk menghapus direktori kunci. rmdirakan gagal jika seseorang (belum tentu Anda) berhasil menambahkan file ke direktori.
chepner
18

Untuk menambah jawaban Bruce Ediger , dan terinspirasi oleh jawaban ini , Anda juga harus menambahkan lebih banyak kecerdasan ke pembersihan untuk menjaga terhadap penghentian skrip:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi
Igor Zevaka
sumber
Atau if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement,.
Kusalananda
6

Ini mungkin terlalu sederhana, tolong perbaiki saya jika saya salah. Bukankah ini pscukup sederhana ?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment
terdon
sumber
1
Bagus dan / atau brilian. :)
Spooky
1
Saya telah menggunakan kondisi ini selama seminggu, dan dalam 2 kesempatan itu tidak mencegah proses baru dari mulai. Saya pikir apa masalahnya - pid baru adalah substring yang lama dan disembunyikan oleh grep -v $$. contoh nyata: lama - 14532, baru - 1453, lama - 28858, baru - 858.
Naktibalda
Saya memperbaikinya dengan mengubah grep -v $$kegrep -v "^${$} "
Naktibalda
@Naktibalda tangkapan yang bagus, terima kasih! Anda juga dapat memperbaikinya dengan grep -wv "^$$"(lihat edit).
terdon
Terima kasih atas pembaruan itu. Pola saya terkadang gagal karena pids pendek dibiarkan penuh dengan spasi.
Naktibalda
4

Saya akan menggunakan file kunci, seperti yang disebutkan oleh Marco

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file
nsg
sumber
1
(Saya pikir Anda lupa membuat file kunci) Bagaimana dengan kondisi balapan ?
Tobias Kienzler
ops :) Ya, kondisi lomba adalah masalah dalam contoh saya, saya biasanya menulis pekerjaan cron per jam atau harian dan kondisi ras jarang.
nsg
Mereka seharusnya tidak relevan dalam kasus saya juga, tetapi itu adalah sesuatu yang harus diingat. Mungkin menggunakan lsof $0itu tidak buruk juga?
Tobias Kienzler
Anda dapat mengurangi kondisi balapan dengan menuliskannya $$di file kunci. Kemudian sleepuntuk interval pendek dan membacanya kembali. Jika PID masih milik Anda, Anda berhasil mendapatkan kunci. Sama sekali tidak membutuhkan alat tambahan.
manatwork
1
Saya tidak pernah menggunakan lsof untuk tujuan ini, saya ini harus bekerja. Perhatikan bahwa lsof benar - benar lambat di sistem saya (1-2 detik) dan kemungkinan besar ada banyak waktu untuk kondisi balapan.
nsg
3

Jika Anda ingin memastikan bahwa hanya satu instance skrip Anda yang berjalan, lihat:

Kunci skrip Anda (terhadap menjalankan paralel)

Kalau tidak, Anda dapat memeriksa psatau memohon lsof <full-path-of-your-script>, karena saya tidak akan menyebut mereka alat tambahan.


Suplemen :

sebenarnya saya berpikir untuk melakukannya seperti ini:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

ini memastikan bahwa hanya proses dengan yang terendah pidtetap berjalan bahkan jika Anda melakukan fork-dan-exec beberapa contoh <your_script>secara bersamaan.

pengguna1146332
sumber
1
Terima kasih atas tautannya, tetapi bisakah Anda memasukkan bagian-bagian penting dalam jawaban Anda? Sudah menjadi kebijakan umum di SE untuk mencegah pembusukan tautan ... Tetapi sesuatu seperti [[(lsof $0 | wc -l) > 2]] && exitmungkin sebenarnya cukup, atau apakah ini juga rentan terhadap kondisi balapan?
Tobias Kienzler
Anda benar, bagian esensial dari jawaban saya tidak ada dan hanya memposting tautan yang cukup lemah. Saya menambahkan saran saya sendiri ke jawabannya.
user1146332
3

Satu cara lain untuk memastikan satu contoh skrip bash berjalan:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 mendapatkan PID dari skrip yang ada jika sudah berjalan atau keluar dengan kode kesalahan 1 jika tidak ada skrip lain yang berjalan

Sethu
sumber
3

Meskipun Anda telah meminta solusi tanpa alat tambahan, ini adalah cara favorit saya menggunakan flock:

#!/bin/sh

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

echo "servus!"
sleep 10

Ini berasal dari bagian contoh man flock, yang selanjutnya menjelaskan:

Ini adalah kode boilerplate yang berguna untuk skrip shell. Letakkan di bagian atas skrip shell yang ingin Anda kunci dan itu akan secara otomatis mengunci sendiri pada proses pertama. Jika env var $ FLOCKER tidak disetel ke skrip shell yang sedang dijalankan, maka jalankan flock dan ambil kunci non-blocking eksklusif (menggunakan skrip itu sendiri sebagai file kunci) sebelum mengeksekusi kembali dengan argumen yang benar. Ini juga mengatur FLOCKER env var ke nilai yang benar sehingga tidak berjalan lagi.

Poin yang perlu dipertimbangkan:

  • Membutuhkan flock, skrip contoh diakhiri dengan kesalahan jika tidak dapat ditemukan
  • Tidak perlu file kunci tambahan
  • Mungkin tidak berfungsi jika skrip berada di NFS (lihat /server/66919/file-locks-on-an-nfs )

Lihat juga /programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at .

schieferstapel
sumber
1

Ini adalah versi modifikasi dari Jawaban Anselmo . Idenya adalah membuat deskriptor file hanya baca menggunakan skrip bash itu sendiri dan gunakan flockuntuk menangani kunci.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

Perbedaan utama dari semua jawaban lain adalah bahwa kode ini tidak mengubah sistem file, menggunakan tapak yang sangat rendah dan tidak memerlukan pembersihan karena deskriptor file ditutup segera setelah skrip selesai terlepas dari status keluar. Jadi tidak masalah jika skrip gagal atau berhasil.

John Doe
sumber
Anda harus selalu mengutip semua referensi variabel shell kecuali Anda memiliki alasan kuat untuk tidak melakukannya, dan Anda yakin tahu apa yang Anda lakukan. Jadi, Anda seharusnya melakukannya exec 6< "$SCRIPT".
Scott
@Scott Saya telah mengubah kode sesuai saran Anda. Terimakasih banyak.
John Doe
1

Saya menggunakan cksum untuk memeriksa skrip saya benar - benar menjalankan instance tunggal, bahkan saya mengubah nama file & path file .

Saya tidak menggunakan file jebakan & kunci, karena jika server saya tiba-tiba mati, saya harus menghapus file kunci secara manual setelah server naik.

Catatan: #! / Bin / bash di baris pertama diperlukan untuk grep ps

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Atau Anda dapat melakukan hardcode cksum di dalam skrip Anda, sehingga Anda tidak perlu khawatir lagi jika Anda ingin mengubah nama file, path, atau konten skrip Anda .

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done
arputra
sumber
1
Tolong jelaskan bagaimana hardcoding checksum adalah ide yang bagus.
Scott
bukan hardcoding checksum, ini hanya membuat kunci identitas skrip Anda, ketika instance lain akan berjalan, ia akan memeriksa proses skrip shell lain dan memasukkan file terlebih dahulu, jika kunci identitas Anda ada pada file itu, jadi itu berarti instance Anda sudah berjalan.
arputra
BAIK; harap edit jawaban Anda untuk menjelaskannya. Dan, di masa depan, jangan memposting beberapa blok kode sepanjang 30 baris yang terlihat seperti (hampir) identik tanpa mengatakan dan menjelaskan perbedaannya. Dan jangan katakan hal-hal seperti "Anda dapat melakukan hardcode [sic] cksum di dalam skrip Anda", dan jangan terus menggunakan nama variabel mysumdan fsum, ketika Anda tidak lagi berbicara tentang checksum.
Scott
Terlihat menarik, terima kasih! Dan selamat datang di unix.stackexchange :)
Tobias Kienzler
0

Anda dapat menggunakan ini: https://github.com/sayanarijit/pidlock

sudo pip install -U pidlock

pidlock -n sleepy_script -c 'sleep 10'
Arijit Basu
sumber
> Solusi yang tidak memerlukan alat tambahan akan lebih disukai.
Dhag
0

Kode saya untuk Anda

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

Berdasarkan man flock, hanya meningkatkan:

  • nama file kunci, didasarkan pada nama lengkap skrip
  • pesan executing

Di mana saya taruh di sini sleep 10, Anda bisa meletakkan semua skrip utama.

Anselmo Blanco Dominguez
sumber