你是否被下面的幾個問題困擾過,甚至至今沒法真正理解?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設置環境變量後,運行的腳本中又調用了其餘腳本,這個環境變量還會繼承下去嗎?