shell 脚本中的锁定是否正确?

shell 脚本中的锁定是否正确?

有时您必须确保同一时间只有一个 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 系统上的默认设置)的手册页,没有显示任何有关锁定的信息。

lockrun将类似的工具包含在其中是一个好主意吗?核心工具

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

答案1

这是在 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

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

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

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

无论如何,文件锁定几乎是尊重系统行为,任何在运行之前不检查锁定文件是否存在的脚本都会执行它们要做的任何事情。只需进行锁定文件测试和正确的行为,您就可以解决 99% 的潜在问题(即使不是 100%)。

如果您经常遇到锁文件竞争条件,则可能表明存在更大的问题,例如您的作业时间不正确,或者如果间隔不如作业完成那么重要,则您的作业可能更适合守护进程。


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


根据@Clint Pachl 下面的评论,如果您使用 ksh88,请使用mkdir而不是noclobber.这主要减轻了潜在的竞争条件,但并没有完全限制它(尽管风险很小)。欲了解更多信息,请阅读克林特在下面发布的链接

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

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

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

答案2

我更喜欢使用硬链接。

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 实现允许原子硬链接而不是原子mkdir。对于 NFS 的现代版本,您不必担心使用其中任何一个。

答案3

我知道这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

答案4

一个简单的方法是使用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"

相关内容