【问题标题】:Quick-and-dirty way to ensure only one instance of a shell script is running at a time确保一次只运行一个 shell 脚本实例的快捷方式
【发布时间】:2010-09-16 04:10:08
【问题描述】:

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

【问题讨论】:

标签: bash shell process lockfile


【解决方案1】:

看看 FLOM(免费锁管理器)http://sourceforge.net/projects/flom/:您可以使用不需要文件系统中的锁文件的抽象资源来同步命令和/或脚本。您可以同步在不同系统中运行的命令,而无需像 NFS(网络文件系统)服务器这样的 NAS(网络附加存储)。

使用最简单的用例,序列化“command1”和“command2”可能就像执行一样简单:

flom -- command1

flom -- command2

来自两个不同的 shell 脚本。

【讨论】:

  • 这是编写非便携式脚本的一种好方法。随机用户安装 flom 的几率是多少?
【解决方案2】:

结合上面提供的答案,这是一个更优雅、安全、快速的 &dirty 方法。

用法

  1. 包括 sh_lock_functions.sh
  2. 使用 sh_lock_init 初始化
  3. 使用 sh_acquire_lock 锁定
  4. 使用 sh_check_lock 检查锁
  5. 使用 sh_remove_lock 解锁

脚本文件

sh_lock_functions.sh

#!/bin/bash

function sh_lock_init {
    sh_lock_scriptName=$(basename $0)
    sh_lock_dir="/tmp/${sh_lock_scriptName}.lock" #lock directory
    sh_lock_file="${sh_lock_dir}/lockPid.txt" #lock file
}

function sh_acquire_lock {
    if mkdir $sh_lock_dir 2>/dev/null; then #check for lock
        echo "$sh_lock_scriptName lock acquired successfully.">&2
        touch $sh_lock_file
        echo $$ > $sh_lock_file # set current pid in lockFile
        return 0
    else
        touch $sh_lock_file
        read sh_lock_lastPID < $sh_lock_file
        if [ ! -z "$sh_lock_lastPID" -a -d /proc/$sh_lock_lastPID ]; then # if lastPID is not null and a process with that pid exists
            echo "$sh_lock_scriptName is already running.">&2
            return 1
        else
            echo "$sh_lock_scriptName stopped during execution, reacquiring lock.">&2
            echo $$ > $sh_lock_file # set current pid in lockFile
            return 2
        fi
    fi
    return 0
}

function sh_check_lock {
    [[ ! -f $sh_lock_file ]] && echo "$sh_lock_scriptName lock file removed.">&2 && return 1
    read sh_lock_lastPID < $sh_lock_file
    [[ $sh_lock_lastPID -ne $$ ]] && echo "$sh_lock_scriptName lock file pid has changed.">&2  && return 2
    echo "$sh_lock_scriptName lock still in place.">&2
    return 0
}

function sh_remove_lock {
    rm -r $sh_lock_dir
}

使用示例

sh_lock_usage_example.sh

#!/bin/bash
. /path/to/sh_lock_functions.sh # load sh lock functions

sh_lock_init || exit $?

sh_acquire_lock
lockStatus=$?
[[ $lockStatus -eq 1 ]] && exit $lockStatus
[[ $lockStatus -eq 2 ]] && echo "lock is set, do some resume from crash procedures";

#monitoring example
cnt=0
while sh_check_lock # loop while lock is in place
do
    echo "$sh_scriptName running (pid $$)"
    sleep 1
    let cnt++
    [[ $cnt -gt 5 ]] && break
done

#remove lock when process finished
sh_remove_lock || exit $?

exit 0

特点

  • 使用文件、目录和进程 ID 的组合来锁定以确保进程尚未运行
  • 您可以检测脚本是否在解除锁定之前停止(例如进程终止、关闭、错误等)
  • 您可以检查锁定文件,并在锁定丢失时使用它来触发进程关闭
  • 详细,输出错误消息以便于调试

