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


当前回答

一些unix具有与前面提到的flock非常相似的lockfile。

从手册中:

Lockfile可以用来创建一个 或者更多的信号量文件。如果锁, 文件不能创建所有指定的 文件(在指定的顺序),它 等待睡眠时间(默认为8) 秒并重试最后一个文件 没有成功。您可以指定 直到重试的次数 返回失败。如果数字 重试次数为-1(默认值,即 -r-1)锁文件将永远重试。

其他回答

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

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

注意:这个特殊的命令是util-linux的一部分。如果您运行的是Linux以外的操作系统,那么它可能不可用,也可能不可用。

羊群走的路才是正确的。想想当脚本突然失效时会发生什么。在羊群的情况下,你只是松散的羊群,但这不是一个问题。另外,请注意,一个邪恶的伎俩是在脚本本身取一群。但这当然会让您完全陷入权限问题。

对于shell脚本,我倾向于使用mkdir而不是flock,因为它使锁更可移植。

不管怎样,使用set -e是不够的。它只在任何命令失败时退出脚本。你的锁还是会留下的。

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

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

接下来会发生什么。所有陷阱都会产生一个出口,所以__sig_exit函数总是会发生(除非SIGKILL),它会清理你的锁。

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

我使用一种简单的方法来处理过期的锁文件。

注意,上面的一些解决方案存储pid,忽略了pid可以环绕的事实。因此,仅仅检查是否有一个有效的进程与存储的pid是不够的,特别是对于长时间运行的脚本。

我使用noclobber来确保一次只能打开一个脚本并写入锁文件。此外,我在锁文件中存储了足够的信息来惟一地标识一个进程。我定义了一组数据来唯一地标识一个进程为pid、ppid、lstart。

当一个新脚本启动时,如果它未能创建锁文件,那么它将验证创建锁文件的进程是否仍然存在。如果不是,我们假设原始进程不体面地死亡,并留下一个过时的锁文件。然后,新脚本获得锁文件的所有权,一切又恢复正常了。

应该与跨多个平台的多个shell一起工作。快速、便携、简单。

#!/usr/bin/env sh
# Author: rouble

LOCKFILE=/var/tmp/lockfile #customize this line

trap release INT TERM EXIT

# Creates a lockfile. Sets global variable $ACQUIRED to true on success.
# 
# Returns 0 if it is successfully able to create lockfile.
acquire () {
    set -C #Shell noclobber option. If file exists, > will fail.
    UUID=`ps -eo pid,ppid,lstart $$ | tail -1`
    if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
        ACQUIRED="TRUE"
        return 0
    else
        if [ -e $LOCKFILE ]; then 
            # We may be dealing with a stale lock file.
            # Bring out the magnifying glass. 
            CURRENT_UUID_FROM_LOCKFILE=`cat $LOCKFILE`
            CURRENT_PID_FROM_LOCKFILE=`cat $LOCKFILE | cut -f 1 -d " "`
            CURRENT_UUID_FROM_PS=`ps -eo pid,ppid,lstart $CURRENT_PID_FROM_LOCKFILE | tail -1`
            if [ "$CURRENT_UUID_FROM_LOCKFILE" == "$CURRENT_UUID_FROM_PS" ]; then 
                echo "Script already running with following identification: $CURRENT_UUID_FROM_LOCKFILE" >&2
                return 1
            else
                # The process that created this lock file died an ungraceful death. 
                # Take ownership of the lock file.
                echo "The process $CURRENT_UUID_FROM_LOCKFILE is no longer around. Taking ownership of $LOCKFILE"
                release "FORCE"
                if (echo "$UUID" > "$LOCKFILE") 2>/dev/null; then
                    ACQUIRED="TRUE"
                    return 0
                else
                    echo "Cannot write to $LOCKFILE. Error." >&2
                    return 1
                fi
            fi
        else
            echo "Do you have write permissons to $LOCKFILE ?" >&2
            return 1
        fi
    fi
}

# Removes the lock file only if this script created it ($ACQUIRED is set), 
# OR, if we are removing a stale lock file (first parameter is "FORCE") 
release () {
    #Destroy lock file. Take no prisoners.
    if [ "$ACQUIRED" ] || [ "$1" == "FORCE" ]; then
        rm -f $LOCKFILE
    fi
}

# Test code
# int main( int argc, const char* argv[] )
echo "Acquring lock."
acquire
if [ $? -eq 0 ]; then 
    echo "Acquired lock."
    read -p "Press [Enter] key to release lock..."
    release
    echo "Released lock."
else
    echo "Unable to acquire lock."
fi

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

使用

包括sh_lock_functions.sh 使用sh_lock_init初始化 使用sh_acquire_lock进行锁定 使用sh_check_lock检查锁 使用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的组合来锁定,以确保进程尚未运行 您可以检测脚本是否在删除锁之前停止(例如。进程终止,关闭,错误等) 您可以检查锁文件,并在丢失锁时使用它来触发进程关闭 Verbose,输出错误消息,以便于调试