确保一次仅运行一个Shell脚本实例的快捷方法


Answers:


109

这是一个使用锁文件并向其回显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


73
正如对花药答案的评论中已经提到的那样,它具有致命的缺陷-如果在检查和回显之间启动了另一个脚本,那么您就很敬酒了。
Paul Tomblin,

1
symlink技巧很巧妙,但是如果锁文件的所有者被杀死-9或系统崩溃,则仍然存在争用条件来读取symlink,请注意所有者已消失,然后将其删除。我坚持我的解决方案。
bmdhacks

9
在外壳程序中,可以使用flock(1)或lockfile(1)进行原子检查和创建。查看其他答案。
dmckee ---前主持人小猫,

3
请参阅我的答复,以获取进行原子检查和创建的便携式方法,而不必依赖于flock或lockfile等实用程序。
lhunath

2
这不是原子的,因此没有用。您需要一种用于测试和设置的原子机制。
K理查德·皮克斯利

214

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


11
什么是200?它在手册中说“ fd”,但我不知道这意味着什么。
chovy

4
@chovy“文件描述符”,一个指定打开文件的整数句柄。
Alex B

6
如果还有其他人想知道:语法为( command A ) command B调用子外壳command A。记录在tldp.org/LDP/abs/html/subshel​​ls.html。我还是不知道的子shell和命令B的调用的时机
扬-菲利普Gehrcke博士

1
我认为子外壳中的代码应该更像:if flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fi这样,如果发生超时(某些其他进程已锁定文件),则此脚本不会继续修改文件。可能...反参数是“但如果花了10秒钟而锁仍然不可用,则它将永远不可用”,大概是因为持有锁的过程没有终止(也许正在运行)在调试器下?)。
Jonathan Leffler

1
重定向到的文件只是锁起作用的位置文件夹,没有有意义的数据进入该文件。该exit是从里侧的部分( )。子进程结束时,将自动释放该锁,因为没有进程持有该锁。
clacke

158

测试“锁定文件”是否存在的所有方法都是有缺陷的。

为什么?因为无法检查文件是否存在并通过单个原子操作创建文件。因为这; 有一个竞争条件是WILL在互斥休息让你尝试。

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

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

有关所有详细信息,请参见出色的BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

如果您想使用过时的锁,可以使用fuser(1)。唯一的缺点是该操作大约需要一秒钟,因此它不是即时的。

这是我曾经编写的一个功能,可以使用热熔器解决问题:

#       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)


1
您可以结合使用该if ! mkdir部分与检查在lockdir中存储的PID(成功启动时)进程是否实际上正在运行,并且与用于保护烯烃的脚本相同。这也可以防止重启后重用PID,甚至不需要fuser
Tobias Kienzler 2012年

4
的确,mkdir没有将其定义为原子操作,因此“副作用”是文件系统的实现细节。我完全相信他,如果他说NFS不能以原子方式实现它。尽管我不怀疑您/tmp将成为NFS共享,并且可能由mkdir原子实现的fs提供。
lhunath 2012年

5
但是,有一种方法可以检查常规文件是否存在,如果不存在,则可以自动创建该文件:ln用于从另一个文件创建硬链接。如果您有无法保证的奇怪文件系统,则可以稍后检查新文件的索引节点,以查看它是否与原始文件相同。
Juan Cespedes 2014年

4
一种 “检查文件是否存在并通过单个原子操作创建文件的方法”,即open(... O_CREAT|O_EXCL)。您只需要一个合适的用户程序即可,例如lockfile-create(in lockfile-progs)或dotlockfile(in liblockfile-bin)。并确保正确清理(例如trap EXIT),或测试过时的锁(例如使用--use-pid)。
Toby Speight

5
“测试“锁定文件”是否存在的所有方法都是有缺陷的。为什么?因为无法检查文件是否存在并通过单个原子操作创建文件。”-要使其原子化,必须在以下位置进行:内核级别-并且它是在内核级别使用flock(1)linux.die.net/man/1/flock完成的,该代码从man版权日期开始至少存在于2006年。因此,我投了反对票(- 1),没有什么私人的,只是坚信使用内核开发人员提供的内核实现工具是正确的。
克雷格·希克斯

42

flock(2)系统调用周围有一个包装器,这简直就是flock(1)。这使得相对容易地可靠地获得排他锁,而不必担心清理等问题。手册页上有一些示例,说明如何在shell脚本中使用它。


