在shell脚本中,我们想要实现多进程高并发,最简单的方法是把命令丢到后台去,如果量不大的话,没问题。 但是如果有几百个进程同一时间丢到后台去就很恐怖了,对于服务器资源的消耗非常大,甚至导致宕机。
那有没有好的解决方案呢? 当然有!
我们先来学习下面的常识。
1 文件描述符
文件描述符(缩写fd)在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。每一个unix进程,都会拥有三个标准的文件描述符,来对应三种不同的流:
除了上面三个标准的描述符外,我们还可以在进程中去自定义其他的数字作为文件描述符,后面例子中会出现自定义数字。每一个文件描述符会对应一个打开文件,同时,不同的文件描述符也可以对应同一个打开文件;同一个文件可以被不同的进程打开,也可以被同一个进程多次打开。
我们可以写一个测试脚本/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”,其实叫做匿名管道,它左边的输出作为右边命令的输入。这个匿名管道只能为两边的命令提供服务,它是无法让其他进程连接的。
实际上,这两个进程(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
exec 6>&-