【讨论】:

    【解决方案3】:

    为什么我们不使用类似的东西

    pgrep -f $cmd || $cmd
    

    【讨论】:

    • 因为这不会阻止启动$cmd 的两个实例。
    • 除非 $cmd 在内部处理它,否则这将有助于在启动新进程之前检查 $cmd 是否已经在运行,这与检查 .lock 文件非常相似,其他脚本通常在启动前执行
    【解决方案4】:
    if [ 1 -ne $(/bin/fuser "$0" 2>/dev/null | wc -w) ]; then
        exit 1
    fi
    

    【讨论】:

    • 您能否编辑您的答案以解释这是做什么的,以及它如何解决问题?
    • 虽然这可能会回答这个问题,但在您的答案中加入一些文字来解释您在做什么总是一个好主意。阅读how to write a good answer
    【解决方案5】:

    我有一个基于文件名的简单解决方案

    #!/bin/bash
    
    MY_FILENAME=`basename "$BASH_SOURCE"`
    
    MY_PROCESS_COUNT=$(ps a -o pid,cmd | grep $MY_FILENAME | grep -v grep | grep -v $$ | wc -
    l)
    
    if [ $MY_PROCESS_COUNT -ne 0  ]; then
      echo found another process
      exit 0
    if
    
    # Follows the code to get the job done.
    

    【讨论】:

      【解决方案6】:

      晚会,使用@Majal 的想法,这是我的脚本,仅启动一个 emacsclient GUI 实例。有了它,我可以设置快捷键打开或跳回同一个emacsclient。我有另一个脚本可以在需要时在终端中调用 emacsclient。这里使用 emacsclient 只是为了展示一个工作示例,可以选择其他的。这种方法对我的小脚本来说既快又好。告诉我哪里脏了:)

      #!/bin/bash
      
      # if [ $(pgrep -c $(basename $0)) -lt 2 ]; then # this works but requires script name to be unique
      if [ $(pidof -x "$0"|wc -w ) -lt 3 ]; then
          echo -e "Starting $(basename $0)"
          emacsclient --alternate-editor="" -c "$@"
      else
          echo -e "$0 is running already"
      fi
      

      【讨论】:

      • 为什么是-lt 3?如果已经有一个实例正在运行,它不会开始吗?还是 emaxclient 总是启动 2 个实例?
      【解决方案7】:

      这一行答案来自相关人员Ask Ubuntu Q&A

      [ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
      #     This is useful boilerplate code for shell scripts.  Put it at the top  of
      #     the  shell script you want to lock and it'll automatically lock itself on
      #     the first run.  If the env var $FLOCKER is not set to  the  shell  script
      #     that  is being run, then execute flock and grab an exclusive non-blocking
      #     lock (using the script itself as the lock file) before re-execing  itself
      #     with  the right arguments.  It also sets the FLOCKER env var to the right
      #     value so it doesn't run again.
      

      【讨论】:

        【解决方案8】:

        我在任何地方都没有提到这个,它使用 read,我不完全知道 read 是否真的是原子的,但到目前为止它对我很有帮助......,它很有趣,因为它只是 bash 内置,这是进程内实现,您启动 locker 协进程并使用它的 i/o 来管理锁,只需将目标 i/o 从 locker 文件描述符交换到文件系统文件描述符 (exec 3&lt;&gt;/file &amp;&amp; exec 4&lt;/file) 即可在进程间完成相同的操作

        ## gives locks
        locker() {
            locked=false
            while read l; do
                case "$l" in
                    lock)
                        if $locked; then
                            echo false
                        else
                            locked=true
                            echo true
                        fi
                        ;;
                    unlock)
                        if $locked; then
                            locked=false
                            echo true
                        else
                            echo false
                        fi
                        ;;
                    *)
                        echo false
                        ;;
                esac
            done
        }
        ## locks
        lock() {
            local response
            echo lock >&${locker[1]}
            read -ru ${locker[0]} response
            $response && return 0 || return 1
        }
        
        ## unlocks
        unlock() {
            local response
            echo unlock >&${locker[1]}
            read -ru ${locker[0]} response
            $response && return 0 || return 1
        }
        

        【讨论】:

          【解决方案9】:

          我对现有答案有以下问题:

          • 一些答案​​尝试清理锁定文件,然后不得不处理由例如导致的过时锁定文件。突然崩溃/重启。 IMO 是不必要的复杂。 保留锁定文件。
          • 一些答案​​使用脚本文件本身$0$BASH_SOURCE 进行锁定,通常参考man flock 中的示例。 当由于更新或编辑导致下一次运行打开并获得对新脚本文件的锁定而替换脚本时,即使另一个持有已删除文件锁定的实例仍在运行,此操作也会失败。
          • 很少有答案使用固定的文件描述符。 这并不理想。 我不想依赖这将如何表现,例如打开锁定文件失败但处理不当并尝试锁定从父进程继承的不相关文件描述符。 另一个失败案例是为不处理自身锁定但固定文件描述符会干扰文件描述符传递给子进程的第 3 方二进制文件注入锁定包装器。
          • 我拒绝使用进程查找已运行脚本名称的答案。 造成这种情况的原因有很多,例如但不限于可靠性/原子性、解析输出以及拥有执行多个相关功能的脚本,其中一些功能不需要锁定。

          这个答案可以:

          • 依赖flock,因为它让内核提供锁定...前提是锁定文件是原子创建的而不是替换的。
          • 假设并依赖存储在本地文件系统而不是 NFS 上的锁定文件。
          • 将锁定文件的存在更改为对正在运行的实例没有任何意义。 它的作用纯粹是防止两个并发实例创建同名文件并替换另一个副本。 锁定文件不会被删除,它会被遗忘并且可以在重新启动后继续存在。 锁定通过flock 指示,而不是通过锁定文件存在。
          • 假设 bash shell,正如问题所标记的那样。

          它不是单行器,但没有 cmets 也没有错误消息,它足够小:

          #!/bin/bash
          
          LOCKFILE=/var/lock/TODO
          
          set -o noclobber
          exec {lockfd}<> "${LOCKFILE}" || exit 1
          set +o noclobber # depends on what you need
          flock --exclusive --nonblock ${lockfd} || exit 1
          

          但我更喜欢 cmets 和错误消息:

          #!/bin/bash
          
          # TODO Set a lock file name
          LOCKFILE=/var/lock/myprogram.lock
          
          # Set noclobber option to ensure lock file is not REPLACED.
          set -o noclobber
          
          # Open lock file for R+W on a new file descriptor
          # and assign the new file descriptor to "lockfd" variable.
          # This does NOT obtain a lock but ensures the file exists and opens it.
          exec {lockfd}<> "${LOCKFILE}" || {
            echo "pid=$$ failed to open LOCKFILE='${LOCKFILE}'" 1>&2
            exit 1
          }
          
          # TODO!!!! undo/set the desired noclobber value for the remainder of the script
          set +o noclobber
          
          # Lock on the allocated file descriptor or fail
          # Adjust flock options e.g. --noblock as needed
          flock --exclusive --nonblock ${lockfd} || {
            echo "pid=$$ failed to obtain lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'" 1>&2
            exit 1
          }
          
          # DO work here
          echo "pid=$$ obtained exclusive lock fd='${lockfd}' LOCKFILE='${LOCKFILE}'"
          
          # Can unlock after critical section and do more work after unlocking
          #flock -u ${lockfd};
          # if unlocking then might as well close lockfd too
          #exec {lockfd}<&-
          

          【讨论】:

          • 不,我不将 PID 写入锁文件,我不希望任何人应用 kill $(cat lockfile) 的习惯并杀死不相关的进程,这是依赖锁时会发生的问题文件存在并且必须清理过时的锁定文件。无需清洁 - 没问题。
          【解决方案10】:

          如果您的脚本名称是唯一的,这将起作用:

          #!/bin/bash
          if [ $(pgrep -c $(basename $0)) -gt 1 ]; then 
            echo $(basename $0) is already running
            exit 0
          fi
          

          如果脚本名不是唯一的,这适用于大多数 linux 发行版:

          #!/bin/bash
          exec 9>/tmp/my_lock_file
          if ! flock -n 9  ; then
             echo "another instance of this script is already running";
             exit 1
          fi
          

          来源: http://mywiki.wooledge.org/BashFAQ/045

          【讨论】:

            【解决方案11】:

            试试下面的方法,

            ab=`ps -ef | grep -v grep | grep -wc processname`
            

            然后使用 if 循环将变量与 1 匹配。

            【讨论】:

            • 在这种情况下,我会使用类似 ab=ps -ef | egrep -v "(grep|$$)" | grep -wc processname 这样的东西,如果检查的目的是禁止当前脚本的多个实例,它将与当前进程不匹配。
            猜你喜欢
            • 2011-09-22
            • 1970-01-01
            • 2012-01-08
            • 2019-02-08
            • 2011-04-27
            • 1970-01-01
            • 1970-01-01
            • 2011-01-15
            • 2021-05-22
            相关资源
            最近更新 更多