3
flock()系统调用不是POSIX和对NFS挂载文件不起作用。
maxschlepzig 2011年

17
我使用Cron作业运行flock -x -n %lock file% -c "%command%"以确保仅执行一个实例。
Ryall 2012年

噢,他们应该使用flock(U)之类的东西,而不是虚构的flock(1)。..。它有些熟悉。。好像我在一两个时间之前就听说过。
肯特·克鲁克伯格'16

值得注意的是,flock(2)文档指定仅用于文件,而flock(1)文档指定用于文件或目录。flock(1)文档未明确说明如何在创建过程中指示差异,但我认为可以通过添加最终的“ /”来完成。无论如何,如果flock(1)可以处理目录,而flock(2)无法处理目录,则flock(1)不会仅在flock(2)上实现。
Craig Hicks

27

您需要原子操作,例如flock,否则最终将失败。

但是如果没有羊群怎么办。好吧,有mkdir。这也是一个原子操作。只有一个进程将导致成功的mkdir,所有其他进程将失败。

所以代码是:

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

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


1
同时运行几次(例如“ ./a.sh&./a.sh&./a.sh&./a.sh&./a.sh&./a.sh&./a.sh &“),该脚本将泄漏几次。
Nippysaurus

7
@Nippysaurus:此锁定方法不会泄漏。您看到的是初始脚本在所有副本启动之前终止,因此另一个脚本能够(正确)获得该锁。为了避免这种误报,请在此sleep 10之前添加一个rmdir并尝试再次级联-没有任何东西会“泄漏”。
阿索斯爵士(Sir Athos)

其他来源声称mkdir在某些文件系统(例如NFS)上不是原子的。顺便说一句,我见过在NFS上并发递归mkdir有时会导致jenkins矩阵作业出错的情况。所以我很确定是这种情况。但是对于要求不高的用例IMO,mkdir非常不错。
akostadinov

您可以对常规文件使用Bash的noclobber选项。
Palec 2014年

26

为了使锁定可靠,您需要原子操作。上述许多提议都不是原子的。如手册页所述,所提出的lockfile(1)实用程序看起来很有希望,即“耐NFS”。如果您的操作系统不支持lockfile(1),并且您的解决方案必须在NFS上运行,则您没有太多选择。

NFSv2具有两个原子操作:

  • 符号链接
  • 改名

使用NFSv3,create调用也是原子的。

在NFSv2和NFSv3下,目录操作不是原子的(请参阅Brent Callaghan的书“ NFS Illustrated”,ISBN 0-201-32570-5; Brent是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

Remove也不是原子的,因此首先重命名(它是原子的),然后再进行删除。

对于符号链接和重命名调用,两个文件名必须位于同一文件系统上。我的建议:仅使用简单的文件名(无路径),然后将文件和锁放在同一目录中。


NFS Illustrated的哪几页支持mkdir不是NFS原子的说法?
maxschlepzig 2011年

此技术的想法。我的新shell库中提供了一个shell互斥体实现:github.com/Offirmo/offirmo-shell-lib,请参见“ mutex”。lockfile如果可用,则使用它;如果不可用,则使用此symlink方法。
Offirmo 2012年

真好 不幸的是,该方法没有提供自动删除陈旧锁的方法。
理查德·汉森

对于两阶段解锁(mvrm),应该rm -f使用,而不是rm在两个进程P1,P2竞速的情况下使用?例如,P1从开始解锁mv,然后P2锁定,然后P2解锁(mvrm),最后P1尝试rm失败。
马特·沃利斯

1
@MattWallis通过$$${f}.deleteme文件名中包含,可以轻松缓解最后一个问题。
Stefan Majewsky 2014年

23

另一种选择是noclobber通过运行shell的选项set -C。然后,>如果该文件已经存在,就会失败。

简单来说:

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,但是它在我所有的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

非常新颖的方法...这似乎是使用锁定文件而非锁定目录来实现原子性的一种方法。
马特·考德威尔

好的方法。:-)在EXIT陷阱上,它应限制可以清除锁定文件的进程。例如:trap'if [[$(cat“ $ lockfile”)==“ $$”]]; 然后rm“ $ lockfile”; fi'EXIT
Kevin Seifert

1
锁文件不是NFS上的原子文件。这就是为什么人们转向使用锁定目录的原因。
K理查德·皮克斯利

