linux shell命名管道FIFO(多程序動態併發)
在shell指令碼中,我們想要實現多程序高併發,最簡單的方法是把命令丟到後臺去,如果量不大的話,沒問題。 但是如果有幾百個程序同一時間丟到後臺去就很恐怖了,對於伺服器資源的消耗非常大,甚至導致宕機。
那有沒有好的解決方案呢? 當然有!
一、基礎知識
1.檔案描述符
檔案描述符(縮寫fd)在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。每一個unix程序,都會擁有三個標準的檔案描述符,來對應三種不同的流:
檔案描述符 | 名稱 |
---|---|
0 | Standard Input |
1 | Standard Output |
2 | Standard Error |
除了上面三個標準的描述符外,我們還可以在程序中去自定義其他的數字作為檔案描述符,後面例子中會出現自定義數字。每一個檔案描述符會對應一個開啟檔案,同時,不同的檔案描述符也可以對應同一個開啟檔案;同一個檔案可以被不同的程序開啟,也可以被同一個程序多次開啟。
我們可以寫一個測試指令碼/tmp/test.sh,內容如下:
#!/bin/bash
echo "該程序的pid為$$"
exec 1>/tmp/test.log 2>&1
ls -l /proc/$$/fd/
執行該指令碼 sh /tmp/test.sh,然後檢視/tmp/test.log內容為:
總用量 0
lrwx—— 1 root root 64 11月 22 10:26 0 -> /dev/pts/3
l-wx—— 1 root root 64 11月 22 10:26 1 -> /tmp/test.log
l-wx—— 1 root root 64 11月 22 10:26 2 -> /tmp/test.log
lr-x—— 1 root root 64 11月 22 10:26 255 -> /tmp/test.sh
lrwx—— 1 root root 64 11月 22 10:26 3 -> socket:[196912101]
其中0為標準輸入,也就是當前終端pts/3,1和2全部指向到了/tmp/test.log,另外兩個數字,咱們暫時不關注。
2.命名管道
我們之前接觸過的管道“1”,其實叫做匿名管道,它左邊的輸出作為右邊命令的輸入。這個匿名管道只能為兩邊的命令提供服務,它是無法讓其他程序連線的。
實際上,這兩個程序(cat和less)並不知道管道的存在,它們只是從標準檔案描述符中讀取資料和寫入資料。
另外一種管道叫做命名管道,英文(First In First Out,簡稱FIFO)。
FIFO本質上和匿名管道的功能一樣,只不過它有一些特點:
1)在檔案系統中,FIFO擁有名稱,並且是以裝置特俗檔案的形式存在的;
2)任何程序都可以通過FIFO共享資料;
3)除非FIFO兩端同時有讀與寫的程序,否則FIFO的資料流通將會阻塞;
4)匿名管道是由shell自動建立的,存在於核心中;而FIFO則是由程式建立的(比如mkfifo命令),存在於檔案系統中;
5)匿名管道是單向的位元組流,而FIFO則是雙向的位元組流;
有了上面的基礎知識儲備後,下面我們來用FIFO來實現shell的多程序併發控制。
二、需求背景:
領導要求小明備份資料庫伺服器裡面的100個庫(資料量在幾十到幾百G),需要以最快的時間完成(5小時內),並且不能影響伺服器效能。
需求分析:
由於資料量比較大,單個庫備份時間少則10幾分鐘,多則幾個小時,我們算平均每個庫30分鐘,若一個庫一個庫的去備份,則需要3000分鐘,相當於50個小時。很明顯不可取。但全部丟到後臺去備份,100個併發,資料庫伺服器也無法承受。所以,需要寫一個指令碼,能夠控制併發數就可以實現了。
三、編寫shell
控制併發的shell指令碼示例:
#!/bin/sh
function a_sub {
sleep 2;
endtime=`date +%s`
sumtime=$[$endtime-$starttime]
echo "我是$i,運行了2秒,整個指令碼已經執行了$sumtime秒"
}
starttime=`date +%s`
export starttime
##其中$$為該程序的pid
tmp_fifofile="/tmp/$$.fifo"
##建立命名管道
mkfifo $tmp_fifofile
##把檔案描述符6和FIFO進行繫結
exec 6<>$tmp_fifofile
##繫結後,該檔案就可以刪除了
rm -f $tmp_fifofile
##併發量為3,用這個數字來控制併發數
thread=3
for ((i=0;i<$thread;i++));
do
##寫一個空行到管道里,因為管道檔案的讀取以行為單位
echo >&6
done
##迴圈10次,相當於要備份100個庫
for ((i=0;i<10;i++))
do
##讀取管道中的一行,每次讀取後,管道都會少一行
read -u6
{
a_sub || {echo "a_sub is failed"}
##每次執行完a_sub函式後,再增加一個空行,這樣下面的程序才可以繼續執行
echo >&6
} & ##這裡要放入後臺去,否則併發實現不了
done
##這裡的wait意思是,需要等待以上所有操作(包括後臺的程序)都結束後,再往下執行。
wait
##關閉檔案描述符6的寫
exec 6>&-