經常在 shell 腳本里要阻止其它進程,比如 msmtp 自帶的mail queue 腳本,這個腳本的互斥做法是不正確的,下面介紹下發現的三個通過文件達到互斥的正確做法。
1. util-linux 的 flock
這個命令有兩種用法: flock LOCKFILE COMMAND ( flock -s 200; COMMAND; ) 200>LOCKFILEflock 需要保持打開鎖文件,對于第二種使用方式并不方便,而且 -s 方式指定文件句柄可能沖突。好處是不需要顯式的解鎖,進程退出后鎖必然釋放。
2. liblockfile1 的 dotlockfile
號稱最靈活可靠的文件鎖實現。其等待時間跟 -r 指定的重試次數有關,重試時間為 sum(5, 10, ..., min(5*n, 60), ...).鎖文件不需要保持打開, 帶來的問題是需要用 trap EXIT 確保進程退出時刪除鎖文件.
3. procmail 的 lockfile
跟 dotlockfile 類似, 但可以一次性創建多個鎖文件.
在SHELL中實現文件鎖,有兩種簡單的方式。
一是利用普通文件,在腳本啟動時檢查特定文件是否存在,如果存在,則等待一段時間后繼續檢查,直到文件不存時創建該文件,在腳本結束時刪除文件。為確保腳本在異常退出時文件仍然能被刪除,可以借助于trap "cmd" EXIT TERM INT命令。一般這類文件存放在/var/lock/目錄下,操作系統在啟動時會對該目錄做清理。
另一種方法是是使用flock命令。使用方式如下,這個命令的好處是等待動作在flock命令中完成,無需另外添加代碼。
( flock 300 ...cmd... flock -u 300 ) > /tmp/file.lock
但flock有個缺陷是,在打開flock之后fork(),子進程也會擁有鎖,如果在flock其間有運行daemon的話,必需確保daemon在啟動時已經關閉了所有的文件句柄,不然該文件會因為daemon一直將其置于打開狀態而無法解鎖。
一個實現linux shell文件鎖的例子
最近看到很多討論如何能不讓腳本重復執行的問題,實際就是文件鎖的概念,寫了一個小例子:
把這個作為文件開頭不會產生重復執行的情況。(我想兩個執行腳本的文件名一模一樣應該不會經常出現吧)
#!/bin/bashLockFile(){ find/dev/shm/* -maxdepth 0 -type l -follow -exec unlink {} /; [ -f /dev/shm/${0##*/}]&&exit ln -s /proc/$$/dev/shm/${0##*/} trap "Exit" 0 1 2 3 15 22 24}Exit(){ unlink /dev/shm/${0##*/}; exit 0;}LockFile# main program# program ......#Exit
/var/lock/subsys目錄的作用的說明
很多程序需要判斷是否當前已經有一個實例在運行,這個目錄就是讓程序判斷是否有實例運行的標志,比如說xinetd,如果存在這個文件,表示已經有xinetd在運行了,否則就是沒有,當然程序里面還要有相應的判斷措施來真正確定是否有實例在運行。
通常與該目錄配套的還有/var/run目錄,用來存放對應實例的PID,如果你寫腳本的話,會發現這2個目錄結合起來可以很方便的判斷出許多服務是否在運行,運行的相關信息等等。
實際上,判斷是否上鎖就是判斷這個文件,所以文件存在與否也就隱含了是否上鎖。而這個目錄的內容并不能表示一定上鎖了,因為很多服務在啟動腳本里用touch來創建這個加鎖文件,在系統結束時該腳本負責清除鎖,這本身就不可靠(比如意外失敗導致鎖文件仍然存在),我在腳本里一般是結合PID文件(如果有PID文件的話),從PID文件里得到該實例的PID,然后用ps測試是否存在該PID,從而判斷是否真正有這個實例在運行,更加穩妥的方法是用進程通訊了,不過這樣的話單單靠腳本就做不到了。
flock命令在我的系統屬于util-linux-2.13-0.46.fc6包,如果沒有此命令,嘗試更新您系統下的util-linux包。
介紹此命令的原因:
論壇中曾有woodie兄寫的腳本串行化的討論,已經很完善了。
但flock此命令既與shell腳本結合的很好,而且與C/PERL/PHP等語言的flock函數用法很相似,使用起來也很簡單。相比之下,woodie兄那篇的內容需要不淺的shell功底來理解。
兩種格式分別為:
flock [-sxon] [-w timeout] lockfile [-c] command...
flock [-sxun] [-w timeout] fd
介紹一下參數:
-s為共享鎖,在定向為某文件的FD上設置共享鎖而未釋放鎖的時間內,其他進程試圖在定向為此文件的FD上設置獨占鎖的請求失敗,而其他進程試圖在定向為此文件的FD上設置共享鎖的請求會成功。
-e為獨占或排他鎖,在定向為某文件的FD上設置獨占鎖而未釋放鎖的時間內,其他進程試圖在定向為此文件的FD上設置共享鎖或獨占鎖都會失敗。只要未設置-s參數,此參數默認被設置。
-u手動解鎖,一般情況不必須,當FD關閉時,系統會自動解鎖,此參數用于腳本命令一部分需要異步執行,一部分可以同步執行的情況。
-n為非阻塞模式,當試圖設置鎖失敗,采用非阻塞模式,直接返回1,并繼續執行下面語句。
-w設置阻塞超時,當超過設置的秒數,就跳出阻塞,返回1,并繼續執行下面語句。
-o必須是使用第一種格式時才可用,表示當執行command前關閉設置鎖的FD,以使command的子進程不保持鎖。
-c執行其后的comand。
舉個實用的例子:
#!/bin/bash{flock -n 3[ $? -eq 1 ] && { echo fail; exit; }echo $$sleep 10} 3<>mylockfile
此例的功能為當有一個腳本實例正在執行時,另一個試圖執行該腳本的進程會失敗退出。
sleep那句可以換成您需要執行的語句段。
這里請注意一點,我使用<>打開mylockfile,原因是定向文件描述符是先于命令執行的。因此假如在您要執行的語句段中需要讀寫mylockfile文件,例如想獲得上一個腳本實例的pid,并將此次的腳本實例的pid寫入mylockfile。此時直接用>打開mylockfile會清空上次存入的內容,而用<打開mylockfile當它不存在時會導致一個錯誤。當然這些問題都可以用其他方法解決,我只是點出這種最通用的方法。
【背景介紹】
CU上曾經有幾個帖子討論到一個實際問題,就是如何限制同一時刻只允許一個腳本實例運行。其中本版新老斑竹和其它網友都參加了討論,但以faintblue兄的帖子對大家啟發最大,下面的背景介紹中許多內容都是來自于他。在此感謝faintblue兄,也感謝斑竹和其它朋友!
woodie總結了一下現有的結果,大體上可以分為兩種思路:
一、簡單的方法是,用ps一類命令找出已經運行腳本的數量,如果大于等于2(別忘了把自己也算進去^_^),就退出當前腳本,等于1,則運行。這種方法簡單是簡單,不過有一些問題:
首先,ps取得腳本文件進程數量就有很多陷阱,例如有時無法ps到腳本文件的名稱;
即使可以ps到腳本名,如果用到管道的話,由于子shell的原因,在大多數平臺下會得到奇怪的結果,有時得到數字a,有時又得到數字b,讓人無所適從;
就算計數的問題已經解決了,還有問題,不過不太嚴重:如果兩個腳本實例同時計數,顯然數字都應該等于2,于是兩個都退出了。于是在這一時間點上沒有一個腳本在執行;
二、加鎖的方法。就是腳本在執行開始先試圖得到一個“鎖”,得到則繼續執行,反之就退出。
加鎖方法也存在一些問題,主要集中在兩個方面:
其一,加鎖時如何避免競態條件(race condition)。即如何找到一些“原子”操作,使得加鎖的動作一步完成,中間不能被打斷。否則就可能出現下面的情況:
腳本1檢測到沒有鎖被占用;
然后腳本2也檢測到沒有鎖被占用;
腳本1加鎖,開始執行;
然后腳本2(錯誤地)加鎖,也開始執行;
看到嗎,兩個腳本在同時執行。:(
可能的一些加鎖的“原子”操作有:
1.創建目錄,當一個進程創建成功后其它進程都會失?。?
2.符號鏈接:ln -s,一個鏈接創建后其它進程的ln -s命令會出錯;
3.文件首行的競爭,多個進程以append的方式同時寫到文件,只有惟一一個進程寫到了文件的第一行,因為不可能有兩個第一行。^_^
4.其它軟件包的加鎖工具,通常是c語言二進制程序,自己寫的也行。
目前加鎖時的問題已經可以解決。
其二,找到一種方法避免出現“死鎖”的情況,這里是指:雖然“鎖”被占用,但卻沒有腳本在執行。這通常在腳本意外退出,來不及釋放占用的“鎖”之后。如收到一些系統信號后退出,機器意外掉電后退出等。
對于前者的情況,可以用trap捕獲一些信號,在退出前釋放鎖;但有些信號是無法捕獲的。
對于后者,可以在機器重起后用腳本自動刪除鎖來解決。不過有點麻煩。
所以比較理想的是腳本自己來檢測死鎖,然后釋放它。不過問題的難點在于如何找到一種“原子”操作,將檢測死鎖和刪除死鎖的動作一步完成,否則又會出現與加鎖時同樣的競態條件的問題。例如:
進程1檢測到死鎖;
進程2監測到死鎖;
進程1刪除死鎖;
進程x(也可能是進程1自己)加鎖,開始運行;
進程2(錯誤地)刪除死鎖;
此時鎖沒有占用,于是任意進程都可以加鎖并投入運行。
這樣又出現了兩個進程同時運行的情況。:(
可惜的是:在迄今為止的討論之后,woodie還沒有找到一種合適的“原子”操作。:(只是找到了一種稍微好些的辦法:就是在刪除時用文件的inode作標識,于是其它進程新建的鎖(文件名雖然相同,但inode相同的機率比較微?。┎蝗菀妆灰馔鈩h除。這個方法已經接近完美了,可惜還是存在誤刪的微小幾率,不能說是100%安全。唉,山重水復疑無路?。?(
最近又有網友問起這個問題,促使我又再次思考。從我以前的一個想法發展了一下,換了一種思路,便有豁然開朗的感覺。不敢藏私,寫出來請大家debug。^_^
基本的想法就是:借鑒多進程編程中臨界區的概念,如果各個進程進入我們設立的臨界區,只可能一個一個地順序進入,不就能保證每次只有一個腳本運行了嗎?怎樣建立這樣一種臨界區呢?我想到了一種方法,就是用管道,多個進程寫到同一個管道,只可能一行一行地進入,相應的,另一端也是一行一行地讀出,如此就可以實現并行執行的多個進程進入臨界區時的“串行化”。這與faintblue兄以前貼出的append文件的方法也是異曲同工。
我們可以讓并行的進程同時向一個管道寫一行請求,內容是其進程號,在管道另一端順序讀取這些請求,但只有第一個請求會得到一個“令牌”,被允許開始運行;后續的請求將被忽略,對應的進程沒有得到令牌,就自己退出。這樣就保證了任意時間只有一個進程運行(嚴格地說是進入臨界區)。說到“令牌”,熟悉網絡發展史的朋友可能會聯想到IBM的Token Ring架構,每一時刻只能有一個主機得到令牌并發送數據,沒有以太網的“碰撞”問題。可惜如同微通道技術一樣,IBM的技術是不錯,但最終還是被淘汰了。不錯,這里令牌的概念就是借用于Token Ring。^_^
當一個進程執行完畢,向管道發送一個終止信號,即交回“令牌”,另一端接受到后,又開始選取下一個進程發放“令牌”。
您可能會問了,那么死鎖問題又如何解決呢?別急,我在以前的討論中曾提出將檢測處理死鎖的代碼單獨拿出來,交給一個專門的進程來處理的想法,這里就具體實踐這樣一種思路。當檢測和刪除死鎖的任務由一個專門的進程來執行時,就沒有多個并發進程對同一個鎖進行操作,所以競態條件發生的物質基礎也就根本不存在了。^_^
再發展一下這個思路,允許同時執行多個進程如何?當然可以!只要設立一個計數器,達到限制的數字就停止發放“令牌”即可。
下面就是woodie上述思路的一個實現,只是在centos 4.2下簡單地測試了一下,可能還有不少錯誤,請大家幫忙“除蟲”。^_^思路上有什么問題也請不吝指教:
腳本1,token.sh,負責令牌管理和死鎖檢測處理。與下一個腳本一樣,為了保持腳本的最大的兼容性,盡量使用Bourne shell的語法,并用printf代替了echo,sed的用法也盡量保持通用性。這里是由一個命名管道接受請求,令牌在一個文件中發出。如果用ksh也許可以用協進程來實現,熟悉ksh的朋友可以試一試。^_^
#!/bin/sh #name: token.sh #function: serialized token distribution, at anytime, only a cerntern number of token given out #usage: token.sh [number] & #number is set to allow number of scripts to run at same time #if no number is given, default value is 1 if [ -p /tmp/p-aquire ]; then rm -f /tmp/p-aquire fi if mkfifo /tmp/p-aquire; then printf "pipe file /tmp/p-aquire created/n" >>token.log else printf "cannot create pipe file /tmp/p-aquire/n" >>token.log exit 1 fi loop_times_before_check=100 if [ -n "$1" ];then limit=$1 else # default concurrence is 1 limit=1 fi number_of_running=0 counter=0 while :;do #check stale token, which owner is died unexpected if [ "$counter" -eq "$loop_times_before_check" ]; then counter=0 for pid in `cat token_file`;do pgrep $pid if [ $? -ne 0 ]; then #remove lock printf "s/ $pid///nwq/n"|ed -s token_file number_of_running=`expr $number_of_running - 1` fi done fi counter=`expr $counter + 1` # if [ "$number_of_running" -ge "$limit" ];then # token is all given out. bypass all request until a instance to give one back pid=`sed -n '/stop/ {s//([0-9]/+/) /+stop//1/p;q}' /tmp/p-aquire` if [ -n "$pid" ]; then # get a token returned printf "s/ $pid///nwq/n"|ed -s token_file number_of_running=`expr $number_of_running - 1` continue fi else # there is still some token to give out. serve another request read pid action < /tmp/p-aquire if [ "$action" = stop ]; then # one token is given back. printf "s/ $pid///nwq/n"|ed -s token_file number_of_running=`expr $number_of_running - 1` else # it's a request, give off a token to instance identified by $pid printf " $pid" >> token_file number_of_running=`expr $number_of_running + 1` fi fi done
--------------------------------------------------------------------------------------------
修訂記錄:
1.修正token.sh的一個BUG,將原來用sed刪除失效令牌的命令用ed命令代替。感謝r2007和waker兩位指出錯誤!
--------------------------------------------------------------------------------------------
腳本2:并發執行的腳本 -- my-script。在"your code goes here"一行后插入你自己的代碼,現有的是我用來測試的。
#!/bin/sh # second to wait that the ditributer gives off a token a_while=1 if [ ! -p /tmp/p-aquire ]; then printf "cannot find file /tmp/p-aquire/n" >&2 exit 1 fi # try to aquire a token printf "$$/n" >> /tmp/p-aquire sleep $a_while # see if we get one grep "$$" token_file if [ $? -ne 0 ]; then # bad luck. :( printf "no token free now, exitting.../n" >&2 exit 2 fi
這個腳本是將文件鎖得,不過我對這腳本還有一些疑惑的地方,暫且不討論,等以后回頭再來談
#!/bin/sh# filelock - A flexible file locking mechanism.retries="10" # default number of retriesaction="lock" # default actionnullcmd="/bin/true" # null command for lockfilewhile getopts "lur:" opt; do case $opt in l ) action="lock" ;; u ) action="unlock" ;; r ) retries="$OPTARG" ;; esacdoneshift $(($OPTIND - 1))if [ $# -eq 0 ] ; then cat << EOF >&2Usage: $0 [-l|-u] [-r retries] lockfilenameWhere -l requests a lock (the default), -u requests an unlock, -r Xspecifies a maximum number of retries before it fails (default = $retries).EOF exit 1fi# Ascertain whether we have lockf or lockfile system appsif [ -z "$(which lockfile | grep -v '^no ')" ] ; then echo "$0 failed: 'lockfile' utility not found in PATH." >&2 exit 1fiif [ "$action" = "lock" ] ; then if ! lockfile -1 -r $retries "$1" 2> /dev/null; then echo "$0: Failed: Couldn't create lockfile in time" >&2 exit 1 fielse # action = unlock if [ ! -f "$1" ] ; then echo "$0: Warning: lockfile $1 doesn't exist to unlock" >&2 exit 1 fi rm -f "$1"fiexit 0
新聞熱點
疑難解答