這裡介紹如何防止 Linux 的 Shell 指令稿重複被執行,確保同一時間只有一個行程在執行。
管理者在平常管理與維護 Linux 系統時,通常都會將例行性的工作寫成指令稿(script),然後以手動或是自動排程(crontab)的方式來執行,而像這類的指令稿通常都不能同時重複執行,否則很容易出問題(例如備份檔案等),所以在撰寫系統管理相關的指令稿時,最好要加上避免重複執行的檢查邏輯。
一般來說,若要防止 Shell 指令稿被重複執行,可以使用鎖定檔案(lock file)或行程 ID 檔案(PID file)的方式來處理。
以下是在一般的 shell 指令稿中,以鎖定檔案的方式防止重複執行的實做範例:
#!/bin/sh # 鎖定檔案路徑 LOCK_FILE=/home/gtwang/my_script.lock # 檢查鎖定檔案 if [ -f $LOCK_FILE ]; then echo "This script is already running!" exit 1 fi # 建立 lock file touch $LOCK_FILE # 檢查鎖定檔案是否成功被建立 if [ ! -f $LOCK_FILE ]; then echo "Cannot create lock file!" exit 1 fi # 主要工作 echo "Doing my job." sleep 30 echo "Done." # 刪除鎖定檔案 rm -f ${LOCK_FILE}
鎖定檔案的路徑可以自由指定,不過不建議放在 /tmp/
或是 /var/tmp/
等暫存目錄之下,因為某些 Linux 會定時清理暫存目錄中的檔案(例如 Red Hat Enterprise Linux 就會每天將太久沒用的暫存檔清除掉)。
以下是以行程 ID 檔案來防止重複執行的實做範例:
#!/bin/sh # 行程 ID 檔案路徑 PID_FILE=/home/gtwang/my_script.pid # 檢查行程 ID 檔案是否存在 if [ -f $PID_FILE ]; then # 取得行程 ID PID=$(cat $PID_FILE) # 檢查行程是否有在執行 ps -p $PID > /dev/null 2>&1 if [ $? -eq 0 ]; then echo "This script is already running!" exit 1 fi fi # 行程沒有在執行,將目前行程 ID 寫入檔案 echo $$ > $PID_FILE # 檢查行程 ID 檔案是否成功被建立 if [ $? -ne 0 ]; then echo "Could not create PID file." exit 1 fi # 主要工作 echo "Doing my job." sleep 30 echo "Done." # 刪除鎖定檔案 rm -f ${PID_FILE}
行程 ID 檔案的方法可以更準確的判斷出指令稿是否有在執行,當前一個指令稿執行到一半異常終止時(例如按下 Ctrl
+ c
),若以鎖定檔案的方式來判斷的話,由於指令稿中止時沒有清除鎖定檔案,所以就會造成誤判,以為上一個指令稿還在執行,而若以行程 ID 來判斷的話,就可以避免這種誤判情形。
flock
指令有時候我們可能會遇到無法自己修改指令稿的狀況,或是根本不想花時間改程式,這時候可以改用 flock
指令,它可以用鎖定檔案的方式,確保被呼叫的指令稿同一時間只被執行一次:
# 以 /home/gtwang/my_script.lock 為鎖定檔案, # 讓 /home/gtwang/my_script.sh 同一時間只執行一次 flock -x /home/gtwang/my_script.lock -c /home/gtwang/my_script.sh
在預設的狀況下,第二次以 flock
執行指令稿時,若前一次的指令稿尚未結束的話,它會持續等待直到前一次執行的指令稿結束再開始執行。
如果加上 -n
參數的話,若遇到前一次的指令稿尚未結束時,就會自動離開,不執行任何動作:
# 若發現重複執行,則直接離開 flock -nx /home/gtwang/my_script.lock -c /home/gtwang/my_script.sh
solo
指令稿solo
是一個簡單的 Perl 指令稿,它是靠著綁定連接埠的方式,確保同一時間只有一個行程被執行,其原始碼如下:
#!/usr/bin/perl -s # # solo v1.7 # Prevents multiple cron instances from running simultaneously. # # Copyright 2007-2016 Timothy Kay # http://timkay.com/solo/ # # It is free software; you can redistribute it and/or modify it under the terms of either: # # a) the GNU General Public License as published by the Free Software Foundation; # either version 1 (http://dev.perl.org/licenses/gpl1.html), or (at your option) # any later version (http://www.fsf.org/licenses/licenses.html#GNUGPL), or # # b) the "Artistic License" (http://dev.perl.org/licenses/artistic.html), or # # c) the MIT License (http://opensource.org/licenses/MIT) # use Socket; alarm $timeout if $timeout; $port =~ /^\d+$/ or $noport or die "Usage: $0 -port=PORT COMMAND\n"; if ($port) { # To work with OpenBSD: change to # $addr = pack(CnC, 127, 0, 1); # but make sure to use different ports across different users. # (Thanks to www.gotati.com .) $addr = pack(CnC, 127, $<, 1); print "solo: bind ", join(".", unpack(C4, $addr)), ":$port\n" if $verbose; $^F = 10; # unset close-on-exec socket(SOLO, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "socket: $!"; bind(SOLO, sockaddr_in($port, $addr)) or $silent? exit: die "solo($port): $!\n"; } sleep $sleep if $sleep; exec @ARGV;
solo
使用綁定連接埠的方式來避免指令稿重複執行,完全不需要使用到鎖定檔案,是一個非常好的作法,其使用方式如下:
# 以綁定 6000 連接埠的方式, # 讓 /home/gtwang/my_script.sh 同一時間只執行一次 ./solo -port=6000 /home/gtwang/my_script.sh
如果重複執行時,就會出現以下的錯誤訊息:
solo(6000): Address already in use
參考資料:Linux 技術手札、Benjamin Cane