正确锁定外壳脚本?


66

有时,您必须确保同时运行一个Shell脚本实例。

例如,通过crond执行的cron作业本身并不提供锁定(例如,默认的Solaris crond)。

实施锁定的常见模式是这样的代码:

#!/bin/sh
LOCK=/var/tmp/mylock
if [ -f $LOCK ]; then            # 'test' -> race begin
  echo Job is already running\!
  exit 6
fi
touch $LOCK                      # 'set'  -> race end
# do some work
rm $LOCK

当然,这样的代码具有竞争条件。在一个时间窗口中,两个实例的执行都可以在第3行之后在一个实例能够触摸$LOCK文件之前进行。

对于cron作业,这通常不是问题,因为两次调用之间的间隔为几分钟。

但是事情可能会出错(例如,当锁文件位于NFS服务器上时)会挂起。在这种情况下,几个cron作业可以在第3行上阻塞并排队。如果NFS服务器再次处于活动状态,那么您将听到大量并行运行的作业。

在网上搜索时,我发现了锁运行工具,这似乎是解决该问题的好方法。使用它可以运行需要锁定的脚本,如下所示:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh

您可以将其放入包装器中,也可以从crontab中使用它。

lockf()如果可用,它将使用(POSIX),然后回退到flock()(BSD)。并lockf()支持NFS上应该是比较普遍的。

有替代品lockrun吗?

那其他的cron守护程序呢?有普通的crond支持健全的锁定吗?快速浏览Vixie Crond的手册页(在Debian / Ubuntu系统上是默认设置)并没有显示有关锁定的任何信息。

lockruncoreutils中包含这样的工具会是一个好主意吗?

在我看来,它实现了非常相似的主题timeoutnice和朋友。


4
切线地,为了对其他可能考虑您的初始模式Good Enough(tm)的人有利,该Shell代码可能应该捕获TERM以便在killed 时删除其锁定文件。而且最好将自己的pid存储在锁文件中,而不是仅仅触摸它。
Ulrich Schwarz,


@Shawn并不是真的,没有提到crond和NFS。
maxschlepzig 2011年


1
@Ulrich非常迟了,将PID存储在NFS锁定文件中只会增加很小的价值。即使添加主机名仍然对检查实时进程还是没有帮助
roaima

Answers:


45

这是锁定外壳脚本的另一种方法,可以防止上述竞争情况发生,其中两个作业都可以通过第3行。该noclobber选项将在ksh和bash中工作。不要使用,set noclobber因为您不应该在csh / tcsh中编写脚本。;)

lockfile=/var/tmp/mylock

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then

        trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

        # do stuff here

        # clean up after yourself, and release your trap
        rm -f "$lockfile"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockfile owned by $(cat $lockfile)"
fi

锁定了NFS的YMMV(您知道,当NFS服务器无法访问时),但总的来说,它比以前更加健壮。(10年前)

如果您有cron作业同时在多台服务器上执行相同的操作,但实际上只需要1个实例即可运行,则类似的操作可能对您有用。

我没有使用lockrun的经验,但是在脚本实际运行之前使用预设的锁定环境可能会有所帮助。否则可能不会。您只是在包装器中的脚本外部设置了对锁文件的测试,理论上,如果锁运行在同一时间调用了两个作业,就像在“ inside-脚本的解决方案?

无论如何,文件锁定几乎都是尊重系统行为的,任何在运行之前不检查锁定文件是否存在的脚本都将执行它们将要执行的操作。仅通过进行锁文件测试和适当的行为,就可以解决99%的潜在问题,即使不是100%。

如果您经常遇到lockfile竞争条件,则可能表明存在较大问题,例如未正确安排您的作业的时间,或者如果间隔不如作业完成那么重要,则也许您的作业更适合被守护。


编辑以下内容-2016-05-06(如果您使用的是KSH88)


