實現思路
實現一個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
喜歡這篇文章?歡迎打賞~~

