【问题标题】:What is the best way to ensure only one instance of a Bash script is running? [duplicate]确保只有一个 Bash 脚本实例正在运行的最佳方法是什么? [复制]
【发布时间】:2010-12-15 11:13:27
【问题描述】:

确保给定脚本只有一个实例正在运行的最简单/最佳方法是什么 - 假设它是 Linux 上的 Bash?

目前我正在做:

ps -C script.name.sh > /dev/null 2>&1 || ./script.name.sh

但它有几个问题:

  1. 它将检查置于脚本之外
  2. 它不允许我从不同的帐户运行相同的脚本 - 我有时会这样做。
  3. -C 只检查进程名称的前 14 个字符

当然,我可以编写自己的 pidfile 处理,但我觉得应该有一个简单的方法来做到这一点。

【问题讨论】:

标签: linux bash pid flock lockfile


【解决方案1】:

建议锁定已经使用了很长时间,并且可以在 bash 脚本中使用。我更喜欢简单的flock(来自util-linux[-ng])而不是lockfile(来自procmail)。并且永远记住那些脚本中的退出陷阱(sigspec == EXIT0,捕获特定信号是多余的)。

2009 年,我发布了我的可锁定脚本样板(最初可在我的 wiki 页面上找到,现在以 gist 的形式提供)。将其转换为每个用户一个实例是微不足道的。使用它,您还可以轻松地为需要锁定或同步的其他场景编写脚本。

为了您的方便,这里是提到的样板。

#!/bin/bash
# SPDX-License-Identifier: MIT

## Copyright (C) 2009 Przemyslaw Pawelczyk <przemoc@gmail.com>
##
## This script is licensed under the terms of the MIT license.
## https://opensource.org/licenses/MIT
#
# Lockable script boilerplate

### HEADER ###

LOCKFILE="/var/lock/`basename $0`"
LOCKFD=99

# PRIVATE
_lock()             { flock -$1 $LOCKFD; }
_no_more_locking()  { _lock u; _lock xn && rm -f $LOCKFILE; }
_prepare_locking()  { eval "exec $LOCKFD>\"$LOCKFILE\""; trap _no_more_locking EXIT; }

# ON START
_prepare_locking

# PUBLIC
exlock_now()        { _lock xn; }  # obtain an exclusive lock immediately or fail
exlock()            { _lock x; }   # obtain an exclusive lock
shlock()            { _lock s; }   # obtain a shared lock
unlock()            { _lock u; }   # drop a lock

### BEGIN OF SCRIPT ###

# Simplest example is avoiding running multiple instances of script.
exlock_now || exit 1

# Remember! Lock file is removed when one of the scripts exits and it is
#           the only script holding the lock or lock is not acquired at all.

【讨论】:

  • @CarlosP:不。在后台flock 只使用flock(2) 系统调用,它不提供此类信息,甚至不应该提供。如果您想不可靠地检查是否存在(或缺少)锁,即不持有它,那么您必须尝试以非阻塞方式获取它(exlock_now)并立即释放它(@987654333 @) 如果你成功了。如果您认为需要在不更改状态的情况下检查锁是否存在,那么您可能使用了错误的工具来解决您的问题。
  • 这个模板很酷。但我不明白你为什么要这样做 { _lock u; _lock xn && rm -f $LOCKFILE; }。刚刚解锁后xn锁的作用是什么?
  • @overthink 只有 &gt; 旁边的文字数字被认为是文件描述符编号,所以没有 evalexec 尝试执行名为 99 的二进制文件(或任何其他放入 @ 987654338@)。值得补充的是,一些 shell(如dash)有一个错误,要求 fd 编号为单个数字。我选择了高 fd 数以避免可能的冲突(不过,它们取决于用例)。我也选择了 BASH,因为陷阱 IIRC 中的EXIT 条件很方便,但看起来我错了,因为it is part of POSIX shell
  • @JayParoline 您误解了您观察到的内容。当您杀死 (-9) 脚本时,即 bash 实例运行脚本文件时,它肯定会死掉,但从它处理 fork()+exec()-ed(就像您的 sleep 一样)继承打开文件描述符的副本以及 @ 987654344@ 锁。在 sleep 睡眠时杀死脚本不会解锁,因为 sleep 进程仍然持有锁。对于可锁定的脚本,这很重要,因为您通常希望保护“环境”(不要在 something 仍在运行时启动另一个实例)。
  • @JayParoline 但是你可以通过在你的东西之前添加( eval "exec $LOCKFD&gt;&amp;-" 和之后添加) 来改变上面解释的行为,所以在这样的块中运行的所有东西都不会继承 LOCKFD (显然锁它)。
