背景:

工作中有两个异地机房需要传数据,数据全名很规范,在某个目录下命名为统一的前缀加上编号。如/path/from/file.{1..100}。而机房间的专线对单个scp进程的传输速度是有限制的,比如最大在100Mb/s,如果直接启动100个scp,则又会遇到ssh的并发连接数限制。

所以需要控制并发数,即不超过ssh的并发限制,又要让单网卡上的带宽接近饱和,尽快完成传输(假设专线带宽远大于单机网卡带宽)

实现

之前知道通过mkfifo创建一个命名管道,可以实现对并发的控制。现在来实现一个。

在此之前,如果对mkfifo不了解,可以参考这个连接,作者写得很详细,我就不造轮子了。

这里直接给出代码,并做一些解释。因为单进程的带宽如上所述,所以考虑9个并发。代码如下:

 #!/bin/bash

 your_func()
{ # use your cmd or func instead of sleep here. don't end with background(&)
date +%s
echo "scp HOSTNAME:/home/USER/path/from/file.$1 REMOTE_HOST:/home/USER/path/to/"
sleep
} concurrent()
{ # from $ to $, (included $,$ itself), con-current $ cmd
start=$ && end=$ && cur_num=$ # ff_file which is opened by fd will be really removed after script stopped
mkfifo ./fifo.$$ && exec <> ./fifo.$$ && rm -f ./fifo.$$ # initial fifo: write $cur_num line to $ff_file
for ((i=$start; i<$cur_num+$start; i++)); do
echo "init time add $i" >&
done for((i=$start; i<=$end; i++)); do
read -u # read from mkfifo file
{ # REPLY is var for read
echo -e "-- current loop: [cmd id: $i ; fifo id: $REPLY ]" your_func $i
echo "real time add $(($i+$cur_num))" >& # write to $ff_file
} & # & to backgroud each process in {}
done
wait # wait all con-current cmd in { } been running over
} concurrent

上面以3为并发数,执行0到8号共9次,以便显示如下执行结果。

 bash concurrent.sh
-- current loop: [cmd id: ; fifo id: init time add ]
-- current loop: [cmd id: ; fifo id: init time add ]
-- current loop: [cmd id: ; fifo id: init time add ] scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/
scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/ scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/
-- current loop: [cmd id: ; fifo id: real time add ]
-- current loop: [cmd id: ; fifo id: real time add ]
-- current loop: [cmd id: ; fifo id: real time add ] scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/ scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/
scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/
-- current loop: [cmd id: ; fifo id: real time add ]
-- current loop: [cmd id: ; fifo id: real time add ]
-- current loop: [cmd id: ; fifo id: real time add ] scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/ scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/
scp HOSTNAME:/home/USER/path/from/file. REMOTE_HOST:/home/USER/path/to/

从date输出的时间上,可以看出,每2秒会执行3个并发。

说明

整体过程

设N的值为并发数。通过在fifo中初始化N行内容(可以为空值),再利用fifo的特性,从fifo中每读一行,启动一次your_func调用,当fifo读完N次时,fifo为空。再读时就会阻塞。这样开始执行时就是N个并发(1-N)。

当并发执行的进程your_func,任意一个完成操作时,下一步会招待如下语句:

echo "real time add $(($i+$cur_num))"  1>&4

这样就对fifo新写入了一行,前面被阻塞的第N+1号待执行的进程read成功,开始进入{}语句块执行。这样通过read fifo的阻塞功能,实现了并发数的控制。

需要注意的是,当并发数较大时,多个并发进程即使在使用sleep相同秒数模拟时,也会存在进程调度的顺序问题,因而并不是按启动顺序结束的,可能会后启动的进程先结束。

从而导致如下语句所示的输出中,两个数字并不一定是相等的。并发数越大,这种差异性越大。

-- current loop: [cmd id: 8 ; fifo id: real time add 9 ]

自定义函数

修改自定义函数your_func,这个函数实际只需要一行就完成了。

your_func()
{ # use your cmd or func instead of sleep here. don't end with background(&)
date +%s
scp HOSTNAME:/home/USER/path/from/file.$ REMOTE_HOST:/home/USER/path/to/
}

需要注意的是,scp命令最后不需要添加压后台的&符号。因为在上一级就已经压后台并发了。

再来说明concurrent函数的第14行。

exec digit<>  filename

这是一个平常很少使用到的命令。特别是‘<>’这个符号。既然不明白我们来查一下系统帮助。

