eval并发 shell_Shell 实现多任务并发

实现思路

实现一个shell进程库,通过类似于init,run,wait几个简单的命令,就可以迅速实现多进程并发,伪码如下:

process_init # 创建进程for city in ${cities[*]}docmd="handler $city"process_run $cmddoneprocess_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 #以读写方式打开文件,并关联文件描述符

# 测试

exec8>file

echo "hello word!" 1>&8

eval

为了让程序有一定的扩展性,不想写死fd,因而引入了变量。

file_fd=8exec ${file_fd}>file

# 测试

bash test.shtest.sh: line 7: exec: 8: not found

原因:shell在重定向操作符()左边不进行变量展开。因而引入eval命令,强制shell进行变量展开。

eval exec "${fd}>file"简单的说,eval将右边参数整体作为一个命令,进行变量的替换,然后将替换后的输出结果给shell去执行。

进程关系

命令执行

functionprocess_run()

{

cmd=$1

if [ -z "$cmd" ]; then

echo "please input command to run"_delete_pipe

exit1

fi_process_get

{

$cmd

_process_post

}&}

_process_get从管道中取得一个令牌,创建一个进程执行任务,任务执行完毕后,通过_process_post将令牌放回管道。

进程创建

chengsun@153_92:~/test> bash process_util.shchengsun@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 COMMAND24904 21976 24904bash19885 24904 19885\_ bash # 爷爷19893 19885 19885\_ bash # 爸爸19894 19893 19885 |\_ a.out # 儿子19895 19885 19885\_ bash19896 19895 19885 |\_ a.out19897 19885 19885\_ bash19898 19897 19885 \_ a.out

Shell 将运行process_util的一堆进程置于一个进程组中。其中爷爷进程作为该进程组的组长,pid == pgid。

wait

wait pid:阻塞等待某个进程结束; 如果没有指定参数,wait会等待所有子进程结束。

清理函数

允许任务通过CTRL+C方式提前结束,因而需要清理函数

信号

trap

类似C语言signal函数,为shell脚本注册信号处理函数。trap arg signals,其中signals为注册的信号列表,arg为收到信号后执行某个命令。

functionPrint

{echo "Hello World!"}

trap Print SIGKILL

kill

kill 命令给进程或进程组发送信号;kill pid 给进程发送默认信号SIGTERM, 通知程序终止执行。

void sig_handler(intsigno)

{

printf("sigterm signal\n");

}intmain()

{

signal(SIGTERM, sig_handler);sleep(100);

return0;

}

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

kill 0:给当前进程组发送默认信号SIGTERM

chengsun@153_92:~/test> man kill

0 All processes in the current process group are signaled.

清理

function_clean_up

{

# 清理管道文件

_delete_pipekill 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

functionhandler()

{

city=$1./main ${city}

}

process_init23

for city in$citiesdocmd= "handler $city"process_run"$cmd"

doneprocess_wait

————————————————

版权声明:本文为CSDN博主「spch2008」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/spch2008/article/details/51433353

喜欢这篇文章?欢迎打赏~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值