Bash子shell之谜

3
《学习Bash书籍》提到,子shell只会继承环境变量和文件描述符等内容,但不会继承未被导出的变量等内容。
$ var=15
$ (echo $var)
15
$ ./file # this file include the same command echo $var

$

据我所知,对于()情况和./file情况,shell会创建两个子shell,但为什么在()情况下子shell可以识别var变量,尽管它没有被导出,在./file情况下却无法识别呢?
# Strace for () 
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f24558b1a10) = 25617
# Strace for ./file
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f24558b1a10) = 25631

我尝试使用strace来弄清楚这是如何发生的,令人惊讶的是我发现bash将使用相同的参数进行克隆系统调用,这意味着()中的两个分叉进程和./file应该具有相同的父进程地址空间,那么为什么在()情况下变量对子shell可见,而对于./file情况却不会发生,尽管基于克隆系统调用的参数相同?


你是如何对()执行strace的? - Wakan Tanka
3个回答

3
使用括号创建的子shell并不使用execve()调用新进程,而是调用脚本。此时,父shell中的变量处理方式不同:execve()传递有意设置的一组变量(调用脚本的情况),而不调用execve()(括号情况)则保留完整的一组变量。您使用strace进行的探测应该显示出这种差异;如果您没有看到它,我只能假设您犯了几个可能的错误。我将简化我的操作以展示差异,然后您可以自行判断您的错误在哪里。我创建了两个跟踪。第一个使用的是:
strace -f -o bash-mystery-1.strace bash -c 'v=15; (echo $v)'

第二个是使用

strace -f -o bash-mystery-2.strace bash -c 'v=15; ./x.sh'

(假设 x.sh 是一个可执行脚本。)

选项 -f 是必要的,以跟踪父 shell(命令行中的 bash)的子进程。

在将所有典型和频繁差异(如地址和 PID)等均衡后,我使用 diff -y -W 300 比较了这些跟踪结果:

q() {
  sed -e 's/0x[0-9a-f]*/ADDR/g' \
      -e 's/12923\|12927/PARENT/g' \
      -e 's/12924\|12928/CHILD/g'
}
diff -y -W 300 <(q < bash-mystery-1.strace) <(q < bash-mystery-2.strace) | less -S

我的父进程PID是12923和12927,我的子进程PID是12924和12928(通过扫描跟踪文件得知)。你肯定会有不同的数字,所以请进行适当调整。为了正确查看差异输出,您需要一个非常宽的终端(超过200个字符)。因此,请将您的窗口变宽;-)

在大约第140行附近,我发现了一个clone()调用,这更或多或少是一个fork(),所以它将当前进程分成两个。在那里,子进程开始做事情,就像我们在跟踪中看到的以下行一样。在大约第165行附近,我看到了execve()的调用,但仅在调用脚本的情况下跟踪到。因此,子进程自愿放弃了其环境并设置了一个有意的环境。括号案例不更改其环境(不调用execve()),因此子进程继续具有完整的集合。


2

你必须将你的var导出给子进程:

export var=15

导出后,该变量在启动时用于所有子进程(而不是导出时)。

var=15
export var

与...相同

export var
var=15

相同
export var=15

使用unset可以取消导出。示例:unset var


2
这与问题有何关联? - bobah
2
感谢您的回答,但我想知道幕后究竟发生了什么。 - user3718463

1
这个谜底的解决方案是:子shell从父shell继承所有内容,包括所有shell变量,因为它们只是通过fork或clone调用,所以与父shell共享同一内存空间,这就是为什么它会起作用的原因。
$ var=15
$ (echo $var)
15

但是在./file中,子shell之后会跟随exec或execv系统调用,这将清除所有先前的父变量,但我们仍然拥有环境变量。您可以使用带有-f选项的strace来检查此内容,以监视子Shell,并发现存在对execv的调用。
[pid 26387] execve("./file", ["./file"], [/* 75 vars */]) = -1 ENOEXEC (Exec format error)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接