shell中實現多進程實際上就是將多個任務放到後臺中執行而已,但是現在需要控制多進程併發的數量該如何實現呢?別急,我們一步一步來實現這個目標,首先從最原始的串行執行開始:
#!/bin/bash
start=`date +%s`
for i in $(seq 1 5); do
echo test
sleep 2
done
end=`date +%s`
time=$(($end - $start))
echo "time: $time"
執行結果:
# sh test1.sh
test
test
test
test
test
time: 10
這是最原始的一種處理方式,串行執行,無法有效利用計算機的資源,並且效率低下。如果可以一次執行多個任務的,把它放到後臺即可,我們做如下改進:
#!/bin/bash
start=`date +%s`
for i in $(seq 1 5); do
{
echo test
sleep 2
}&
done
wait
end=`date +%s`
time=$(($end - $start))
echo "time: $time"
首先看下執行結果:
# sh test2.sh
test
test
test
test
test
time: 2
說下改動,首先我把for循環中的代碼用{}包爲一個塊,然後增加&符號使其後臺運行,之後增加wait指令,該指令會等待所有後臺進程結束,如果不加wait,程序直接往下走,最終打出的time將會是0。現在程序已經由之前的10秒縮短爲2秒,似乎效果不錯,不過試想這樣一個場景,有1000個這樣的任務,如果還是以這種方式執行,機器負載是扛不住的,我們必須想一種辦法來控制進程的併發數,那就是管道和文件描述符。
首先介紹下管道(pipe):
- 無名管道
它經常出現在我們的命令中,例如cat /etc/passwd | awk -F: '{print $1}',其中"|"就是管道,它將前一個命令的結果輸出到後一個進程中,作爲兩個進程的數據通道,不過他是無名的。 - 有名管道
使用mkfifo命令創建的管道即爲有名管道,例如,mkfifo pipefile, pipefile即爲有名管道。
管道有一個顯著的特點,如果管道里沒有數據,那麼去取管道數據時,程序會阻塞住,直到管道內進入數據,然後讀取才會終止這個操作,反之,管道在執行寫入操作時,如果沒有讀取操作,同樣會阻塞,下面是實例:
# mkfifo pipefile
# echo test > pipefile
# 此時會阻塞在這
此時我新開一個窗口再執行讀操作
# cat pipefile
test
這時窗口一中的進程纔會終止,再來看先讀的情況:
# cat pipefile
# 同樣阻塞停滯在此
新開窗口執行寫操作:
# echo test > pipefile
這時窗口一會立刻讀出內容並順利完成
接下來看一下文件描述符
Linux系統在初始運行時,會自動綁定三個文件描述符0 1 2 對應 stdin ,stdout, stderr,在/proc/self/fd可以找到
# cd /proc/self/fd
# ll
total 0
lrwx------. 1 root root 64 Mar 27 03:22 0 -> /dev/pts/0
lrwx------. 1 root root 64 Mar 27 03:22 1 -> /dev/pts/0
lrwx------. 1 root root 64 Mar 27 03:22 2 -> /dev/pts/0
lrwx------. 1 root root 64 Mar 27 03:23 255 -> /dev/pts/0
# 綁定到我們的終端設備上
# echo test > /proc/self/fd/1
test
# echo test > /proc/self/fd/2
test
輸出到幾個文件的內容會打印到控制檯上
我們可以使用exec 指令自行定義、綁定文件描述符,文件描述符的取值範圍是3-(ulimit -n)-1
在我本機這個範圍是3-1023
# ulimit -n
1024
# exec 1024<>pipefile
-bash: 1024: Bad file descriptor
# exec 1000<>pipefile
#
下面開始介紹如何使用管道文件和文件描述符來控制進程併發:
mkfifo tm1
exec 5<>tm1
rm -f tm1
首先創建一個管道文件tm1,然後使用exec命令將該文件的輸入輸出綁定到5號文件描述符,而後刪除該管道文件,這裏刪除的只是該文件的Inode,實際文件已由內核open函數打開,這裏刪除並不會影響程序執行,當程序執行完後,文件描述符會被系統自動回收。
for ((i=1;i<=10;i++)); do
echo >&5
done
通過一個for循環向該文件描述符寫入10個空行,這個10其實就是我們定義的後臺進程數量,這裏需要注意的是,管道文件的讀取是以行爲單位的。
for ((j=1;j<=100;j++)); do
read -u5
{
echo test$j
sleep 2
echo >&5
}&
done
wait
這裏假定我後臺有100個任務,而我們在後臺保證只有10個進程在同時運行
read -u5 的作用是,讀取5號文件描述符中的一行,就是讀取一個空行
在減少文件描述符的一個空行之後,在後臺執行一次任務,而任務在執行完成以後,會向文件描述符中再寫入一個空行,這是爲什麼呢,因爲如果我們寫入空行的話,當後臺放入了10個任務之後,由於沒有可讀取的空行,read -u5就會被阻塞住!
exec 5>&-
exec 5<&-
我們生成做綁定時 可以用 exec 5<>tm1 來實現,但關閉時必須分開來寫> 讀的綁定,< 標識寫的綁定 <> 則標識 對文件描述符5的所有操作等同於對管道文件tm1的操作。
完整代碼如下:
#!/bin/bash
mkfifo tm1
exec 5<>tm1
rm -f tm1
for ((i=1;i<=10;i++)); do
echo >&5
done
for ((j=1;j<=100;j++)); do
read -u5
{
echo test$j
sleep 2
echo >&5
}&
done
wait
exec 5>&-
exec 5<&-
這樣,就實現了進程的併發控制