27

怎么正经的实现shell脚本单例运行?

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI2OTA3NTk3Ng%3D%3D&%3Bmid=2649285889&%3Bidx=1&%3Bsn=5a339841dda3ba9060077b98d7e21327
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

来源:公众号【编程珠玑】

作者:守望先生

ID:shouwangxiansheng

假设你的一个脚本已经在运行了,如果避免再次被执行呢?也就是如何实现单例运行?

看起来可行的方法

一个非常简单的思路就是,新的脚本被执行时,先检测当前脚本是否有其他实例正在运行,如果有则直接退出。

#!/usr/bin/env bash
#test.sh 来源:公众号编程珠玑
#获取当前运行的test.sh脚本数
runCount=$(ps -ef|grep test.sh | grep -v grep -c)
if [ "${runCount}" -ge 1 ]
then
    echo -e "test.sh already running,num:${runCount}"
    exit 1;
fi
while true
do
    echo "test.sh run"
    sleep 1
done

这里通过ps获取到当前在运行的test.sh脚本数,如果大于1,说明已经有在运行的了。

但是你运行会发现,其程序数量不只是一个。

$ ./test.sh
test.sh already running,num:2

惊不惊喜?为什么为这样呢?原因在于,shell脚本中一个命令执行相当于fork了一个进程执行,这里执行的是查找tesh.sh并grep的程序,另外还有一个就是当前运行的脚本程序,这样的方式自然就会出现每次都有两个了。

当然判断条件这里你可以换一下,例如数量大于2,但终归不太好。

文件锁

实际上这种方法你已经在《如何让你的程序同时只有一个在运行》介绍过了,只不过之前是用于编写C/C++程序,而这里是用于shell脚本。

我们来回顾一下,这是一个怎样的过程:

  • 1.运行前检查是否有该锁文件,并且文件中的进程正在运行

  • 2.如果有并且程序正在运行,则已经有实例在运行

  • 3.否则,无实例,创建锁文件,写入进程id

  • 4.退出时,删除锁文件

解释一下第一条,为什么一定要确定锁文件中的进程正在运行,因为,有些情况下如果运行的时候退出没有删除该文件,则会导致新的实例永远无法运行。

#!/usr/bin/env bash
#来源:公众号编程珠玑
LOCKFILE=/tmp/test.lock
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo " $0 already running"
    exit
fi

# 确保退出时,锁文件被删除
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
#将当前程序进程id写入锁文件
echo $$ > ${LOCKFILE}

# 做你需要的事情
sleep 1000

# 删除锁文件
rm -f ${LOCKFILE}

我们试着运行其中一个,然后另外一个窗口尝试运行:

$ ./test.sh
 ./test.sh already running

由于已经有实例在运行,发现新的程序无法运行了。而等旧的脚本运行完之后,新的就可以运行了。

实际上这里面有几个点非常巧妙:

  • kill -0 `cat \${LOCKFILE}` 这里用于检测该进程是否存在,避免进程不在了,但是锁文件还在,导致后面的脚本无法运行。

  • trap "rm -f \${LOCKFILE}; exit" INT TERM EXIT 用于确保脚本退出时,锁文件会被删除。

  • rm -f {LOCKFILE} 脚本最后需要删除锁文件

flock

说到锁文件,这里就不得不提flock命令了。没有前面的一些巧妙处理,我们很多时候会很难删除原先创建的锁文件,比如:

  • 脚本被意外中断,没来得及执行删除

  • 多个脚本产生竞争,导致判断异常,比如前面有一个脚本运行,判断没有锁文件,下一步准备创建,但是另外一个脚本又先创建了,就会导致异常了。

因此我们可以考虑使用flock:

#!/usr/bin/env bash
#来源:公众号编程珠玑
LOCK_FILE=/tmp/test.lock
exec 99>"$LOCK_FILE"
flock -n 99
if [ "$?" != 0 ]; then
    echo "$0 already running"
    exit 1
fi
#脚本要做的其他事情
sleep 1024

解释一下:

  • exec 99>"$LOCK_FILE" 表示创建文件描述符99,指向锁文件,为何是99?110其实也是可以的,只是为了和当前脚本可能打开的文件描述符冲突(例如和0,1,2冲突)。

  • flock -n 99 尝试对该文件描述符加锁,由操作系统保证原子性

  • 一旦flock失败了,我们这里可以退出

  • 而即使锁定了,脚本退出后,也会自动释放

因此这里避免了锁没有释放的情况。

另一种做法

查看flock的man手册,我们发现它还有一个例子是这么做的:

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

在脚本开头加上上面这么一行就可以了。例如:

#!/usr/bin/env bash
#来源:公众号编程珠玑
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en  "$0"  "$0"  "$@" || :
#脚本要做的其他事情
sleep 1024

解释一下:

  • 如果 ${FLOCKER} 环境变量没有设置,则尝试将脚本本身加锁,如果加锁成功,则运行当前脚本,(并且带上原有的参数),否则的话静默退出。

总结

单例运行本身思路是很简单的,就是探测当前是否有实例在运行,如果有,则退出,但是这里如何判断,却并不是那么容易。

最后,总结一下本文出现的一些该掌握的信息

  • $0 脚本名称

  • $@ 脚本参数

  • $$ 当前脚本进程id

  • $? 上一条命令执行结果

  • 描述符0 标准输入

  • 描述符1 标准输出

  • 描述符2 标准错误

  • > 重定向

相关精彩推荐

教你写出健壮可靠的shell脚本!

如何理解 Linux shell中“2>&1”?

这些必备的shell知识你都掌握了吗

关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。 后台免费获取经典电子书和视频资源

2eM7Zzb.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK