天天看點

限制同時運作腳本執行個體的個數 -- 串行化

原文連結: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