确保只运行一个Bash脚本实例的最佳方法是什么?

时间:2009-11-11 13:21:25

标签: linux bash pid flock lockfile

确保只有一个给定脚本的实例正在运行的最简单/最好的方法是什么 - 假设它是Linux上的Bash?

我正在做的那一刻:

ps -C script.name.sh > /dev/null 2>&1 || ./script.name.sh

但它有几个问题:

  1. 它将检查放在脚本之外
  2. 它不允许我从不同的帐户运行相同的脚本 - 我有时会这样做。
  3. -C仅检查进程名称的前14个字符
  4. 当然,我可以编写自己的pidfile处理,但我觉得应该有一个简单的方法来实现它。

14 个答案:

答案 0 :(得分:129)

咨询锁定已经使用了很长时间,它可以在bash脚本中使用。我更喜欢简单的flock(来自util-linux[-ng])而不是lockfile(来自procmail)。并始终记住这些脚本中退出时的陷阱(sigspec == EXIT0,捕获特定信号是多余的)。

2009年,我发布了我的可锁定脚本样板(最初在我的维基页面上提供,现在可以gist获得)。将其转换为每用户一个实例是微不足道的。使用它,您还可以轻松地为需要锁定或同步的其他场景编写脚本。

以下是为方便起见所提到的样板文件。

#!/bin/bash
# SPDX-License-Identifier: MIT

## Copyright (C) 2009 Przemyslaw Pawelczyk <przemoc@gmail.com>
##
## This script is licensed under the terms of the MIT license.
## https://opensource.org/licenses/MIT
#
# Lockable script boilerplate

### HEADER ###

LOCKFILE="/var/lock/`basename $0`"
LOCKFD=99

# PRIVATE
_lock()             { flock -$1 $LOCKFD; }
_no_more_locking()  { _lock u; _lock xn && rm -f $LOCKFILE; }
_prepare_locking()  { eval "exec $LOCKFD>\"$LOCKFILE\""; trap _no_more_locking EXIT; }

# ON START
_prepare_locking

# PUBLIC
exlock_now()        { _lock xn; }  # obtain an exclusive lock immediately or fail
exlock()            { _lock x; }   # obtain an exclusive lock
shlock()            { _lock s; }   # obtain a shared lock
unlock()            { _lock u; }   # drop a lock

### BEGIN OF SCRIPT ###

# Simplest example is avoiding running multiple instances of script.
exlock_now || exit 1

# Remember! Lock file is removed when one of the scripts exits and it is
#           the only script holding the lock or lock is not acquired at all.

答案 1 :(得分:95)

如果所有用户的脚本相同,则可以使用lockfile方法。如果获得锁定,请继续显示消息并退出。

举个例子:

