Shell 實現多任務並發


實現思路

實現一個shell進程庫,通過類似於init,run,wait幾個簡單的命令,就可以迅速實現多進程並發,偽碼如下:

process_init # 創建進程
for city in ${cities[*]}
do
cmd="handler $city"
process_run $cmd 
done
process_wait # 等待進程

原理解析

在實現C++線程庫的時候,通常會有一個任務隊列,線程從隊列中取任務並運行。在實現shell進程庫的時候,采用了類似原理,通過一個有名管道充當任務隊列。嚴格來說,並不是一個任務隊列,而是一個令牌桶。進程從桶中取得令牌后才可以運行,運行結束后將令牌放回桶中。沒有取得令牌的進程不能運行。令牌的數目即允許並發的最大進程數。

 

 

 

 

管道

主要思路:通過mkfifo創建一個有名管道,將管道與一個文件描述符綁定,通過往管道中寫數據的方式,控制進程數量。

function _create_pipe()
{
_PROCESS_PIPE_NAME=$(_get_uid)

mkfifo ${_PROCESS_PIPE_NAME}
eval exec "${_PROCESS_PIPE_ID}""<>${_PROCESS_PIPE_NAME}"

for ((i=0; i < $_PROCESS_NUM; i++))
do
echo -ne "\n" 1>&${_PROCESS_PIPE_ID}
done
}

 

exec

exec fd < file #以讀方式打開文件,並關聯文件描述符fd exec fd > file #以寫方式打開文件,並關聯文件描述符fd exec fd <> file #以讀寫方式打開文件,並關聯文件描述符

 

# 測試
exec 8>file
echo "hello word!" 1>&8

 

eval
為了讓程序有一定的擴展性,不想寫死fd,因而引入了變量。

file_fd=8
exec ${file_fd}>file

 

# 測試
bash test.sh
test.sh: line 7: exec: 8: not found


原因:shell在重定向操作符(<、>)左邊不進行變量展開。因而引入eval命令,強制shell進行變量展開。 
eval exec "${fd}>file"簡單的說,eval將右邊參數整體作為一個命令,進行變量的替換,然后將替換后的輸出結果給shell去執行。

進程關系

命令執行

function process_run()
{
cmd=$1
if [ -z "$cmd" ]; then
echo "please input command to run"
_delete_pipe 
exit 1
fi

_process_get
{
$cmd
_process_post
}&
}

 

_process_get從管道中取得一個令牌,創建一個進程執行任務,任務執行完畢后,通過_process_post將令牌放回管道。

進程創建

chengsun@153_92:~/test> bash process_util.sh
chengsun@153_92:~/test> pstree -a

|`-sshd
|    |-bash
|    |   `-bash process_util.sh         #爺爺
|    |       |-bash process_util.sh     #爸爸
|    |       |   `-a.out                #兒子
|    |       |-bash process_util.sh        
|    |       |   `-a.out                   
|    |       `-bash process_util.sh
|    |           `-a.out

 

腳本運行后,通過pstree命令,得到如上父子進程關系。稍微做一下解釋:當運行腳本bash process_util.sh的時候,創建一個shell進程,相當於爺爺被創建起來,而遇到{ command1; command2 } &時,shell 又創建一個子shell進程(爸爸進程)處理命令序列;而對於每一個外部命令,shell都會創建一個子進程運行該命令,即兒子進程被創建。

困惑:為什么處理{ command1; command2; } &需要單獨創建子進程? 
按照bash manual說法,{ list }並不會創建一個新的shell來運行命令序列。但由於加入&,代表將命令族放入后台執行,就必須新開subshell,否則shell會阻塞。

進程組

 

chengsun@153_92:~/test> ps -f -e -o pid,ppid,pgid,comm

 PID  PPID  PGID  COMMAND
24904 21976 24904 bash
19885 24904 19885  \_ bash            # 爺爺
19893 19885 19885      \_ bash        # 爸爸
19894 19893 19885      |   \_ a.out   # 兒子
19895 19885 19885      \_ bash
19896 19895 19885      |   \_ a.out
19897 19885 19885      \_ bash
19898 19897 19885          \_ a.out

 

Shell 將運行process_util的一堆進程置於一個進程組中。其中爺爺進程作為該進程組的組長,pid == pgid。

wait
wait pid:阻塞等待某個進程結束; 如果沒有指定參數,wait會等待所有子進程結束。

清理函數

允許任務通過CTRL+C方式提前結束,因而需要清理函數

信號

trap 
類似C語言signal函數,為shell腳本注冊信號處理函數。trap arg signals,其中signals為注冊的信號列表,arg為收到信號后執行某個命令。

 

function Print
{
echo "Hello World!"
}

trap Print SIGKILL

kill 
kill 命令給進程或進程組發送信號;kill pid 給進程發送默認信號SIGTERM, 通知程序終止執行。

 

void sig_handler(int signo)
{
printf("sigterm signal\n");
}

int main()
{
    signal(SIGTERM, sig_handler);
    sleep(100);

return 0;
}
chengsun@153_92:~/test> ./a.out &
[1] 19254
chengsun@153_92:~/test> kill 19254
sigterm signal

 

kill 0:給當前進程組發送默認信號SIGTERM

chengsun@153_92:~/test> man kill
0  All processes in the current process group are signaled.

清理

 

function _clean_up
{
# 清理管道文件
    _delete_pipe

kill 0
kill -9 $$
}

trap _clean_up SIGINT SIGHUP SIGTERM SIGKILL

 

kill -9 $$ 非常重要

 

 

 

實際上,最上層是爺爺進程,當發送Ctrl + C命令的時候,爺爺進程捕獲SIGINT信號,調用_clean_up函數。爺爺進程對整個進程組發送SIGTERM信號,並調用kill -9結束自己。爸爸進程接收SIGTERM信號,同時也發送SIGTERN給整個進程組,如果沒有kill -9,爸爸進程始終無法結束,進入無限遞歸環節。兒子為CPP二進制程序,內部沒有捕獲SIGTERM,該信號默認動作是結束進程。

使用范例

# file: run.sh
#!/bin/sh

#load process library
source ./process_util.sh

function handler()
{
    city=$1
    ./main ${city}
}

process_init 23
for city in $cities
do
    cmd = "handler $city"
    process_run "$cmd"
done
process_wait

————————————————
版權聲明:本文為CSDN博主「spch2008」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/spch2008/article/details/51433353

喜歡這篇文章?歡迎打賞~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM