确保一次只运行 shell 脚本的一个实例的快速而肮脏的方法


什么是

一种快速而肮脏的方法,以确保在给定时间只有一个 shell 脚本实例在运行?

使用 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,则它可能可用,也可能不可用。

测试

"锁定文件"是否存在的幼稚方法是有缺陷的。

为什么? 因为他们不检查文件是否存在并在单个原子操作中创建它。 正因为如此;有一个竞争条件将使您的相互排斥尝试中断。

相反,您可以使用 mkdir . 如果目录尚不存在,mkdir创建一个目录,如果存在,则设置退出代码。 更重要的是,它在单个原子作用中完成所有这些操作,使其非常适合这种情况。

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

有关所有详细信息,请参阅优秀的 Bash常见问题解答:http://mywiki.wooledge.org/BashFAQ/045

如果你想照顾陈旧的锁,热熔器(1(会派上用场。 这里唯一的缺点是操作大约需要一秒钟,所以它不是即时的。

这是我曾经写过的一个函数,它使用fuser解决了这个问题:

#       mutex file
#
# Open a mutual exclusion lock on the file, unless another process already owns one.
#
# If the file is already locked by another process, the operation fails.
# This function defines a lock on a file as having a file descriptor open to the file.
# This function uses FD 9 to open a lock on the file.  To release the lock, close FD 9:
# exec 9>&-
#
mutex() {
    local file=$1 pid pids 
    exec 9>>"$file"
    { pids=$(fuser -f "$file"); } 2>&- 9>&- 
    for pid in $pids; do
        [[ $pid = $$ ]] && continue
        exec 9>&- 
        return 1 # Locked by a pid.
    done 
}

您可以在脚本中使用它,如下所示:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

如果你不关心可移植性(这些解决方案应该适用于几乎任何 UNIX 机器(,Linux 的 fuser(1( 提供了一些额外的选项,还有 flock(1(。

下面是一个使用锁文件并将 PID 回显到其中的实现。如果在删除 pidfile 之前终止进程,这将作为一种保护:

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi
# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}
# do stuff
sleep 1000
rm -f ${LOCKFILE}

这里的诀窍是kill -0它不提供任何信号,而只是检查是否存在具有给定PID的进程。 此外,对trap的调用将确保即使您的进程被终止(kill -9 除外(,也会删除锁定文件

在 flock(2( 系统调用周围有一个包装器,难以想象地称为 flock(1(。这使得可靠地获得独占锁相对容易,而不必担心清理等。手册页上有一些关于如何在 shell 脚本中使用它的示例。

为了使锁定可靠,您需要原子操作。上述许多建议不是原子的。建议的 lockfile(1( 工具作为手册页看起来很有前途提到,它的"耐 NFS"。如果您的操作系统不支持锁定文件(1( 和您的解决方案必须在 NFS 上运行,您没有太多选择....

NFSv2 有两个原子操作:

  • 符号链接
  • 重命名

使用 NFSv3,创建调用也是原子的。

目录操作在 NFSv2 和 NFSv3 下不是原子的(请参阅布伦特卡拉汉的"NFS 插图"一书,ISBN 0-201-32570-5;布伦特是Sun的NFS老手(。

知道了这一点,你可以为文件和目录实现旋转锁(在shell中,而不是PHP(:

锁定电流目录:

while ! ln -s . lock; do :; done

锁定文件:

while ! ln -s ${f} ${f}.lock; do :; done

解锁当前目录(假设,正在运行的进程确实获得了锁(:

mv lock deleteme && rm deleteme

解锁文件(假设正在运行的进程确实获得了锁(:

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme
删除也不是原子的

,因此首先重命名(这是原子的(,然后删除。

对于符号链接和重命名调用,两个文件名必须驻留在同一个文件系统上。我的建议:只使用简单的文件名(没有路径(,并将文件和锁放在同一个目录中。

你需要一个原子操作,比如 flock,否则这最终会失败。

但是,如果羊群不可用怎么办。嗯,有 mkdir。这也是一个原子操作。只有一个进程会导致成功的 mkdir,所有其他进程都会失败。

所以代码是:

if mkdir /var/lock/.myscript.exclusivelock
then
  # do stuff
  :
  rmdir /var/lock/.myscript.exclusivelock
fi

您需要处理过时的锁,否则您的脚本将永远不会再次运行崩溃。

您可以使用GNU Parallel,因为它在调用时用作互斥锁 sem 。因此,具体而言,您可以使用:

sem --id SCRIPTSINGLETON yourScript

如果您还想要超时,请使用:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

超时 <0 表示退出而不运行脚本,如果信号量未在超时内释放,超时>0 表示仍然运行脚本。

请注意,您应该给它一个名称(带 --id (,否则它默认为控制终端。

GNU Parallel在大多数Linux/OSX/Unix平台上是一个非常简单的安装 - 它只是一个Perl脚本。

另一种选择是通过运行 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

这会导致外壳调用:

open(pathname, O_CREAT|O_EXCL)

以原子方式创建文件,如果文件已存在,则失败。


根据对 BashFAQ 045 的评论,这可能会在 ksh88 中失败,但它适用于我的所有外壳:

$ 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

对于 shell 脚本,我倾向于使用 mkdir 而不是flock,因为它使锁更便携。

无论哪种方式,使用set -e都是不够的。仅当任何命令失败时,才会退出脚本。您的锁仍然会被留下。

为了正确清理锁,您确实应该将陷阱设置为类似以下内容的伪代码(提升,简化和未经测试,但来自活跃使用的脚本(:

#=======================================================================
# Predefined Global Variables
#=======================================================================
TMPDIR=/tmp/myapp
[[ ! -d $TMP_DIR ]] 
    && mkdir -p $TMP_DIR 
    && chmod 700 $TMPDIR
LOCK_DIR=$TMP_DIR/lock
#=======================================================================
# Functions
#=======================================================================
function mklock {
    __lockdir="$LOCK_DIR/$(date +%s.%N).$$" # Private Global. Use Epoch.Nano.PID
    # If it can create $LOCK_DIR then no other instance is running
    if $(mkdir $LOCK_DIR)
    then
        mkdir $__lockdir  # create this instance's specific lock in queue
        LOCK_EXISTS=true  # Global
    else
        echo "FATAL: Lock already exists. Another copy is running or manually lock clean up required."
        exit 1001  # Or work out some sleep_while_execution_lock elsewhere
    fi
}
function rmlock {
    [[ ! -d $__lockdir ]] 
        && echo "WARNING: Lock is missing. $__lockdir does not exist" 
        || rmdir $__lockdir
}
#-----------------------------------------------------------------------
# Private Signal Traps Functions {{{2
#
# DANGER: SIGKILL cannot be trapped. So, try not to `kill -9 PID` or 
#         there will be *NO CLEAN UP*. You'll have to manually remove 
#         any locks in place.
#-----------------------------------------------------------------------
function __sig_exit {
    # Place your clean up logic here 
    # Remove the LOCK
    [[ -n $LOCK_EXISTS ]] && rmlock
}
function __sig_int {
    echo "WARNING: SIGINT caught"    
    exit 1002
}
function __sig_quit {
    echo "SIGQUIT caught"
    exit 1003
}
function __sig_term {
    echo "WARNING: SIGTERM caught"    
    exit 1015
}
#=======================================================================
# Main
#=======================================================================
# Set TRAPs
trap __sig_exit EXIT    # SIGEXIT
trap __sig_int INT      # SIGINT
trap __sig_quit QUIT    # SIGQUIT
trap __sig_term TERM    # SIGTERM
mklock
# CODE
exit # No need for cleanup code here being in the __sig_exit trap function

这是将要发生的事情。所有陷阱都会产生一个出口,因此功能__sig_exit将始终发生(SIGKILL除外(,从而清理您的锁。

注意:我的退出值不是低值。为什么?各种批处理系统产生或期望数字 0 到 31。将它们设置为其他内容,我可以让我的脚本和批处理流相应地响应以前的批处理作业或脚本。

真的 很快,真的很脏?脚本顶部的这一行代码将起作用:

[[ $(pgrep -c "`basename "$0"`") -gt 1 ]] && exit

当然,只要确保脚本名称是唯一的。 :)

这是一种将原子目录锁定与通过 PID 检查过时锁定并在过时时重新启动的方法相结合。此外,这不依赖于任何巴希姆。

#!/bin/dash
SCRIPTNAME=$(basename $0)
LOCKDIR="/var/lock/${SCRIPTNAME}"
PIDFILE="${LOCKDIR}/pid"
if ! mkdir $LOCKDIR 2>/dev/null
then
    # lock failed, but check for stale one by checking if the PID is really existing
    PID=$(cat $PIDFILE)
    if ! kill -0 $PID 2>/dev/null
    then
       echo "Removing stale lock of nonexistent PID ${PID}" >&2
       rm -rf $LOCKDIR
       echo "Restarting myself (${SCRIPTNAME})" >&2
       exec "$0" "$@"
    fi
    echo "$SCRIPTNAME is already running, bailing out" >&2
    exit 1
else
    # lock successfully acquired, save PID
    echo $$ > $PIDFILE
fi
trap "rm -rf ${LOCKDIR}" QUIT INT TERM EXIT

echo hello
sleep 30s
echo bye

在已知位置创建一个锁定文件,并在脚本启动时检查是否存在?如果有人试图跟踪阻止脚本执行的错误实例,将 PID 放在文件中可能会有所帮助。

这个例子在 man flock 中解释过,但它需要一些改进,因为我们应该管理 bug 和退出代码:

   #!/bin/bash
   #set -e this is useful only for very stupid scripts because script fails when anything command exits with status more than 0 !! without possibility for capture exit codes. not all commands exits >0 are failed.
( #start subprocess
  # Wait for lock on /var/lock/.myscript.exclusivelock (fd 200) for 10 seconds
  flock -x -w 10 200
  if [ "$?" != "0" ]; then echo Cannot lock!; exit 1; fi
  echo $$>>/var/lock/.myscript.exclusivelock #for backward lockdir compatibility, notice this command is executed AFTER command bottom  ) 200>/var/lock/.myscript.exclusivelock.
  # Do stuff
  # you can properly manage exit codes with multiple command and process algorithm.
  # I suggest throw this all to external procedure than can properly handle exit X commands
) 200>/var/lock/.myscript.exclusivelock   #exit subprocess
FLOCKEXIT=$?  #save exitcode status
    #do some finish commands
exit $FLOCKEXIT   #return properly exitcode, may be usefull inside external scripts

您可以使用另一种方法,列出我过去使用的进程。但这比上面的方法更复杂。您应该按 ps 列出进程,按其名称过滤,用于删除寄生虫 nad 的额外过滤器 grep -v grep 最后通过 grep -c 计数。 并与数字进行比较。它复杂且不确定

发布的

现有答案要么依赖于 CLI 实用程序flock,要么没有正确保护锁定文件。 flock 实用程序并非在所有非 Linux 系统(即 FreeBSD(上都可用,并且在 NFS 上无法正常工作。

在我早期的系统管理和系统开发中,有人告诉我,创建锁定文件的一种安全且相对便携的方法是使用 mkemp(3)mkemp(1) 创建一个临时文件,将标识信息写入临时文件(即 PID(,然后将临时文件硬链接到锁定文件。 如果链接成功,则表示您已成功获取锁。

在 shell 脚本中使用锁时,我通常会将 obtain_lock() 函数放在共享配置文件中,然后从脚本中获取它。下面是我的锁定功能的示例:

obtain_lock()
{
  LOCK="${1}"
  LOCKDIR="$(dirname "${LOCK}")"
  LOCKFILE="$(basename "${LOCK}")"
  # create temp lock file
  TMPLOCK=$(mktemp -p "${LOCKDIR}" "${LOCKFILE}XXXXXX" 2> /dev/null)
  if test "x${TMPLOCK}" == "x";then
     echo "unable to create temporary file with mktemp" 1>&2
     return 1
  fi
  echo "$$" > "${TMPLOCK}"
  # attempt to obtain lock file
  ln "${TMPLOCK}" "${LOCK}" 2> /dev/null
  if test $? -ne 0;then
     rm -f "${TMPLOCK}"
     echo "unable to obtain lockfile" 1>&2
     if test -f "${LOCK}";then
        echo "current lock information held by: $(cat "${LOCK}")" 1>&2
     fi
     return 2
  fi
  rm -f "${TMPLOCK}"
  return 0;
};
以下是

如何使用锁定功能的示例:

#!/bin/sh
. /path/to/locking/profile.sh
PROG_LOCKFILE="/tmp/myprog.lock"
clean_up()
{
  rm -f "${PROG_LOCKFILE}"
}
obtain_lock "${PROG_LOCKFILE}"
if test $? -ne 0;then
   exit 1
fi
trap clean_up SIGHUP SIGINT SIGTERM
# bulk of script
clean_up
exit 0
# end of script

请记住在脚本中的任何退出点调用clean_up

我已经在 Linux 和 FreeBSD 环境中使用了上述方法。

如果 flock 的限制(已经在此线程的其他地方描述过(对您来说不是问题,那么这应该有效:

#!/bin/bash
{
    # exit if we are unable to obtain a lock; this would happen if 
    # the script is already running elsewhere
    # note: -x (exclusive) is the default
    flock -n 100 || exit
    # put commands to run here
    sleep 100
} 100>/tmp/myjob.lock 

在脚本开头添加此行

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

这是来自人群的样板代码。

如果你想要更多日志记录,请使用这个

[ "${FLOCKER}" != "$0" ] && { echo "Trying to start build from queue... "; exec bash -c "FLOCKER='$0' flock -E $E_LOCKED -en '$0' '$0' '$@' || if [ "$?" -eq $E_LOCKED ]; then echo 'Locked.'; fi"; } || echo "Lock is free. Completing."

这将使用flock实用程序设置和检查锁。此代码通过检查 FLOCKER 变量来检测它是否首次运行,如果未设置为脚本名称,则它尝试使用 flock 递归地再次启动脚本并初始化 FLOCKER 变量,如果 FLOCKER 设置正确,则 flock 在上一次迭代中成功,可以继续。如果锁定繁忙,则失败并显示可配置的退出代码。

它似乎不适用于 Debian 7,但似乎在实验性的 util-linux 2.25 软件包中再次工作。它写道"羊群:...文本文件繁忙"。可以通过禁用脚本的写入权限来覆盖它。

当针对 Debian 机器时,我发现 lockfile-progs 软件包是一个很好的解决方案。 procmail还附带了一个lockfile工具。然而,有时我被这些都困住了。

这是我的解决方案,它使用原子性的mkdir和PID文件来检测过时的锁。此代码目前正在Cygwin设置上生产,并且运行良好。

要使用它,只需在需要获得对某些内容的独占访问权限时调用exclusive_lock_require。可选的锁名称参数允许您在不同脚本之间共享锁。还有两个较低级别的函数(exclusive_lock_tryexclusive_lock_retry(,如果你需要更复杂的东西。

function exclusive_lock_try() # [lockname]
{
    local LOCK_NAME="${1:-`basename $0`}"
    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"
    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo ""$LOCK_NAME" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo ""$LOCK_NAME" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf "$LOCK_DIR"; exit;" EXIT
    return 0 # got lock
}
function exclusive_lock_retry() # [lockname] [retries] [delay]
{
    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"
    local TRIES=0
    local LOCK_RETVAL
    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do
        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))
        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"
        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi
    done
    return "$LOCK_RETVAL"
}
function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}

一些 unix 具有与已经提到的flock非常相似的lockfile

从手册页:

锁文件可用于创建一个 或更多信号量文件。 如果锁定- 文件无法创建所有指定的 文件(按指定顺序(,它 等待睡眠时间(默认为 8( 秒并重试最后一个文件 没有成功。 您可以指定 在 之前要执行的重试次数 返回失败。 如果数字 重试次数为 -1(默认值,即 -r-1( 锁定文件将永远重试。

我使用一种处理过时锁定文件的简单方法。

请注意,上述一些存储 pid 的解决方案忽略了 pid 可以环绕的事实。所以 - 仅仅检查存储的 pid 是否有有效的进程是不够的,特别是对于长时间运行的脚本。

我使用 noclobber 来确保一次只有一个脚本可以打开并写入锁定文件。此外,我存储了足够的信息来唯一标识锁定文件中的进程。我定义数据集以唯一标识要pid,ppid,lstart的进程。

当新脚本启动时,如果它无法创建锁定文件,则会验证创建锁定文件的进程是否仍然存在。如果没有,我们假设原始进程不优雅地死亡,并留下一个陈旧的锁定文件。然后,新脚本将获得锁定文件的所有权,一切正常

应该在多个平台上使用多个外壳。快速、便携且简单。

#!/usr/bin/env sh
# Author: rouble
LOCKFILE=/var/tmp/lockfile #customize this line
trap release INT TERM EXIT
# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}
# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}
# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi
我想

取消锁文件、锁文件、特殊锁定程序甚至pidof,因为它在所有 Linux 安装中都找不到。还希望拥有最简单的代码(或至少尽可能少的行(。最简单的if语句,在一行中:

if [[ $(ps axf | awk -v pid=$$ '$1!=pid && $6~/'$(basename $0)'/{print $1}') ]]; then echo "Already running"; exit; fi

实际上,尽管 bmdhacks 的答案几乎是好的,但在第一次检查锁定文件之后和写入它之前,第二个脚本运行的可能性很小。因此,它们都将写入锁定文件,并且它们都将运行。以下是使其确定工作的方法:

lockfile=/var/lock/myscript.lock
if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null ; then
  trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
else
  # or you can decide to skip the "else" part if you want
  echo "Another instance is already running!"
fi

noclobber选项将确保如果文件已存在,重定向命令将失败。所以重定向命令实际上是原子的 - 你用一个命令编写和检查文件。您不需要删除文件末尾的锁定文件 - 它将被陷阱删除。我希望这对以后阅读它的人有所帮助。

附言我没有看到 Mikel 已经正确回答了这个问题,尽管他没有包含 trap 命令以减少例如使用 Ctrl-C 停止脚本后锁定文件留下的机会。所以这是完整的解决方案

一个有 flock(1( 但没有子壳的例子。 flock((ed 文件/tmp/foo 永远不会被删除,但这并不重要,因为它得到了 flock(( 和 un-flock((ed。

#!/bin/bash
exec 9<> /tmp/foo
flock -n 9
RET=$?
if [[ $RET -ne 0 ]] ; then
    echo "lock failed, exiting"
    exit
fi
#Now we are inside the "critical section"
echo "inside lock"
sleep 5
exec 9>&- #close fd 9, and release lock
#The part below is outside the critical section (the lock)
echo "lock released"
sleep 5

这一行答案来自一个相关的 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.

我对现有答案有以下问题:

  • 一些答案尝试清理锁定文件,然后不得不处理由例如突然崩溃/重新启动引起的陈旧锁定文件。IMO这是不必要的复杂。让锁定文件保留。
  • 一些答案使用脚本文件本身$0$BASH_SOURCE进行锁定,通常参考man flock中的示例。当由于更新或编辑而导致脚本被替换时,即使另一个对已删除文件持有锁的实例仍在运行,这也将失败,下次运行以打开并锁定新脚本文件。
  • 很少有答案使用固定的文件描述符。这并不理想。我不想依赖这将如何表现,例如打开锁定文件失败但处理不当并尝试锁定从父进程继承的不相关的文件描述符。另一个失败的情况是为第三方二进制文件注入锁定包装器,该二进制文件不处理锁定本身,但固定的文件描述符可能会干扰文件描述符传递给子进程。
  • 我拒绝使用进程查找已经运行的脚本名称的答案。它有几个原因,例如但不限于可靠性/原子性、解析输出以及具有执行多个相关功能的脚本,其中一些不需要锁定。

这个答案是:

  • 依靠flock因为它让内核提供锁定......提供的锁定文件是以原子方式创建的,而不是替换的。
  • 假设并依赖于存储在本地文件系统上的锁定文件,而不是 NFS。
  • 将锁定文件状态更改为"不表示正在运行的实例的任何内容"。它的作用纯粹是防止两个并发实例创建具有相同名称的文件并替换另一个实例的副本。锁定文件不会被删除,它会留下,并且可以在重新启动后幸存下来。锁定是通过flock而不是通过锁定文件的存在来指示的。
  • 假设 bash shell,如问题标记的那样。

不是单行,但没有注释或错误消息,它足够小:

#!/bin/bash
LOCKFILE=/var/lock/TODO
set -o noclobber
exec {lockfd}<> "${LOCKFILE}" || exit 1
set +o noclobber # depends on what you need
flock --exclusive --nonblock ${lockfd} || exit 1

但我更喜欢评论和错误消息:

#!/bin/bash
# TODO Set a lock file name
LOCKFILE=/var/lock/myprogram.lock
# Set noclobber option to ensure lock file is not REPLACED.
set -o noclobber
# Open lock file for R+W on a new file descriptor
# and assign the new file descriptor to "lockfd" variable.
# This does NOT obtain a lock but ensures the file exists and opens it.
exec {lockfd}<> "${LOCKFILE}" || {
  echo "pid=$$ failed to open LOCKFILE='${LOCKFILE}'" 1>&2
  exit 1
}
# TODO!!!! undo/set the desired noclobber value for the remainder of the script
set +o noclobber
# Lock on the allocated file descriptor or fail
# Adjust flock options e.g. --noblock as needed
flock --exclusive --nonblock ${lockfd} || {
  echo "pid=$$ failed to obtain lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'" 1>&2
  exit 1
}
# DO work here
echo "pid=$$ obtained exclusive lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'"
# Can unlock after critical section and do more work after unlocking
#flock -u ${lockfd};
# if unlocking then might as well close lockfd too
#exec {lockfd}<&-

PID和锁文件绝对是最可靠的。 当您尝试运行该程序时,它可以检查锁定文件,如果它存在,它可以使用 ps 来查看进程是否仍在运行。 如果不是,脚本可以启动,将锁定文件中的 PID 更新为其自己的 PID。

我发现 bmdhack 的解决方案是最实用的,至少对于我的用例来说是这样。使用 flock 和 lockfile 依赖于在脚本终止时使用 rm 删除锁定文件,这并不总是可以保证的(例如,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(如上所述,例如通过presto8(来实现计数信号量。它支持您想要的任何特定数量的并发进程。我们使用它来限制各种队列工作进程的并发级别。

它就像sem,但重量更轻。(完全披露:我在发现 sem 对于我们的需求来说太重并且没有简单的计数信号量实用程序可用后编写了它。

已经回答了一百万次,但另一种方式,不需要外部依赖:

LOCK_FILE="/var/lock/$(basename "$0").pid"
trap "rm -f ${LOCK_FILE}; exit" INT TERM EXIT
if [[ -f $LOCK_FILE && -d /proc/`cat $LOCK_FILE` ]]; then
   // Process already exists
   exit 1
fi
echo $$ > $LOCK_FILE

每次它将当前 PID ($$( 写入锁定文件并在脚本启动时检查进程是否正在使用最新的 PID 运行。

使用进程的锁要强大得多,并且还可以处理不优雅的退出。只要进程正在运行,lock_file就会保持打开状态。一旦进程存在(即使它被杀死(,它将被关闭(通过外壳(。我发现这非常有效:

lock_file=/tmp/`basename $0`.lock
if fuser $lock_file > /dev/null 2>&1; then
    echo "WARNING: Other instance of $(basename $0) running."
    exit 1
fi
exec 3> $lock_file 

我使用 oneliner @ 脚本的最开头:

#!/bin/bash
if [[ $(pgrep -afc "$(basename "$0")") -gt "1" ]]; then echo "Another instance of "$0" has already been started!" && exit; fi
.
the_beginning_of_actual_script
很高兴

看到内存中存在进程(无论进程的状态如何(;但它为我完成了这项工作。

相关内容

  • 没有找到相关文章

最新更新