一、说明

关于单例模式,最开始的是一些小工具,运行起来后再点击运行时会提示已经运行了一个实例,觉得挺有意思但也没有很在意

前段时间看了前领导的一段代码不太懂是做什么用的,同事查了下资料说是为了实现单例模式,讨论之下才知道单例模是是设计模式中的一种,具体表现也即上边说的只能运行一个实例。

上周被反馈说写的shell脚本在系统是运行了好多个进程,排查之下发现是yum命令一直等不到锁导致整个脚本卡住所致,脚本每次运行都会拉起一个卡死的进程。由此感觉shell脚本也应当考虑一下单例模式。

(更新:后来发现“单进程实例”和单例模式并不是一个东西,但在shell中也不会很混淆,所以也就不追究了。要追究见“Python3脚本单进程实例实现”。)

二、不标准的单实例实现

2.1 如果之前已有进程则结束当前进程的单例模式

#!/bin/bash
main(){
# $0是当前文件的文件名
# 如果运行是bash test.sh则$0就是test.sh
# 如果运行是bash /tmp/test.sh则$0就是/tmp/test.sh
# 如果不管怎么运行都只想要文件名,可以basename $0也可以file_name=${0##*/}
file_name=$0
# 如果匹配当前文件名的进程数量大于1,即文件实例不只当前进程,则退出当前进程
# 从认知上来说应该是大于1,这里要大于2的原因,后边说
# 但其实发现如果直接使用`pgrep -c -f ${file_name}`而不是使用wc -l计数,那数值就该是正常认知的大于1
if [ `pgrep -f ${file_name} | wc -l` -gt 2 ]
then
echo "${file_name} process existed, will be exit."
exit 1
fi
# sleep 60
}
main

2.2 如果之前已有进程则结束之前的进程继续当前进程的单例模式

#!/bin/bash
main(){
# $0是当前文件名
file_name=$
# 文件除当前进程外的的所有其他进程
pid_list=$(pgrep -f ${file_name} | grep -v ^$$\$)
# 逐个进程杀除
for tmp_pid in ${pid_list}
do
# 先杀除其所有子进程
pkill -P ${tmp_pid}
# 如子进程处于卡死状态,无法接收默认的15) SIGTERM状态进而直行退出;直接发送9) SIGKILL从内核将其杀死
# pkill - -P ${tmp_pid}
# 再杀除其自身
# pkill -P 只会杀除子进程不会杀除其自身
# 但有可能阻塞的子进程杀除后,自身进程后续步骤快速,导致kill去杀除时已没有该进程
kill - ${tmp_pid}
done
}
main

2.3 上边两个模式的注意事项说明

2.3.1 第二大节中的if语句为什么是大于2而不是大于1

从一般认知上说,我们运行了此脚本就启动了一个进程,如果此脚本对应的进程数大于1那就说明存在其他进程。但在上边代码中我们是要求大于2。

这是因为从观察来看,运行脚本启动了一个进程。然后在运行脚本中具体每一条命令时都会新建一个子进程去执行执行完后就结束该子进程;对于pgrep等普通的命令子进程名仍与父进程名一样。所以统计出来的进程数会是2,所以第二大节的代码要完成大于2。

另外这里强调pgrep等“普通命令”,sleep等命令的进程名则是自己。至于哪些算普通哪些算特殊暂时还没搞得很清楚。

2.3.2 这对第三大节中的代码有没有影响

既然每执行一条命令都会建一个子进程来执行,且子进程名与父进程名相一致,而第三大节的代码又相当于把当前父进程之外的所有进程都关闭。那会不会出现把当前父进程的子进程也杀掉导致当前父进程也出现问题的状况?答案是并不会。

pgrep传递给grep的是父进程pid、pgrep子进程pid及其他可能存在的先前运行脚本的pid;grep之后传给kill的是pgrep子进程pid及其他可能存在的先前运行脚本的pid。

我们前边也说过,在执行命令前启动子进程在命令执行完后退出子进程,所以等不到kill命令把pgrep子进程kill掉,在传递给grep时它就已经自己退出了。

2.3.3 父进程退出时子进程会不会退出

个人理解:父进程退出时会向所有子进程发送SIGTERM信号,处于R状态的子进程能及时收到信号然后结束自己,处于S状态的进程内核会状信号放入消息退列待其被唤醒后再处理信号。但比如yum在等待锁但一直等不到锁,yum进程应不会被唤醒,也就不能处理信号,也就不会退出,也就变成了孤儿进程。

