展示代碼
#!/bin/bash
trap "exec 1000>&-;exec 1000<&-;exit 0" 2
# 分別為 創(chuàng)建管道文件,文件操作符綁定,刪除管道文件
mkfifo testfifo
exec 1000<>testfifo
rm -rf testfifo
# 對(duì)文件操作符進(jìn)行寫(xiě)入操作。
# 通過(guò)一個(gè)for循環(huán)寫(xiě)入10個(gè)空行,這個(gè)10就是我們要定義的后臺(tái)線程數(shù)量。
for ((n=1; n<=10; n++))
do
echo >&1000
done
# 創(chuàng)建一個(gè)備份目錄
if [[ ! -d back ]]; then
mkdir back
fi
# 開(kāi)始時(shí)間記錄
start=`date "+%s"`
# 獲取URL總數(shù),如果總數(shù)為0,直接退出
total=`cat urls | wc -l`
if [[ $total == 0 ]]; then
echo 'urls總數(shù)為空'
exit 0
fi
# 遍歷URLS文件,開(kāi)始執(zhí)行下載
for ((i=1;i<=$total;i++))
do {
# 從testfifo中讀取一行
read -u1000
{
# 增加嘗試次數(shù),最大5次
for j in {1..5}
do
# 判斷單獨(dú)進(jìn)程文件目錄是否存在,不存在則創(chuàng)建目錄
download_dir=audio"$i"
if [[ ! -d $download_dir ]]; then
mkdir -p $download_dir
fi
echo "往目錄${download_dir}中開(kāi)始下載文件,嘗試次數(shù):${j}"
# 讀取URLS中的一行,下載文件
you-get -o $download_dir `sed -n "$i"p urls | tr -d '\r'`
# 校驗(yàn)是否有異常,如果沒(méi)有異常,則跳出循環(huán),執(zhí)行外下一條,如果有異常,再次嘗試下載
if [[ $? != 0 ]]; then
mv $download_dir/* back
rm -rf $download_dir
else
break
fi
done
# 向文件操作符中寫(xiě)入一個(gè)空行
echo >&1000
}&
}
done
# 等待所有任務(wù)完成
wait
end=`date "+%s"`
echo "time: `expr $end - $start`"
exec 1000>&-
exec 1000<&-
所謂多進(jìn)程,就是將一個(gè)任務(wù)劃分成多個(gè)子任務(wù)放在后臺(tái)執(zhí)行。"FIFO"是一種特殊的文件類(lèi)型,它允許獨(dú)立的進(jìn)程通訊. 一個(gè)進(jìn)程打開(kāi)FIFO文件進(jìn)行寫(xiě)操作,而另一個(gè)進(jìn)程對(duì)之進(jìn)行讀操作, 然后數(shù)據(jù)便可以如同在shell或者其它地方常見(jiàn)的的匿名管道一樣流線執(zhí)行。默認(rèn)情況下,創(chuàng)建的FIFO的模式為0666('a+rw')減去umask中設(shè)置的位。
串行、并行
串行任務(wù)
為了比較并行和串行的區(qū)別,我們先寫(xiě)個(gè)串行的腳本:
#!/bin/bash
start=`date "+%s"`
for i in {1..10}
do
echo $i;
sleep 2
done
end=`date "+%s"`
echo "Time: `expr $end - $start`"
執(zhí)行結(jié)果如下:
$ sh series.sh
1
2
3
4
5
6
7
8
9
10
Time: 21
從結(jié)果來(lái)開(kāi),執(zhí)行完上面10次任務(wù),每次任務(wù)2秒,總耗時(shí)21秒,符合代碼的邏輯。
并行任務(wù)
先將上面的串行任務(wù)改成多線程并行任務(wù)。
#!/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`"
執(zhí)行上面腳本的結(jié)果如下:
$ sh paralle.sh
1
2
3
4
5
6
7
8
9
10
Time: 2
通常,用{}將不占處理器卻很耗時(shí)的任務(wù)放包裝一個(gè)塊,通過(guò)&放置在后臺(tái)運(yùn)行,以達(dá)到節(jié)約時(shí)間的效果。上面并行代碼,我們把10次任務(wù)全部放在后臺(tái)執(zhí)行,每個(gè)人物耗時(shí)2秒,由于并行執(zhí)行,總耗時(shí)也就是Max(任務(wù)耗時(shí))=2秒。
{
echo $i;
sleep 2
}&
在小任務(wù)跟前,這種方式運(yùn)用起來(lái)得心應(yīng)手,但是在任務(wù)量過(guò)大的時(shí)候,這種方式的缺點(diǎn)也就顯而易見(jiàn)了:無(wú)法控制運(yùn)行在后臺(tái)的進(jìn)程數(shù),不能就10萬(wàn)個(gè)任務(wù)就是跑10萬(wàn)個(gè)進(jìn)程吧。為了控制進(jìn)程,我們引入了管道和文件操作符。
管道、文件操作符
管道
管道就像水管,有流入才會(huì)有流出,水管數(shù)水流的通道,管道是數(shù)據(jù)的通道。管道分為無(wú)名管道和有名管道。
| 管道類(lèi)別 | 命令 | 栗子 | ||
|---|---|---|---|---|
| 無(wú)名管道 | 常用的` | `就是管道,只不過(guò)是無(wú)名的,可以直接作為兩個(gè)進(jìn)程的數(shù)據(jù)通道 | `echo "hello world, I'm a test" | grep "test"` |
| 有名管道 |
mkfilo 可以創(chuàng)建一個(gè)管道文件 |
mkfiflo testfifo |
管道有一個(gè)特點(diǎn),如果管道中沒(méi)有數(shù)據(jù),那么取管道數(shù)據(jù)的操作就會(huì)阻塞,直到管道內(nèi)進(jìn)入數(shù)據(jù),然后讀出后才會(huì)終止這一操作,同理,寫(xiě)入管道的操作如果沒(méi)有讀取操作,這一個(gè)動(dòng)作也會(huì)阻塞。

當(dāng)通過(guò)echo命令往fifotest管道中寫(xiě)入數(shù)據(jù)時(shí),由于沒(méi)有任何其他消費(fèi)進(jìn)程對(duì)管道操作,所以,該管道阻塞,直到再打開(kāi)一個(gè)窗口且通過(guò)cat操作該管道。

同理,先操作讀取管道也會(huì)出現(xiàn)阻塞的情況。

通過(guò)以上實(shí)驗(yàn),看以看到,僅僅一個(gè)管道文件似乎很難實(shí)現(xiàn)控制后臺(tái)線程數(shù),因此我們接下來(lái)簡(jiǎn)單介紹 文件操作符。
文件操作符
系統(tǒng)運(yùn)行起始,就相應(yīng)設(shè)備自動(dòng)綁定到了 三個(gè)文件操作符 分別為0、1 、2 對(duì)應(yīng) stdin、stdout、 stderr 。在 /proc/self/fd 或者/dev/fd中可以看到這三個(gè)對(duì)應(yīng)文件:

輸出到這三個(gè)文件的內(nèi)容都會(huì)顯示出來(lái)。只是因?yàn)轱@示器作為最常用的輸出設(shè)備而被綁定。
在Linux中,可以通過(guò)exec指令自行定義、綁定文件操作符,文件操作符一般從3~(n-1)都可以隨便使用,此處的n為ulimit -n的定義值。

從上圖可以看出本機(jī)的n值為8192 ,所以文件操作符只能使用0-8192 ,可自行定義的就只能是3-8192。
雖然exec和source都是在父進(jìn)程中直接執(zhí)行,但exec這個(gè)與source有很大的區(qū)別,source是執(zhí)行shell腳本,而且執(zhí)行后會(huì)返回以前的shell。而exec的執(zhí)行不會(huì)返回以前的shell了,而是直接把以前登陸shell作為一個(gè)程序看待,在其上經(jīng)行復(fù)制。
exec可參考此文:《linux 下的 mkfifo、exec 命令使用》
代碼分析

第3行:
- 接受信號(hào) 2 (ctrl +C)做的操作。
- 我們生成文件描述符并做綁定時(shí),可以用
exec 1000<>testfifo來(lái)實(shí)現(xiàn),但關(guān)閉時(shí)必須分開(kāi)來(lái)寫(xiě)。 -
>讀的綁定,<標(biāo)識(shí)寫(xiě)的綁定<>則標(biāo)識(shí)對(duì)文件描述符1000的所有操作,其等同于對(duì)管道文件testfifo的操作。
第6-8行:
- 分別為
創(chuàng)建管道文件,文件操作符綁定,刪除管道文件 - 可能會(huì)有疑問(wèn),為什么不能直接使用管道文件呢?事實(shí)上,這并非多此一舉,剛才已經(jīng)說(shuō)明了管道文件的一個(gè)重要特性了,那就是讀寫(xiě)必須同時(shí)存在,缺少某一種操作,另一種操作就是阻塞,而綁定文件操作符正好解決了這個(gè)問(wèn)題。
第12-15行:
- 對(duì)文件操作符進(jìn)行寫(xiě)入操作。 通過(guò)一個(gè) for 循環(huán)寫(xiě)入 10 個(gè)空行,這個(gè) 10 就是我們要定義的后臺(tái)線程數(shù)量。
- 為什么寫(xiě)入空行而不是 10 個(gè)字符呢?這是因?yàn)?,管道文件的讀取是以
行為單位的。 - 當(dāng)我們?cè)噲D用 read 讀取管道中的一個(gè)字符時(shí),結(jié)果是不成功的,上面的例子已經(jīng)證實(shí)了使用cat是可以讀取的。

第32-61行:
- 遍歷urls的總行數(shù),循環(huán)處理url
- 25-29行是讀取urls文件的總行數(shù)的邏輯(看開(kāi)篇代碼)。
- 這里我們有
$total個(gè)任務(wù)($total是變量,是讀取的urls的總行數(shù),值大于0),我們需要保證后臺(tái)只有10個(gè)進(jìn)程在同步運(yùn)行(當(dāng)然這段代碼有點(diǎn)小遺憾,就是未能根據(jù)總行數(shù)決定用多少個(gè)進(jìn)程,加入總行數(shù)小于10,但我們創(chuàng)建了10行空字符串,但這并不影響我們的測(cè)試) 。 -
read -u1000的作用是:讀取一次管道中的一行,在這兒就是讀取一個(gè)空行。 - 減少操作附中的一個(gè)空行之后,執(zhí)行一次任務(wù)(當(dāng)然是放到后臺(tái)執(zhí)行),需要注意的是,這個(gè)任務(wù)在后臺(tái)執(zhí)行結(jié)束以后會(huì)向文件操作符中寫(xiě)入一個(gè)空行,這就是重點(diǎn)所在,如果我們不在某種情況某種時(shí)刻向操作符中寫(xiě)入空行,那么結(jié)果就是:在后臺(tái)放入10個(gè)任務(wù)之后,由于操作符中沒(méi)有可讀取的空行,導(dǎo)致
read -u1000這兒始終停頓。 - 第38-56行,處理自己的業(yè)務(wù),這里面是通過(guò)
you-get下載url中的圖片、語(yǔ)音,如果下載失敗,最多嘗試5次。關(guān)于you-get參考這篇文章《You-Get:支持 80 多個(gè)網(wǎng)站的命令行多媒體下載器》了解其更多。

第64-69行:
- 等待所有進(jìn)程執(zhí)行結(jié)束。
-
exec 1000>&-和exec 1000<&-是關(guān)閉fd1000。
該文首發(fā)《虛懷若谷》個(gè)人博客。轉(zhuǎn)發(fā)請(qǐng)注明原創(chuàng)作者:若谷
古之善為道者,微妙玄通,深不可識(shí)。夫唯不可識(shí),故強(qiáng)為之容:
豫兮若冬涉川,猶兮若畏四鄰,儼兮其若客,渙兮若冰之釋?zhuān)刭馄淙魳?,曠兮其若谷,混兮其若濁?/p>
孰能濁以靜之徐清?孰能安以動(dòng)之徐生?
保此道不欲盈。夫唯不盈,故能敝而新成。