原来Bash的并行计算是这样的
今天有个批量下载图片的任务,不打算用python或者其他高级语言实现,决定用bash尝试下。
接下来我们一起了解下bash的并发任务是如何实现的?先上一版简单粗暴的代码:
#!/bin/bash
todo () { sleep 3;echo "$1 OK"; }
# --- 串行
for x in `seq 5`;do
echo $x;
todo $x;
done
# --- 并行
for x in `seq 5`;do
{
echo $x;
todo $x;
}&
done
wait
# 是的,你没看错,并行相较于串行之多了几个符号 `{ }&` 和 `wait`。
# 其中,wait函数,该函数将等待后台所有子进程结束。正是因为有了此函数,
# 才能保证commands2在所有commands1并行子任务执行完后再执行。
具体执行效果如下:
注意⚠️:这个并行计算会傻瓜式的扩容,假设这样的任务我们安排100000个同时计算,再牛的服务器终究也会打趴下。运行之前确认你有足够的内存、cpu或者带宽资源,不建议用到复杂的场景,
那么我们有没有办法去实现像其它语言那种优雅的并发呢?答案肯定是有的,这里我们就要发挥下Linux系统的优势了。
++首先我们了解下管道(pipe)
++
无名管道:熟悉Linux命令的同学一定用过,比如
ps aux|grep nginx|awk '{print $2}'
,其中|
就是管道连接符号,它将前一个命令的结果输出到后一个进程中,作为两个进程的数据通道,不过他是无名的。但是这里要注意下:|
对错误信息信息没有直接处理能力。有名管道:使用
mkfifo
命令创建的管道即为有名管道,例如,mkfifo pipefile
,pipefile
即为有名管道。
$ mkfifo -m 644 pipe1 # 创建管道, `-m MODE`设置新创建的FIFO文件的许可权位的值,MODE变量与为`chmod`命令定义的方式操作数相同。
$ ls -al > pipe1 # 终端1: 向管道 pipe1 输入内容,这个时候终端会阻塞
$ cat < pipe1 # 终端2: 读取管道 pipe1 内容,这个终端1才会退出,终端2获取管道中数据并输出。
$ ls -al pipe1
prw-r--r-- 1 root root 0 Apr 29 22:47 pipe1
++Linux文件描述符++
Linux中读写文件都会通过文件描述符来完成,而不是通过文件名来完成的,所以只要打开文件,Linux内核就会为程序分配一个文件描述符。例如cat access.log会打开access.log文件并分配一个文件描述符,cat命令退出后,打开的文件就会被关闭,所分配的文件描述符也会释放。
linux启动后,会默认打开3个文件描述符:标准输入fd=0
、标准输出fd=1
、标准错误fd=2
,在/proc/self/fd可以找到。用户可以自定义文件描述符范围是:3 ~ NUM
,这个最大数字,跟用户的:ulimit –n 定义数字有关系,不能超过最大值,即 NUM = ulimit -n
-1。
$ ls -al /proc/self/fd
total 0
dr-x------ 2 root root 0 Apr 29 23:13 .
dr-xr-xr-x 9 root root 0 Apr 29 23:13 ..
lrwx------ 1 root root 64 Apr 29 23:13 0 -> /dev/pts/0
lrwx------ 1 root root 64 Apr 29 23:13 1 -> /dev/pts/0
lrwx------ 1 root root 64 Apr 29 23:13 2 -> /dev/pts/0
lr-x------ 1 root root 64 Apr 29 23:13 3 -> /proc/1216486/fd
- 基础重定向操作
对于基本的重定向操作应该都很熟悉,比如:
$ echo hello world >/tmp/access.log
$ cat </tmp/access.log
# 在命令行中指定的重定向目标都只在该命令中有效,命令退出后,重定向行为就消失了。
$ exec >/tmp/access.log
# exec是shell内置命令,会设置当前bash进程的标准输出fd=1的数据流目标为/tmp/access.log。
$ echo helloworld
$ ls
$ exec </tmp/a.log
# 恢复bash的标准输出
$ exec >$(tty)
$ ls
pipe1 tmp.txt # 结果即可输出终端
- 打开文件
其实,每个重定向操作也是打开文件的操作,同时会分配文件描述符。比如echo hello > /tmp/access.log
命令,Shell会打开access.log文件并将fd=1
关联到该文件,当echo进程运行时会继承Shell的fd=1
以及它关联的access.log属性,于是echo的标准输出就会输出到access.log中。但是在命令中打开的文件(包括重定向)都是临时的,命令退出完后文件就会关闭,所分配的文件描述符也会释放。
在Shell中,可以手动打开文件:
exec N<> FILENAME # 表示打开文件描述符N的写/读
整数N
建议在[3,9]
范围内,超出9
的文件描述符有可能已经被bash内部使用了。表示在当前Shell进程内打开文件FILENAME并分配文件描述符N,只要不手动关闭文件,只要当前Shell进程不退出,那么打开的FILENAME就一直处于打开状态。
例如:
$ exec 3<> /tmp/access.log
$ lsof -n | grep /tmp/access.log | grep -v grep
bash 1216519 root 3u REG 252,1 0 1190008 /tmp/access.log
# 这里我们顺带验证下用户自定义文件描述符的范围
$ ulimit -n
65535
$ exec 65535<> /tmp/max.log
-bash: 65535: Bad file descriptor
$ exec 65534<> /tmp/max.log
$ ls /proc/self/fd
0 1 2 3 4 65534
- 关闭文件
在编写脚本的时候,不再使用的文件就要关闭,以便释放文件描述符,防止程序一直占用文件,导致无法释放文件占用的磁盘空间。如果大量文件描述符不关闭,还可能达到打开文件的上限,使得程序报错。
关闭文件描述符的方式:
exec N>&- # 或者 exec N<&- 表示关闭文件描述符N的写/读
这表示关闭文件描述符N。如果是用exec N<>FILENAME
打开的文件,则上面两种方式都能关闭N。
exec 3<&- # 或者 exec 3>&-
$ lsof -n | grep /tmp/access.log | grep -v grep
关于文件描述符的其他有用的操作,比如:文件描述符的复制
、移动
、自动分配文件描述符号
、read从文件描述符读取数据
等等这些操作这里我们先不做赘述了,具体的可以参见本文文末的参考文档。
好了,我们回归正题,接着我们开始的话题,如何编写我们优雅的并行shell任务。利用管道文件和文件描述符来控制进程并发,直接上脚本:
#!/bin/bash
#
# trap是捕获中断命令。接受信号2 `ctrl+C`做的操作,
# 表示在脚本运行过程中,如果接收到`Ctrl+C`中断命令,
# 则关闭文件描述符1000的读写,并正常退出。
trap "exec 1000>&-;exec 1000<&-;exit 0" 2
FIFO_FILE=$$.fifo # $表示当前执行文件的PID
mkfifo $FIFO_FILE # 创建管道文件
exec 1000<>$FIFO_FILE # 将管道文件和文件操作符绑定
rm -rf $FIFO_FILE # 删除管道文件
# task
todo () { sleep 3;echo "$1 OK"; }
# 并行数目
PROCESS_NUM=20
# 对文件操作符进行写入操作,通过for循环写入空行,
# 空行数目为定义的后台线程数量
for _ in `seq $PROCESS_NUM`;do
echo>&1000
done
for x in `seq 999`;do
# 从文件描述符读入空行。
# read -u 后面跟fd,从文件描述符中读入,该文件描述符可以是exec新开启的。
read -u1000
{
todo $x # 要执行的命令
# 上一条命令执行完毕后,在文件描述符中写入空行
echo >&1000
} & # &表示进程放到linux后台执行
done
wait # wait指令等待所有后台进程执行结束
exec 1000>&- # 表示关闭文件描述符1000的写
好了,今天就到这里,其实Linux还有很多优秀的设计理念,这里只是个开胃菜。
参考文档:
- https://blog.csdn.net/u013937038/article/details/107411025
- https://www.junmajinlong.com/shell/script_course/shell_redirection/
- https://zzzqiii.github.io/p/shell%E8%84%9A%E6%9C%AC%E5%AE%9E%E7%8E%B0%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%B9%B6%E5%8F%91%E4%BB%A5%E5%8F%8A%E5%B9%B6%E5%8F%91%E6%95%B0%E6%8E%A7%E5%88%B6/
- https://blog.csdn.net/qq_40644809/article/details/110947076
- https://blog.51cto.com/hld1992/2370135
- http://ouyangyewei.github.io/2016/01/10/linux-multi-process-concurrent-control/