简介
在我们平时写代码过程中,可能经常会遇到串行执行速度慢 ,串行无法执行多个任务,这时便需要使用子进程同时执行。使用父进程创建子进程时,子进程会复制父进程的内存、文件描述符和其他相关信息。当然,子进程可以独立运行,并可以执行不同的操作。父进程和子进程是平行运行的,它们可以同时执行不同的任务。这篇文章的目的是为了让初学者了解如何通过shell实现多进程,多进程之间如何管理。
目录
1. 如何实现多进程
1.1. 后台命令的实现
1.2. 子进程的实现
1.3. 总结
2. 管理子进程
2.1.wait(等待子进程 )
2.2. trap(中断子进程)
2.3. 子进程随主进程退出
2.4. 总结
1. 如何实现多进程
在学习管理子进程之前,我们需要先了解2个基本的知识点
- 后台进程如何实现?
- 子进程如何实现?
1.1. 后台命令的实现
对于Linux来说,我们在某条命令后面加上 & 符号,就表示一个后台进程。比如 sleep 10 &
这里可以看到,我们在一条命令后面加了 & 符号,Linux会执行将其推入到后台执行,并显示PID,ps命令查询到该进程确实在运行中。
注意:这里有个坑,如果我们在某条Linux命令或某个shell脚本后面加入 & 符号,系统确实会推入后台执行,但如果我们退出了这个窗口,那么进程也将会自动退出。
上述的例子可以明显的看到,在窗口1执行后台命令时,退出当前窗口,使用窗口2查询,实际上是查询不到的,因为系统已经自动退出了。
避免这种情况的方法也很简单,在命令前方加入 nohup 命令即可
- nohup 命令用于在后台运行程序,并且不受终端关闭或退出的影响。
- nohup 是 “no hangup” 的缩写,它允许你在关闭终端后继续运行程序。
使用 nohup 后,即使当前终端关闭也不会影响到后台进程,并且后台进程输出所有正常或异常的结果会直接打印到当前路径下的 nohup.out 文件中。
1.2. 子进程的实现
通过上述《后台命令的实现》,小伙伴们理解了如何实现后台进程,而实现子进程的方式实际上也是使用同样的方法,例如:
#!/bin/bash
echo "我是子进程 1" &
echo "我是子进程 2" &
echo "我是子进程 3" &
我们在shell中对某条命令加上了 & 符号,那么系统将会把这几个命令当作子进程执行
上述的方法看不出实际的应用场景,我们来试试循环呢
#!/bin/bash
# 子进程1
for i in {1..3};do
echo "我是子进程 ${i}"
sleep 1
done &
# 子进程2
for i in {10..13};do
echo "我是子进程 ${i}"
sleep 1
done &
# 子进程3
for i in {20..23};do
echo "我是子进程 ${i}"
sleep 1
done &
使用for循环同时执行不同的命令,如下图(可以看到三个循环是同时执行的)
使用ps查询发现子进程的PPID都指向了最高等级进程1。这是由于主进程并不会随着子进程的运行而等待,这对我们管理起来是非常麻烦的。
解决的办法是在执行子进程下面加入 wait 命令,表示等待上面所有子进程执行完成后再继续执行下一步。
#!/bin/bash
# 子进程1
for i in {1..3};do
echo "我是子进程 ${i}"
sleep 1
done &
# 子进程2
for i in {10..13};do
echo "我是子进程 ${i}"
sleep 1
done &
# 子进程3
for i in {20..23};do
echo "我是子进程 ${i}"
sleep 1
done &
wait
这次可以很清晰的看到子进程指向的父ID是主进程
for 循环只是一个例子,在我们真正场景中用的最多的还是函数。下述举一个简单的例子:
#!/bin/bash
func1(){
for i in {1..3};do
echo "我是子进程 ${i}"
sleep 1
done
}
func2(){
for i in {10..13};do
echo "我是子进程 ${i}"
sleep 1
done
}
# 将函数推入后台执行
func1 &
func2 &
wait # 等待子进程运行完成后再退出
1.3. 总结
我们想要实现一个脚本,并且该脚本需要进行多个任务处理,参考目录《1.2. 子进程的实现》;而我们运行这个脚本时,参考目录《1.1. 后台命令的实现》。总结起来就是:
- 使用函数封装代码,在需要指定某个函数为子进程时,后面加上 & 符号;
- 当脚本运行时间长,那么需要使用 nohup + & 实现后台运行,保证程序正常运行。
2. 管理子进程
2.1.wait(等待子进程 )
wait 命令用于等待所有运行的子进程执行完毕,使用格式如下
wait [jobspec]
- jobspec:表示进程ID,如果不指定则等待所有子进程。
案例一:编写两个子进程,使用wait指定等待其中一个进程
#!/bin/bash
func1(){
sleep 3
echo "我是子进程1, 运行3秒"
}
func2(){
sleep 10
echo "我是子进程2, 运行10秒"
}
# 将函数推入后台执行
func1 &
func1_pid=$! #获取上面子进程的PID
func2 &
wait ${func1_pid}
echo -e "主进程运行完成, 退出!"
从脚本中,我们编写了2个子进程函数:函数1等待3s后打印,函数2等待10s后打印。在将函数1推入后台后获取该子进程的PID,而后使用 wait 指定等待该进程,对函数2不做等待。所以在执行时,子进程1执行3s后完成,主进程也随之执行完成;再等待7s后子进程2运行结束。
案例二:不指定wait,等待全部子进程
#!/bin/bash
func1(){
sleep 3
echo "我是子进程1, 运行3秒"
}
func2(){
sleep 10
echo "我是子进程2, 运行10秒"
}
# 将函数推入后台执行
func1 &
func2 &
wait # 等待全部子进程
echo -e "主进程运行完成, 退出!"
从结果来看,与案例一明显不同的是,主进程是等待了所以的子进程才自动退出。
案例三:wait 的位置处于多个子进程中间
#!/bin/bash
func1(){
sleep 3
echo "我是子进程1, 运行3秒"
}
func2(){
sleep 10
echo "我是子进程2, 运行10秒"
}
func1 &
wait
echo "=====等待子进程1运行完成!====="
func2 &
wait
echo "=====等待子进程2运行完成!====="
我们将 wait 放在指定子进程的后面,在 wait 后面再放一个子进程,它的运行流程是从上往下依次执行。
2.2. trap(中断子进程)
命令如下(选择其一即可)
trap 'trap - EXIT; kill -s HUP -- -$$' EXIT
trap 'kill -s HUP -- -$$' EXIT
trap - EXIT
:这部分的作用是取消当前进程对EXIT
信号的附加处理,以防止进程在接收到EXIT
信号时再次触发。kill -s HUP -- -$$
:这部分的作用是向进程组发送SIGHUP
信号(通常是用来通知终端关闭的信号),-$$
表示向当前进程的进程组发送信号。
trap 命令没有固定的位置规则,放子进程前面或后面都可以
#!/bin/bash
func1(){
for i in {1..3};do
echo "我是子进程1, 执行:${i}"
sleep 1
done
}
func2(){
for i in {10..20};do
echo "我是子进程2, 执行:${i}"
sleep 1
done
}
#trap 'trap - EXIT; kill -s HUP -- -$$' EXIT
func1 &
func2 &
trap 'kill -s HUP -- -$$' EXIT
sleep 2
echo -e "主进程运行完成, 退出!"
脚本中,子进程1的运行时间是3s,子进程2的运行时间是10s,主进程的运行时间是2s。设置退出信号后,主进程运行2s后退出,相关子进程也随之退出。
注意:这里的退出要么是主进程正常运行结束,要么是 Ctrl + C 退出才能使子进程也随之退出。如果中途主进程被 kill 掉,那么 trap 信号也随之消失,所以主进程被 kill 后,子进程无法随主进程而退出,只能等子进程运行结束后自动退出。
2.3. 子进程随主进程退出
【案例一】
在脚本的最开始读取主进程PID,在子进程中使用 while 判断该进程是否存在,如果不存在则结束循环。这种方式相比于 trap 命令,即使主进程被 kill 也不会影响子进程正常退出。
#!/bin/bash
# 定义当前脚本的PID
SCR_PID=$$
func1(){
while [ -d /proc/${SCR_PID} ];do # 判断主进程PID是否存在
echo "我是子进程1"
sleep 1
done
}
func2(){
while [ -d /proc/${SCR_PID} ];do # 判断主进程PID是否存在
echo "我是子进程2"
sleep 1
done
}
func1 &
func2 &
sleep 2
echo -e "主进程运行完成, 退出!"
【案例二】
如果我们希望主进程在退出前结束子进程,可以让子进程判断修改为判断某个文件是否存在
#!/bin/bash
# 定义用于判断的文件路径
check_file="./.check.txt"
func1(){
while [ ! -f ${check_file} ];do # 如果该文件不存在则一直运行
echo "我是子进程1"
sleep 1
done
}
func2(){
while [ ! -f ${check_file} ];do # 如果该文件不存在则一直运行
echo "我是子进程2"
sleep 1
done
}
func1 &
func2 &
sleep 2
touch ${check_file} # 创建一个文件
echo -e "主进程运行完成, 退出!"
【案例三】
如果我们希望主进程退出后,子进程需要做其他事,那么在子进程的循环下继续编写代码
#!/bin/bash
# 定义用于判断的文件路径
check_file="./.check.txt"
func1(){
while [ ! -f ${check_file} ];do
echo "我是子进程1"
sleep 1
done
echo "子进程1的循环结束,继续执行xxx" # 继续执行下一步
}
func2(){
while [ ! -f ${check_file} ];do
echo "我是子进程2"
sleep 1
done
}
func1 &
func2 &
sleep 2
touch ${check_file}
echo -e "主进程运行完成, 退出!"
2.4. 总结
管理子进程 wait 命令是必要的,它的作用是等待指定的某个子进程结束或等待全部子进程结束后才能继续执行下一步。如果主进程退出后,我们希望子进程也一同退出,可以使用 trap 命令或者目录《2.3. 子进程随主进程退出》的方法,当然,如果主进程被 kill 的话,trap就无效了,所以这里更推荐2.3的方法。