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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/185451/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 17:43:48  来源:igfitidea点击:

Quick-and-dirty way to ensure only one instance of a shell script is running at a time

bashshellprocesslockfile

提问by raldi

What's a quick-and-dirty way to make sure that only one instance of a shell script is running at a given time?

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

采纳答案by bmdhacks

Here's an implementation that uses a lockfileand echoes a PID into it. This serves as a protection if the process is killed before removing the pidfile:

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

The trick here is the kill -0which doesn't deliver any signal but just checks if a process with the given PID exists. Also the call to trapwill ensure that the lockfileis removed even when your process is killed (except kill -9).

这里的技巧是kill -0它不传递任何信号,而只是检查具有给定 PID 的进程是否存在。此外,即使您的进程被终止,调用 也trap将确保删除锁文件(除了kill -9)。

回答by Alex B

Use flock(1)to make an exclusive scoped lock a on file descriptor. This way you can even synchronize different parts of the script.

用于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

This ensures that code between (and )is run only by one process at a time and that the process doesn't wait too long for a lock.

这确保了(和之间的代码)一次仅由一个进程运行,并且该进程不会为锁定等待太长时间。

Caveat: this particular command is a part of util-linux. If you run an operating system other than Linux, it may or may not be available.

警告:这个特定的命令是util-linux. 如果您运行 Linux 以外的操作系统,则它可能可用也可能不可用。

回答by lhunath

All approaches that test the existence of "lock files" are flawed.

所有测试“锁定文件”是否存在的方法都有缺陷。

Why? Because there is no way to check whether a file exists and create it in a single atomic action. Because of this; there is a race condition that WILLmake your attempts at mutual exclusion break.

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

Instead, you need to use mkdir. mkdircreates a directory if it doesn't exist yet, and if it does, it sets an exit code. More importantly, it does all this in a single atomic action making it perfect for this scenario.

相反,您需要使用mkdir. mkdir如果目录尚不存在,则创建一个目录,如果存在,则设置退出代码。更重要的是,它在单个原子操作中完成所有这些,使其非常适合这种情况。

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

For all details, see the excellent BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

有关所有详细信息,请参阅优秀的 BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

If you want to take care of stale locks, fuser(1)comes in handy. The only downside here is that the operation takes about a second, so it isn't instant.

如果您想处理过时的锁,fuser(1)会派上用场。这里唯一的缺点是操作需要大约一秒钟,所以它不是即时的。

Here's a function I wrote once that solves the problem using fuser:

这是我曾经写过的一个函数,它使用 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= 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 
}

You can use it in a script like so:

您可以在脚本中使用它,如下所示:

mutex /var/run/myscript.lock || { echo "Already running." >&2; exit 1; }

If you don't care about portability (these solutions should work on pretty much any UNIX box), Linux' fuser(1)offers some additional options and there is also flock(1).

如果你不关心可移植性(这些解决方案几乎可以在任何 UNIX 机器上运行),Linux 的fuser(1)提供了一些额外的选项,还有flock(1)

回答by Cowan

There's a wrapper around the flock(2) system call called, unimaginatively, flock(1). This makes it relatively easy to reliably obtain exclusive locks without worrying about cleanup etc. There are examples on the man pageas to how to use it in a shell script.

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

回答by Gunstick

You need an atomic operation, like flock, else this will eventually fail.

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

But what to do if flock is not available. Well there is mkdir. That's an atomic operation too. Only one process will result in a successful mkdir, all others will fail.

但是如果 flock 不可用怎么办。嗯,有 mkdir。这也是一个原子操作。只有一个进程会导致 mkdir 成功,所有其他进程都将失败。

So the code is:

所以代码是:

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

You need to take care of stale locks else aftr a crash your script will never run again.

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

回答by Gunstick

To make locking reliable you need an atomic operation. Many of the above proposals are not atomic. The proposed lockfile(1) utility looks promising as the man-page mentioned, that its "NFS-resistant". If your OS does not support lockfile(1) and your solution has to work on NFS, you have not many options....

为了使锁定可靠,您需要一个原子操作。上述许多提议都不是原子的。正如手册页提到的那样,提议的 lockfile(1) 实用程序看起来很有前途,它的“抗 NFS”。如果您的操作系统不支持 lockfile(1) 并且您的解决方案必须在 NFS 上运行,那么您没有太多选择....

NFSv2 has two atomic operations:

NFSv2 有两个原子操作:

  • symlink
  • rename
  • 符号链接
  • 改名

With NFSv3 the create call is also atomic.

使用 NFSv3,创建调用也是原子的。

Directory operations are NOT atomic under NFSv2 and NFSv3 (please refer to the book 'NFS Illustrated' by Brent Callaghan, ISBN 0-201-32570-5; Brent is a NFS-veteran at Sun).

