在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/
1
2
3
4
執行該腳本 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>&-