确保 Bash 脚本只有一个实例正在运行的最佳方法是什么?

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

现在我正在做:

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

但它有几个问题:

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

当然,我可以编写自己的 pidfile 处理,但是我感觉应该有一种简单的方法来实现它。

139065 次浏览

如果所有用户的脚本都相同,则可以使用 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

来自你的剧本:

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

我不确定是否存在单行的鲁棒解决方案, 所以你最后可能会卷自己的。

锁文件并不完美,但比使用‘ 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

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

使用单独的控件脚本意味着您可以检查边缘情况: 删除陈旧的日志文件,验证锁文件是否与 当前正在运行的脚本实例,提供一个选项来终止正在运行的进程,等等。 这也意味着您有更好的机会在 ps输出上成功地使用 grep。 Ps-grep 可用于验证锁文件是否具有与之关联的正在运行的进程。 也许你可以用某种方式来命名你的锁文件,以包含有关过程的信息: 用户、 pid 等,后面的脚本调用可以使用它们来决定进程是否 创建锁文件的人还在附近。

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

解决方案:

在剧本的开头,我说:

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

显然,不好意思在以下方面表现得很好:

  • 它没有像 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

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

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

在2009年,我发布了我的可锁定脚本样板(最初可以在我的 wiki 页面上找到,现在可以用 大意)。将其转换为每个用户一个实例的方法很简单。使用它,您还可以轻松地为需要某种锁定或同步的其他场景编写脚本。

为方便起见,这是上面提到的样板文件。

#!/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.

Ubuntu/Debian 发行版有 start-stop-daemon工具,它的用途和你描述的一样。另请参见 /etc/init.d/bone,了解如何在编写启动/停止脚本时使用它。

诺亚

我还建议看看 海关总署(runit 的一部分) :

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

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

# 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

最终解决方案只有一条:

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

第一个测试例子

[[ $(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

解释

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

命令“ lof”有两个优点。

  1. 忽略由编辑器(如 vim)编辑的 pid,因为 vim 编辑其映射文件(如“ . file.swp”)。
  2. 忽略当前运行的 shell 脚本分叉的 pid,因为大多数“ grep”派生命令都无法实现它。使用“ pstree-pH pidnum”命令查看当前工艺分叉状态的详细信息。

我遇到了同样的问题,并且提出了一个使用 lockfile 的 模板、一个保存进程 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

这是我们的标准位。它可以从脚本中恢复,不用清理它的锁文件就可以以某种方式死亡。

如果正常运行,它将进程 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}

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

这个技术对我在一个长期运行的 Ubuntu16主机上可靠地工作。主机定期对 bash 脚本的许多实例进行排队,这些脚本使用相同的系统范围的“锁”文件来协调工作。

(这与@Barry Kelly 的 这篇文章很相似,后来人们注意到了这一点。)

我在 procmail 包的依赖项中发现了这一点:

Apt install lilockfile-bin

跑步: Dotlockfile-l file. lock

将创建 file.lock。

解锁: dotlockfile -u file.lock

使用此命令列出此包文件/命令: 查询-L lilockfile-bin