shell中控制多個進程併發執行的方法

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<&-

這樣,就實現了進程的併發控制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章