但按网上更多资料来看,父进程退出时并不会向子进程发送任何信号,父进程退出之所以导致子进程退出,是因为其他原因。比如子进程尝试向与父进程的管道进行读写时产生了异常。

根据ps的man手册,进程有如下一些状态:

PROCESS STATE CODES
Here are the different values that the s, stat and state output specifiers (header "STAT" or "S")
will display to describe the state of a process: D uninterruptible sleep (usually IO)
R running or runnable (on run queue)
S interruptible sleep (waiting for an event to complete)
T stopped by job control signal
t stopped by debugger during the tracing
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z defunct ("zombie") process, terminated but not reaped by its parent For BSD formats and when the stat keyword is used, additional characters may be displayed: < high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group

三、标准的单实例实现

#!/bin/bash

# 此函数用于获取不到锁时主动退出
activate_exit(){
echo "`date +'%Y-%m-%d %H:%M:%S'`--error. get lock fail. there is other instance running. will be exit."
exit
} # 此函数用于申请锁
get_lock(){
lock_file_name="`basename $0`.pid"
# exec <>${lock_file_name},即以6作为lock_file_name的文件描述符(file descriptor number)
# 6是随便取的一个数值,但不要是0//,也不要太大(不要太太包含不能使用$$,$$值可能会比较大)
# 不用担心如test.sh和test1.sh都使用
exec <>${lock_file_name}
# 如果获取不到锁,flock语句就为假,就会执行||后的activate_exit
# 引入一个activate_exit函数的原因是||后不知道怎么写多个命令
flock -n || activate_exit
# 如果没有执行activate_exit,那么程序就可以继续执行
echo "`date +'%Y-%m-%d %H:%M:%S'`--ok. get lock success. there is not any other instance running."
# 将当前获取锁的进程id写入文件
echo "$$">& # 设置监听信号
# 当进程因这些信号致使进程中断时,最后仍要释放锁。类似java等中的final
# 这个其实不需要,因为进程结束时fd会自动关闭
# trap 'release_lock && activate_exit "1002" "break by some signal."'
} # 程序主要逻辑
exec_main_logic(){
# echo "you can code your main logic in this function."
# 这个sleep只是为了用于演示,替换成自己的代码即可
sleep
} # 程序主体逻辑
main(){
# 获取锁
get_lock $@
# 程序主要逻辑
exec_main_logic
} main $@

旧版代码:

#!/bin/bash

# 主动退出
activate_exit(){
result_code=${}
result_desc=${}
echo "result_code: ${result_code}; result_desc: ${result_desc}"
# 这个exit一定要有,不然程序就算执行到这里也没有退出
exit
} # 此函数用于释放锁
release_lock(){
flock -u ${LOCKFD}
eval "exec ${LOCKFD}>&-"
} # 此函数用于申请锁
get_lock(){
# readonly是说让变量只读,而不是其指向的文件只读
readonly LOCKFILE="/var/run/${file_name}.pid"
readonly FD=$(ls -l /proc/$$/fd | sed -n '$p' | awk '{print $9}')
readonly LOCKFD=$(( ${FD}+ )) # 申请锁
# 申请到就把pid写入到文件中
# 申请不到则说明已有运行实例,主动退出
eval "exec ${LOCKFD}>${LOCKFILE}"
flock -n ${LOCKFD} && echo "${BASHPID}" > "${LOCKFILE}" || activate_exit "" "${0} process is already running." # 设置监听信号
# 当进程因这些信号致使进程中断时,最后仍要释放锁。类似java等中的final
trap 'release_lock && activate_exit "1002" "break by some signal."'
} # 程序主要逻辑
exec_main_logic(){
# echo "you can code your main logic in this function."
# 这个sleep只是为了用于演示,替换成自己的代码即可
sleep
} # 程序主体逻辑
main(){
# 获取锁
get_lock $@
# 程序主要逻辑
exec_main_logic
# 释放锁
release_lock
} main $@

参考:

https://www.mylinuxplace.com/bash-singleton-process/

https://stackoverflow.com/questions/15740481/prevent-process-from-killing-itself-using-pkill

https://my.oschina.net/superwjc/blog/1810999

https://blog.csdn.net/taiyang1987912/article/details/41016987

https://stackoverflow.com/questions/34518233/what-would-cause-a-sigterm-to-not-propagate-to-child-processes

https://www.putorius.net/lock-files-bash-scripts.html