基于以下@Clint Pachl的注释,如果您使用ksh88,请使用mkdir代替noclobber。这在很大程度上缓解了潜在的比赛条件,但并没有完全限制它(尽管风险很小)。有关更多信息,请阅读Clint在下面发布的链接

lockdir=/var/tmp/mylock
pidfile=/var/tmp/mylock/pid

if ( mkdir ${lockdir} ) 2> /dev/null; then
        echo $$ > $pidfile
        trap 'rm -rf "$lockdir"; exit $?' INT TERM EXIT
        # do stuff here

        # clean up after yourself, and release your trap
        rm -rf "$lockdir"
        trap - INT TERM EXIT
else
        echo "Lock Exists: $lockdir owned by $(cat $pidfile)"
fi

并且,另外一个优势是,如果您需要在脚本中创建tmpfile,则可以使用lockdir它们的目录,因为知道在脚本退出时会清理它们。

对于更现代的bash,顶部的noclobber方法应该是合适的。


1
不,使用lockrun不会有问题-NFS服务器挂起时,所有lockrun调用都会(至少)在lockf()系统调用中挂起-备份时,将恢复所有进程,但只有一个进程将赢得该锁。没有比赛条件。我不会在cronjobs中遇到很多此类问题-情况恰恰相反-但这是一个问题,当它打击您时,它有可能造成很多痛苦。
maxschlepzig 2011年

1
我接受了这个答案,因为该方法是安全的,并且是迄今为止最优雅的方法。我建议使用一个小的变体:set -o noclobber && echo "$$" > "$lockfile"当外壳不支持noclobber选项时获得安全的后备。
maxschlepzig 2011年

3
很好的答案,但是您还应该“杀死-0”锁文件中的值,以确保创建锁的进程仍然存在。
奈杰尔·霍恩

1
noclobber选项可能易于出现竞争状况。请参阅mywiki.wooledge.org/BashFAQ/045,以获取一些思想上的帮助。
克林特·帕奇

2
注意:在ksh88中使用noclobber(或-C)不起作用,因为ksh88不O_EXCL用于noclobber。如果您使用的是更新的Shell,则可能没问题...
jrw32982

14

我更喜欢使用硬链接。

lockfile=/var/lock/mylock
tmpfile=${lockfile}.$$
echo $$ > $tmpfile
if ln $tmpfile $lockfile 2>&-; then
    echo locked
else
    echo locked by $(<$lockfile)
    rm $tmpfile
    exit
fi
trap "rm ${tmpfile} ${lockfile}" 0 1 2 3 15
# do what you need to

硬链接在NFS上是原子的,在大多数情况下,mkdir也是。在实际水平上使用mkdir(2)link(2)大致相同;我只喜欢使用硬链接,因为NFS的更多实现允许原子硬链接多于atomic mkdir。使用现代版本的NFS,您不必担心使用任何一种。


12

我了解这mkdir是原子的,所以也许:

lockdir=/var/tmp/myapp
if mkdir $lockdir; then
  # this is a new instance, store the pid
  echo $$ > $lockdir/PID
else
  echo Job is already running, pid $(<$lockdir/PID) >&2
  exit 6
fi

# then set traps to cleanup upon script termination 
# ref http://www.shelldorado.com/goodcoding/tempfiles.html
trap 'rm -r "$lockdir" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15

好的,但是我找不到mkdir()NFS(> = 3)是否标准化为原子的信息。
maxschlepzig 2011年

2
@maxschlepzig RFC 1813并未明确要求mkdir要求是原子的(它要求rename)。在实践中,已知有些实现不是。相关:一个有趣的主题,包括GNU arch的作者的贡献
吉尔斯

8

一种简单的方法是使用lockfile通常与procmail包装一起提供的包装。

LOCKFILE="/tmp/mylockfile.lock"
# try once to get the lock else exit
lockfile -r 0 "$LOCKFILE" || exit 0

# here the actual job

rm -f "$LOCKFILE"

5

