当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《快速而又脏的方法,确保一次只运行一个shell脚本实例》经验,为你挑选了13个好方法。

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



1> Alex B..:

用于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以外的操作系统,它可能可用,也可能不可用.


什么是200?它在manul中说"fd",但我不知道这意味着什么.
如果有人想知道:语法`(命令A)命令B`为`命令A`调用子shell.记录在http://tldp.org/LDP/abs/html/subshel​​ls.html.我仍然不确定调用子shell和命令B的时间.
@chovy"文件描述符",一个指定打开文件的整数句柄.

2> lhunath..:

所有测试"锁定文件"存在的方法都存在缺陷.

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

相反,你需要使用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

如果你想要处理过时的锁,热熔器(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).


毫无疑问,`mkdir`不是*定义*是原子操作,因此"副作用"是文件系统的实现细节.如果他说NFS不以原子方式实现它,我完全相信他.虽然我不怀疑你的`/ tmp`将是一个NFS共享,并且很可能是由一个以原子方式实现`mkdir`的fs提供的.
有*'*'是一种检查文件是否存在并在单个原子动作中创建的方法' - 它是`open(... O_CREAT | O_EXCL)`.您只需要一个合适的用户程序来执行此操作,例如`lockfile-create`(在`lockfile-progs`中)或`dotlockfile`(在`liblockfile-bin`中).并确保正确清理(例如`trap EXIT`),或测试陈旧锁(例如使用`--use-pid`).
"所有测试"锁定文件"存在的方法都存在缺陷.为什么?因为无法检查文件是否存在并在单个原子动作中创建它." - 为了使其成为原子,必须在内核级别 - 它是在内核级别使用flock(1)https://linux.die.net/man/1/flock完成的,它出现在人类版权日期至少从2006年开始.所以我做了downvote(-1),没什么个人的,只是坚信使用内核开发人员提供的内核实现的工具是正确的.
但有一种方法可以检查是否存在常规文件,如果不存在则自动创建它:使用`ln`从另一个文件创建硬链接.如果您有不能保证的奇怪文件系统,则可以在之后检查新文件的inode以查看它是否与原始文件相同.

3> bmdhacks..:

这是一个使用锁文件并将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).


正如在对花药回答的评论中已经提到的,这有一个致命的缺陷 - 如果另一个脚本在检查和回声之间启动,你就是干杯.
shell中可以使用flock(1)或lockfile(1)进行原子检查和创建.看到其他答案.
请参阅我的回复,了解可执行原子检查和创建的便携方式,而无需依赖flock或lockfile等实用程序.
这不是原子的,因此没有用。您需要一种用于测试和设置的原子机制。

4> Cowan..:

flock(2)系统调用周围有一个包装器,称为univginative,flock(1).这使得可靠地获得排他锁而不用担心清理等相对容易.手册页中有关于如何在shell脚本中使用它的示例.


从Cron作业运行我使用`flock -x -n%lock file%-c"%command%"`来确保只执行一个实例.
`flock()`系统调用不是POSIX,不适用于NFS挂载上的文件.

5> 小智..:

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

但是如果没有flock怎么办.那么有mkdir.这也是一个原子操作.只有一个进程会导致成功的mkdir,所有其他进程都将失败.

所以代码是:

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

你需要处理陈旧的锁定,否则你的脚本永远不会再运行崩溃.


@Nippysaurus:这种锁定方法不会泄漏.您看到的是在启动所有副本之前初始脚本终止,因此另一个副本能够(正确地)获得锁定.为了避免这种假阳性,在`rmdir`之前添加一个`sleep 10`并尝试再次级联 - 没有任何东西会"泄漏".

6> 小智..:

要使锁定可靠,您需要进行原子操作.上述许多提案都不是原子的.建议的lockfile(1)实用程序看起来很有前途,因为它提到了"NFS-resistant".如果您的操作系统不支持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

解锁当前dir(假设,运行进程确实获得了锁定):

mv lock deleteme && rm deleteme

解锁文件(假设,正在运行的进程确实获得了锁定):

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

删除也不是原子的,因此首先重命名(这是原子的)然后删除.

对于符号链接和重命名调用,两个文件名必须驻留在同一文件系统上.我的建议:只使用简单的文件名(没有路径),并将文件和锁定放在同一目录中.



7> Mikel..:

另一种选择是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,但它适用于我的所有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



8> Mark Setchel..:

您可以使用GNU Parallel它,因为它在调用时用作互斥锁sem.因此,具体而言,您可以使用:

sem --id SCRIPTSINGLETON yourScript

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

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

超时<0表示在没有运行脚本的情况下退出,如果在超时内未释放信号量,则超时> 0表示无论如何都要运行脚本.

请注意,您应该为其命名(with --id),否则它默认为控制终端.

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


我们只需要很多赞成票.这样一个整洁而鲜为人知的答案.(虽然是迂腐的OP想要快速而又脏,但这很快就干净!)更多关于`sem`的相关问题http://unix.stackexchange.com/a/322200/199525.

9> 小智..:

对于shell脚本,我倾向于使用mkdirover,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产生或期望.将它们设置为其他东西,我可以让我的脚本和批处理流相应地响应先前的批处理作业或脚本.


你的脚本太冗长了,我认为可能会短得多,但总的来说,是的,你必须设置陷阱才能正确地执行此操作.我还要添加SIGHUP.

10> Majal..:

真的很快,真的很脏?脚本顶部的这个单行将起作用:

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

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


很快,
很脏?脚本顶部的这个单行将起作用:
这根本不起作用!为什么要检查`-gt 2`?grep并不总是在ps的结果中找到自己!

11> Rob..:

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



12> Znik..:

这个例子在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用于删除寄生虫nad最后用grep -c计算它.并与数字进行比较.它复杂而不确定



13> bk138..:

这是一种将原子目录锁定与通过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

推荐阅读
凹凸曼00威威_694
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有