一、說明
關於單例模式,最開始的是一些小工具,運行起來后再點擊運行時會提示已經運行了一個實例,覺得挺有意思但也沒有很在意
前段時間看了前領導的一段代碼不太懂是做什么用的,同事查了下資料說是為了實現單例模式,討論之下才知道單例模是是設計模式中的一種,具體表現也即上邊說的只能運行一個實例。
上周被反饋說寫的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=$0 # 文件除當前進程外的的所有其他進程 pid_list=$(pgrep -f ${file_name} | grep -v ^$$\$) # 逐個進程殺除 for tmp_pid in ${pid_list} do # 先殺除其所有子進程 pkill -P ${tmp_pid} # 如子進程處於卡死狀態,無法接收默認的15) SIGTERM狀態進而直行退出;直接發送9) SIGKILL從內核將其殺死 # pkill -9 -P ${tmp_pid} # 再殺除其自身 # pkill -P 只會殺除子進程不會殺除其自身 # 但有可能阻塞的子進程殺除后,自身進程后續步驟快速,導致kill去殺除時已沒有該進程 kill -9 ${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 1 } # 此函數用於申請鎖 get_lock(){ lock_file_name="`basename $0`.pid" # exec 6<>${lock_file_name},即以6作為lock_file_name的文件描述符(file descriptor number) # 6是隨便取的一個數值,但不要是0/1/2,也不要太大(不要太太包含不能使用$$,$$值可能會比較大) # 不用擔心如test.sh和test1.sh都使用 exec 6<>${lock_file_name} # 如果獲取不到鎖,flock語句就為假,就會執行||后的activate_exit # 引入一個activate_exit函數的原因是||后不知道怎么寫多個命令 flock -n 6 || 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 "$$">&6 # 設置監聽信號 # 當進程因這些信號致使進程中斷時,最后仍要釋放鎖。類似java等中的final # 這個其實不需要,因為進程結束時fd會自動關閉 # trap 'release_lock && activate_exit "1002" "break by some signal."' 1 2 3 9 15 } # 程序主要邏輯 exec_main_logic(){ # echo "you can code your main logic in this function." # 這個sleep只是為了用於演示,替換成自己的代碼即可 sleep 30 } # 程序主體邏輯 main(){ # 獲取鎖 get_lock $@ # 程序主要邏輯 exec_main_logic } main $@
舊版代碼:

#!/bin/bash # 主動退出 activate_exit(){ result_code=${1} result_desc=${2} echo "result_code: ${result_code}; result_desc: ${result_desc}" # 這個exit一定要有,不然程序就算執行到這里也沒有退出 exit 1 } # 此函數用於釋放鎖 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}+1 )) # 申請鎖 # 申請到就把pid寫入到文件中 # 申請不到則說明已有運行實例,主動退出 eval "exec ${LOCKFD}>${LOCKFILE}" flock -n ${LOCKFD} && echo "${BASHPID}" > "${LOCKFILE}" || activate_exit "1001" "${0} process is already running." # 設置監聽信號 # 當進程因這些信號致使進程中斷時,最后仍要釋放鎖。類似java等中的final trap 'release_lock && activate_exit "1002" "break by some signal."' 1 2 3 9 15 } # 程序主要邏輯 exec_main_logic(){ # echo "you can code your main logic in this function." # 這個sleep只是為了用於演示,替換成自己的代碼即可 sleep 30 } # 程序主體邏輯 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