20

您可以GNU Parallel为此使用它,因为它在称为时可用作互斥体sem。因此,具体来说,您可以使用:

sem --id SCRIPTSINGLETON yourScript

如果您也想超时,请使用:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

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

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

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


太糟糕的人不愿意拒绝无用的答案:这导致新的相关答案被埋在一堆垃圾中。
德米特里·格里戈列耶夫

4
我们只需要大量的投票。这是一个整洁而鲜为人知的答案。(尽管学究的OP希望快速而又肮脏,而这却是快速而干净的!)有关sem相关问题的更多信息unix.stackexchange.com/a/322200/199525
局部多云

16

对于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或期望数字0到31。将它们设置为其他值,我可以让我的脚本和批处理流对先前的批处理作业或脚本做出相应的反应。


2
您的脚本太冗长了,我认为它可能要短得多,但是总的来说,是的,您必须设置陷阱才能正确执行此操作。我还要添加SIGHUP。
mojuba 2012年

这很好用,除了似乎检查$ LOCK_DIR而删除$ __ lockdir。也许我应该建议删除锁时执行rm -r $ LOCK_DIR吗?
bevada 2015年

感谢您的建议。上面的代码经过了提升,并以伪代码方式放置,因此需要根据人们的使用情况进行调整。不过,我特意用命令rmdir便在我的情况下,命令rmdir安全去除目录only_if他们是空的。如果人们将诸如PID文件之类的资源放入其中,他们应该将其锁清除方式更改为更具攻击性rm -r $LOCK_DIR,甚至在必要时强制执行(如在特殊情况下(如保存相对的暂存文件),我也这样做)。干杯。
Mark Stinson

你测试了exit 1002吗?
Gilles Quenot '16

13

真正快速,真的脏吗?脚本顶部的这种单行代码将起作用:

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

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


我如何模拟此测试?有没有一种方法可以在一行中两次启动脚本,如果已经运行,则可能会收到警告?
rubo77'9

2
这根本不起作用!为什么要检查-gt 2?grep并不总是在ps的结果中发现自己!
rubo77

pgrep不在POSIX中。如果要使此工作可移植,则需要POSIX ps并处理其输出。
Palec

在OSX -c上不存在,您将不得不使用| wc -l。关于数字比较:-gt 1因为第一个实例可见,所以将对其进行检查。
本杰明·彼得

6

这是将原子目录锁定与通过PID检查过时锁定相结合的方法,如果过时则重新启动。而且,这不依赖任何bashisms。

#!/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

5

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


5

该示例在man flock中进行了解释,但是它需要一些改进,因为我们应该管理错误和退出代码:

   #!/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列出进程,按其名称进行过滤,附加过滤器grep -v grep用于删除寄生虫nad最后由grep -c对其进行计数。并与数字进行比较。其复杂而不确定


1
您可以使用ln -s,因为只有在不存在文件或符号链接的情况下(与mkdir相同),它才能创建符号链接。许多系统进程过去都使用符号链接,例如init或inetd。synlink保留进程ID,但实际上什么也没有指向。多年来,这种行为已经改变。流程使用群和信号量。
Znik

5

发布的现有答案依赖于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环境中都使用了上述方法。


4

以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
}

谢谢,自己在cygwin上尝试过,它通过了简单的测试。
ndemou

4

如果在该线程的其他地方已经介绍过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 

3
只是以为我会指出-x(写锁定)已默认设置。
Keldon Alleyne

-nexit 1在无法锁定时立即
发出通知-Anentropic

感谢@KeldonAlleyne,我更新了代码以删除“ -x”,因为它是默认的。
presto8

3

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

从联机帮助页:

lockfile可用于创建一个或多个信号文件。如果锁定文件不能(按指定顺序)创建所有指定文件,则它将等待睡眠时间(默认为8秒),然后重试最后一个未成功的文件。您可以指定重试次数,直到返回失败为止。如果重试次数为-1(默认值,即-r-1),则锁定文件将永远重试。


我们如何获得lockfile实用程序?
Offirmo 2012年

lockfile与一起分发procmail。也有另一种dotlockfile与去liblockfile包。他们俩都声称可以在NFS上可靠地工作。
Deadless先生,2014年

3

实际上,尽管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选项将确保如果文件已经存在,则重定向命令将失败。因此,重定向命令实际上是原子的-您可以使用一个命令编写并检查文件。您不需要删除文件末尾的锁定文件-陷阱将删除它。我希望这对以后会读的人有所帮助。

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