Linux shell脚本单例模式实现的更多相关文章

  1. Linux shell脚本编程(三)

    Linux shell脚本编程 流程控制: 循环语句:for,while,until while循环: while CONDITION; do 循环体 done 进入条件:当CONDITION为“真” ...

  2. Linux shell脚本编程(二)

    Linux shell脚本编程(二) 练习:求100以内所有偶数之和; 使用至少三种方法实现; 示例1: #!/bin/bash # declare -i sum=0 #声明一个变量求和,初始值为0 ...

  3. Linux shell脚本编程(一)

    Linux shell脚本编程: 守护进程,服务进程:启动?开机时自动启动: 交互式进程:shell应用程序 广义:GUI,CLI GUI: CLI: 词法分析:命令,选项,参数 内建命令: 外部命令 ...

  4. Linux Shell 脚本入门

    linux shell 脚本格式 #!/bin/sh#..... (注释)命令...命令... 使用vi 创建完成之后需设置权限 chmod +x filename.sh 执行命令: ./filena ...

  5. Linux Shell脚本入门--cut命令

    Linux Shell脚本入门--cut命令 cut cut 命令可以从一个文本文件或者文本流中提取文本列. cut语法 [root@www ~]# cut -d'分隔字符' -f fields &l ...

  6. Linux Shell脚本攻略 读书笔记

    Linux Shell脚本攻略 读书笔记 这是一本小书,总共253页,但内容却很丰富,书中的示例小巧而实用,对我这样总是在shell门前徘徊的人来说真是如获至宝:最有价值的当属文本处理,对这块我单独整 ...

  7. 阿里Linux Shell脚本面试25个经典问答

    转载: 阿里Linux Shell脚本面试25个经典问答 Q:1 Shell脚本是什么.它是必需的吗? 答:一个Shell脚本是一个文本文件,包含一个或多个命令.作为系统管理员,我们经常需要使用多个命 ...

  8. Linux Shell脚本教程

    v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VM ...

  9. Linux shell 脚本攻略之统计文件的行数、单词数和字符数

    摘自:<Linux shell 脚本攻略>

随机推荐

  1. 数据库——可视化工具Navicat、pymysql模块、sql注入问题

    数据库--可视化工具Navicat.pymysql模块.sql注入问题 Navicat可视化工具 Navicat是数据库的一个可视化工具,可直接在百度搜索下载安装,它可以通过鼠标"点点点&q ...

  2. android studio学习----Failed to resolve: com.android.support:design:22.1.1

    这个目前好像没有合适的办法,唯一可行的就是 点击那个提示  进行SDK Manager下载就可以了 但是天朝的网啊,我试了很多次,突然的可以下载,运气啊 类似这一系列问题解决办法就是  重新更新SDK ...

  3. 一、Hadoop入门概述

    一.Hadoop是什么 Hadoop是一个由Apche基金会所开发的分布式系统基础架构. 主要解决海量数据的存储和海量数据的分析计算问题. 广义上来说,Hadoop通常是指一个更广泛的概念—Hadoo ...

  4. C++ OpenSSL 之三:生成CSR文件

    1.等同于使用: openssl req -new -key "key_path" -out "save_path" -subj "/emailAdd ...

  5. ETHINK组件取值手册

    Ethink组件取值手册 一.取值 Sql查询配置中取值方式:所有可以对外过滤的组件都可以用id.output取值 就是取组件setOutput()里输出的值 ,具体分为以下两种: 1)$p{OBJ_ ...

  6. Jenkins+SVN+Ant在Linux环境下自动完成版本的增量更新与编译

    第一步:查看安装的jdk版本,查看是否安装ant,查看是否安装Jenkins java -version ant -version rpm -qa|grep jenkins 第二步:安装ant 官网: ...

  7. The 2019 Asia Nanchang First Round Online Programming Contest E. Magic Master

    题目链接:https://nanti.jisuanke.com/t/41352 题目意思还是好理解的,看过的人不多,感觉是被通过量吓到了.其实就是个水题,反向模拟就好了, 用队列模拟,反向模拟,它要放 ...

  8. 前端性能优化 css和js的加载与执行

    一个网站在浏览器端是如何进行渲染的? html本身首先会被渲染成 DOM 树,实际上 html 是最先通过网址请求过来的,请求过来之后,html 本身会由一个字节流转化成一个字符流,浏览器端拿的就是字 ...

  9. python 和 R 中的整数序列

    python 中的 range() 函数是很常用的,R  中相应的函数是 seq(), 其实,R 中的“ :”也能代替 python 中的 range() 函数. 1.生成升序整数序列 python: ...

  10. VUE的路由器的总结

    vue的路由器,我们在使用vue进行开发的时候,是必须用到的一个vue自带的组件,下面进行vue经常的操作的一些说明 1.vue-router的安装 在命令行里面使用 cnpm install vue ...