昨天出去面试,被问到一个问题,shell多进程有写过吗,确实之前的工作内容中也没有过这样的需求,所以决定复盘,找资料学习下多进程的内容。
适用环境
在日常工作中写的关于结合 Jenkins 使用 Shell 脚本,其中的 CI/CD 过程也是必须有先后顺序的,如果脚本中任务或者函数模块之间没有存在依赖关系,相互独立,可以使用多进程的方式,快速完成脚本。
简单多进程
简单的多进程方式,可以通过 &
和 wait
来完成,缺点是无法控制并发数量,有多少任务就有多少进程在同时执行。
&:将命令放到后台执行
wait:一般放在并发脚本的尾部,等待前面的后台任务全部完成才会继续往下执行。
以检测主机存活为例
普通脚本
#!/bin/bash
IP=(
"192.168.1.12"
"192.168.1.13"
"192.168.1.14"
)
start_time=$(date +%s)
echo "开始检测主机存活"
for i in "${IP[@]}";do
ping -c2 $i &> /dev/null
if [ $? -eq 0 ];then
echo "$i is OK" >> OK.file
else
echo "$i is not OK" >> not_OK.file
fi
done
end_time=$(date +%s)
sum_time=$(expr $end_time - $start_time)
echo "检测完毕,共用时 $sum_time 秒"
# ----------------------------------------
# 执行结果
开始检测主机存活
检测完毕,共用时 3 秒
查看进程数量,是执行完上一条才会继续执行下一条
多进程脚本
#!/bin/bash
IP=(
"192.168.1.12"
"192.168.1.13"
"192.168.1.14"
)
start_time=$(date +%s)
echo "开始检测主机存活"
for i in "${IP[@]}";do
{
ping -c2 $i &> /dev/null
if [ $? -eq 0 ];then
echo "$i is OK" >> OK.file
else
echo "$i is not OK" >> not_OK.file
fi
} &
done
wait
end_time=$(date +%s)
sum_time=$(expr $end_time - $start_time)
echo "检测完毕,共用时 $sum_time 秒"
# ----------------------------------------
# 执行结果
开始检测主机存活
检测完毕,共用时 1秒
查看进程数量,是3条同时执行的
有兴趣的可以更改 ip 的数量来查看进程的数量,肯定是和 ip 数量一致的,如果有大量的 ip 需要检测,大量进程的运行就会消耗系统资源,少量肯定也消耗,但是不会影响正常使用,为了避免这种情况需要通过其他手段进行并发数量的控制。
并发数量的控制需要另外两个概念,命名管道和文件描述符
命名管道
管道分为无名管道和命名管道,无名管道就是日常使用的 |
符号,而命名管道通过 mkfifo
命令创建一个 fifo 文件,文件类型表示为p,表示为管道文件,不占磁盘空间。
$ mkfifo fifofile
$ ll
prw-r--r--. 1 root root 0 Mar 17 11:14 fifofile
该管道的两段要同时有读写的过程,如果只有写,没有读,管道的数据流通就会阻塞
$ echo "test" > fifofile # 执行该命令会阻塞,因为只有写入没有读取数据
# 需要打开另一个终端进行读取
$ cat fifofile # 执行完之后,写入的命令也会断开阻塞,完成管道的整个过程
test
利用管道的这个特性,将在管道文件中写入n个空白行,n就是并发进程的数量,然后在读取一次管道中的一行,执行一次任务,任务在后台执行完成后会向管道中写入一个空行。
在后台放n个任务后,也就是读取n行后,如果任务没有执行完,就不会写入空行,也就不在读取数据了,也完不成管道的读写过程,所以就把后台的线程数控制在了n。
文件描述符
文件描述符(fd)也就是之前接触过的,标准输入/标准输出/标准错误
,在形式上使用0-2
的数字表示。
0:标准输入
1:标准输出
2:标准错误
每一个进程都有这三个标准的文件描述符,每一个文件描述符会对应一个打开文件,同时,不同的文件描述符也可以对应同一个打开文件;同一个文件可以被不同的进程打开,也可以被同一个进程多次打开。
文件操作符可以使用 exex
命令自行定义和绑定,文件操作符一般从3-(n-1)都可以随便使用,此处的n 为 ulimit -n 的定义值得
控制进程并发数
正是由于管道和文件描述符的特性,可以结合起来就会解决管道文件在读写不同步时造成的阻塞
#!/bin/bash
# exec 1000>&-;表示关闭文件描述符1000的写
# exec 1000<&-;表示关闭文件描述符1000的读
# trap是捕获中断命令
# #接受信号2(ctrl +C)做的操作,表示在脚本运行过程中,如果接收到Ctrl+C中断命令,则关闭文件描述符1000的读写,并正常退出。
trap "exec 1000>&-;exec 1000<&-;exit 0" 2
FIFO_FILE=$$.fifo # 使用脚本运行的pid创建管道文件
mkfifo $FIFO_FILE # 创建管道文件
exec 1000<>$FIFO_FILE # 将管道文件和文件描述符绑定,<是读取,>是写入
rm -rf $FIFO_FILE # 绑定之后就可以删除管道文件
# 并发进程数
thread_num=5 # 运用到此脚本可以理解为同时执行ping的数量
# 10台主机ip
IP=(
"192.168.1.12"
"192.168.1.13"
"192.168.1.14"
"192.168.1.15"
"192.168.1.16"
"192.168.1.17"
"192.168.1.18"
"192.168.1.19"
"192.168.1.20"
"192.168.1.21"
"192.168.1.22"
)
# 对文件描述符进行写入操作,通过for写入空行,空行的数目就是并发的数量
for ((i=0; i<$thread_num; i++))
do
echo >&1000
done
# 此时文件操作符中有5个空行
start_time=$(date +%s)
echo "开始检测主机存活"
for i in "${IP[@]}"
do
# read读取文件描述符中的空行,每读取一次,就减少一个空行,然后执行一次并发任务
read -u1000
{
ping -c2 $i &> /dev/null
if [ $? -eq 0 ];then
echo "$i is OK" >> OK.file
else
echo "$i is not OK" >> not_OK.file
fi
# 以上的命令执行完后,在文件描述符中写入一个空行,如果不写入会导致read -u1000造成阻塞
echo >&1000
} &
done
wait
exec 1000>&- # 关闭文件描述符的写入操作
end_time=$(date +%s)
sum_time=$(expr $end_time - $start_time)
echo "检测完毕,共用时 $sum_time 秒"