【解决方案2】:

如果所有用户的脚本都相同,您可以使用lockfile 方法。如果您获得了锁,则继续,否则显示一条消息并退出。

举个例子:

[Terminal #1] $ lockfile -r 0 /tmp/the.lock
[Terminal #1] $ 

[Terminal #2] $ lockfile -r 0 /tmp/the.lock
[Terminal #2] lockfile: Sorry, giving up on "/tmp/the.lock"

[Terminal #1] $ rm -f /tmp/the.lock
[Terminal #1] $ 

[Terminal #2] $ lockfile -r 0 /tmp/the.lock
[Terminal #2] $ 

在获得/tmp/the.lock 后,您的脚本将是唯一有权执行的脚本。完成后,只需卸下锁即可。在脚本形式中,这可能看起来像:

#!/bin/bash

lockfile -r 0 /tmp/the.lock || exit 1

# Do stuff here

rm -f /tmp/the.lock

【讨论】:

  • 可以给个示例代码sn-p吗?
  • 添加了示例和框架脚本。
  • 我的 linux 上没有 lockfile 程序,但有一件事让我很困扰 - 如果第一个脚本会在没有删除锁的情况下死掉,它会起作用吗?即在这种情况下,我希望下一次运行脚本运行,而不是死“因为以前的副本仍在工作”
  • 您还应该使用内置的 trap 来捕获任何可能提前终止您的脚本的信号。在脚本顶部附近,添加如下内容: trap " [ -f /var/run/my.lock ] && /bin/rm -f /var/run/my.lock" 0 1 2 3 13 15 你可以搜索/usr/bin/* 获取更多示例。
  • @user80168 当前的 Ubuntu (14.04) 提供了一个名为“lockfile-progs”(NFS 安全锁定库)的包,它提供了 lockfile-{check,create,remove,touch}。手册页说:“一旦文件被锁定,必须至少每五分钟触摸一次锁,否则锁将被视为陈旧,随后的锁定尝试将成功......”。似乎是一个很好的包,并提到了一个“--use-pid”选项。
【解决方案3】:

我认为flock 可能是最简单(也是最令人难忘)的变体。我在 cron 作业中使用它来自动编码 dvdscds

# try to run a command, but fail immediately if it's already running
flock -n /var/lock/myjob.lock   my_bash_command

使用-w 进行超时或省略选项以等待锁定被释放。最后,手册页显示了一个很好的多命令示例:

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

【讨论】:

  • 我同意,flock 很好,尤其是与 lockfile 相比,因为通常在大多数 Linux 发行版上都预装了 flock,并且不需要像 lockfile 那样的大型无关实用程序。
  • @jake Biesinger 我是在锁定 .sh 文件还是我用 .sh 文件编写应用程序输出的文件?我是脚本 bash 的新手,所以我必须将它放在我的脚本中的哪里以及如何进行解锁?
  • @Cerin 我需要对 ffmpeg 进程转换做同样的事情,所以我需要在每分钟内完成第一个进程而不考虑 crontab?请问我需要帮助
  • 非常好!想
  • flock 运行良好,直到您意识到您的应用程序没有终止或挂起。我将它与超时一起使用以限制执行时间或防止由于应用程序挂起而无法释放锁定文件
【解决方案4】:

使用 bash set -o noclobber 选项并尝试覆盖公共文件。

flock 不可用或不适用时,这种“bash 友好”技术将很有用。

一个简短的例子

if ! (set -o noclobber ; echo > /tmp/global.lock) ; then
    exit 1  # the global.lock already exists
fi

# ... remainder of script ...

一个更长的例子

此示例将等待global.lock 文件,但时间过长后超时。

 function lockfile_waithold()
 {
    declare -ir time_beg=$(date '+%s')
    declare -ir time_max=7140  # 7140 s = 1 hour 59 min.
 
    # poll for lock file up to ${time_max}s
    # put debugging info in lock file in case of issues ...
    while ! \
       (set -o noclobber ; \
        echo -e "DATE:$(date)\nUSER:$(whoami)\nPID:$$" > /tmp/global.lock \ 
       ) 2>/dev/null
    do
        if [ $(($(date '+%s') - ${time_beg})) -gt ${time_max} ] ; then
            echo "Error: waited too long for lock file /tmp/global.lock" 1>&2
            return 1
        fi
        sleep 1
    done
 
    return 0
 }
 
 function lockfile_release()
 {
    rm -f /tmp/global.lock
 }
 
 if ! lockfile_waithold ; then
      exit 1
 fi
 trap lockfile_release EXIT
 
 # ... remainder of script ...

这在 Ubuntu 16 主机上可靠地为我工作,该主机具有多个使用相同系统范围“锁定”文件的 bash 脚本实例。

(这类似于@Barry Kelly 的this post,后来才注意到。)

【讨论】:

  • 这样做的一个缺点(与flock 式锁定相反)是您的锁定不会在kill -9、重启、断电等时自动释放。
  • @CharlesDuffy ,您可以添加一个 trap lockfile_release EXIT ,它应该涵盖大多数情况。如果断电是一个问题,那么使用锁定文件的临时目录将起作用,例如/tmp.
  • 除了重启 &c 之外,退出陷阱不会在 SIGKILL 上触发(OOM 杀手使用它,因此在某些环境中是一个非常现实的问题)。我仍然认为这种方法通常对内核提供发布保证的任何东西都不那么健壮。 (/tmp 有内存支持,因此硬保证在重新启动时被清除是大部分近年来的情况,但我已经足够老了,不相信这些设施是可用的; 我想一些关于孩子和院子的咆哮是合适的)。
  • 我不确定我是否理解为什么这是一个问题;您当然可以在程序启动后使用flock 获取具有动态文件名的锁,并在不退出的情况下释放它。使用一些现代(bash 4.1)工具来避免需要手动分配 FD:exec {lock_fd}&gt;"$filename" &amp;&amp; flock -x "$lock_fd" || { echo "Lock failed" &gt;&amp;2; exit 1; }; ...stuff here...; exec {lock_fd}&gt;&amp;-
  • 这个解决方案在我的情况下很有用,flocklockfile 在环境中不可用。
【解决方案5】:

我在 procmail 包依赖项中找到了这个:

apt install liblockfile-bin

运行: dotlockfile -l file.lock

file.lock 将被创建。

解锁: dotlockfile -u file.lock

使用它来列出这个包文件/命令: dpkg-query -L liblockfile-bin

【讨论】:

    【解决方案6】:

    我不确定是否有任何可靠的单行解决方案, 所以你最终可能会自己动手。

    锁文件并不完美,但不如使用 'ps | grep | grep -v' 管道。

    话虽如此,您可能会考虑保持过程控制 与您的脚本分开 - 有一个启动脚本。 或者,至少将其分解为保存在单独文件中的函数, 所以你可能在调用者脚本中有:

    . my_script_control.ksh
    
    # Function exits if cannot start due to lockfile or prior running instance.
    my_start_me_up lockfile_name;
    trap "rm -f $lockfile_name; exit" 0 2 3 15
    

    在每个需要控制逻辑的脚本中。 trap 确保在调用者退出时删除锁定文件, 因此您不必在脚本中的每个退出点都编写此代码。

    使用单独的控制脚本意味着您可以对边缘情况进行健全性检查: 删除过时的日志文件,验证锁定文件是否正确关联 当前正在运行的脚本实例,提供终止正在运行的进程的选项,等等。 这也意味着您有更好的机会在ps 输出上成功使用grep。 ps-grep 可用于验证锁定文件是否有与之关联的正在运行的进程。 也许您可以以某种方式命名您的锁定文件以包含有关该过程的信息: user、pid等,后面的脚本调用可以使用这些来决定进程是否 创建锁文件的那个还在。

    【讨论】:

    • +1 用于提及trap
    • 0 信号是什么?在kill -l看不到
    • @qed - 这意味着在退出脚本时运行陷阱。见gnu.org/software/bash/manual/bashref.html#index-trap
    • 看起来很像python中的try...catch...finally...
    • @qed:@martin 是对的,文档指出trap ... 0trap ... EXIT 的别名。但是,当发送信号0kill -0 ... 时,您只需检查进程是否存在并允许您向其发送信号。这用于等待(轮询)您的一个不是当前进程的子进程的结束。信号 0 没有任何作用。
    【解决方案7】:

    第一个测试示例

    [[ $(lsof -t $0| wc -l) > 1 ]] && echo "At least one of $0 is running"
    

    第二个测试示例

    currsh=$0
    currpid=$$
    runpid=$(lsof -t $currsh| paste -s -d " ")
    if [[ $runpid == $currpid ]]
    then
      sleep 11111111111111111
    else
      echo -e "\nPID($runpid)($currpid) ::: At least one of \"$currsh\" is running !!!\n"
      false
      exit 1
    fi
    

    解释

    “lsof -t”列出当前运行的名为“$0”的脚本的所有pid。

    命令“lsof”有两个好处。

    1. 忽略 vim 等编辑器正在编辑的 pid,因为 vim 会编辑其映射文件,例如“.file.swp”。
    2. 忽略当前运行的 shell 脚本派生的 pid,这是大多数“grep”衍生命令无法实现的。使用“pstree -pH pidnum”命令查看当前进程分叉状态的详细信息。

    【讨论】:

    • 为我工作!需要了解,这个答案是否有任何理由反对?
    • lsof 并不总是在您的 $PATH 中。
    • lsof 可能不是原子动作,因此它在竞争条件下会受到影响。
    【解决方案8】:

    我还建议查看chpst(runit 的一部分):

    chpst -L /tmp/your-lockfile.loc ./script.name.sh
    

    【讨论】:

    • +1 因为它的简单性。
    【解决方案9】:

    Ubuntu/Debian 发行版具有 start-stop-daemon 工具,其用途与您描述的相同。另请参阅 /etc/init.d/skeleton,了解它如何用于编写启动/停止脚本。

    -- 诺亚

    【讨论】:

      【解决方案10】:

      一线终极解决方案:

      [ "$(pgrep -fn $0)" -ne "$(pgrep -fo $0)" ] && echo "At least 2 copies of $0 are running"
      

      【讨论】:

      • pgrep -fn ... -fo $0 还与您的文本编辑器匹配,该编辑器已打开脚本进行编辑。这种情况有解决办法吗?
      • 对于不能使用传统方式的情况,这是一个非常具体的解决方案,如果它不符合您的需求,您仍然可以使用锁定文件。如果您仍然需要这一行解决方案,您可以使用带有 $0 的 $* 对其进行修改,并将唯一参数传递给您的脚本,该参数不会出现在文本编辑器命令行中。
      • 此解决方案在竞争条件下受到影响:测试构造不是原子的。
      【解决方案11】:

      我遇到了同样的问题,并提出了一个使用 lockfile 的 template、一个保存进程 ID 号的 pid 文件和一个 kill -0 $(cat $pid_file) 检查以使中止的脚本不会停止下一次运行。 这会在 /tmp 中创建一个 foobar-$USERID 文件夹,锁定文件和 pid 文件所在的位置。

      您仍然可以调用脚本并执行其他操作,只要将这些操作保留在 alertRunningPS 中即可。

      #!/bin/bash
      
      user_id_num=$(id -u)
      pid_file="/tmp/foobar-$user_id_num/foobar-$user_id_num.pid"
      lock_file="/tmp/foobar-$user_id_num/running.lock"
      ps_id=$$
      
      function alertRunningPS () {
          local PID=$(cat "$pid_file" 2> /dev/null)
          echo "Lockfile present. ps id file: $PID"
          echo "Checking if process is actually running or something left over from crash..."
          if kill -0 $PID 2> /dev/null; then
              echo "Already running, exiting"
              exit 1
          else
              echo "Not running, removing lock and continuing"
              rm -f "$lock_file"
              lockfile -r 0 "$lock_file"
          fi
      }
      
      echo "Hello, checking some stuff before locking stuff"
      
      # Lock further operations to one process
      mkdir -p /tmp/foobar-$user_id_num
      lockfile -r 0 "$lock_file" || alertRunningPS
      
      # Do stuff here
      echo -n $ps_id > "$pid_file"
      echo "Running stuff in ONE ps"
      
      sleep 30s
      
      rm -f "$lock_file"
      rm -f "$pid_file"
      exit 0
      

      【讨论】:

        【解决方案12】:

        我找到了一种非常简单的方法来处理“每个系统一个脚本副本”。 它不允许我从多个帐户运行脚本的多个副本(在标准 Linux 上)。

        解决方案:

        在脚本的开头,我给出了:

        pidof -s -o '%PPID' -x $( basename $0 ) > /dev/null 2>&1 && exit
        

        显然pidof 在以下方面表现出色:

        • 它没有像ps -C ...这样的程序名称限制
        • 不需要我做grep -v grep(或类似的事情)

        而且它不依赖于锁文件,这对我来说是一个很大的胜利,因为对它们进行中继意味着您必须添加对陈旧锁文件的处理——这并不复杂,但如果可以避免的话——为什么不呢?

        至于检查“每个正在运行的用户一个脚本副本”,我写了这个,但我对此并不太满意:

        (
            pidof -s -o '%PPID' -x $( basename $0 ) | tr ' ' '\n'
            ps xo pid= | tr -cd '[0-9\n]'
        ) | sort | uniq -d
        

        然后我检查它的输出 - 如果它是空的 - 没有来自同一用户的脚本副本。

        【讨论】:

          【解决方案13】:

          来自您的脚本:

          ps -ef | grep $0 | grep $(whoami)
          

          【讨论】:

          • 这有一个相对众所周知的错误,即 grep 发现自己。当然我可以解决它,但这不是我所说的简单和健壮的东西。
          • 我见过很多'grep -v grep'。你的 ps 可能也支持 -u $LOGNAME。
          • 它相对健壮,因为它使用 $0 和 whoami 来确保您只获取由您的用户 ID 启动的脚本
          • ennuikiller: no - grep $0 会找到类似 $0 的进程(例如,现在正在运行这个 ps 的进程),但它会找到一个 grep 本身!所以基本上 - 它几乎总是会成功。
          • @ennukiller:你的例子中没有这个假设。此外 - 即使在“call.sh”之类的东西中,它也会找到“call.sh”。如果我从 ./call.sh 本身调用它,它也会失败(它会找到正在检查的 call.sh 副本,而不是以前的) - 所以。简而言之 - 这不是解决方案。可以通过添加至少 2 个 grep 或更改现有的 grep 将其更改为解决方案,但它本身并不能解决问题。
          【解决方案14】:

          这是我们的标准位。它可以从脚本以某种方式死亡而无需清理它的锁定文件来恢复。

          如果正常运行,它将进程ID写入锁定文件。如果它在开始运行时发现一个锁定文件,它将从锁定文件中读取进程 ID 并检查该进程是否存在。如果该进程不存在,它将删除陈旧的锁定文件并继续。只有当锁定文件存在并且进程仍在运行时,它才会退出。它在退出时会写一条消息。

          # lock to ensure we don't get two copies of the same job
          script_name="myscript.sh"
          lock="/var/run/${script_name}.pid"
          if [[ -e "${lock}" ]]; then
              pid=$(cat ${lock})
              if [[ -e /proc/${pid} ]]; then
                  echo "${script_name}: Process ${pid} is still running, exiting."
                  exit 1
              else
                  # Clean up previous lock file
                  rm -f ${lock}
             fi
          fi
          trap "rm -f ${lock}; exit $?" INT TERM EXIT
          # write $$ (PID) to the lock file
          echo "$$" > ${lock}
          

          【讨论】:

          • 该解决方案具有非常明显的竞争条件(并非其他解决方案没有)。
          • 另外,exit $? 将始终返回零。
          猜你喜欢
          • 2010-10-02
          • 1970-01-01
          • 2011-09-22
          • 2011-01-19
          • 1970-01-01
          • 2014-12-08
          • 1970-01-01
          • 2011-02-01
          • 2010-09-27
          相关资源
          最近更新 更多