linux shell之檔案鎖_linux shell

來源:互聯網
上載者:User

經常在 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相同的機率比較微小)不容易被意外刪除。這個方法已經接近完美了,可惜還是存在誤刪的微小几率,不能說是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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.