背景
日常运维工作中编写shell脚本处理事务,很多时候需要一次性处理很多,需要用到循环,但是循环体内还是线性的,还是要一个个处理,这样并不会节省很多时间,只是节省了人工一次次输入的繁琐。但是对于提高处理能力,没有实质性的提高。这就需要考虑并发。但Shell中并没有真正意义的多线程,要实现多线程可以启动多个后端进程,最大程度利用cpu性能。即:多进程并发,本篇教程由浅入深详细介绍了shell中的多进程并发。
范例
#!/bin/bashtrap "exec 1000>&-;exec 1000testfiform -fr testfifofor((n&#61;1;n<&#61;10;n&#43;&#43;))do echo >&1000donestart&#61;&#96;date "&#43;%s"&#96;for((i&#61;1;i<&#61;100;i&#43;&#43;))do read -u1000 { echo "success $i"; sleep 5 echo >&1000 }&donewaitend&#61;&#96;date "&#43;%s"&#96;echo "Time: &#96;expr $end - $start&#96;"exec 1000>&-exec 1000
实验
所谓的多进程只不过是将多个任务放到后台执行而已&#xff0c;很多人都用到过&#xff0c;所以现在讲的主要是控制&#xff0c;而不是实现。
实验一
先看一个小shell&#xff1a;
看执行结果&#xff1a;
很明显是8s&#xff0c;这种不占处理器却有很耗时的进程&#xff0c;我们可以通过一种后台运行的方式
来达到节约时间的目的。
实验二
如下为改进&#xff1a;
用“{}”将主执行程序变为一个块&#xff0c;用&放入后台&#xff0c;四次执行全部放入后台后&#xff0c;我们需要用一个wait指令&#xff0c;等待所有后台进程执行结束&#xff0c;不然 系统是不会等待的&#xff0c;直接继续执行后续指令&#xff0c;知道整个程序结束。
看结果&#xff1a;
可以看到&#xff0c;时间已经大大缩短了&#xff01;
实验三
以上实验虽然达到了多线程并发的目的&#xff0c;但有一个缺陷&#xff0c;不能控制运行在后台的进程数。为了控制进程&#xff0c;我们引入了管道 和文件操作符。
无名管道&#xff1a; 就是我们经常使用的 例如&#xff1a; cat text | grep “abc” 那个“|”就是管道&#xff0c;只不过是无名的&#xff0c;可以直接作为两个进程的数据通道
有名管道&#xff1a; mkfilo 可以创建一个管道文件 &#xff0c;例如&#xff1a; mkfifo fifo_file
管道有一个特点&#xff0c;如果管道中没有数据&#xff0c;那么取管道数据的操作就会停滞&#xff0c;直到管道内进入数据&#xff0c;然后读出后才会终止这一操作&#xff0c;同理&#xff0c;写入管道的操作&#xff0c;如果没有读取操作&#xff0c;这一个动作也会停滞。
当我们试图用echo想管道文件中写入数据时&#xff0c;由于没有任何进程在对它做读取操作&#xff0c;所以它会一直停留在那里等待读取操作&#xff0c;此时我们在另一终端上用cat指令做读取操作
你会发现读取操作一旦执行&#xff0c;写入操作就可以顺利完成了&#xff0c;同理&#xff0c;先做读取操作也是一样的&#xff1a;
由于没有管道内没有数据&#xff0c;所以读取操作一直滞留在那里等待写入的数据
一旦有了写入的数据&#xff0c;读取操作立刻顺利完成
以上实验&#xff0c;看以看到&#xff0c;仅仅一个管道文件似乎很难实现 我们的目的(控制后台线程数), 所以 接下来介绍 文件操作符&#xff0c;这里只做简单的介绍&#xff0c;如果不熟悉的可以自行查阅资料。
系统运行起始&#xff0c;就相应设备自动绑定到了 三个文件操作符 分别为 0 1 2 对应 stdin &#xff0c;stdout&#xff0c; stderr 。
在 /proc/self/fd 中 可以看到 这三个三个对应文件
输出到这三个文件的内容都会显示出来。只是因为显示器作为最常用的输出设备而被绑定。
我们可以exec 指令自行定义、绑定文件操作符&#xff0c;文件操作符一般从3–(n-1)都可以随便使用
此处的n 为 ulimit -n 的定义值得
可以看到 我的 n值为1024 &#xff0c;所以文件操作符只能使用 0-1023&#xff0c;可自行定义的 就只能是 3-1023 了。
直接上代码&#xff0c;然后根据代码分析每行代码的含义&#xff1a;
代码解释
第3行&#xff1a; 接受信号 2 (ctrl &#43;C)做的操作。exec 1000>&-和exec 1000testfifo 来实现&#xff0c;但关闭时必须分开来写&#xff0c;> 读的绑定&#xff0c;<标识写的绑定 <> 则 标识 对文件描述符 1000的所有操作等同于对管道文件testfifo的操作。
第5-7行&#xff1a;分别为 创建管道文件&#xff0c;文件操作符绑定&#xff0c;删除管道文件
可能会有疑问&#xff0c;为什么不能直接使用管道文件呢&#xff1f;
事实上&#xff0c;这并非多此一举&#xff0c;刚才已经说明了管道文件的一个重要特性了&#xff0c;那就是读写必须同时存在
缺少某一种操作&#xff0c;另一种操作就是滞留&#xff0c;而绑定文件操作符 正好解决了这个问题。
第9-12 行&#xff1a; 对文件操作符进行写入操作。通过一个for循环写入10个空行&#xff0c;这个10就是我们要定义的后台线程数 量。
为什么写入空行而不是10个字符呢 &#xff1f;
这是因为&#xff0c;管道文件的读取 是以行为单位的。
当我们试图用 read 读取管道中的一个字符时&#xff0c;结果是不成功的&#xff0c;而刚才我们已经证实使用cat是可以 读取的。
第17-24行&#xff1a;这里假定我们有100个任务&#xff0c;我们要实现的时 &#xff0c;保证后台只有10个进程在同步运行 。read -u1000 的 作用是&#xff1a;读取一次管道中的一行&#xff0c;在这儿就是读取一个空行。减少操作附中的一个空行之后&#xff0c;执行一 次任务(当然是放到后台执行)&#xff0c;需要注意的是&#xff0c;这个任务在后台执行结束以后会向文件操作符中写 入一个空行&#xff0c;这就是重点所在&#xff0c;如果我们不在某种情况某种时刻向操作符中写入空行&#xff0c;那么结果就 是&#xff1a;在后台放入10个任务之后&#xff0c;由于操作符中没有可读取的空行&#xff0c;导致 read -u1000 这儿 始终停顿。
后边的 就不用解释了。
贴下执行结果&#xff1a;
每次的停顿中都能看到 只有10个进程在运行
一共耗时50s 一共100个任务&#xff0c;每次10个 &#xff0c;每个5s 正好50s。上边的结果图之所以这么有规律&#xff0c;这是因为我们所执行的100个任务耗时都是相同的。
比如&#xff0c;系统将第一批10个任务放入后台的过程所消耗的时间 几乎可以忽略不计&#xff0c;也就是说这10个任务几乎可以任务是同时运行&#xff0c;当然也就可以认为是同时结束了&#xff0c;而按照刚才的分析&#xff0c;一个任务结束时就会向文件描述符写入空行&#xff0c;既然是同时结束的&#xff0c;那么肯定是同时写入的空行&#xff0c;所以下一批任务又几乎同时运行&#xff0c;如此循环下去的。实际应用时&#xff0c;肯定不是这个样子的&#xff0c;比如&#xff0c;第一个放到后台执行的任务&#xff0c;是最耗时间的&#xff0c;那他肯定就会是最后一个执行完毕。所以&#xff0c;实际上来说&#xff0c;只要有一个任务完成&#xff0c;那么下一个任务就可以被放到后台并发执行了。