简单理解Bash中子进程 (child process)和子shell (subshell)的区别以及SHLVL和BASH_SUBSHELL

0. 结论

  1. 在bash脚本中,在运行 命令的alias, (), 和 & 时会复制当前的shell环境并新建一个子shell环境。子shell环境有自己独立的 工作目录(pwd),继承原先shell环境中的alias和function。
  2. 创建子shell时新建了子进程但子进程由bash维护,只能通过$BASHPID获取PID,与父进程共用同一个POSIX语义下的PID与PPID。本质上实现了多进程。
  3. SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,而 BASH_SUBSHELL 是记录一个 Bash 进程实例中多个子 Shell(subshell)嵌套深度的累加器。

1. 简介

在写一些bash批处理脚本的时候,通常希望能够快速的声明新function,并在不影响主进程的情况下执行。常用的解决方案是

#!/bin/bash

func() {
	cd /tmp
	echo func中的pwd: $PWD
	exit
}

echo 运行func之前的pwd: $PWD
(func)
echo 运行func之后的pwd: $PWD

输出是

运行func之前的pwd: /home/aris/sandbox
func中的pwd: /tmp
运行func之后的pwd: /home/aris/sandbox

注意假如第10行的func不加括号的话func中的命令就会直接在当前shell中运行,所以脚本会在第6行的exit中退出,就没办法继续运行后面的命令了。

但是假如运行

#!/bin/bash

func() {
	echo func中的PID和PPID是$$, $PPID
}

echo 主进程里的PID和PPID是$$, $PPID
(func)

结果是

主进程里的PID和PPID是53881, 29470
func中的PID和PPID是53881, 29470

能发现(func)好像没有在系统内核(kernel)中新建一个子进程,PID和PPID没有变化。但这又解释不了为什么cd没有改变系统环境变量的PWD和exit为什么没有把这个进程结束掉。(这里还要注意的一点是func中的$$会在运行时候展开,而不是定义的时候。可以通过 `var=10; func(){echo $var}; var=20; func`快速验证打印出来的结果是20而不是10)。所以就有必要区分子进程(child process)和子shell(subshell)了。

2. 子进程 (child process)和子shell (subshell)

2.1 子进程 (child process)

子进程很好理解,就是Unix和C语言中传统意义上的fork+exec。所有外部的命令(包括运行bash这样的命令和./script.sh这样的脚本)都是以新的子进程的方式创建的。子进程只继承被export的环境变量,不继承alias和定义的function。子进程一定会被分配新的PID,所以在bash中可以用echo $$来测试是不是创建了新的子进程。详细可以参考fork(2) — Linux manual page

2.2 子shell (subshell)

在IEEE POSIX标准中,subshell指的是Shell Execution Environment.

A subshell environment shall be created as a duplicate of the shell environment, except that signal traps that are not being ignored shall be set to the default action. Changes made to the subshell environment shall not affect the shell environment. Command substitution, commands that are grouped with parentheses, and asynchronous lists shall be executed in a subshell environment. Additionally, each command of a multi-command pipeline is in a subshell environment; as an extension, however, any or all commands in a pipeline may be executed in the current environment. All other commands shall be executed in the current shell environment.

 换句话说,在bash脚本中,在运行 命令的alias, (), 和 & 的时候会复制当前的shell环境并新建一个子shell环境。子shell环境有自己独立的工作目录(pwd),继承原先shell环境中的alias和function。

可是假如运行

#!/bin/bash

func() {
	sleep 2
	echo func中的PID和PPID是$$, $PPID
	sleep 10
}

echo 主进程里的PID和PPID是$$, $PPID
(func &)
(func &)
(func &)
(func &)
(func &)
(func &)
exit

输出是

主进程里的PID和PPID是5049, 3760
func中的PID和PPID是5049, 3760
func中的PID和PPID是5049, 3760
func中的PID和PPID是5049, 3760
func中的PID和PPID是5049, 3760
func中的PID和PPID是5049, 3760
func中的PID和PPID是5049, 3760

这和System Monitor中显示的不匹配

很显然新的子进程(child process)被创建了,但是他们共用同一个$$返回的POSIX PID。

于是我找到了$BASHPID,引用Advanced Bash-Scripting Guide

$BASHPID

Process ID of the current instance of Bash. This is not the same as the $$ variable, but it often gives the same result.

所以我们改一下代码

#!/bin/bash

func() {
	sleep 2
	echo func中的PID, BASHPID, 和PPID是$$, $BASHPID, $PPID
	sleep 10
}

echo 主进程里的PID, BASHPID, 和PPID是$$, $BASHPID, $PPID
(func &)
(func &)
(func &)
(func &)
(func &)
(func &)
exit

现在输出变成了

主进程里的PID, BASHPID, 和PPID是6152, 6152, 3760
func中的PID, BASHPID, 和PPID是6152, 6154, 3760
func中的PID, BASHPID, 和PPID是6152, 6157, 3760
func中的PID, BASHPID, 和PPID是6152, 6160, 3760
func中的PID, BASHPID, 和PPID是6152, 6163, 3760
func中的PID, BASHPID, 和PPID是6152, 6166, 3760
func中的PID, BASHPID, 和PPID是6152, 6169, 3760

可以看到每一个子shell确实有了不同的BASHPID。所以,创建子shell时新建子进程但子进程由bash维护,只能通过$BASHPID获取PID,与父进程共用同一个POSIX语义下的PID与PPID。本质上实现了多进程。

3. SHLVL和BASH_SUBSHELL

知道了子进程和子shell的区别之后,$SHLVL和$BASH_SUBSHELL就容易理解了。

SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,而 BASH_SUBSHELL 是记录一个 Bash 进程实例中多个子 Shell(subshell)嵌套深度的累加器。

看个例子 

#!/bin/bash

func() {
	echo $BASH_SUBSHELL
}

( ( (func) ) )
( ( (func &) ) )

sleep 1
echo $SHLVL
bash -c 'echo $SHLVL'

 在我当时的terminal上输出为

3
4
3
4

能看出来每一个() 和 & 都会让$BASH_SUBSHELL + 1,每一次调用bash创建子进程都会让$SHLVL + 1。

4. 应用

在批处理脚本的编写中,subshell的使用频率非常高。其中一个应用场景是实现linux的初始化启动脚本。我们希望脚本可以执行多个任务,支持多进程,且前面的任务不会影响其他任务的工作路径而且当一个任务exit时不会exit所有的任务。

我们可以将每一个任务封装成bash的function,然后通过命令行参数运行这些function。

#!/bin/bash

task1() {
	# do something...
}

task2() {
	# do something...
}

task3() {
	# do something...
}

# define some other tasks...

# use & if we want to implement multiprocess
# (task1 &)
(task1)
(task2)
(task3)
# run some other tasks...

# run commands from command line arguments
for cmd in "$@"; do
  ($cmd)
done

如果文件名是 test.sh 的话可以在terminal中执行

bash test.sh task1 task3

来控制只运行其中的某几个task。

5. 引用

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ariseus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值