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

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

132052 次浏览

使用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以外的操作系统,那么它可能不可用,也可能不可用。

羊群走的路才是正确的。想想当脚本突然失效时会发生什么。在羊群的情况下,你只是松散的羊群,但这不是一个问题。另外,请注意,一个邪恶的伎俩是在脚本本身取一群。但这当然会让您完全陷入权限问题。

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

又快又脏?

#!/bin/sh


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


..do what you want here..


rm sometempfile

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

下面是一个使用lockfile并将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将确保即使进程被杀死,lockfile也会被删除(kill -9除外)。

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

一些unix有lockfile,它与前面提到的flock非常相似。

从手册中:

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

要使锁定可靠,您需要一个原子操作。以上许多建议 不是原子的。建议的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也不是原子的,因此首先是rename(它是原子的),然后是Remove。

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

测试“锁文件”是否存在的简单方法;是有缺陷的。

为什么?因为它们不会检查文件是否存在,也不会在单个原子操作中创建文件。因为这个;存在一个竞争条件,使你的互斥尝试中断。

相反,你可以使用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

如果你想处理过期的锁,熔化炉(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的熔化炉(1)提供了一些额外的选项,还有群(1)

当针对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
}

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

但是如果没有羊群,该怎么办呢?这是mkdir。这也是一个原子操作。只有一个进程会成功执行mkdir,其他进程都会失败。

所以代码是:

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

你需要处理陈旧的锁,否则崩溃后你的脚本将永远不会再次运行。

我发现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的解决方案有一个缺陷,即被杀死的脚本可能会留下目录,从而阻止其他实例运行。

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

对于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。将它们设置为其他内容,我可以让我的脚本和批处理流对前一个批处理作业或脚本做出相应的反应。

如果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

实际上,尽管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停止脚本后留下锁文件的机会。这就是完整的解

这个例子是在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列出进程,按其名称过滤,附加过滤器grep -v grep清除寄生虫,最后按grep -c计数。和数字比较。这是复杂而不确定的

真的<强> < / >强快和真的脏?脚本顶部的一行代码可以工作:

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

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

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

注意,上面的一些解决方案存储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

看看FLOM (Free LOck Manager) http://sourceforge.net/projects/flom/:你可以使用抽象资源同步命令和/或脚本,不需要文件系统中的锁文件。您可以在没有NFS(网络文件系统)服务器这样的NAS(网络附加存储)的情况下同步在不同系统中运行的命令。

使用最简单的用例,序列化“command1”和“command2”可能和执行一样简单:

flom -- command1

而且

flom -- command2

来自两个不同的shell脚本。

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

使用

  1. 包括sh_lock_functions.sh
  2. 使用sh_lock_init初始化
  3. 使用sh_acquire_lock锁定
  4. 使用sh_check_lock检查锁
  5. 使用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,输出错误消息,以便于调试

在脚本的开头添加这一行

[ "${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包上再次工作。上面写着“羊群:……文本文件繁忙”。可以通过禁用脚本上的写权限来覆盖它。

下面是一种方法,它结合了原子目录锁定和通过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

试试下面的方法,

ab=`ps -ef | grep -v grep | grep -wc processname`

然后使用if循环将变量与1匹配。

短篇小说家实用程序使用flock(如上所述,例如通过presto8)来实现计数信号量。它支持您想要的任意数量的并发进程。我们使用它来限制各种队列工作者进程的并发级别。

它类似于扫描电镜,但是更轻。(完全披露:我是在发现sem对我们的需求来说太繁重了,而且没有一个简单的计数信号量实用程序可用后写的。)

为什么我们不用像这样的东西

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.

你可以为此使用GNU Parallel,因为当它被调用为sem时,它可以作为互斥量工作。所以,具体来说,你可以使用:

sem --id SCRIPTSINGLETON yourScript

如果你也想要一个超时,使用:

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

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

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

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

迟来的派对,使用来自@Majal的想法,这是我的脚本,只启动emacsclient GUI的一个实例。有了它,我可以设置快捷键打开或跳回相同的瘦弱。我有另一个脚本调用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

这将工作,如果你的脚本名称是唯一的:

#!/bin/bash
if [ $(pgrep -c $(basename $0)) -gt 1 ]; then
echo $(basename $0) is already running
exit 0
fi

如果scriptname不是唯一的,这在大多数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

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

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

现有的答案要么依赖于CLI实用程序flock,要么没有正确地保护锁文件。flock实用程序在所有非linux系统(即FreeBSD)上都不可用,在NFS上也不能正常工作。

在我从事系统管理和系统开发的早期,有人告诉我,一种安全且相对可移植的创建锁文件的方法是使用mkemp(3)mkemp(1)创建临时文件,将标识信息写入临时文件(即PID),然后将临时文件硬链接到锁文件。如果链接成功,那么您已经成功地获得了锁。

当在shell脚本中使用锁时,我通常会在共享配置文件中放置obtain_lock()函数,然后从脚本中获取它。下面是一个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;
};

lock功能的使用示例如下:

#!/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(1)但没有subshell的例子。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

这一行回答来自与问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.

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

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

这个我在任何地方都没有发现,它使用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
}

使用进程的锁更强大,并且还可以处理不优雅的退出。 只要进程在运行,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

我使用onlineer @脚本的开头:

#!/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$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}<&-

如果你不想或不能使用flock(例如,你没有使用共享文件系统),可以考虑使用像可锁定的这样的外部服务。

它公开咨询锁原语,就像flock那样。特别地,你可以通过以下方式获取锁:

https://lockable.dev/api/acquire/my-lock-name

然后通过

https://lockable.dev/api/release/my-lock-name

通过将脚本执行与锁获取和释放结合在一起,您可以确保在任何给定时间只有一个流程实例在运行。