昨天出去面试,被问到一个问题,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 秒

查看进程数量,是执行完上一条才会继续执行下一条

Shell_single-Process

多进程脚本

#!/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条同时执行的

Shell_multi-Progress

有兴趣的可以更改 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 秒"

评论




正在载入...
PoweredHexo
HostedAliyun
DNSAliyun
ThemeVolantis
UV
PV
BY-NC-SA 4.0