這裡介紹如何防止 Linux 的 Shell 指令稿重複被執行,確保同一時間只有一個行程在執行。

管理者在平常管理與維護 Linux 系統時,通常都會將例行性的工作寫成指令稿(script),然後以手動或是自動排程(crontab)的方式來執行,而像這類的指令稿通常都不能同時重複執行,否則很容易出問題(例如備份檔案等),所以在撰寫系統管理相關的指令稿時,最好要加上避免重複執行的檢查邏輯。


以下我們將介紹避免指令稿重複執行的程式設計方法,靠著這個技巧就可以讓程式本身在執行時,自動檢查是否有重複執行,防止管理者不小心重複執行而出錯。

Shell 指令稿防止重複執行

一般來說,若要防止 Shell 指令稿被重複執行,可以使用鎖定檔案(lock file)或行程 ID 檔案(PID file)的方式來處理。

鎖定檔案(lock file)
依據特定的檔案是否存在來判斷是否已經有另外一個行程正在執行。當指令稿執行時,先檢查鎖定檔案是否存在,如果存在的話就直接離開,反之若鎖定檔案不存在的話,則建立一個鎖定檔案,接著執行正常的工作,當工作結束後,離開指令稿前再刪除鎖定檔案。
行程 ID 檔案(PID file)
行程 ID 檔案的作法跟鎖定檔案類似,只不過在檔案中多放了一個行程 ID 的資訊,這樣一來就可以透過行程 ID 來驗證該行程是否有真的在執行。

鎖定檔案

以下是在一般的 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 檔案

以下是以行程 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