semparallel您正在寻找GNU 工具中的一部分:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command

如:

sem --id my_semaphore --fg "echo 1 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 2 ; date ; sleep 3" &
sem --id my_semaphore --fg "echo 3 ; date ; sleep 3" &

输出:

1
Thu 10 Nov 00:26:21 UTC 2016
2
Thu 10 Nov 00:26:24 UTC 2016
3
Thu 10 Nov 00:26:28 UTC 2016

请注意,不能保证订购。同样,直到完成输出时,才会显示输出(令人讨厌!)。但是即使如此,这也是我所知道的最简洁的方法,可以防止并发执行,而不必担心锁文件,重试和清除。


执行过程中是否将sem手柄提供的锁定击落?
roaima

2

我用dtach

$ dtach -n /tmp/socket long_running_task ; echo $?
0
$ dtach -n /tmp/socket long_running_task ; echo $?
dtach: /tmp/socket: Address already in use
1

1

我使用命令行工具“ flock”来管理bash脚本中的锁定,如此此处所述。我已经使用了flock联机帮助页中的这种简单方法,可以在子shell中运行一些命令...

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

在该示例中,如果无法获取锁定文件,则失败,退出代码为1。但是,flock也可以不需要在子shell中运行命令的方式使用:-)


3
flock()系统调用不超过NFS工作
maxschlepzig 2011年

1
BSD有一个类似的工具“ lockf”。
dubiousjim 2012年

2
@ dubiousjim,BSD lockf也调用flock(),因此在NFS上存在问题。顺便说一句,与此同时,Linux上的flock()现在回退到fcntl()文件位于NFS挂载上的时间,因此,在仅Linux的NFS环境中,flock()现在可以在NFS上运行。
maxschlepzig

1

不要使用文件。

如果您的脚本是这样执行的,例如:

bash my_script

您可以使用以下命令检测它是否正在运行:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

嗯,ps检查代码从内部运行my_script?如果另一个实例正在运行-是否不running_proc包含两条匹配的行?我喜欢这个主意,但当然-当另一个用户运行具有相同名称的脚本时,您会得到错误的结果……
maxschlepzig 2011年

3
它还包括一个竞争条件:如果2个实例并行执行第一行,则没有一个实例获得“锁定”,并且都以状态6退出。这将是一种单轮的相互饥饿。顺便说一句,我不确定您为什么使用$!而不是$$您的示例。
maxschlepzig 2011年

@maxschlepzig确实对不正确的$感到抱歉!vs. $$
frogstarr78 2011年

@maxschlepzig可处理多个运行脚本的用户,将euser =添加到-o参数。
frogstarr78

@maxschlepzig为防止出现多行,您还可以将参数更改为grep或其他“过滤器”(例如grep -v $$)。基本上,我试图提供一种解决该问题的方法。
frogstarr78 2011年

1

对于实际使用,您应该使用投票率最高的答案

但是,ps由于我一直看到人们在使用它们,所以我想讨论一些使用的各种破碎的和半可行的方法,以及它们的许多警告。

这个答案实际上是“为什么不使用处理外壳中的锁定?”的答案psgrep

坏方法#1

首先,在另一个答案中给出的一种方法尽管没有(而且永远不可能)有效并且显然从未经过测试,但还是有一些缺点:

running_proc=$(ps -C bash -o pid=,cmd= | grep my_script);
if [[ "$running_proc" != "$$ bash my_script" ]]; do 
  echo Already locked
  exit 6
fi

让我们修复语法错误和无效的ps参数,然后获取:

running_proc=$(ps -C bash -o pid,cmd | grep "$0");
echo "$running_proc"
if [[ "$running_proc" != "$$ bash $0" ]]; then
  echo Already locked
  exit 6
fi

无论您如何运行,此脚本始终总是退出6。

如果使用来运行它./myscript,那么ps输出将仅为12345 -bash,它与所需的字符串不匹配12345 bash ./myscript,因此将失败。

