你是否被下面的几个问题困扰过,甚至至今没法真正理解?git
export
,何时用export,为何有时用了export还要source
?env
来设置环境变量,不用export,有什么好处?source
和exec
有什么区别?本文试图经过普及unix进程、环境变量等概念,让读者真真理解这些shell命令的本质,知道这些命令的使用场合。github
首先,先对这些命令作一个解释,若是读者能彻底理解,那么本文也许对你帮助不大。shell
set
设置了当前shell进程的本地变量
,本地变量只在当前shell的进程内有效,不会被子进程继承和传递。env
仅为将要执行的子进程设置环境变量
。export
将一个shell本地变量提高为当前shell进程的环境变量
,从而被子进程自动继承,可是export的变量没法改变父进程的环境变量。source
运行脚本的时候,不会启用一个新的shell进程,而是在当前shell进程环境中运行脚本。exec
运行脚本或命令的时候,不会启用一个新的shell进程,而且exec后续的脚本内容不会获得执行,即当前shell进程结束了。在这些表述中,反复提到进程
和环境变量
的概念。若是但愿深刻理解其中的含义,还必须理解进程的相关概念。编程
进程是一个程序执行的上下文集合,这个集合包括程序代码、数据段、堆栈、环境变量、内核标识进程的数据结构等。一个进程能够生成另外一个进程,生成的进程称为子进程
,那么相应的就有父进程
,所谓子子孙孙无穷尽也。子进程
从父进程
处会继承一些遗传因素,其中就包括本文的主题环境变量
。环境变量是一组特殊的字符型变量,因为具备继承性质,环境变量也常常用于父子进程传递参数用,这一点在shell编程中尤其突出。bash
在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
参数设置环境变量ui
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
做为父进程而言,能够经过waitpid()
函数等待子进程退出,并得到退出状态。spa
进程可经过setenv
或putenv
更改本身的环境变量,但环境变量的继承只能单向,即从父进程继承给fork
出来的子进程。子进程即便修改了本身的环境变量也没法动摇到父进程的环境变量。命令行
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设置环境变量后,运行的脚本中又调用了其余脚本,这个环境变量还会继承下去吗?