你是否被下面的幾個問題困擾過,甚至至今無法真正理解?
什么是export,什么時候用export,為什么有時用了export還要source? 為什么用env來設置環境變量,不用export,有什么好處? source和exec有什么區別?
本文試圖通過普及unix進程、環境變量等概念,讓讀者真真理解這些shell命令的本質,知道這些命令的使用場合。
clipboard.png
首先,先對這些命令做一個解釋,如果讀者能完全理解,那么本文也許對你幫助不大。
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參數設置環境變量
intexecve(constchar* filename,char*constargv[ ],char*constenvp[ ]);
作為父進程而言,可以通過waitpid()函數等待子進程退出,并獲得退出狀態。
clipboard.png
進程可通過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本身都被替換掉了。
clipboard.png
上圖的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設置環境變量后,運行的腳本中又調用了其他腳本,這個環境變量還會繼承下去嗎?