man bash
# search 'exec ' Opening File Descriptors for Reading and Writing
The redirection operator [n]<>word causes the file whose name is the expansion of word to be opened for both reading and writing on file
descriptor n, or on file descriptor if n is not specified. If the file does not exist, it is created.

通过man bash来搜索exec加空格,会找到对exec的说明。注意如果直接man exec,会搜索到linux programer's manual,是对execl, execlp, execle, execv, execvp, execvpe - execute a file这一堆系统函数的调用说明。

还要注意哦,4<> 这几个字符不要加空格,必然连着写。word前可以加空格。

rm file

mkfifo先创建管道文件,再通过exec将该文件绑定到文件描述符4。也许你在疑惑后面的rm操作。其实当该文件绑定到文件描述符后,内核已经通过open系统调用打开了该文件,这个时候执行rm操作,删除的是文件的Inode,但concurrent函数已经连接到文件的block块区。

如果你遇到过这样的情况,你就明白了:如果线上的nginx日志是没有切分的,access.log会越来越大,这时你直接rm access.log文件后,文件不见了,但df查看系统并没有释放磁盘空间。这就是因为rm只是删除了inode,但这之前nginx早已经通过open打开了这个文件,nginx进程的进程控制块中的文件描述符表中对应的fd,已经有相应的文件指针指向了该文件在内存中的文件表,以及其在内存中的v节点表,并最终指向文件的实际存储块。因此nginx依然可以继续写日志,磁盘还在被写入。只有重启或者reload,让进程重新读一次配置,重新打开一遍相应的文件时,才会发现该文件不存在的,并新建该文件。而这时因为Inode节点已经释放,再用df查看时就能看到可用空间增大了。

不懂可以参考APUE的图3.1及想着说明。

因此14行的rm并不影响后继脚本执行,直到脚本结束,系统收回所有文件描述符。

初始化

18-20行在做初始化管道的工作。其中读取管道有两类写法:

 # style
for ((i=$start; i<$cur_num+$start; i++)); do
echo "init time add $i" >&
done # style
for ((i=$start; i<$cur_num+$start; i++)); do
echo "init time add $i"
done >&

差别就是‘>&4’ 这几个字符放在echo语句后面,还是放在done后面,两者都可以,前者针对echo语句,后者针对整个for循环。

同理,在下一个for循环中,read命令也有两种方式:

# style
for((i=$start; i<=$end; i++)); do
read -u
{
your_func $i
echo "real time add $(($i+$cur_num))" >& # write to $ff_file
} &
done # style
for((i=$start; i<=$end; i++)); do
read
{
your_func $i
echo "real time add $(($i+$cur_num))" >& # write to $ff_file
} &
done <&

关于REPLY

再解释一下REPLY变量。这是上述循环中,用来存放read命令从fifo中读到的内容。其实在整个脚本中,是不需要关注这个点的。不过这里随带也解释一下。

通过能fifo的不断读写,才实现了echo如下语句:

-- current loop: [cmd id: 7 ; fifo id: real time add 7 ]

如何了解到REPLY呢?我们又得man一下了。为了找到read的参数。先man read发现不对。再如下查找,因为read是bash自建命令。

 man  bash
# search 'Shell Variables' REPLY Set to the line of input read by the read builtin command when no arguments are supplied.

