有什么快速而简单的方法可以确保在给定时间内只有一个shell脚本实例在运行?


当前回答

下面这一行的回答来自一个与Ask Ubuntu问答相关的人:

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
#     This is useful boilerplate code for shell scripts.  Put it at the top  of
#     the  shell script you want to lock and it'll automatically lock itself on
#     the first run.  If the env var $FLOCKER is not set to  the  shell  script
#     that  is being run, then execute flock and grab an exclusive non-blocking
#     lock (using the script itself as the lock file) before re-execing  itself
#     with  the right arguments.  It also sets the FLOCKER env var to the right
#     value so it doesn't run again.

其他回答

另一个选项是通过运行set -C来使用shell的noclobber选项。如果文件已经存在,那么>将失败。

简而言之:

set -C
lockfile="/tmp/locktest.lock"
if echo "$$" > "$lockfile"; then
    echo "Successfully acquired lock"
    # do work
    rm "$lockfile"    # XXX or via trap - see below
else
    echo "Cannot acquire lock - already locked by $(cat "$lockfile")"
fi

这会导致shell调用:

open(pathname, O_CREAT|O_EXCL)

自动创建文件,如果文件已经存在则失败。


根据BashFAQ 045上的评论,这可能在ksh88中失败,但它在我所有的shell中都有效:

$ strace -e trace=creat,open -f /bin/bash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/zsh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_NOCTTY|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/pdksh /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC|O_LARGEFILE, 0666) = 3

$ strace -e trace=creat,open -f /bin/dash /home/mikel/bin/testopen 2>&1 | grep -F testopen.lock
open("/tmp/testopen.lock", O_WRONLY|O_CREAT|O_EXCL|O_LARGEFILE, 0666) = 3

有趣的是pdksh添加了O_TRUNC标志,但显然这是多余的: 要么创建一个空文件,要么什么也不做。


如何进行rm取决于您希望如何处理不干净的出口。

在干净退出时删除

新的运行失败,直到导致不干净退出的问题得到解决,并手动删除锁文件。

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

在任何出口删除

只要脚本尚未运行,新的运行就会成功。

trap 'rm "$lockfile"' EXIT

下面是一个更优雅、更安全、更快速、更脏的方法,结合了上面提供的答案。

使用

包括sh_lock_functions.sh 使用sh_lock_init初始化 使用sh_acquire_lock进行锁定 使用sh_check_lock检查锁 使用sh_remove_lock解锁

脚本文件

sh_lock_functions.sh

#!/bin/bash

function sh_lock_init {
    sh_lock_scriptName=$(basename $0)
    sh_lock_dir="/tmp/${sh_lock_scriptName}.lock" #lock directory
    sh_lock_file="${sh_lock_dir}/lockPid.txt" #lock file
}

function sh_acquire_lock {
    if mkdir $sh_lock_dir 2>/dev/null; then #check for lock
        echo "$sh_lock_scriptName lock acquired successfully.">&2
        touch $sh_lock_file
        echo $$ > $sh_lock_file # set current pid in lockFile
        return 0
    else
        touch $sh_lock_file
        read sh_lock_lastPID < $sh_lock_file
        if [ ! -z "$sh_lock_lastPID" -a -d /proc/$sh_lock_lastPID ]; then # if lastPID is not null and a process with that pid exists
            echo "$sh_lock_scriptName is already running.">&2
            return 1
        else
            echo "$sh_lock_scriptName stopped during execution, reacquiring lock.">&2
            echo $$ > $sh_lock_file # set current pid in lockFile
            return 2
        fi
    fi
    return 0
}

function sh_check_lock {
    [[ ! -f $sh_lock_file ]] && echo "$sh_lock_scriptName lock file removed.">&2 && return 1
    read sh_lock_lastPID < $sh_lock_file
    [[ $sh_lock_lastPID -ne $$ ]] && echo "$sh_lock_scriptName lock file pid has changed.">&2  && return 2
    echo "$sh_lock_scriptName lock still in place.">&2
    return 0
}

function sh_remove_lock {
    rm -r $sh_lock_dir
}

使用的例子

sh_lock_usage_example.sh

#!/bin/bash
. /path/to/sh_lock_functions.sh # load sh lock functions

sh_lock_init || exit $?

sh_acquire_lock
lockStatus=$?
[[ $lockStatus -eq 1 ]] && exit $lockStatus
[[ $lockStatus -eq 2 ]] && echo "lock is set, do some resume from crash procedures";

#monitoring example
cnt=0
while sh_check_lock # loop while lock is in place
do
    echo "$sh_scriptName running (pid $$)"
    sleep 1
    let cnt++
    [[ $cnt -gt 5 ]] && break
done

#remove lock when process finished
sh_remove_lock || exit $?

exit 0

特性

使用文件、目录和进程id的组合来锁定,以确保进程尚未运行 您可以检测脚本是否在删除锁之前停止(例如。进程终止,关闭,错误等) 您可以检查锁文件,并在丢失锁时使用它来触发进程关闭 Verbose,输出错误消息,以便于调试

我发现bmdhack的解决方案是最实用的,至少对我的用例来说是这样。使用flock和lockfile依赖于在脚本终止时使用rm删除lockfile,这不能总是得到保证(例如,kill -9)。

关于bmdhack的解决方案,我要改变一件小事:它强调删除锁文件,但没有说明这对于这个信号量的安全工作是不必要的。他使用kill -0来确保死进程的旧锁文件将被忽略/覆盖。

因此,我的简化解决方案是简单地将以下内容添加到单例的顶部:

## Test the lock
LOCKFILE=/tmp/singleton.lock 
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "Script already running. bye!"
    exit 
fi

## Set the lock 
echo $$ > ${LOCKFILE}

当然,这个脚本仍然有一个缺陷,即可能同时启动的进程存在竞争风险,因为锁定测试和设置操作不是单个原子动作。但是lhunath提出的使用mkdir的解决方案有一个缺陷,即被杀死的脚本可能会留下目录,从而阻止其他实例运行。

使用flock(1)在文件描述符上创建一个独占作用域锁。通过这种方式,您甚至可以同步脚本的不同部分。

#!/bin/bash

(
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200 || exit 1

  # Do stuff

) 200>/var/lock/.myscript.exclusivelock

这确保(和)之间的代码一次只由一个进程运行,并且该进程不会为锁等待太长时间。

注意:这个特殊的命令是util-linux的一部分。如果您运行的是Linux以外的操作系统,那么它可能不可用,也可能不可用。

我有一个基于文件名的简单解决方案

#!/bin/bash

MY_FILENAME=`basename "$BASH_SOURCE"`

MY_PROCESS_COUNT=$(ps a -o pid,cmd | grep $MY_FILENAME | grep -v grep | grep -v $$ | wc -
l)

if [ $MY_PROCESS_COUNT -ne 0  ]; then
  echo found another process
  exit 0
if

# Follows the code to get the job done.