NFSv2 和 NFSv3 下的目录操作不是原子操作(请参阅 Brent Callaghan 所著的“NFS Illustrated”一书,ISBN 0-201-32570-5;Brent 是 Sun 的 NFS 资深人士)。

Knowing this, you can implement spin-locks for files and directories (in shell, not PHP):

知道了这一点,您可以为文件和目录实现自旋锁(在 shell 中,而不是在 PHP 中):

lock current dir:

锁定当前目录:

while ! ln -s . lock; do :; done

lock a file:

锁定文件:

while ! ln -s ${f} ${f}.lock; do :; done

unlock current dir (assumption, the running process really acquired the lock):

解锁当前目录(假设,正在运行的进程确实获得了锁):

mv lock deleteme && rm deleteme

unlock a file (assumption, the running process really acquired the lock):

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

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

Remove is also not atomic, therefore first the rename (which is atomic) and then the remove.

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

For the symlink and rename calls, both filenames have to reside on the same filesystem. My proposal: use only simple filenames (no paths) and put file and lock into the same directory.

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

回答by Mikel

Another option is to use shell's noclobberoption by running set -C. Then >will fail if the file already exists.

另一种选择是noclobber通过运行来使用 shell 的选项set -C。然后,>如果该文件已经存在,就会失败。

In brief:

简单来说:

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

This causes the shell to call:

这会导致外壳调用:

open(pathname, O_CREAT|O_EXCL)

which atomically creates the file or fails if the file already exists.

它以原子方式创建文件,或者如果文件已经存在则失败。



According to a comment on BashFAQ 045, this may fail in ksh88, but it works in all my shells:

根据对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

Interesting that pdkshadds the O_TRUNCflag, but obviously it's redundant:
either you're creating an empty file, or you're not doing anything.

pdksh添加O_TRUNC标志很有趣,但显然它是多余的:
要么您正在创建一个空文件,要么您什么都不做。



How you do the rmdepends on how you want unclean exits to be handled.

您如何执行rm取决于您希望如何处理不干净的出口。

Delete on clean exit

在干净退出时删除

New runs fail until the issue that caused the unclean exit to be resolved and the lockfile is manually removed.

新运行失败,直到解决导致不干净退出的问题并手动删除锁文件。

# acquire lock
# do work (code here may call exit, etc.)
rm "$lockfile"

Delete on any exit

在任何出口删除

New runs succeed provided the script is not already running.

如果脚本尚未运行,则新运行成功。

trap 'rm "$lockfile"' EXIT

回答by Mark Setchell

You can use GNU Parallelfor this as it works as a mutex when called as sem. So, in concrete terms, you can use:

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

sem --id SCRIPTSINGLETON yourScript

If you want a timeout too, use:

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

sem --id SCRIPTSINGLETON --semaphoretimeout -10 yourScript

Timeout of <0 means exit without running script if semaphore is not released within the timeout, timeout of >0 mean run the script anyway.

超时 <0 表示如果在超时内没有释放信号量,则退出而不运行脚本,超时 >0 表示无论如何都要运行脚本。

Note that you should give it a name (with --id) else it defaults to the controlling terminal.

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

GNU Parallelis a very simple install on most Linux/OSX/Unix platforms - it is just a Perl script.

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

回答by Mark Stinson

For shell scripts, I tend to go with the mkdirover flockas it makes the locks more portable.

对于 shell 脚本,我倾向于使用mkdirover,flock因为它使锁更易于移植。

Either way, using set -eisn't enough. That only exits the script if any command fails. Your locks will still be left behind.

无论哪种方式,使用set -e是不够的。只有在任何命令失败时才会退出脚本。你的锁仍然会被留下。

For proper lock cleanup, you really should set your traps to something like this psuedo code (lifted, simplified and untested but from actively used scripts) :

为了正确清理锁,你真的应该将你的陷阱设置为这样的伪代码(提升、简化和未经测试,但来自积极使用的脚本):

#=======================================================================
# 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

Here's what will happen. All traps will produce an exit so the function __sig_exitwill always happen (barring a SIGKILL) which cleans up your locks.

这就是将会发生的事情。所有陷阱都会产生一个退出,因此该函数__sig_exit将始终发生(除非 SIGKILL),它会清除您的锁。

Note: my exit values are not low values. Why? Various batch processing systems make or have expectations of the numbers 0 through 31. Setting them to something else, I can have my scripts and batch streams react accordingly to the previous batch job or script.

注意:我的退出值不低。为什么?各种批处理系统生成或期望数字 0 到 31。将它们设置为其他值,我可以让我的脚本和批处理流对之前的批处理作业或脚本做出相应的反应。

回答by Majal

Reallyquick and reallydirty? This one-liner on the top of your script will work:

真正快速,真的脏吗?脚本顶部的这一行将起作用:

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

Of course, just make sure that your script name is unique. :)

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