【说解】在shell中通过mkfifo创建命名管道来控制多个进程并发执行的更多相关文章

  1. 【linux】mkfifo 命令创建命名管道实现进程之间通信

    mkfifo 命令 mkfifo命令创建一个FIFO特殊文件,是一个命名管道(可以用来做进程之间通信的桥梁) 管道也是一种文件,一般是linux中的一个页大小,4k,管道数据一旦被读取就没了.(管道大 ...

  2. Linux shell中的I/O重定向相关(转)

    1. 基本概念(这是理解后面的知识的前提,请务必理解)  a. I/O重定向通常与 FD有关,shell的FD通常为10个,即 0-9: b. 常用FD有3个,为0(stdin,标准输入).1(std ...

  3. Linux Shell中管道的原理及C实现框架

    在shell中我们经常用到管道,有没考虑过Shell是怎么实现管道的呢? cat minicom.log | grep "error" 标准输入.标准输出与管道 我们知道,每一个进 ...

  4. shell 匿名管道和命名管道

    管道的特点:如果管道中没有数据,那么取管道数据的操作就会滞留,直到管道内进入数据,然后读出后才会终止这一操作:同理,写入管道的操作如果没有读取管道的操作,这一动作也会滞留. 1,匿名管道 匿名管道使用 ...

  5. Linux进程间通信(四):命名管道 mkfifo()、open()、read()、close()

    在前一篇文章—— Linux进程间通信 -- 使用匿名管道 中,我们看到了如何使用匿名管道来在进程之间传递数据,同时也看到了这个方式的一个缺陷,就是这些进程都由一个共同的祖先进程启动,这给我们在不相关 ...

  6. shell 命名管道,进程间通信

    命名管道基础 命名管道也被称为FIFO文件, 在文件系统中是可见的,并且跟其它文件一样可以读写! 命名管道特点: 当写进程向管道中写数据的时候,如果没有进程读取这些数据,写进程会堵塞 当读取管道中的数 ...

  7. shell 命名管道,进程间通信, ncat作http server

    命名管道基础 命名管道也被称为FIFO文件, 在文件系统中是可见的,并且跟其它文件一样可以读写! 命名管道特点: 当写进程向管道中写数据的时候,如果没有进程读取这些数据,写进程会堵塞 当读取管道中的数 ...

  8. 命名管道FIFO和mkfifo函数

    进程间通信必须通过内核提供的通道,而且必须有一种办法在进程中标识内核提供的某个通道,前面讲过的匿名管道是用打开的文件描述符来标识的.如果要互相通信的几个进程没有从公共祖先那里继承文件描述符,它们怎么通 ...

  9. 本地方法中printf如何传给java--java系统级命名管道

    本地方法中printf如何传给java--java系统级命名管道 摘自:https://blog.csdn.net/dog250/article/details/6007301 2010年11月13日 ...

随机推荐

  1. jQuery.Ajax IE8 无效(CORS)

    今天在开发的时候,遇到一个问题,$.get()在 IE8 浏览器不起作用,但 Chrome,Firefox 却是可以的,网上资料很多,最后发现是 IE8 默认不支持 CORS 请求,需要手动开启下: ...

  2. Java进击C#——前言

    本章简言 记得三年前笔者来到现在的公司的时候,公司人口不出十个人.那个时候笔者刚从日本回来,想在福州.厦门.青岛找一个合适自己发展的机会.最后我的一个福州的朋友打电话希望我能过去帮他,跟他一起创业.这 ...

  3. 解决VS2008在win7找不到输入序列号的地方

    1.VS2008在Windows7 打开维护界面看不到可以输序列号的地方. 因为微软把他隐藏了. 2.我们可以借用工具把他显示出来 下载地址:http://www.zlsoft.com/techbbs ...

  4. Linux杀死进程,查看进程

    http://blog.csdn.net/wojiaopanpan/article/details/7286430/

  5. iOS之判断手机号码、邮箱格式是否正确

    //判断手机号码格式是否正确 + (BOOL)valiMobile:(NSString *)mobile{     mobile = [mobile stringByReplacingOccurren ...

  6. 在禅道中实现WORD等OFFICE文档转换为PDF进行在线浏览

    条件: 安装好禅道的服务器 能直接浏览PDF的浏览器(或通过 安装插件实现 ) 文档转换服务程序(建议部署在另一台服务器上)     实现 原理: 修改禅道的文件预览功能(OFFICE文档其使用的是下 ...

  7. 解决mysql插入数据时出现Incorrect string value: '\xF0\x9F...' for column 'name' at row 1的异常

    这个问题,原因是UTF-8编码有可能是两个.三个.四个字节.Emoji表情或者某些特殊字符是4个字节,而MySQL的utf8编码最多3个字节,所以数据插不进去. 我的解决方案是这样的 1.在mysql ...

  8. 【教程】SQLite数据库修复

    SQLite 大家都知道,就不多说了. 有时候数据量大了,或者存储过程中出现异常,数据库就可能会出问题. 这是以前公司产品出现过的问题,导致软件都打不开了,我花了不少时间才解决的,趁现在有空贡献出来. ...

  9. ubuntu下配置vimtab空格数

    vim ~/.vimrc  没有就创建 set tabstop=4 //4就是4个空格

  10. struts2国际化

    struts2国际化 1:什么是国际化? 国际化(internationalization)是设计和制造容易适应不同区域要求的产品的一种方式.它要求从产品中抽离所有的与语言,国家/地区和文化相关的元素 ...