set,env,export,source,exec傻傻分不清楚?
你是否被下面的几个问题困扰过,甚至至今无法真正理解?
- 什么是
export
,什么时候用export,为什么有时用了export还要source
? - 为什么用
env
来设置环境变量,不用export,有什么好处? source
和exec
有什么区别?
本文试图通过普及unix进程、环境变量等概念,让读者真真理解这些shell命令的本质,知道这些命令的使用场合。
首先,先对这些命令做一个解释,如果读者能完全理解,那么本文也许对你帮助不大。
set
设置了当前shell进程的本地变量
,本地变量只在当前shell的进程内有效,不会被子进程继承和传递。env
仅为将要执行的子进程设置环境变量
。export
将一个shell本地变量提升为当前shell进程的环境变量
,从而被子进程自动继承,但是export的变量无法改变父进程的环境变量。source
运行脚本的时候,不会启用一个新的shell进程,而是在当前shell进程环境中运行脚本。exec
运行脚本或命令的时候,不会启用一个新的shell进程,并且exec后续的脚本内容不会得到执行,即当前shell进程结束了。
在这些表述中,反复提到进程
和环境变量
的概念。如果希望深入理解其中的含义,还必须理解进程的相关概念。
进程和环境变量
进程是一个程序执行的上下文集合,这个集合包括程序代码、数据段、堆栈、环境变量、内核标识进程的数据结构等。一个进程可以生成另一个进程,生成的进程称为子进程
,那么相应的就有父进程
,所谓子子孙孙无穷尽也。子进程
从父进程
处会继承一些遗传因素,其中就包括本文的主题环境变量
。环境变量是一组特殊的字符型变量,由于具有继承性质,环境变量也经常用于父子进程传递参数用,这一点在shell编程中尤为突出。
fork和exec
在unix系统中进程通过依次调用fork()
和exec()
系统调用来实现创建一个子进程。fork
其实就是克隆
,为什么github复刻别人的项目叫fork?就是这么来的,所谓“克隆”,就是在内存中将当前进程的所有内存镜像复制一份,所有东西都一样,只修改新进程的进程号(PID)。有点类似细胞分裂,细胞分裂后生成的细胞具有与原细胞完全相同的遗传因素。因为fork()
会复制整个进程,包括进程运行到哪句代码,这意味着新的进程会继续执行fork()
后面的代码,父进程也会运行fork()
后面的代码,从fork()
开始父子进程才分道扬镳。如果fork返回>0,那么说明在父进程中,如果fork返回==0,说明在子进程中:
pid = fork(); if(pid == 0) { //子进程中 } else if(pid > 0) { //父进程 }
精确的说exec
是一组函数的统称,并且exec
的准确定义是,用磁盘上的一个新的程序替换当前的进程的正文段、数据段、堆栈段。所以exec并不产生新的进程,而是替换。如此一来进程将从新代码的main开始执行,相当于另外运行了一个完全不同的程序,但保留了原来环境变量。
依据本文的主题,可以把exec
函数分为两类,一类是可以设置并传递新环境变量的,一类是不能传递新环境变量的,只能继承原环境变量的。换句话说,在运行新的程序时,是有机会改变新程序的环境变量的,而不只是继承。如下面这个变种,可以通过envp
参数设置环境变量
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
作为父进程而言,可以通过waitpid()
函数等待子进程退出,并获得退出状态。
进程可通过setenv
或putenv
更改自己的环境变量,但环境变量的继承只能单向,即从父进程继承给fork
出来的子进程。子进程即使修改了自己的环境变量也无法动摇到父进程的环境变量。
shell
shell并没有什么特殊,也是一个进程,当我们在命令行中敲入一个命令,并且按下Enter
后,shell这个进程会通过fork和exec为我们创建一个子进程(存在一小部分命令不需要启动子进程,称为build-in
命令),并且等待(waitpid)这个子进程完成退出。那么进程的内存镜像显然就包含本文的主题环境变量
。比如,如果我们在shell命令行中执行ls -al
,shell实际执行如下伪代码:
pid = fork(); if(pid == 0) { //子进程中,调用exec exec("ls -al"); } else if(pid > 0) { //父进程中,waitpid等待子进程退出 waitpid(pid); }
上面讨论了shell执行命令的情况,如果在命令行中执行一个shell脚本呢?默认情况下,shell进程会创建一个sub-shell子进程来执行这个shell脚本,并且等待这个子进程执行结束。
最后,再来审视一下本文的主题。首先set,source,export都是shell的build-in命令,命令本身不会创建新进程。
set
其实跟进程创建无关,也跟环境变量无关,它只是当前shell进程内部维护的变量(本地变量),用于变量的引用和展开,不能遗传和继承。
但shell的export
命令可以通过调用putenv
将一个本地变量提升为当前shell的环境变量。但是,记住环境变量的继承只是单向的,sub-shell
中export
的变量在父shell中是看不到的。有什么办法可以让一个脚本中的export印象到父进程的环境变量呢?
答案是使用source执行脚本,source
的用法如下:
source ./test.sh
如果用source
执行脚本,意味着fork和exec不会被调用,当前shell直接对test.sh解释执行。这样的话,如果此时test.sh中有export(即putenv),那么将会改变当前shell的环境变量。
export
如此好用,但是问题是它几乎会影响到其后的所有命令,有没有办法可以在运行某个命令时,临时启用某个环境变量,而不影响后面的命令呢?
答案是使用env
,env
的用法如下:
env GOTRACEBACK=crash ./test.sh
env不是shell的build-in命令,所以shell执行env的时候还是需要创建子进程的
env的作用从本质上说,相当于shell先fork,然后在子进程中运行env,子进程env调用execve
运行test.sh
时,多传了一个GOTRACEBACK=crash
的环境变量(上文提到过execve
是可以改变默认的继承行为的),这样test.sh
可以看到这个GOTRACEBACK
环境变量,但由于没有调用putenv
改变父shell的环境变量,所以后续启动的进程并不继承GOTRACEBACK
。
exec
意味着不调用fork,而是直接调用exec执行!这意味着当前shell的代码执行到exec后,代码被替换成了exec要执行的程序,自然地,后续的shell脚本不会得到执行,因为shell本身都被替换掉了。
上图的env实际并不准确,因为env不是build-in命令,读者可自行脑补
嗯,光是从理论去理解,或许没那么好消化,不如动手“实作+思考”来的印象深刻哦。
问题一:写两个简单的script,分别命名为1.sh及2.sh:
1.sh
#!/bin/bash A=B echo "PID for 1.sh before exec/source/fork:$$" export A echo "1.sh: \$A is $A" case $1 in exec) echo "using exec…" exec ./2.sh;; source) echo "using source…" ../2.sh;; *) echo "using fork by default…" ./2.sh;; esac echo "PID for 1.sh after exec/source/fork:$$" echo "1.sh: \$A is $A"
2.sh
#!/bin/bash echo "PID for 2.sh: $$" echo "2.sh get \$A=$A from 1.sh" A=C export A echo "2.sh: \$A is $A"
然后,分别跑如下参数来观察结果:
$ ./1.sh fork $ ./1.sh source $ ./1.sh exec
问题二:用env设置环境变量后,运行的脚本中又调用了其他脚本,这个环境变量还会继承下去吗?