【Linux】Shell 和 Shell 脚本的执行

Posted by 西维蜀黍 on 2020-06-13, Last Modified on 2022-04-01

fork - 创建子 shell 进程

父子 shell 进程是相对而言的,它描述了两个 shell 进程的fork关系。

以bash为例,当在bash shell中执行一个命令时,其实是在当前 bash shell 进程中 fork 出一个子 bash shell 进程,然后在这个子进程中运行相应的命令,当命令执行完成后,这个子进程就被end了。

简单来说,父Shell进程创建子Shell进程时,调用的是 fork() 函数。

在这种情况中:

  • 子 shell 进程会继承父 shell 进程中设置的环境变量
  • 但是子 shell 进程中设置的环境变量并不会带回父 shell 进程(即不会影响父 shell 进程),以下为证明:
$ export SW="aaa"
$ echo $SW
aaa
$ bash
$ echo $SW
aaa
$ export SW2="bbb"
$ echo $SW2
bbb
$ exit
exit
$ echo $SW2

Case 1 - shell 中执行命令

我们可以做一个实验来验证这个事实:

$ echo $$
84289
$ sleep 500

当然我用的是zsh啦,当前 zsh shell 的 PID 为 43423。我们执行 sleep 500 ,它在 PID 为 43468 的子 zsh shell 中执行。

注意到,它的父进程 PID 为43423:

Case 2 - shell 中执行 shell 脚本

这是一个简单的bash shell脚本(test.sh):

#!/bin/bash
sleep 100

我们执行这个脚本:

$ echo $$
43742
$ ./test.sh

# 以上执行方式和以下两种执行方式完全等效(从都通过 fork 操作的角度来说),只是在下面的两种方法中,前者用 bash 这种 shell 来执行,而后者用 sh 这种 shell 罢了
$ bash ./test.sh
# or
$ sh ./test.sh

bash ./test.sh

可以看到:

  • 终端的bash shell 进程(进程ID43742fork 创建出一个用于执行 test.sh 的新子进程(PID43749)。
  • 当执行到 sleep 命令时, bash ./test.sh 这个进程会通过 fork 产生了一个新 sleep 子进程用于这个sleep 命令(PID为 43750)。

sh ./test.sh

可以看到,这时,shell 脚本的中内容会通过当前 shell 进程(PID 为 43742) fork 出一个子 shell 进程(PID 为 43768)来执行。

不同 shell 脚本执行方式对文件权限的要求

# 当 shell 脚本没有任何权限时,三种方式均不能执行这个 shell 脚本
$ chmod 000 test.sh
$ ll
-r--r--r--@  1 wei.shi  345931250    46B Jun 13 18:27 test.sh
$ ./test.sh
zsh: permission denied: ./test.sh
$ source ./test.sh
source: permission denied: ./test.sh
$ bash ./test.sh
bash: ./test.sh: Permission denied

# 当 shell 脚本只有可读权限时,./test.sh 方式不能执行这个 shell 脚本
$ chmod 444 test.sh
$ ll
-r--r--r--@  1 wei.shi  345931250    46B Jun 13 18:27 test.sh
$ ./test.sh
zsh: permission denied: ./test.sh
$ bash ./test.sh
...
$ sh ./test.sh
...

source <script> - 执行 shell 脚本

Case 1 - source <script> 执行 shell 脚本

在上面的实验中,我们发现通过 source <script> 的方式来执行 shell 脚本,只需要当前用户对该 shell 脚本文件有可读权限时,即可成功执行该脚本。

我们继续来看看,这种执行方法与其他几种执行方式还有什么区别

$ echo $$
43742
$ source ./test.sh

可以看到,这时,shell 脚本的中内容会在当前 shell 进程(PID 为 43742) 中直接执行。


值得一提的是,以下两种表达是完全等效的:

$ source ./test.sh

$ . ./test.sh

man source 是这样解释的:

.  filename [arguments]
source filename [arguments]
      Read and execute commands from filename in the current shell environment and return the exit status of the last command executed from filename.  If filename does not contain a slash, file names in  PATH  are used to find the directory containing filename.  The file searched for in PATH need not be executable.  When bash is not in posix mode, the current directory is searched if no  file is  found in PATH.  If the sourcepath option to the shopt builtin command is turned off, the PATH is not searched.  If any arguments are supplied, they become the positional parameters when filename  is  executed.  Otherwise the positional parameters are unchanged.  The return status is the status of the last command exited within the script (0 if no commands are executed), and false if filename is not found or cannot be read.

通常,我们在修改了 bash 或 zsh 的启动脚本 (如 .zshrc.bash_profile),比如增加一个环境变量的设置,都要 source 一下这个启动脚本,目的其实是使这个新设置的环境变量在当前 shell process 中立即生效,而如果通过 bash .zshrc 的方式则显然不会使得新设置的环境变量在当前 shell process 中立即生效 。

不同 shell 脚本执行方式对环境变量的影响

$ echo $SW_TEST
$ echo `export SW_TEST="aaabbb"` > ./test.sh
$ bash ./test.sh
$ echo $SW_TEST

$ sh ./test.sh
$ echo $SW_TEST

# 只有通过这种方式执行,在 shell script 中修改的环境变量才会对当前 shell 有影响作用(作用范围)
$ source ./test.sh
$ echo $SW_TEST
aaabbb

这其实是因为,使用 source 方式运行 script 时, 就是让 script 在当前 shell process内执行, 而不是创建一个新的 child process 来执行。

而由于所有 shell 脚本中的所有命令均在当前 shell process中执行,在 shell 脚本中进行的对环境变量的修改, 当然就会改变当前process环境了。

exec <script> - 执行 shell 脚本

exec 与 source 类似的一点在于,他们均在当前 shell 进程中执行 shell 脚本(而不是专门创建一个 child process 去执行该script)。

唯一的一点区别在于,使用 exec 执行时,当该 shell 脚本执行完成后,当前shell 进程也就退出了。

总结

我们可以使用 fork、source 或者 exec 中任何一种方式来执行 shell 脚本:

fork

使用 fork 方式运行 script 时, 当前 shell 进程会创建一个 child process 去专门执行该script,当child process结束后(意味着这个script 执行完成),会返回parent process,但 parent process 的环境是不会因 child process 的改变而改变。

这意味着在 shell 脚本中进行的对环境变量的修改并不会在当前 shell 中生效。

source

使用 source 方式运行 script 时, 就是让 script 在当前 shell process内执行, 而不是创建一个child process来执行。

而由于所有 shell 脚本中的所有命令均在当前 shell process中执行,在 shell 脚本中进行的对环境变量的修改, 当然就会改变当前process环境了。

exec

source 类似,使用 exec 方式运行 script 时, 就是让 script 在当前 shell process 内执行(而不是创建一个child process来执行),而 script 执行结束后,当前 shell process 也就被结束了。