[Terminal #1] $ lockfile -r 0 /tmp/the.lock
[Terminal #1] $ 

[Terminal #2] $ lockfile -r 0 /tmp/the.lock
[Terminal #2] lockfile: Sorry, giving up on "/tmp/the.lock"

[Terminal #1] $ rm -f /tmp/the.lock
[Terminal #1] $ 

[Terminal #2] $ lockfile -r 0 /tmp/the.lock
[Terminal #2] $ 

获取/tmp/the.lock后,您的脚本将是唯一具有执行权限的脚本。完成后,只需取下锁即可。在脚本形式中,这可能如下所示:

#!/bin/bash

lockfile -r 0 /tmp/the.lock || exit 1

# Do stuff here

rm -f /tmp/the.lock

答案 2 :(得分:28)

我认为flock可能是最简单(也是最难忘)的变体。我在cron作业中使用它来自动编码dvdscds

# try to run a command, but fail immediately if it's already running
flock -n /var/lock/myjob.lock   my_bash_command

使用-w进行超时或省略选项以等待释放锁定。最后,手册页显示了多个命令的一个很好的例子:

   (
     flock -n 9 || exit 1
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

答案 3 :(得分:7)

使用set -o noclobber选项并尝试覆盖公共文件。

一个简短的例子

if ! (set -o noclobber ; echo > /tmp/global.lock) ; then
    exit 1  # the global.lock already exists
fi

# ... remainder of script ...

更长的例子

此示例将等待global.lock文件,但过了很长时间后会超时。

 function lockfile_waithold()
 {
    declare -ir time_beg=$(date '+%s')
    declare -ir time_max=7140  # 7140 s = 1 hour 59 min.

    # poll for lock file up to ${time_max}s
    # put debugging info in lock file in case of issues ...
    while ! \
       (set -o noclobber ; \
        echo -e "DATE:$(date)\nUSER:$(whoami)\nPID:$$" > /tmp/global.lock \ 
       ) 2>/dev/null
    do
        if [ $(($(date '+%s') - ${time_beg})) -gt ${time_max} ] ; then
            echo "Error: waited too long for lock file /tmp/global.lock" 1>&2
            return 1
        fi
        sleep 1
    done

    return 0
 }

 function lockfile_release()
 {
    rm -f /tmp/global.lock
 }

 if ! lockfile_waithold ; then
      exit 1
 fi
 trap lockfile_release EXIT

 # ... remainder of script ...


(这与@Barry Kelly的this post类似,后来被注意到了。)

答案 4 :(得分:3)

我不确定是否有任何一线强大的解决方案, 所以你最终可能会自己动手。

Lockfiles不完美,但不如使用'ps | grep | grep -v'管道。

话虽如此,您可以考虑保持过程控制 与脚本分开 - 有一个启动脚本。 或者,至少将其分解为单独文件中保存的函数, 所以你可能在调用者脚本中有:

. my_script_control.ksh

# Function exits if cannot start due to lockfile or prior running instance.
my_start_me_up lockfile_name;
trap "rm -f $lockfile_name; exit" 0 2 3 15

在每个需要控制逻辑的脚本中。 trap确保在调用者退出时删除锁定文件, 因此您不必在脚本中的每个出口点对此进行编码。

使用单独的控制脚本意味着您可以检查边缘情况: 删除过时的日志文件,验证锁定文件是否正确关联 当前运行的脚本实例,提供一个杀死正在运行的进程的选项,依此类推。 这也意味着你有更好的机会成功地在ps输出上使用grep。 ps-grep可用于验证lockfile是否具有与之关联的正在运行的进程。 也许您可以通过某种方式命名您的锁定文件以包含有关该过程的信息: user,pid等,可以由以后的脚本调用来决定是否进程 创建锁文件的人仍在。

答案 5 :(得分:2)

第一个测试示例

[[ $(lsof -t $0| wc -l) > 1 ]] && echo "At least one of $0 is running"

第二个测试示例

currsh=$0
currpid=$$
runpid=$(lsof -t $currsh| paste -s -d " ")
if [[ $runpid == $currpid ]]
then
  sleep 11111111111111111
else
  echo -e "\nPID($runpid)($currpid) ::: At least one of \"$currsh\" is running !!!\n"
  false
  exit 1
fi

解释

&#34; lsof -t&#34;列出当前正在运行的脚本的所有pids,名为&#34; $ 0&#34;。

命令&#34; lsof&#34;将有两个好处。

  1. 忽略由编辑器编辑的pid,例如vim,因为vim编辑其映射文件,例如&#34; .file.swp&#34;。
  2. 忽略当前正在运行的shell脚本分叉的pids,其中大多数&#34; grep&#34;衍生命令无法实现它。使用&#34; pstree -pH pidnum&#34;命令查看有关当前进程分叉状态的详细信息。

答案 6 :(得分:1)

Ubuntu / Debian发行版具有 start-stop-daemon 工具,其目的与您描述的目的相同。另请参阅 /etc/init.d/skeleton ,了解它在编写启动/停止脚本时的用法。

- 诺亚

答案 7 :(得分:1)

我还建议查看chpst(runit的一部分):

chpst -L /tmp/your-lockfile.loc ./script.name.sh

答案 8 :(得分:1)

一线终极解决方案:

[ "$(pgrep -fn $0)" -ne "$(pgrep -fo $0)" ] && echo "At least 2 copies of $0 are running"

答案 9 :(得分:1)

我在procmail包依赖项中找到了这个:

apt install liblockfile-bin

运行: dotlockfile -l file.lock

将创建file.lock。

解锁: dotlockfile -u file.lock

使用此列出此包文件/命令: dpkg-query -L liblockfile-bin

答案 10 :(得分:0)

我遇到了同样的问题,并提出了一个使用lockfile的template,一个保存进程ID号的pid文件,以及一个kill -0 $(cat $pid_file)检查,以使中止的脚本不会停止下一次运行。 这会在/ tmp中创建一个foobar- $ USERID文件夹,其中lockfile和pid文件存在。

只要您将这些操作保留在alertRunningPS中,您仍然可以调用脚本并执行其他操作。

#!/bin/bash

user_id_num=$(id -u)
pid_file="/tmp/foobar-$user_id_num/foobar-$user_id_num.pid"
lock_file="/tmp/foobar-$user_id_num/running.lock"
ps_id=$$

function alertRunningPS () {
    local PID=$(cat "$pid_file" 2> /dev/null)
    echo "Lockfile present. ps id file: $PID"
    echo "Checking if process is actually running or something left over from crash..."
    if kill -0 $PID 2> /dev/null; then
        echo "Already running, exiting"
        exit 1
    else
        echo "Not running, removing lock and continuing"
        rm -f "$lock_file"
        lockfile -r 0 "$lock_file"
    fi
}

echo "Hello, checking some stuff before locking stuff"

# Lock further operations to one process
mkdir -p /tmp/foobar-$user_id_num
lockfile -r 0 "$lock_file" || alertRunningPS

# Do stuff here
echo -n $ps_id > "$pid_file"
echo "Running stuff in ONE ps"

sleep 30s

rm -f "$lock_file"
rm -f "$pid_file"
exit 0

答案 11 :(得分:-1)

我找到了一种非常简单的方法来处理“每个系统的一个脚本副本”。 它不允许我从许多帐户运行脚本的多个副本(在标准Linux上)。

解决方案:

在剧本开头,我给了:

pidof -s -o '%PPID' -x $( basename $0 ) > /dev/null 2>&1 && exit

显然pidof以下列方式运作良好:

  • 它对ps -C ...
  • 等程序名称没有限制
  • 它不要求我grep -v grep(或任何类似的)

它并不依赖于锁定文件,对我来说这是一个很大的胜利,因为转发它们意味着你必须添加陈旧锁定文件的处理 - 这不是很复杂,但如果可以避免 - 为什么不呢?

至于检查“每个正在运行的用户的一个脚本副本”,我写了这个,但我对此并不满意:

(
    pidof -s -o '%PPID' -x $( basename $0 ) | tr ' ' '\n'
    ps xo pid= | tr -cd '[0-9\n]'
) | sort | uniq -d

然后我检查它的输出 - 如果它是空的 - 没有来自同一用户的脚本副本。

答案 12 :(得分:-2)

来自您的脚本:

ps -ef | grep $0 | grep $(whoami)

答案 13 :(得分:-2)

这是我们的标准位。它可以以某种方式从脚本中恢复而不会清理它的锁定文件。

如果正常运行,它会将进程ID写入锁定文件。如果它在开始运行时找到锁定文件,它将从锁定文件中读取进程ID并检查该进程是否存在。如果该进程不存在,它将删除过时的锁文件并继续。并且只有当锁文件存在并且进程仍在运行时它才会退出。它会在退出时写入消息。

# lock to ensure we don't get two copies of the same job
script_name="myscript.sh"
lock="/var/run/${script_name}.pid"
if [[ -e "${lock}" ]]; then
    pid=$(cat ${lock})
    if [[ -e /proc/${pid} ]]; then
        echo "${script_name}: Process ${pid} is still running, exiting."
        exit 1
    else
        # Clean up previous lock file
        rm -f ${lock}
   fi
fi
trap "rm -f ${lock}; exit $?" INT TERM EXIT
# write $$ (PID) to the lock file
echo "$$" > ${lock}