如果使用来运行它bash myscript,事情将会变得更加有趣。bash进程派生运行管道,外壳运行psgrep。原始外壳程序和子外壳程序都将显示在ps输出中,如下所示:

25793 bash myscript
25795 bash myscript

这不是预期的输出$$ bash $0,因此您的脚本将退出。

坏办法#2

现在,对于写了坏方法#1的用户来说,很公平,当我第一次尝试这样做时,我做了类似的事情:

if otherpids="$(pgrep -f "$0" | grep -vFx "$$")" ; then
  echo >&2 "There are other copies of the script running; exiting."
  ps >&2 -fq "${otherpids//$'\n'/ }" # -q takes about a tenth the time as -p
  exit 1
fi

几乎可行。但是,分叉运行管道的事实使这一点不成立。因此,这也将始终存在。

不可靠的方法#3

pids_this_script="$(pgrep -f "$0")"
if not_this_process="$(echo "$pids_this_script" | grep -vFx "$$")"; then
  echo >&2 "There are other copies of this script running; exiting."
  ps -fq "${not_this_process//$'\n'/ }"
  exit 1
fi

此版本通过首先获取在其命令行参数中包含当前脚本的所有PID,然后分别过滤该pidlist来忽略当前脚本的PID,从而避免了方法2中的管道分叉问题。

如果没有其他进程具有与匹配的命令行$0,并且始终以相同的方式调用脚本(例如,如果使用相对路径然后使用绝对路径调用脚本,则后者不会注意到前者),这可能会起作用)。

不可靠的方法#4

那么,如果我们跳过检查完整的命令行(因为这可能并不表示脚本实际上正在运行),lsof而是查找所有已打开此脚本的进程,该怎么办?

好吧,是的,这种方法实际上还不错:

if otherpids="$(lsof -t "$0" | grep -vFx "$$")"; then
  echo >&2 "Error: There are other processes that have this script open - most likely other copies of the script running.  Exiting to avoid conflicts."
  ps >&2 -fq "${otherpids//$'\n'/ }"
  exit 1
fi

当然,如果脚本的副本正在运行,则新实例将很好地启动,并且您将有两个副本在运行。

或者,如果修改了正在运行的脚本(例如,用Vim或使用git checkout),则该脚本的“新”版本将毫无问题地启动,因为Vim和都会git checkout产生一个新文件(新inode)来代替该脚本。旧的。

但是,如果脚本从未被修改且从未被复制过,则此版本相当不错。没有竞争条件,因为在可以进行检查之前必须已打开脚本文件。

如果另一个进程打开了脚本文件,仍然会有误报,但是请注意,即使打开了该文件以便在Vim中进行编辑,vim并不会真正保持脚本文件打开,因此不会导致误报。

但是请记住,如果脚本可能会被编辑或复制,则不要使用这种方法,因为这样会得到假阴性,即一次运行多个实例-因此,用Vim编辑不会产生假阳性的事实不重要给你。不过,我提到了它,因为如果您使用Vim打开脚本,则方法3 确实给出了误报(即拒绝启动)。

那该怎么办呢?

该问题的最高投票答案是一个很好的坚实方法。

也许您可以编写一个更好的方法...但是,如果您不了解上述所有方法的所有问题和注意事项,则不太可能编写一种避免所有这些方法的锁定方法。



0

我有时在服务器上添加一些内容,以轻松处理计算机上任何作业的竞争条件。这与蒂姆·肯尼迪(Tim Kennedy)的帖子类似,但是通过这种方式,您只需向需要它的每个bash脚本添加一行就可以进行比赛处理。

将下面的内容放在例如/ opt / racechecker / racechecker中:

ZPROGRAMNAME=$(readlink -f $0)
EZPROGRAMNAME=`echo $ZPROGRAMNAME | sed 's/\//_/g'`
EZMAIL="/usr/bin/mail"
EZCAT="/bin/cat"

