經(jīng)常在 shell 腳本里要阻止其它進(jìn)程,比如 msmtp 自帶的mail queue 腳本,這個(gè)腳本的互斥做法是不正確的,下面介紹下發(fā)現(xiàn)的三個(gè)通過(guò)文件達(dá)到互斥的正確做法。
1. util-linux 的 flock
這個(gè)命令有兩種用法: flock LOCKFILE COMMAND ( flock -s 200; COMMAND; ) 200>LOCKFILEflock 需要保持打開(kāi)鎖文件,對(duì)于第二種使用方式并不方便,而且 -s 方式指定文件句柄可能沖突。好處是不需要顯式的解鎖,進(jìn)程退出后鎖必然釋放。
2. liblockfile1 的 dotlockfile
號(hào)稱最靈活可靠的文件鎖實(shí)現(xiàn)。其等待時(shí)間跟 -r 指定的重試次數(shù)有關(guān),重試時(shí)間為 sum(5, 10, ..., min(5*n, 60), ...).鎖文件不需要保持打開(kāi), 帶來(lái)的問(wèn)題是需要用 trap EXIT 確保進(jìn)程退出時(shí)刪除鎖文件.
3. procmail 的 lockfile
跟 dotlockfile 類似, 但可以一次性創(chuàng)建多個(gè)鎖文件.
在SHELL中實(shí)現(xiàn)文件鎖,有兩種簡(jiǎn)單的方式。
一是利用普通文件,在腳本啟動(dòng)時(shí)檢查特定文件是否存在,如果存在,則等待一段時(shí)間后繼續(xù)檢查,直到文件不存時(shí)創(chuàng)建該文件,在腳本結(jié)束時(shí)刪除文件。為確保腳本在異常退出時(shí)文件仍然能被刪除,可以借助于trap "cmd" EXIT TERM INT命令。一般這類文件存放在/var/lock/目錄下,操作系統(tǒng)在啟動(dòng)時(shí)會(huì)對(duì)該目錄做清理。
另一種方法是是使用flock命令。使用方式如下,這個(gè)命令的好處是等待動(dòng)作在flock命令中完成,無(wú)需另外添加代碼。
( flock 300 ...cmd... flock -u 300 ) > /tmp/file.lock
但flock有個(gè)缺陷是,在打開(kāi)flock之后fork(),子進(jìn)程也會(huì)擁有鎖,如果在flock其間有運(yùn)行daemon的話,必需確保daemon在啟動(dòng)時(shí)已經(jīng)關(guān)閉了所有的文件句柄,不然該文件會(huì)因?yàn)閐aemon一直將其置于打開(kāi)狀態(tài)而無(wú)法解鎖。
一個(gè)實(shí)現(xiàn)linux shell文件鎖的例子
最近看到很多討論如何能不讓腳本重復(fù)執(zhí)行的問(wèn)題,實(shí)際就是文件鎖的概念,寫(xiě)了一個(gè)小例子:
把這個(gè)作為文件開(kāi)頭不會(huì)產(chǎn)生重復(fù)執(zhí)行的情況。(我想兩個(gè)執(zhí)行腳本的文件名一模一樣應(yīng)該不會(huì)經(jīng)常出現(xiàn)吧)
#!/bin/bash
LockFile()
{
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目錄的作用的說(shuō)明
很多程序需要判斷是否當(dāng)前已經(jīng)有一個(gè)實(shí)例在運(yùn)行,這個(gè)目錄就是讓程序判斷是否有實(shí)例運(yùn)行的標(biāo)志,比如說(shuō)xinetd,如果存在這個(gè)文件,表示已經(jīng)有xinetd在運(yùn)行了,否則就是沒(méi)有,當(dāng)然程序里面還要有相應(yīng)的判斷措施來(lái)真正確定是否有實(shí)例在運(yùn)行。
通常與該目錄配套的還有/var/run目錄,用來(lái)存放對(duì)應(yīng)實(shí)例的PID,如果你寫(xiě)腳本的話,會(huì)發(fā)現(xiàn)這2個(gè)目錄結(jié)合起來(lái)可以很方便的判斷出許多服務(wù)是否在運(yùn)行,運(yùn)行的相關(guān)信息等等。
實(shí)際上,判斷是否上鎖就是判斷這個(gè)文件,所以文件存在與否也就隱含了是否上鎖。而這個(gè)目錄的內(nèi)容并不能表示一定上鎖了,因?yàn)楹芏喾?wù)在啟動(dòng)腳本里用touch來(lái)創(chuàng)建這個(gè)加鎖文件,在系統(tǒng)結(jié)束時(shí)該腳本負(fù)責(zé)清除鎖,這本身就不可靠(比如意外失敗導(dǎo)致鎖文件仍然存在),我在腳本里一般是結(jié)合PID文件(如果有PID文件的話),從PID文件里得到該實(shí)例的PID,然后用ps測(cè)試是否存在該P(yáng)ID,從而判斷是否真正有這個(gè)實(shí)例在運(yùn)行,更加穩(wěn)妥的方法是用進(jìn)程通訊了,不過(guò)這樣的話單單靠腳本就做不到了。
flock命令在我的系統(tǒng)屬于util-linux-2.13-0.46.fc6包,如果沒(méi)有此命令,嘗試更新您系統(tǒng)下的util-linux包。
介紹此命令的原因:
論壇中曾有woodie兄寫(xiě)的腳本串行化的討論,已經(jīng)很完善了。
但flock此命令既與shell腳本結(jié)合的很好,而且與C/PERL/PHP等語(yǔ)言的flock函數(shù)用法很相似,使用起來(lái)也很簡(jiǎn)單。相比之下,woodie兄那篇的內(nèi)容需要不淺的shell功底來(lái)理解。
兩種格式分別為:
flock [-sxon] [-w timeout] lockfile [-c] command...
flock [-sxun] [-w timeout] fd
介紹一下參數(shù):
-s為共享鎖,在定向?yàn)槟澄募腇D上設(shè)置共享鎖而未釋放鎖的時(shí)間內(nèi),其他進(jìn)程試圖在定向?yàn)榇宋募腇D上設(shè)置獨(dú)占鎖的請(qǐng)求失敗,而其他進(jìn)程試圖在定向?yàn)榇宋募腇D上設(shè)置共享鎖的請(qǐng)求會(huì)成功。
-e為獨(dú)占或排他鎖,在定向?yàn)槟澄募腇D上設(shè)置獨(dú)占鎖而未釋放鎖的時(shí)間內(nèi),其他進(jìn)程試圖在定向?yàn)榇宋募腇D上設(shè)置共享鎖或獨(dú)占鎖都會(huì)失敗。只要未設(shè)置-s參數(shù),此參數(shù)默認(rèn)被設(shè)置。
-u手動(dòng)解鎖,一般情況不必須,當(dāng)FD關(guān)閉時(shí),系統(tǒng)會(huì)自動(dòng)解鎖,此參數(shù)用于腳本命令一部分需要異步執(zhí)行,一部分可以同步執(zhí)行的情況。
-n為非阻塞模式,當(dāng)試圖設(shè)置鎖失敗,采用非阻塞模式,直接返回1,并繼續(xù)執(zhí)行下面語(yǔ)句。
-w設(shè)置阻塞超時(shí),當(dāng)超過(guò)設(shè)置的秒數(shù),就跳出阻塞,返回1,并繼續(xù)執(zhí)行下面語(yǔ)句。
-o必須是使用第一種格式時(shí)才可用,表示當(dāng)執(zhí)行command前關(guān)閉設(shè)置鎖的FD,以使command的子進(jìn)程不保持鎖。
-c執(zhí)行其后的comand。
舉個(gè)實(shí)用的例子:
#!/bin/bash
{
flock -n 3
[ $? -eq 1 ] { echo fail; exit; }
echo $$
sleep 10
} 3>mylockfile
此例的功能為當(dāng)有一個(gè)腳本實(shí)例正在執(zhí)行時(shí),另一個(gè)試圖執(zhí)行該腳本的進(jìn)程會(huì)失敗退出。
sleep那句可以換成您需要執(zhí)行的語(yǔ)句段。
這里請(qǐng)注意一點(diǎn),我使用>打開(kāi)mylockfile,原因是定向文件描述符是先于命令執(zhí)行的。因此假如在您要執(zhí)行的語(yǔ)句段中需要讀寫(xiě)mylockfile文件,例如想獲得上一個(gè)腳本實(shí)例的pid,并將此次的腳本實(shí)例的pid寫(xiě)入mylockfile。此時(shí)直接用>打開(kāi)mylockfile會(huì)清空上次存入的內(nèi)容,而用打開(kāi)mylockfile當(dāng)它不存在時(shí)會(huì)導(dǎo)致一個(gè)錯(cuò)誤。當(dāng)然這些問(wèn)題都可以用其他方法解決,我只是點(diǎn)出這種最通用的方法。
【背景介紹】
CU上曾經(jīng)有幾個(gè)帖子討論到一個(gè)實(shí)際問(wèn)題,就是如何限制同一時(shí)刻只允許一個(gè)腳本實(shí)例運(yùn)行。其中本版新老斑竹和其它網(wǎng)友都參加了討論,但以faintblue兄的帖子對(duì)大家啟發(fā)最大,下面的背景介紹中許多內(nèi)容都是來(lái)自于他。在此感謝faintblue兄,也感謝斑竹和其它朋友!
woodie總結(jié)了一下現(xiàn)有的結(jié)果,大體上可以分為兩種思路:
一、簡(jiǎn)單的方法是,用ps一類命令找出已經(jīng)運(yùn)行腳本的數(shù)量,如果大于等于2(別忘了把自己也算進(jìn)去^_^),就退出當(dāng)前腳本,等于1,則運(yùn)行。這種方法簡(jiǎn)單是簡(jiǎn)單,不過(guò)有一些問(wèn)題:
首先,ps取得腳本文件進(jìn)程數(shù)量就有很多陷阱,例如有時(shí)無(wú)法ps到腳本文件的名稱;
即使可以ps到腳本名,如果用到管道的話,由于子shell的原因,在大多數(shù)平臺(tái)下會(huì)得到奇怪的結(jié)果,有時(shí)得到數(shù)字a,有時(shí)又得到數(shù)字b,讓人無(wú)所適從;
就算計(jì)數(shù)的問(wèn)題已經(jīng)解決了,還有問(wèn)題,不過(guò)不太嚴(yán)重:如果兩個(gè)腳本實(shí)例同時(shí)計(jì)數(shù),顯然數(shù)字都應(yīng)該等于2,于是兩個(gè)都退出了。于是在這一時(shí)間點(diǎn)上沒(méi)有一個(gè)腳本在執(zhí)行;
二、加鎖的方法。就是腳本在執(zhí)行開(kāi)始先試圖得到一個(gè)“鎖”,得到則繼續(xù)執(zhí)行,反之就退出。
加鎖方法也存在一些問(wèn)題,主要集中在兩個(gè)方面:
其一,加鎖時(shí)如何避免競(jìng)態(tài)條件(race condition)。即如何找到一些“原子”操作,使得加鎖的動(dòng)作一步完成,中間不能被打斷。否則就可能出現(xiàn)下面的情況:
腳本1檢測(cè)到?jīng)]有鎖被占用;
然后腳本2也檢測(cè)到?jīng)]有鎖被占用;
腳本1加鎖,開(kāi)始執(zhí)行;
然后腳本2(錯(cuò)誤地)加鎖,也開(kāi)始執(zhí)行;
看到嗎,兩個(gè)腳本在同時(shí)執(zhí)行。:(
可能的一些加鎖的“原子”操作有:
1.創(chuàng)建目錄,當(dāng)一個(gè)進(jìn)程創(chuàng)建成功后其它進(jìn)程都會(huì)失?。?
2.符號(hào)鏈接:ln -s,一個(gè)鏈接創(chuàng)建后其它進(jìn)程的ln -s命令會(huì)出錯(cuò);
3.文件首行的競(jìng)爭(zhēng),多個(gè)進(jìn)程以append的方式同時(shí)寫(xiě)到文件,只有惟一一個(gè)進(jìn)程寫(xiě)到了文件的第一行,因?yàn)椴豢赡苡袃蓚€(gè)第一行。^_^
4.其它軟件包的加鎖工具,通常是c語(yǔ)言二進(jìn)制程序,自己寫(xiě)的也行。
目前加鎖時(shí)的問(wèn)題已經(jīng)可以解決。
其二,找到一種方法避免出現(xiàn)“死鎖”的情況,這里是指:雖然“鎖”被占用,但卻沒(méi)有腳本在執(zhí)行。這通常在腳本意外退出,來(lái)不及釋放占用的“鎖”之后。如收到一些系統(tǒng)信號(hào)后退出,機(jī)器意外掉電后退出等。
對(duì)于前者的情況,可以用trap捕獲一些信號(hào),在退出前釋放鎖;但有些信號(hào)是無(wú)法捕獲的。
對(duì)于后者,可以在機(jī)器重起后用腳本自動(dòng)刪除鎖來(lái)解決。不過(guò)有點(diǎn)麻煩。
所以比較理想的是腳本自己來(lái)檢測(cè)死鎖,然后釋放它。不過(guò)問(wèn)題的難點(diǎn)在于如何找到一種“原子”操作,將檢測(cè)死鎖和刪除死鎖的動(dòng)作一步完成,否則又會(huì)出現(xiàn)與加鎖時(shí)同樣的競(jìng)態(tài)條件的問(wèn)題。例如:
進(jìn)程1檢測(cè)到死鎖;
進(jìn)程2監(jiān)測(cè)到死鎖;
進(jìn)程1刪除死鎖;
進(jìn)程x(也可能是進(jìn)程1自己)加鎖,開(kāi)始運(yùn)行;
進(jìn)程2(錯(cuò)誤地)刪除死鎖;
此時(shí)鎖沒(méi)有占用,于是任意進(jìn)程都可以加鎖并投入運(yùn)行。
這樣又出現(xiàn)了兩個(gè)進(jìn)程同時(shí)運(yùn)行的情況。:(
可惜的是:在迄今為止的討論之后,woodie還沒(méi)有找到一種合適的“原子”操作。:(只是找到了一種稍微好些的辦法:就是在刪除時(shí)用文件的inode作標(biāo)識(shí),于是其它進(jìn)程新建的鎖(文件名雖然相同,但inode相同的機(jī)率比較微小)不容易被意外刪除。這個(gè)方法已經(jīng)接近完美了,可惜還是存在誤刪的微小幾率,不能說(shuō)是100%安全。唉,山重水復(fù)疑無(wú)路??!:(
最近又有網(wǎng)友問(wèn)起這個(gè)問(wèn)題,促使我又再次思考。從我以前的一個(gè)想法發(fā)展了一下,換了一種思路,便有豁然開(kāi)朗的感覺(jué)。不敢藏私,寫(xiě)出來(lái)請(qǐng)大家debug。^_^
基本的想法就是:借鑒多進(jìn)程編程中臨界區(qū)的概念,如果各個(gè)進(jìn)程進(jìn)入我們?cè)O(shè)立的臨界區(qū),只可能一個(gè)一個(gè)地順序進(jìn)入,不就能保證每次只有一個(gè)腳本運(yùn)行了嗎?怎樣建立這樣一種臨界區(qū)呢?我想到了一種方法,就是用管道,多個(gè)進(jìn)程寫(xiě)到同一個(gè)管道,只可能一行一行地進(jìn)入,相應(yīng)的,另一端也是一行一行地讀出,如此就可以實(shí)現(xiàn)并行執(zhí)行的多個(gè)進(jìn)程進(jìn)入臨界區(qū)時(shí)的“串行化”。這與faintblue兄以前貼出的append文件的方法也是異曲同工。
我們可以讓并行的進(jìn)程同時(shí)向一個(gè)管道寫(xiě)一行請(qǐng)求,內(nèi)容是其進(jìn)程號(hào),在管道另一端順序讀取這些請(qǐng)求,但只有第一個(gè)請(qǐng)求會(huì)得到一個(gè)“令牌”,被允許開(kāi)始運(yùn)行;后續(xù)的請(qǐng)求將被忽略,對(duì)應(yīng)的進(jìn)程沒(méi)有得到令牌,就自己退出。這樣就保證了任意時(shí)間只有一個(gè)進(jìn)程運(yùn)行(嚴(yán)格地說(shuō)是進(jìn)入臨界區(qū))。說(shuō)到“令牌”,熟悉網(wǎng)絡(luò)發(fā)展史的朋友可能會(huì)聯(lián)想到IBM的Token Ring架構(gòu),每一時(shí)刻只能有一個(gè)主機(jī)得到令牌并發(fā)送數(shù)據(jù),沒(méi)有以太網(wǎng)的“碰撞”問(wèn)題??上缤⑼ǖ兰夹g(shù)一樣,IBM的技術(shù)是不錯(cuò),但最終還是被淘汰了。不錯(cuò),這里令牌的概念就是借用于Token Ring。^_^
當(dāng)一個(gè)進(jìn)程執(zhí)行完畢,向管道發(fā)送一個(gè)終止信號(hào),即交回“令牌”,另一端接受到后,又開(kāi)始選取下一個(gè)進(jìn)程發(fā)放“令牌”。
您可能會(huì)問(wèn)了,那么死鎖問(wèn)題又如何解決呢?別急,我在以前的討論中曾提出將檢測(cè)處理死鎖的代碼單獨(dú)拿出來(lái),交給一個(gè)專門(mén)的進(jìn)程來(lái)處理的想法,這里就具體實(shí)踐這樣一種思路。當(dāng)檢測(cè)和刪除死鎖的任務(wù)由一個(gè)專門(mén)的進(jìn)程來(lái)執(zhí)行時(shí),就沒(méi)有多個(gè)并發(fā)進(jìn)程對(duì)同一個(gè)鎖進(jìn)行操作,所以競(jìng)態(tài)條件發(fā)生的物質(zhì)基礎(chǔ)也就根本不存在了。^_^
再發(fā)展一下這個(gè)思路,允許同時(shí)執(zhí)行多個(gè)進(jìn)程如何?當(dāng)然可以!只要設(shè)立一個(gè)計(jì)數(shù)器,達(dá)到限制的數(shù)字就停止發(fā)放“令牌”即可。
下面就是woodie上述思路的一個(gè)實(shí)現(xiàn),只是在centos 4.2下簡(jiǎn)單地測(cè)試了一下,可能還有不少錯(cuò)誤,請(qǐng)大家?guī)兔Α俺x(chóng)”。^_^思路上有什么問(wèn)題也請(qǐng)不吝指教:
腳本1,token.sh,負(fù)責(zé)令牌管理和死鎖檢測(cè)處理。與下一個(gè)腳本一樣,為了保持腳本的最大的兼容性,盡量使用Bourne shell的語(yǔ)法,并用printf代替了echo,sed的用法也盡量保持通用性。這里是由一個(gè)命名管道接受請(qǐng)求,令牌在一個(gè)文件中發(fā)出。如果用ksh也許可以用協(xié)進(jìn)程來(lái)實(shí)現(xiàn),熟悉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的一個(gè)BUG,將原來(lái)用sed刪除失效令牌的命令用ed命令代替。感謝r2007和waker兩位指出錯(cuò)誤!
--------------------------------------------------------------------------------------------
腳本2:并發(fā)執(zhí)行的腳本 -- my-script。在"your code goes here"一行后插入你自己的代碼,現(xiàn)有的是我用來(lái)測(cè)試的。
#!/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
這個(gè)腳本是將文件鎖得,不過(guò)我對(duì)這腳本還有一些疑惑的地方,暫且不討論,等以后回頭再來(lái)談
#!/bin/sh
# filelock - A flexible file locking mechanism.
retries="10" # default number of retries
action="lock" # default action
nullcmd="/bin/true" # null command for lockfile
while getopts "lur:" opt; do
case $opt in
l ) action="lock" ;;
u ) action="unlock" ;;
r ) retries="$OPTARG" ;;
esac
done
shift $(($OPTIND - 1))
if [ $# -eq 0 ] ; then
cat EOF >2
Usage: $0 [-l|-u] [-r retries] lockfilename
Where -l requests a lock (the default), -u requests an unlock, -r X
specifies a maximum number of retries before it fails (default = $retries).
EOF
exit 1
fi
# Ascertain whether we have lockf or lockfile system apps
if [ -z "$(which lockfile | grep -v '^no ')" ] ; then
echo "$0 failed: 'lockfile' utility not found in PATH." >2
exit 1
fi
if [ "$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
fi
else # action = unlock
if [ ! -f "$1" ] ; then
echo "$0: Warning: lockfile $1 doesn't exist to unlock" >2
exit 1
fi
rm -f "$1"
fi
exit 0
您可能感興趣的文章:- linux 可執(zhí)行文件與寫(xiě)操作的同步問(wèn)題(文件讀寫(xiě)操作產(chǎn)生的鎖機(jī)制)
- 淺析Linux下一個(gè)簡(jiǎn)單的多線程互斥鎖的例子
- 詳解Linux文件鎖flock
- Linux多線程鎖屬性設(shè)置方法
- linux中各種鎖機(jī)制的使用與區(qū)別詳解