正确锁定 shell 脚本?

max*_*zig 71 cron nfs shell-script coreutils lock

有时您必须确保同一时间只有一个 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
Run Code Online (Sandbox Code Playgroud)

当然,这样的代码有竞争条件。有一个时间窗口,其中两个实例的执行都可以在第 3 行之后才能访问$LOCK文件。

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

但是事情可能会出错——例如,当锁定文件在 NFS 服务器上时——挂起。在这种情况下,多个 cron 作业可以在第 3 行阻塞并排队。如果NFS服务器再次处于活动状态,那么你已经惊群并行运行的作业。

在网上搜索我发现工具lockrun似乎是该问题的一个很好的解决方案。使用它,您可以运行一个需要像这样锁定的脚本:

$ lockrun --lockfile=/var/tmp/mylock myscript.sh
Run Code Online (Sandbox Code Playgroud)

您可以将其放在包装器中或从您的 crontab 中使用它。

lockf()如果可用,它使用(POSIX) 并回退到flock()(BSD)。并且lockf()对 NFS 的支持应该相对广泛。

有替代品lockrun吗?

其他 cron 守护进程呢?是否有常见的 crond 支持以理智的方式锁定?快速查看 Vixie Crond 的手册页(Debian/Ubuntu 系统上的默认设置)不会显示任何有关锁定的信息。

难道是一个好主意,包括像一个工具lockrun进入的coreutils

在我看来,它实现了一个与timeout,nice和朋友非常相似的主题。

Tim*_*edy 51

这是在 shell 脚本中进行锁定的另一种方法,可以防止上面描述的竞争条件,其中两个作业都可能通过第 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
Run Code Online (Sandbox Code Playgroud)

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

如果您有从多个服务器同时执行相同操作的 cron 作业,但您只需要实际运行 1 个实例,这样的操作可能适合您。

我没有使用 lockrun 的经验,但是在脚本实际运行之前拥有一个预设的锁定环境可能会有所帮助。或者它可能不会。您只是在包装器中为脚本外部的锁文件设置测试,从理论上讲,如果 lockrun 同时调用两个作业,您不能遇到相同的竞争条件,就像“内部-脚本的解决方案?

无论如何,文件锁定在很大程度上尊重系统行为,并且任何在运行之前不检查锁定文件是否存在的脚本都将执行它们将要执行的任何操作。仅仅通过进行锁定文件测试和正确的行为,您将解决 99% 的潜在问题,如果不是 100%。

如果您经常遇到锁文件争用情况,这可能表明存在更大的问题,例如您的工作时间安排不正确,或者如果时间间隔不如工作完成那么重要,那么您的工作可能更适合进行守护进程.


在下面编辑 - 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
Run Code Online (Sandbox Code Playgroud)

而且,作为一个额外的优势,如果您需要在脚本中创建 tmpfiles,您可以使用lockdir它们的目录,知道它们将在脚本退出时被清除。

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

  • 很好的答案,但您还应该“杀死 -0”锁文件中的值,以确保创建锁的进程仍然存在。 (3认同)
  • 注意:在 ksh88 中使用 `noclobber`(或 `-C`)不起作用,因为 ksh88 没有将 `O_EXCL` 用于 `noclobber`。如果您使用较新的外壳运行,则可能没问题... (2认同)

Arc*_*ege 15

我更喜欢使用硬链接。

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
Run Code Online (Sandbox Code Playgroud)

硬链接在 NFS 上是原子的,并且在大多数情况下,mkdir 也是. 在实用层面上使用mkdir(2)link(2)大致相同;我只是更喜欢使用硬链接,因为 NFS 的更多实现允许原子硬链接而不是 atomic mkdir。使用 NFS 的现代版本,您不必担心使用它们。


gle*_*man 13

我知道这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
Run Code Online (Sandbox Code Playgroud)

  • @maxschlepzig [RFC 1813](http://www.ietf.org/rfc/rfc1813.txt) 没有明确要求 `mkdir` 是原子的(它是为了 `rename`)。实际上,[已知](http://www.mail-archive.com/freebsd-hackers@freebsd.org/msg20456.html)有些实现不是。相关:[一个有趣的线程,包括 GNU arch 的作者的贡献](http://news.ycombinator.com/item?id=1035100)。 (2认同)

jof*_*fel 9

一种简单的方法是使用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"
Run Code Online (Sandbox Code Playgroud)


小智 8

sem作为 GNUparallel工具的一部分提供的可能正是您正在寻找的:

sem [--fg] [--id <id>] [--semaphoretimeout <secs>] [-j <num>] [--wait] command
Run Code Online (Sandbox Code Playgroud)

如:

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" &
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

请注意,不能保证顺序。此外,在完成之前不会显示输出(令人讨厌!)。但即便如此,这是我所知道的防止并发执行的最简洁的方法,无需担心锁定文件、重试和清理。

  • `sem` 提供的锁定是否会在执行中被击落? (2认同)

小智 5

我使用命令行工具flock来管理 bash 脚本中的锁,如此处此处所述。它包含在 Debian 软件包中util-linux,并且默认安装在许多 Linux 系统上。

以下是如何将它用于 cron 作业:

* * * * * flock -n /tmp/foobar /usr/bin/foobar arg1 arg2 arg3
Run Code Online (Sandbox Code Playgroud)

如果文件/tmp/foobar尚不存在,这将创建该文件,尝试获取该文件的独占锁,运行/usr/bin/foobar arg1 arg2 arg3,然后释放该锁,并使用命令的退出代码退出。如果无法获得锁定,则flock由于-n选项( 的缩写--non-block),该命令会立即失败并返回 1 退出代码。

我使用了flock手册页中的这个简单方法,在子shell中运行一些命令......

   (
     flock -n 9
     # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile
Run Code Online (Sandbox Code Playgroud)

在该示例中,如果无法获取锁定文件,则会失败并退出代码为 1。

  • @dubiousjim,[BSD lockf](https://www.freebsd.org/cgi/man.cgi?query=lockf) 也调用 `flock()`,因此在 NFS 上存在问题。顺便说一句,与此同时,当文件位于 NFS 挂载上时,Linux 上的集群()现在会回退到“fcntl()”,因此,在仅限 Linux 的 NFS 环境中,“flock()”现在可以通过 NFS 工作。 (4认同)
  • “flock()”系统调用[不能通过 NFS 工作](http://linux.die.net/man/2/flock)。 (3认同)