如何快速简单地确保一个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,则可能无法使用该命令。
( 命令 A ) 命令 B
会为 命令 A
调用一个子 shell。详见 http://tldp.org/LDP/abs/html/subshells.html。但我仍不确定子 shell 和命令 B 的调用时机。 - Dr. Jan-Philip Gehrckeif flock -x -w 10 200; then ...Do stuff...; else echo "Failed to lock file" 1>&2; fi
这样,如果超时发生(其他进程已锁定文件),则此脚本不会继续修改文件。可能的反驳是:“但如果它已经花费了10秒钟而锁仍然不可用,那么它永远不会可用”,这可能是因为持有锁的进程没有终止(也许它正在被调试器运行?)。 - Jonathan Leffler测试“锁文件”是否存在的朴素方法是有缺陷的。
为什么呢?因为它们不会在单个原子操作中检查文件是否存在并创建它。由于这个原因,就会出现竞争条件,一定会导致你试图进行互斥的尝试失败。
相反,您可以使用mkdir
。 mkdir
如果目录不存在则创建一个目录,并在目录已经存在时设置一个退出代码。更重要的是,它会在单个原子操作中执行所有操作,在这种情况下非常适用。
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)非常方便。唯一的缺点是该操作需要大约一秒钟的时间,因此不是即时的。
以下是我曾经编写的使用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)。
if ! mkdir
部分与检查存储在lockdir中(在成功启动时)的PID对应的进程是否正在运行并且与脚本的稳定性保护相同结合起来。这样还可以防止在重启后重新使用PID,甚至不需要fuser
命令。 - Tobias Kienzlermkdir
的文件系统提供。 - lhunathln
从另一个文件创建硬链接。如果你使用的是奇怪的文件系统,无法保证该方式能正常工作,你可以之后检查新文件的i节点(inode),看它是否与原文件相同。 - Juan Cespedesopen(... O_CREAT|O_EXCL)
。您只需要一个合适的用户程序来执行此操作,例如lockfile-create
(在lockfile-progs
中)或dotlockfile
(在liblockfile-bin
中)。确保正确清理文件(例如,使用trap EXIT
),或测试过期的锁定(例如,使用--use-pid
)。 - Toby Speight以下是使用 锁文件 并将 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脚本中使用它的示例。
flock()
系统调用不符合 POSIX 标准,并且不能用于 NFS 挂载的文件。 - maxschlepzigflock -x -n %lock file% -c "%command%"
确保只有一个实例在执行。 - Ryallwhile ! 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
移除操作也不是原子的,因此首先进行重命名(这是原子性操作),然后再进行删除。
对于符号链接和重命名调用,两个文件名都必须驻留在同一文件系统上。我的建议是:仅使用简单的文件名(没有路径)并将文件和锁定放入同一目录中。
mv
,rm
),如果两个进程P1,P2同时进行,应该使用rm -f
而不是rm
吗?例如,P1开始使用mv
解锁,然后P2锁定,然后P2解锁(mv
和rm
都执行),最后P1尝试rm
并失败。 - Matt Wallis${f}.deleteme
文件名中包含$$
来缓解。 - Stefan Majewsky您需要一个原子操作,比如flock,否则这将最终失败。
但如果flock不可用怎么办?嗯,有mkdir。那也是原子操作。只有一个进程会成功创建目录,其他所有进程都会失败。
所以代码是:
if mkdir /var/lock/.myscript.exclusivelock
then
# do stuff
:
rmdir /var/lock/.myscript.exclusivelock
fi
你需要注意处理过期的锁,否则在崩溃后你的脚本将永远无法运行。
rmdir
之前加入sleep 10
,然后再次级联尝试 - 没有东西会“泄漏”。 - Sir Athos当作为sem
调用时,您可以使用GNU Parallel
作为互斥量。因此,具体而言,您可以使用:
sem --id SCRIPTSINGLETON yourScript
如果你也想使用超时功能,可以使用以下方法:
sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript
如果信号量在超时时间内未被释放,则超时时间<0表示退出而不运行脚本,超时时间>0表示仍然运行脚本。
请注意,您应该给它一个名称(使用--id
),否则它将默认为控制终端。
GNU Parallel
在大多数Linux/OSX/Unix平台上都非常简单,只需安装一个Perl脚本即可。
sem
的更多信息,请参见相关问题http://unix.stackexchange.com/a/322200/199525。 - Partly Cloudy另一种选项是通过运行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
bash
手册没有说明它必须使用特定标志打开文件,只是说noclobber不会覆盖现有文件。在bash
中有多少代码路径以及在不同情况下可能使用哪些给定标志是不清楚的。目前这个答案可能是完全正确的,但没有规范可以证明这一点,也没有维护者承诺坚持这一点。在我看来,应该以此答案为基础创建锁文件,而不会危及现有锁文件,然后使用flock
或类似工具获取锁。 - AnyDev对于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的数字预期。将它们设置为其他值,我可以使我的脚本和批处理流对先前的批处理作业或脚本做出相应。
rm -r $LOCK_DIR
,甚至必要时强制执行(在特殊情况下,例如保留相对临时文件时,我也这样做)。干杯。 - Mark Stinsonexit 1002
吗? - Gilles Quénotfun() { return 255; }; fun; echo $?
与fun() { return 256; }; fun; echo $?
。 - ACK_stoverflow真的 快速且 简单粗暴? 把这个一行代码放在你的脚本顶部即可:
[[ $(pgrep -c "`basename \"$0\"`") -gt 1 ]] && exit
当然,只要确保您的脚本名称是唯一的即可。 :)
pgrep
不在 POSIX 中。如果您想要在不同系统上使用它,您需要使用 POSIX 的 ps
命令并处理其输出。 - Palec-c
不存在,你需要使用| wc -l
。关于数字比较:检查-gt 1
是因为第一个实例看到了自己。 - Benjamin Peter