确保在给定时间只运行一个 shell 脚本实例的快速而简单的方法是什么?
41 回答
用于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
有关所有详细信息,请参阅出色的 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)。
这是一个使用锁定文件并将 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”。如果您的操作系统不支持 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
删除也不是原子的,因此首先是重命名(这是原子的),然后是删除。
对于 symlink 和 rename 调用,两个文件名必须驻留在同一个文件系统上。我的建议:仅使用简单的文件名(无路径)并将文件和锁定放入同一目录。
你需要一个原子操作,比如flock,否则最终会失败。
但是如果羊群不可用怎么办。那么有mkdir。这也是一个原子操作。只有一个进程会导致 mkdir 成功,所有其他进程都会失败。
所以代码是:
if mkdir /var/lock/.myscript.exclusivelock
then
# do stuff
:
rmdir /var/lock/.myscript.exclusivelock
fi
您需要处理过时的锁,否则在崩溃后您的脚本将永远不会再次运行。
另一种选择是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
这会导致 shell 调用:
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
您可以使用GNU Parallel
它,因为它在调用时用作互斥锁sem
。因此,具体而言,您可以使用:
sem --id SCRIPTSINGLETON yourScript
如果您也想要超时,请使用:
sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript
超时<0意味着如果信号量没有在超时内释放,则退出而不运行脚本,超时>0意味着无论如何都运行脚本。
请注意,您应该给它一个名称(使用--id
),否则它默认为控制终端。
GNU Parallel
在大多数 Linux/OSX/Unix 平台上安装非常简单——它只是一个 Perl 脚本。
对于 shell 脚本,我倾向于使用mkdir
over 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 检查陈旧锁定并在陈旧时重新启动相结合的方法。此外,这不依赖于任何 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
在已知位置创建锁定文件并在脚本启动时检查是否存在?如果有人试图追踪阻止脚本执行的错误实例,将 PID 放入文件中可能会有所帮助。
这个例子在 manflock 中有解释,但它需要一些改进,因为我们应该管理错误和退出代码:
#!/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 用于删除寄生虫,最后按 grep -c 计算。并与数字比较。它的复杂和不确定
发布的现有答案要么依赖于 CLI 实用程序flock
,要么没有正确保护锁定文件。flock 实用程序并非在所有非 Linux 系统(即 FreeBSD)上都可用,并且不能在 NFS 上正常工作。
在我早期的系统管理和系统开发中,有人告诉我创建锁定文件的一种安全且相对便携的方法是使用mkemp(3)
or创建一个临时文件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 环境中都使用了上述内容。
当以 Debian 机器为目标时,我发现该lockfile-progs
软件包是一个很好的解决方案。procmail
还附带一个lockfile
工具。但是,有时我对这些都没有。
这是我的解决方案,它mkdir
用于原子性和 PID 文件来检测过时的锁。此代码目前在 Cygwin 设置上生产,并且运行良好。
要使用它,只需exclusive_lock_require
在您需要独占访问某些内容时调用。可选的锁名称参数允许您在不同脚本之间共享锁。如果您需要更复杂的东西,还有两个较低级别的函数 (exclusive_lock_try
和)。exclusive_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
}
如果该线程的其他地方已经描述了羊群的限制对您来说不是问题,那么这应该有效:
#!/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 软件包再次恢复。它写“flock:...文本文件忙”。可以通过禁用脚本的写入权限来覆盖它。
一些 unix 具有lockfile
与已经提到的非常相似的flock
.
从手册页:
lockfile 可用于创建一个或多个信号量文件。如果 lock-file 不能创建所有指定的文件(按指定的顺序),它会等待 sleeptime(默认为 8)秒并重试最后一个不成功的文件。您可以指定在返回失败之前要进行的重试次数。如果重试次数为-1(默认值,即-r-1),lockfile 将永远重试。
我使用一种简单的方法来处理陈旧的锁定文件。
请注意,上述一些存储 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
我想取消 lockfiles、lockdirs、特殊锁定程序,即使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
选项将确保重定向命令将失败。所以重定向命令实际上是原子的——你用一个命令编写和检查文件。您不需要在文件末尾删除锁定文件 - 它会被陷阱删除。我希望这对以后会阅读它的人有所帮助。
PS 我没有看到 Mikel 已经正确回答了这个问题,尽管他没有包含 trap 命令以减少在使用 Ctrl-C 停止脚本后留下锁定文件的机会。所以这是完整的解决方案
一个有羊群(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
PID 和 lockfiles 绝对是最可靠的。当您尝试运行程序时,它可以检查锁定文件,如果它存在,它可以ps
用来查看进程是否仍在运行。如果不是,则脚本可以启动,将锁定文件中的 PID 更新为自己的。
我发现 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 的解决方案存在一个缺陷,即被终止的脚本可能会留在目录后面,从而阻止其他实例运行。
semaphoric实用程序使用(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 就会保持打开状态。一旦进程存在(即使它被杀死),它将被关闭(通过 shell)。我发现这非常有效:
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
很高兴看到内存中存在进程(无论进程的状态如何);但它对我有用。
羊群路径是要走的路。想想当脚本突然死掉时会发生什么。在羊群案例中,您只需松开羊群,但这不是问题。另外,请注意一个邪恶的技巧是在脚本本身上采取羊群..但这当然会让您全速前进到权限问题。
又快又脏?
#!/bin/sh
if [ -f sometempfile ]
echo "Already running... will now terminate."
exit
else
touch sometempfile
fi
..do what you want here..
rm sometempfile
查看 FLOM(免费锁管理器)http://sourceforge.net/projects/flom/:您可以使用不需要文件系统中的锁定文件的抽象资源来同步命令和/或脚本。您可以同步在不同系统中运行的命令,而无需像 NFS(网络文件系统)服务器这样的 NAS(网络附加存储)。
使用最简单的用例,序列化“command1”和“command2”可能就像执行一样简单:
flom -- command1
和
flom -- command2
来自两个不同的 shell 脚本。
结合上面提供的答案,这是一种更优雅,故障安全,快速且肮脏的方法。
用法
- 包括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 的组合来锁定以确保进程尚未运行
- 您可以检测脚本是否在解除锁定之前停止(例如进程终止、关闭、错误等)
- 您可以检查锁定文件,并在锁定丢失时使用它来触发进程关闭
- 详细,输出错误消息以便于调试
为什么我们不使用类似的东西
pgrep -f $cmd || $cmd
if [ 1 -ne $(/bin/fuser "$0" 2>/dev/null | wc -w) ]; then
exit 1
fi
我有一个基于文件名的简单解决方案
#!/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.
晚会,使用@Majal 的想法,这是我的脚本,仅启动一个 emacsclient GUI 实例。有了它,我可以设置快捷键打开或跳回同一个emacsclient。我有另一个脚本可以在需要时在终端中调用 emacsclient。这里使用 emacsclient 只是为了展示一个工作示例,可以选择其他的。这种方法对我的小脚本来说既快又好。告诉我哪里脏了:)
#!/bin/bash
# if [ $(pgrep -c $(basename $0)) -lt 2 ]; then # this works but requires script name to be unique
if [ $(pidof -x "$0"|wc -w ) -lt 3 ]; then
echo -e "Starting $(basename $0)"
emacsclient --alternate-editor="" -c "$@"
else
echo -e "$0 is running already"
fi
这一行答案来自与Ask Ubuntu Q&A相关的人:
[ "${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.
我在任何地方都没有发现它,它使用 read,我不知道 read 是否真的是原子的,但到目前为止它对我很有帮助......,它很有趣,因为它只是 bash 内置函数,这是一个进程实现时,您启动 locker 协进程并使用它的 i/o 来管理锁,只需将目标 i/o 从 locker 文件描述符交换到文件系统文件描述符 ( exec 3<>/file && exec 4</file
)即可在进程间完成相同的操作
## gives locks
locker() {
locked=false
while read l; do
case "$l" in
lock)
if $locked; then
echo false
else
locked=true
echo true
fi
;;
unlock)
if $locked; then
locked=false
echo true
else
echo false
fi
;;
*)
echo false
;;
esac
done
}
## locks
lock() {
local response
echo lock >&${locker[1]}
read -ru ${locker[0]} response
$response && return 0 || return 1
}
## unlocks
unlock() {
local response
echo unlock >&${locker[1]}
read -ru ${locker[0]} response
$response && return 0 || return 1
}
我对现有答案有以下问题:
- 一些答案试图清理锁定文件,然后不得不处理由突然崩溃/重启引起的陈旧锁定文件。IMO 是不必要的复杂。让锁定文件保留。
- 一些答案使用脚本文件本身
$0
或$BASH_SOURCE
用于锁定,通常引用来自man flock
. 当由于更新或编辑导致下一次运行打开并获得对新脚本文件的锁定而替换脚本时,即使另一个持有已删除文件锁定的实例仍在运行,这也会失败。 - 很少有答案使用固定的文件描述符。这并不理想。我不想依赖这将如何表现,例如打开锁定文件失败但处理不当并尝试锁定从父进程继承的不相关文件描述符。另一个失败案例是为第 3 方二进制文件注入锁定包装器,该二进制文件本身不处理锁定,但固定的文件描述符会干扰文件描述符传递给子进程。
- 我拒绝使用进程查找已经运行的脚本名称的答案。它有几个原因,例如但不限于可靠性/原子性、解析输出以及拥有执行多个相关功能的脚本,其中一些不需要锁定。
这个答案可以:
- 依赖,
flock
因为它让内核提供锁定......提供的锁定文件是原子创建的而不是替换的。 - 假设并依赖存储在本地文件系统上的锁定文件,而不是 NFS。
- 将锁定文件的存在更改为对正在运行的实例没有任何意义。它的作用纯粹是防止两个并发实例创建同名文件并替换另一个副本。锁定文件不会被删除,它会被遗忘并且可以在重新启动后继续存在。锁定是通过
flock
不通过锁定文件存在来指示的。 - 假设 bash shell,如问题所标记。
它不是 oneliner,但没有注释或错误消息,它足够小:
#!/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}<&-
如果您的脚本名称是唯一的,这将起作用:
#!/bin/bash
if [ $(pgrep -c $(basename $0)) -gt 1 ]; then
echo $(basename $0) is already running
exit 0
fi
如果脚本名不是唯一的,这适用于大多数 linux 发行版:
#!/bin/bash
exec 9>/tmp/my_lock_file
if ! flock -n 9 ; then
echo "another instance of this script is already running";
exit 1
fi
来源: http: //mywiki.wooledge.org/BashFAQ/045
尝试类似下面的东西,
ab=`ps -ef | grep -v grep | grep -wc processname`
然后使用 if 循环将变量与 1 匹配。