3

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

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

1
这对“ ps”输出很敏感,在我的机器上(Ubuntu 14.04,来自procps-ng版本3.3.9的/ bin / ps),“ ps axf”命令打印出会破坏字段编号的ascii树字符。这对我/bin/ps -a --format pid,cmd | awk -v pid=$$ '/'$(basename $0)'/ { if ($1!=pid) print $1; }'
有用

2

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

请注意,上面存储pid的某些解决方案忽略了pid可以环绕的事实。因此-仅检查存储的pid是否存在有效的进程是不够的,尤其是对于长时间运行的脚本而言。

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

当新脚本启动时,如果未能创建锁定文件,则它会验证创建锁定文件的进程是否还在。如果不是这样,我们假设原始进程死于不合时宜的死亡,并留下了过时的锁定文件。然后,新脚本获得了锁定文件的所有权,一切都恢复了正常。

应该可以在多个平台上使用多个shell。快速,便携式和简单。

#!/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

我为您+1提供了其他解决方案。虽然在AIX中它也不起作用(> ps -eo pid,ppid,lstart $$ | tail -1 ps:带有-o的无效列表)不是HP-UX(> ps -eo pid,ppid,lstart $$ | tail -1 ps:非法选项-o)。谢谢。
塔加尔

2

在脚本开头添加此行

[ "${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设置正确,则在上一次迭代时聚集成功,可以继续。如果锁正忙,它将失败,并显示可配置的退出代码。

它在Debian 7上似乎不起作用,但在实验性util-linux 2.25软件包中似乎可以再次使用。它写“群:...文本文件忙”。可以通过在脚本上禁用写权限来覆盖它。


1

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


1

我发现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}

当然,此脚本仍然存在一个缺陷,即可能同时启动的进程存在竞争危险,因为锁定测试和置位操作不是单个原子动作。但是,通过使用mkdir来解决此问题的建议解决方案存在一个缺陷,即被杀死的脚本可能会留下目录,从而阻止其他实例运行。


1

信号灯实用程序使用flock(由presto8如上所讨论的,例如)来实现一个计数信号。它启用所需的任意数量的并发进程。我们使用它来限制各种队列工作进程的并发级别。

这就像SEM多少重量更轻。(完整披露:我发现该sem太重了,无法满足我们的需求,并且没有可用的简单计数信号量实用程序。)


1

一个带有flock(1)但没有子shell的示例。flock()ed文件/ tmp / foo不会被删除,但这无关紧要,因为它获得了flock()和unflock()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

1

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

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运行。


1
没有陷阱调用(或在正常情况下至少没有清理结束的情况下),您会遇到误报错误,即上一次运行后留下了锁文件,并且PID被以后的另一个进程重用。(并且在最坏的情况下,它被赋予了一个长期运行的过程,例如apache...。)
Philippe Chaintreuil

1
我同意,我的方法有缺陷,确实需要陷阱。我已经更新了解决方案。我仍然更喜欢没有外部依赖。
维立德·维斯(Filidor Wiese)'18年

1

使用进程的锁要强大得多,并且还可以处理不舒适的出口。只要进程正在运行,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 

1

我使用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

很高兴看到内存中存在进程(无论进程的状态如何);但这对我有用。


0

羊群路径是要走的路。想想脚本突然死掉会发生什么。在羊群的情况下,您只需松散羊群,但这不是问题。另外,请注意,一个恶作剧的方法是在脚本本身上大批蜂拥而至。但是,这当然可以让您提前全面解决权限问题。


0

又快又脏?

#!/bin/sh

if [ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile

7
取决于使用方式,这可能不是问题,但在测试锁与创建锁之间存在竞争条件,因此可以同时启动两个脚本。如果一个首先终止,则另一个将保持运行而没有锁定文件。
TimB

3
C News,它教了我很多有关可移植外壳脚本的知识,曾经制作了一个lock。$$文件,然后尝试将其链接为“ lock”-如果链接成功,则说明您拥有该锁,否则就删除了lock。$$。并退出了。
Paul Tomblin,

这是一种非常好的方法,除非您仍然遇到一些麻烦,并且如果不删除锁文件,则需要手动删除锁文件。
马修·沙利

2
快速又肮脏,这就是他要的:)
Aupajo,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.