展示代碼
#!/bin/bash
trap "exec 1000>&-;exec 1000<&-;exit 0" 2
# 分別為 創建管道文件,文件操作符綁定,刪除管道文件
mkfifo testfifo
exec 1000<>testfifo
rm -rf testfifo
# 對文件操作符進行寫入操作。
# 通過一個for循環寫入10個空行,這個10就是我們要定義的后台線程數量。
for ((n=1; n<=10; n++))
do
echo >&1000
done
# 創建一個備份目錄
if [[ ! -d back ]]; then
mkdir back
fi
# 開始時間記錄
start=`date "+%s"`
# 獲取URL總數,如果總數為0,直接退出
total=`cat urls | wc -l`
if [[ $total == 0 ]]; then
echo 'urls總數為空'
exit 0
fi
# 遍歷URLS文件,開始執行下載
for ((i=1;i<=$total;i++))
do {
# 從testfifo中讀取一行
read -u1000
{
# 增加嘗試次數,最大5次
for j in {1..5}
do
# 判斷單獨進程文件目錄是否存在,不存在則創建目錄
download_dir=audio"$i"
if [[ ! -d $download_dir ]]; then
mkdir -p $download_dir
fi
echo "往目錄${download_dir}中開始下載文件,嘗試次數:${j}"
# 讀取URLS中的一行,下載文件
you-get -o $download_dir `sed -n "$i"p urls | tr -d '\r'`
# 校驗是否有異常,如果沒有異常,則跳出循環,執行外下一條,如果有異常,再次嘗試下載
if [[ $? != 0 ]]; then
mv $download_dir/* back
rm -rf $download_dir
else
break
fi
done
# 向文件操作符中寫入一個空行
echo >&1000
}&
}
done
# 等待所有任務完成
wait
end=`date "+%s"`
echo "time: `expr $end - $start`"
exec 1000>&-
exec 1000<&-
所謂多進程,就是將一個任務划分成多個子任務放在后台執行。"FIFO"是一種特殊的文件類型,它允許獨立的進程通訊. 一個進程打開FIFO文件進行寫操作,而另一個進程對之進行讀操作, 然后數據便可以如同在shell或者其它地方常見的的匿名管道一樣流線執行。默認情況下,創建的FIFO的模式為0666('a+rw')減去umask中設置的位。
串行、並行
串行任務
為了比較並行和串行的區別,我們先寫個串行的腳本:
#!/bin/bash
start=`date "+%s"`
for i in {1..10}
do
echo $i;
sleep 2
done
end=`date "+%s"`
echo "Time: `expr $end - $start`"
執行結果如下:
$ sh series.sh
1
2
3
4
5
6
7
8
9
10
Time: 21
從結果來開,執行完上面10次任務,每次任務2秒,總耗時21秒,符合代碼的邏輯。
並行任務
先將上面的串行任務改成多線程並行任務。
#!/bin/bash
start=`date "+%s"`
for i in {1..10}
do
{
echo $i;
sleep 2
}&
done
wait
end=`date "+%s"`
echo "Time: `expr $end - $start`"
執行上面腳本的結果如下:
$ sh paralle.sh
1
2
3
4
5
6
7
8
9
10
Time: 2
通常,用{}
將不占處理器卻很耗時的任務放包裝一個塊,通過&
放置在后台運行,以達到節約時間的效果。上面並行代碼,我們把10次任務全部放在后台執行,每個人物耗時2秒,由於並行執行,總耗時也就是Max(任務耗時)=2秒。
{
echo $i;
sleep 2
}&
在小任務跟前,這種方式運用起來得心應手,但是在任務量過大的時候,這種方式的缺點也就顯而易見了:無法控制運行在后台的進程數,不能就10萬個任務就是跑10萬個進程吧。為了控制進程,我們引入了管道
和文件操作符
。
管道、文件操作符
管道
管道就像水管,有流入才會有流出,水管數水流的通道,管道是數據的通道。管道分為無名管道和有名管道。
管道類別 | 命令 | 栗子 |
---|---|---|
無名管道 | 常用的| 就是管道,只不過是無名的,可以直接作為兩個進程的數據通道 |
echo "hello world, I'm a test" | grep "test" |
有名管道 | mkfilo 可以創建一個管道文件 |
mkfiflo testfifo |
管道有一個特點,如果管道中沒有數據,那么取管道數據的操作就會阻塞,直到管道內進入數據,然后讀出后才會終止這一操作,同理,寫入管道的操作如果沒有讀取操作,這一個動作也會阻塞。
當通過echo命令往fifotest管道中寫入數據時,由於沒有任何其他消費進程對管道操作,所以,該管道阻塞,直到再打開一個窗口且通過cat操作該管道。
同理,先操作讀取管道也會出現阻塞的情況。
通過以上實驗,看以看到,僅僅一個管道文件似乎很難實現控制后台線程數,因此我們接下來簡單介紹 文件操作符
。
文件操作符
系統運行起始,就相應設備自動綁定到了 三個文件操作符 分別為0
、1
、2
對應 stdin
、stdout
、 stderr
。在 /proc/self/fd
或者/dev/fd
中可以看到這三個對應文件:
輸出到這三個文件的內容都會顯示出來。只是因為顯示器作為最常用的輸出設備而被綁定。
在Linux中,可以通過exec
指令自行定義、綁定文件操作符,文件操作符一般從3~(n-1)都可以隨便使用,此處的n為ulimit -n
的定義值。
從上圖可以看出本機的n
值為8192
,所以文件操作符只能使用0-8192
,可自行定義的就只能是3-8192
。
雖然exec和source都是在父進程中直接執行,但exec這個與source有很大的區別,source是執行shell腳本,而且執行后會返回以前的shell。而exec的執行不會返回以前的shell了,而是直接把以前登陸shell作為一個程序看待,在其上經行復制。
exec可參考此文:《linux 下的 mkfifo、exec 命令使用》
代碼分析
第3行:
- 接受信號 2 (ctrl +C)做的操作。
- 我們生成文件描述符並做綁定時,可以用
exec 1000<>testfifo
來實現,但關閉時必須分開來寫。 >
讀的綁定,<
標識寫的綁定<>
則標識對文件描述符1000的所有操作,其等同於對管道文件testfifo的操作。
第6-8行:
- 分別為
創建管道文件
,文件操作符綁定
,刪除管道文件
- 可能會有疑問,為什么不能直接使用管道文件呢?事實上,這並非多此一舉,剛才已經說明了管道文件的一個重要特性了,那就是讀寫必須同時存在,缺少某一種操作,另一種操作就是阻塞,而綁定文件操作符正好解決了這個問題。
第12-15行:
- 對文件操作符進行寫入操作。 通過一個 for 循環寫入 10 個空行,這個 10 就是我們要定義的后台線程數量。
- 為什么寫入空行而不是 10 個字符呢?這是因為,管道文件的讀取是以
行
為單位的。 - 當我們試圖用 read 讀取管道中的一個字符時,結果是不成功的,上面的例子已經證實了使用cat是可以讀取的。
第32-61行:
- 遍歷urls的總行數,循環處理url
- 25-29行是讀取urls文件的總行數的邏輯(看開篇代碼)。
- 這里我們有
$total
個任務($total是變量,是讀取的urls的總行數,值大於0),我們需要保證后台只有10個進程在同步運行(當然這段代碼有點小遺憾,就是未能根據總行數決定用多少個進程,加入總行數小於10,但我們創建了10行空字符串,但這並不影響我們的測試) 。 read -u1000
的作用是:讀取一次管道中的一行,在這兒就是讀取一個空行。- 減少操作附中的一個空行之后,執行一次任務(當然是放到后台執行),需要注意的是,這個任務在后台執行結束以后會向文件操作符中寫入一個空行,這就是重點所在,如果我們不在某種情況某種時刻向操作符中寫入空行,那么結果就是:在后台放入10個任務之后,由於操作符中沒有可讀取的空行,導致
read -u1000
這兒始終停頓。 - 第38-56行,處理自己的業務,這里面是通過
you-get
下載url中的圖片、語音,如果下載失敗,最多嘗試5次。關於you-get
參考這篇文章《You-Get:支持 80 多個網站的命令行多媒體下載器》了解其更多。
第64-69行:
- 等待所有進程執行結束。
exec 1000>&-
和exec 1000<&-
是關閉fd1000
。