if  [ -n "$EZPROGRAMNAME" ] ;then
        EZPIDFILE=/tmp/$EZPROGRAMNAME.pid
        if [ -e "$EZPIDFILE" ] ;then
                EZPID=$($EZCAT $EZPIDFILE)
                echo "" | $EZMAIL -s "$ZPROGRAMNAME already running with pid $EZPID"  alarms@someemail.com >>/dev/null
                exit -1
        fi
        echo $$ >> $EZPIDFILE
        function finish {
          rm  $EZPIDFILE
        }
        trap finish EXIT
fi

这是使用方法。注意shebang之后的行:

     #/bin/bash
     . /opt/racechecker/racechecker
     echo "script are running"
     sleep 120

它的工作方式是找出主要bashscript文件名,并在“ / tmp”下创建一个pidfile。它还向完成信号添加了一个侦听器。主脚本正确完成后,侦听器将删除pidfile。

相反,如果在启动实例时存在一个pidfile,则将执行包含第二个if语句内的代码的if语句。在这种情况下,我决定在发生这种情况时启动警报邮件。

如果脚本崩溃了怎么办

进一步的练习将是处理崩溃。理想情况下,即使主脚本由于任何原因崩溃也应删除pidfile,但在我上面的版本中并未这样做。这意味着,如果脚本崩溃,则必须手动删除pidfile才能恢复功能。

万一系统崩溃

将pidfile / lockfile存储在例如/ tmp下是个好主意。这样,您的脚本将在系统崩溃后继续继续执行,因为pidfile总是在启动时被删除。


与Tim Kennedy的ansatz不同,您的脚本中确实包含竞争条件。这是因为您没有在原子操作中检查PIDFILE的存在及其有条件的创建。
maxschlepzig 2015年

+1!我将考虑这一点并修改我的脚本。
ziggestardust

-2

检查我的脚本...

您可能会喜欢 ...

[rambabu@Server01 ~]$ sh Prevent_cron-OR-Script_against_parallel_run.sh
Parallel RUN Enabled
Now running
Task completed in Parallel RUN...
[rambabu@Server01 ~]$ cat Prevent_cron-OR-Script_against_parallel_run.sh
#!/bin/bash
#Created by RambabuKella
#Date : 12-12-2013

#LOCK file name
Parallel_RUN="yes"
#Parallel_RUN="no"
PS_GREP=0
LOCK=/var/tmp/mylock_`whoami`_"$0"
#Checking for the process
PS_GREP=`ps -ef |grep "sh $0" |grep -v grep|wc -l`
if [ "$Parallel_RUN" == "no" ] ;then
echo "Parallel RUN Disabled"

 if [ -f $LOCK ] || [ $PS_GREP -gt 2   ] ;then
        echo -e "\nJob is already running OR LOCK file exists. "
        echo -e "\nDetail are : "
        ps -ef |grep  "$0" |grep -v grep
        cat "$LOCK"
  exit 6
 fi
echo -e "LOCK file \" $LOCK \" created on : `date +%F-%H-%M` ." &> $LOCK
# do some work
echo "Now running"
echo "Task completed on with single RUN ..."
#done

rm -v $LOCK 2>/dev/null
exit 0
else

echo "Parallel RUN Enabled"

# do some work
echo "Now running"
echo "Task completed in Parallel RUN..."
#done

exit 0
fi
echo "some thing wrong"
exit 2
[rambabu@Server01 ~]$

-3

我在名为“ flocktest”的脚本中提供了以下解决方案

#!/bin/bash
export LOGFILE=`basename $0`.logfile
logit () {
echo "$1" >>$LOGFILE
}
PROGPATH=$0
(
flock -x -n 257
(($?)) && logit "'$PROGPATH' is already running!" && exit 0
logit "'$PROGPATH', proc($$): sleeping 30 seconds"
sleep 30
)257<$PROGPATH
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.