原文連結:http://bbs.chinaunix.net/viewthread.php?tid=840050
【背景介紹】
大體上可以分為兩種思路:
一、簡單的方法是,用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相同的機率比較微小)不容易被意外删除。這個方法已經接近完美了,可惜還是存在誤删的微小幾率,不能說是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 "___FCKpd___1quot; token_file
if [ $? -ne 0 ]; then
# bad luck. :(
printf "no token free now, exitting.../n" >&2
exit 2
fi
# yeah, got token!
# be sure to return the token before we exit
trap 'printf "___FCKpd___1nbsp;stop/n" > /tmp/p-aquire' 0
trap "exit 3" 1 2 3 15
#get to run, your code goes here
printf "$: running.../n" >&2
sleep 5
printf "$: exitting.../n" >&2
#end of your code