linux shell命名管道FIFO


在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

##并发量为8,用这个数字来控制并发数
thread=8

for ((i=0;i<$thread;i++));
do
    ##写一个空行到管道里,因为管道文件的读取以行为单位
    echo >&6
done

##循环100次,相当于要备份100个库
for ((i=0;i<100;i++))
do
    ##读取管道中的一行,每次读取后,管道都会少一行
    read -u6
    {
        a_sub || {echo "a_sub is failed"}
        ##每次执行完a_sub函数后,再增加一个空行,这样下面的进程才可以继续执行
        echo >&6
    } & ##这里要放入后台去,否则并发实现不了
done

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

##关闭文件描述符6的写
exec 6>&-