在shell脚本中,我们想要实现多进程高并发,最简单的方法是把命令丢到后台去,如果量不大的话,没问题。 但是如果有几百个进程同一时间丢到后台去就很恐怖了,对于服务器资源的消耗非常大,甚至导致宕机。


那有没有好的解决方案呢? 当然有!


我们先来学习下面的常识。


1 文件描述符


文件描述符(缩写fd)在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。每一个unix进程,都会拥有三个标准的文件描述符,来对应三种不同的流:


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=


除了上面三个标准的描述符外,我们还可以在进程中去自定义其他的数字作为文件描述符,后面例子中会出现自定义数字。每一个文件描述符会对应一个打开文件,同时,不同的文件描述符也可以对应同一个打开文件;同一个文件可以被不同的进程打开,也可以被同一个进程多次打开。


我们可以写一个测试脚本/tmp/test.sh,内容如下:


#!/bin/bash 

echo "该进程的pid为$$"

exec 1>/tmp/test.log 2>&1

ls -l /proc/$$/fd/


执行该脚本 sh /tmp/test.sh,然后查看/tmp/test.log内容为:


总用量 0

lrwx------ 1 root root 64 11月 22 10:26 0 -> /dev/pts/3

l-wx------ 1 root root 64 11月 22 10:26 1 -> /tmp/test.log

l-wx------ 1 root root 64 11月 22 10:26 2 -> /tmp/test.log

lr-x------ 1 root root 64 11月 22 10:26 255 -> /tmp/test.sh

lrwx------ 1 root root 64 11月 22 10:26 3 -> socket:[196912101]


其中0为标准输入,也就是当前终端pts/3,1和2全部指向到了/tmp/test.log,另外两个数字,咱们暂时不关注。


2 命名管道


我们之前接触过的管道“1”,其实叫做匿名管道,它左边的输出作为右边命令的输入。这个匿名管道只能为两边的命令提供服务,它是无法让其他进程连接的。


640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=


实际上,这两个进程(cat和less)并不知道管道的存在,它们只是从标准文件描述符中读取数据和写入数据。


另外一种管道叫做命名管道,英文(First In First Out,简称FIFO)。


FIFO本质上和匿名管道的功能一样,只不过它有一些特点:


1)在文件系统中,FIFO拥有名称,并且是以设备特俗文件的形式存在的;

2)任何进程都可以通过FIFO共享数据;

3)除非FIFO两端同时有读与写的进程,否则FIFO的数据流通将会阻塞;

4)匿名管道是由shell自动创建的,存在于内核中;而FIFO则是由程序创建的(比如mkfifo命令),存在于文件系统中;

5)匿名管道是单向的字节流,而FIFO则是双向的字节流;


有了上面的基础知识储备后,下面我们来用FIFO来实现shell的多进程并发控制。


需求背景:


领导要求小明备份数据库服务器里面的100个库(数据量在几十到几百G),需要以最快的时间完成(5小时内),并且不能影响服务器性能。


需求分析:


由于数据量比较大,单个库备份时间少则10几分钟,多则几个小时,我们算平均每个库30分钟,若一个库一个库的去备份,则需要3000分钟,相当于50个小时。很明显不可取。但全部丢到后台去备份,100个并发,数据库服务器也无法承受。所以,需要写一个脚本,能够控制并发数就可以实现了。


控制并发的shell脚本示例:


#!/bin/sh

function a_sub {

    sleep 2;

    endtime=`date +%s` 

    sumtime=$[$endtime-$starttime]

    echo "我是$i,运行了2秒,整个脚本已经执行了$sumtime秒"

}


starttime=`date +%s`

export starttime

##其中$$为该进程的pid

tmp_fifofile="/tmp/$$.fifo"

##创建命名管道

mkfifo $tmp_fifofile

##把文件描述符6和FIFO进行绑定

exec 6<>$tmp_fifofile

##绑定后,该文件就可以删除了

rm -f $tmp_fifofile


##并发量为3,用这个数字来控制并发数

thread=3

for ((i=0;i<$thread;i++));

do

    ##写一个空行到管道里,因为管道文件的读取以行为单位

    echo >&6

done


##循环10次,相当于要备份100个库

for ((i=0;i<10;i++))

do

    ##读取管道中的一行,每次读取后,管道都会少一行

    read -u6

    {

        a_sub || {echo "a_sub is failed"}

        ##每次执行完a_sub函数后,再增加一个空行,这样下面的进程才可以继续执行

        echo >&6

    } & ##这里要放入后台去,否则并发实现不了

done


##这里的wait意思是,需要等待以上所有操作(包括后台的进程)都结束后,再往下执行。

wait


##关闭文件描述符6的写

exec 6>&-