如何确保仅运行bash脚本的一个实例?


26

首选不需要其他工具的解决方案。


锁定文件呢?
马可(Marco)

@Marco我使用该答案找到了这样的答案,但正如评论中所述,这可能会导致比赛情况
Tobias Kienzler 2012年

3
这是BashFAQ 45
2012年

@ jw013谢谢!因此,也许像这样ln -s my.pid .lock会要求锁定(后跟echo $$ > my.pid),并且在失败时可以检查存储在其中的PID是否.lock确实是脚本的活动实例
Tobias Kienzler 2012年

Answers:


18

几乎像nsg的答案:使用lock 目录。在Linux和Unix和* BSD和许多其他操作系统下,目录创建是原子的。

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

您可以将锁定sh的PID放在lock目录中的文件中以进行调试,但不要陷入可以检查PID来查看锁定过程是否仍在执行的想法。这条道路上有很多比赛条件。


1
我会考虑使用存储的PID来检查锁定实例是否仍然有效。然而,这里的一个要求是mkdir不是在NFS原子(这是不适合我的情况,但我想应该提到的是,如果真)
托比亚斯Kienzler

是的,请务必使用存储的PID来查看锁定过程是否仍在执行,但是除了记录消息外,请勿尝试执行其他任何操作。检查存储的pid,创建新的PID文件等工作为比赛留出了一个大窗口。
Bruce Ediger 2012年

好的,正如Ihunath所说,lockdir很可能/tmp通常不在NFS共享中,因此应该没问题。
Tobias Kienzler 2012年

我会rm -rf用来删除锁目录。rmdir如果有人(不一定是您)设法将文件添加到目录,将失败。
chepner 2012年

18

要添加到布鲁斯Ediger的回答,并启发这个答案,你也应该增加更多的智慧来清理打击脚本终止后卫:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi

或者,if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement
库萨兰达

6

这可能太简单了,如果我错了,请纠正我。还ps不够简单吗?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment

1
尼斯和/或辉煌。:)
诡异的

1
我已经使用了这种情况一周,而且有2次没有阻止新程序的启动。我想出了什么问题-新pid是旧字符串的子字符串,并被隐藏grep -v $$。真实案例:老- 14532,新的- 1453年,老- 28858,新- 858
Naktibalda

我通过更改grep -v $$grep -v "^${$} "
Naktibalda,

@Naktibalda好收成,谢谢!您也可以使用grep -wv "^$$"(参见编辑)对其进行修复。
terdon

感谢您的更新。我的模式偶尔会失败,因为较短的pid留有空格。
Naktibalda

4

正如Marco所述,我将使用一个锁定文件

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file

1
(我认为您忘记了创建锁定文件)竞争状况如何?
Tobias Kienzler 2012年

ops :)是的,在我的示例中,竞赛条件是一个问题,我通常每小时或每天写一次cron作业,竞赛条件很少见。
nsg 2012年

在我的情况下,它们也不应该相关,但是应该牢记这一点。也许使用lsof $0还不错吗?
Tobias Kienzler 2012年

您可以通过将您的信息写入$$锁定文件来减少竞争状况。然后sleep短时间间隔回读。如果PID仍然是您的PID,则说明您已成功获取该锁。绝对不需要其他工具。
manatwork 2012年

1
我从未为此目的使用过lsof,我认为它应该起作用。请注意,lsof 在我的系统中确实很慢(1-2秒),并且很可能有很多时间用于比赛条件。
nsg 2012年

3

如果要确保脚本的仅一个实例正在运行,请查看:

锁定脚本(针对并行运行)

否则,您可以检查ps或调用lsof <full-path-of-your-script>,因为我不会称其为其他工具。


补品

实际上,我想到了这样做:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

这样可以确保pid即使<your_script>同时分叉并执行多个实例,只有最低的进程仍可以继续运行。


1
感谢您提供的链接,但您能否在答案中包含必要的部分?SE的通用策略是防止链接腐烂...但是[[(lsof $0 | wc -l) > 2]] && exit实际上可能已经足够了,或者这也容易产生竞争状况吗?
Tobias Kienzler 2012年

您是对的,我的答案的基本部分丢失了,仅发布链接非常la脚。我在回答中添加了自己的建议。
user1146332 2012年

3

确保bash脚本的单个实例运行的另一种方法:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 如果现有脚本的PID已在运行,则获取该PID;如果没有其他脚本在运行,则以错误代码1退出


3

尽管您要求的解决方案没有其他工具,但这是我最喜欢的使用方式flock

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

这来自的示例部分man flock,进一步说明了以下内容:

这对于s​​hell脚本很有用。将其放在要锁定的Shell脚本的顶部,它将在第一次运行时自动锁定自身。如果未将env var $ FLOCKER设置为正在运行的shell脚本,则在使用正确的参数重新执行自身之前,执行flock并获取排他的非阻塞锁(使用脚本本身作为锁定文件)。它还会将FLOCKER env var设置为正确的值,因此不会再次运行。

要考虑的要点:

  • 要求flock,如果找不到示例脚本,它将以错误终止
  • 不需要额外的锁定文件
  • 如果脚本位于NFS上,则可能无法工作(请参阅/server/66919/file-locks-on-an-nfs

另请参阅/programming/185451/quick-and-dirty-way-to-ensure-only-one-instance-of-a-shell-script-is-running-at


1

这是Anselmo的Answer的修改版本。这个想法是使用bash脚本本身创建一个只读文件描述符,并flock用来处理锁。

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

与其他所有答案的主要区别在于,此代码不会修改文件系统,占用的内存非常少,并且不需要任何清理,因为一旦脚本结束,文件描述符就会关闭,而与退出状态无关。因此,脚本是成功还是失败都没有关系。


除非您有充分的理由不这么做,否则您应该始终引用所有shell变量引用,并且您确定自己知道自己在做什么。所以你应该做exec 6< "$SCRIPT"
斯科特,

@Scott我已根据您的建议更改了代码。非常感谢。
约翰·多伊

1

我正在使用cksum来检查我的脚本是否确实在运行单个实例,即使更改了filename和file path

我没有使用陷阱和锁定文件,因为如果服务器突然关闭,我需要在服务器启动后手动删除锁定文件。

注意:grep ps需要第一行中的#!/ bin / bash

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

或者,您可以在脚本中对cksum进行硬编码,因此,如果您要更改脚本的文件名,路径或内容,就不必再担心了

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done

1
请确切说明如何对校验和进行硬编码是个好主意。
斯科特

而不是对校验和进行硬编码,它仅创建脚本的身份密钥,当另一个实例运行时,它将检查其他shell脚本进程并首先处理该文件,如果您的身份密钥在该文件上,则意味着您的实例已在运行。
arputra

好; 请编辑您的答案以解释这一点。而且,将来,请不要发布多个看上去像(几乎)完全相同的30行长代码块,而不必解释它们之间的不同之处。而且,当您不再谈论校验mysum和时fsum,请不要说“您可以在脚本中硬编码[sic] cksum”之类的话,也不要继续使用变量名和。
斯科特

看起来很有趣,谢谢!欢迎来到unix.stackexchange :)
Tobias Kienzler


0

我给你的代码

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

基于man flock,仅改进:

  • 锁定文件的名称,基于脚本的全名
  • 讯息 executing

我在这里放置sleep 10,您可以放置​​所有主要脚本。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.