你是否被下面的幾個(gè)問題困擾過,甚至至今無法真正理解?
什么是export,什么時(shí)候用export,為什么有時(shí)用了export還要source? 為什么用env來設(shè)置環(huán)境變量,不用export,有什么好處? source和exec有什么區(qū)別?
本文試圖通過普及unix進(jìn)程、環(huán)境變量等概念,讓讀者真真理解這些shell命令的本質(zhì),知道這些命令的使用場合。
clipboard.png
首先,先對這些命令做一個(gè)解釋,如果讀者能完全理解,那么本文也許對你幫助不大。
set設(shè)置了當(dāng)前shell進(jìn)程的本地變量,本地變量只在當(dāng)前shell的進(jìn)程內(nèi)有效,不會(huì)被子進(jìn)程繼承和傳遞。 env僅為將要執(zhí)行的子進(jìn)程設(shè)置環(huán)境變量。 export將一個(gè)shell本地變量提升為當(dāng)前shell進(jìn)程的環(huán)境變量,從而被子進(jìn)程自動(dòng)繼承,但是export的變量無法改變父進(jìn)程的環(huán)境變量。 source運(yùn)行腳本的時(shí)候,不會(huì)啟用一個(gè)新的shell進(jìn)程,而是在當(dāng)前shell進(jìn)程環(huán)境中運(yùn)行腳本。 exec運(yùn)行腳本或命令的時(shí)候,不會(huì)啟用一個(gè)新的shell進(jìn)程,并且exec后續(xù)的腳本內(nèi)容不會(huì)得到執(zhí)行,即當(dāng)前shell進(jìn)程結(jié)束了。
在這些表述中,反復(fù)提到進(jìn)程和環(huán)境變量的概念。如果希望深入理解其中的含義,還必須理解進(jìn)程的相關(guān)概念。
進(jìn)程和環(huán)境變量
進(jìn)程是一個(gè)程序執(zhí)行的上下文集合,這個(gè)集合包括程序代碼、數(shù)據(jù)段、堆棧、環(huán)境變量、內(nèi)核標(biāo)識(shí)進(jìn)程的數(shù)據(jù)結(jié)構(gòu)等。一個(gè)進(jìn)程可以生成另一個(gè)進(jìn)程,生成的進(jìn)程稱為子進(jìn)程,那么相應(yīng)的就有父進(jìn)程,所謂子子孫孫無窮盡也。子進(jìn)程從父進(jìn)程處會(huì)繼承一些遺傳因素,其中就包括本文的主題環(huán)境變量。環(huán)境變量是一組特殊的字符型變量,由于具有繼承性質(zhì),環(huán)境變量也經(jīng)常用于父子進(jìn)程傳遞參數(shù)用,這一點(diǎn)在shell編程中尤為突出。
fork和exec
在unix系統(tǒng)中進(jìn)程通過依次調(diào)用fork()和exec()系統(tǒng)調(diào)用來實(shí)現(xiàn)創(chuàng)建一個(gè)子進(jìn)程。
fork其實(shí)就是克隆,為什么github復(fù)刻別人的項(xiàng)目叫fork?就是這么來的,所謂“克隆”,就是在內(nèi)存中將當(dāng)前進(jìn)程的所有內(nèi)存鏡像復(fù)制一份,所有東西都一樣,只修改新進(jìn)程的進(jìn)程號(PID)。有點(diǎn)類似細(xì)胞分裂,細(xì)胞分裂后生成的細(xì)胞具有與原細(xì)胞完全相同的遺傳因素。因?yàn)閒ork()會(huì)復(fù)制整個(gè)進(jìn)程,包括進(jìn)程運(yùn)行到哪句代碼,這意味著新的進(jìn)程會(huì)繼續(xù)執(zhí)行fork()后面的代碼,父進(jìn)程也會(huì)運(yùn)行fork()后面的代碼,從fork()開始父子進(jìn)程才分道揚(yáng)鑣。如果fork返回>0,那么說明在父進(jìn)程中,如果fork返回==0,說明在子進(jìn)程中:
pid = fork();
if(pid == 0) {
//子進(jìn)程中
} else if(pid > 0) {
//父進(jìn)程
}
精確的說exec是一組函數(shù)的統(tǒng)稱,并且exec的準(zhǔn)確定義是,用磁盤上的一個(gè)新的程序替換當(dāng)前的進(jìn)程的正文段、數(shù)據(jù)段、堆棧段。所以exec并不產(chǎn)生新的進(jìn)程,而是替換。如此一來進(jìn)程將從新代碼的main開始執(zhí)行,相當(dāng)于另外運(yùn)行了一個(gè)完全不同的程序,但保留了原來環(huán)境變量。
依據(jù)本文的主題,可以把exec函數(shù)分為兩類,一類是可以設(shè)置并傳遞新環(huán)境變量的,一類是不能傳遞新環(huán)境變量的,只能繼承原環(huán)境變量的。換句話說,在運(yùn)行新的程序時(shí),是有機(jī)會(huì)改變新程序的環(huán)境變量的,而不只是繼承。如下面這個(gè)變種,可以通過envp參數(shù)設(shè)置環(huán)境變量
intexecve(constchar* filename,char*constargv[ ],char*constenvp[ ]);
作為父進(jìn)程而言,可以通過waitpid()函數(shù)等待子進(jìn)程退出,并獲得退出狀態(tài)。
clipboard.png
進(jìn)程可通過setenv或putenv更改自己的環(huán)境變量,但環(huán)境變量的繼承只能單向,即從父進(jìn)程繼承給fork出來的子進(jìn)程。子進(jìn)程即使修改了自己的環(huán)境變量也無法動(dòng)搖到父進(jìn)程的環(huán)境變量。
shell
shell并沒有什么特殊,也是一個(gè)進(jìn)程,當(dāng)我們在命令行中敲入一個(gè)命令,并且按下Enter后,shell這個(gè)進(jìn)程會(huì)通過fork和exec為我們創(chuàng)建一個(gè)子進(jìn)程(存在一小部分命令不需要啟動(dòng)子進(jìn)程,稱為build-in命令),并且等待(waitpid)這個(gè)子進(jìn)程完成退出。那么進(jìn)程的內(nèi)存鏡像顯然就包含本文的主題環(huán)境變量。比如,如果我們在shell命令行中執(zhí)行l(wèi)s -al,shell實(shí)際執(zhí)行如下偽代碼:
pid = fork();
if(pid == 0) {
//子進(jìn)程中,調(diào)用exec
exec(“ls -al”);
} else if(pid > 0) {
//父進(jìn)程中,waitpid等待子進(jìn)程退出
waitpid(pid);
}
上面討論了shell執(zhí)行命令的情況,如果在命令行中執(zhí)行一個(gè)shell腳本呢?默認(rèn)情況下,shell進(jìn)程會(huì)創(chuàng)建一個(gè)sub-shell子進(jìn)程來執(zhí)行這個(gè)shell腳本,并且等待這個(gè)子進(jìn)程執(zhí)行結(jié)束。
最后,再來審視一下本文的主題。首先set,source,export都是shell的build-in命令,命令本身不會(huì)創(chuàng)建新進(jìn)程。
set其實(shí)跟進(jìn)程創(chuàng)建無關(guān),也跟環(huán)境變量無關(guān),它只是當(dāng)前shell進(jìn)程內(nèi)部維護(hù)的變量(本地變量),用于變量的引用和展開,不能遺傳和繼承。
但shell的export命令可以通過調(diào)用putenv將一個(gè)本地變量提升為當(dāng)前shell的環(huán)境變量。但是,記住環(huán)境變量的繼承只是單向的,sub-shell中export的變量在父shell中是看不到的。有什么辦法可以讓一個(gè)腳本中的export印象到父進(jìn)程的環(huán)境變量呢?
答案是使用source執(zhí)行腳本,source的用法如下:
source ./test.sh
如果用source執(zhí)行腳本,意味著fork和exec不會(huì)被調(diào)用,當(dāng)前shell直接對test.sh解釋執(zhí)行。這樣的話,如果此時(shí)test.sh中有export(即putenv),那么將會(huì)改變當(dāng)前shell的環(huán)境變量。
export如此好用,但是問題是它幾乎會(huì)影響到其后的所有命令,有沒有辦法可以在運(yùn)行某個(gè)命令時(shí),臨時(shí)啟用某個(gè)環(huán)境變量,而不影響后面的命令呢?
答案是使用env,env的用法如下:
env GOTRACEBACK=crash ./test.sh
env不是shell的build-in命令,所以shell執(zhí)行env的時(shí)候還是需要?jiǎng)?chuàng)建子進(jìn)程的
env的作用從本質(zhì)上說,相當(dāng)于shell先fork,然后在子進(jìn)程中運(yùn)行env,子進(jìn)程env調(diào)用execve運(yùn)行test.sh時(shí),多傳了一個(gè)GOTRACEBACK=crash的環(huán)境變量(上文提到過execve是可以改變默認(rèn)的繼承行為的),這樣test.sh可以看到這個(gè)GOTRACEBACK環(huán)境變量,但由于沒有調(diào)用putenv改變父shell的環(huán)境變量,所以后續(xù)啟動(dòng)的進(jìn)程并不繼承GOTRACEBACK。
exec意味著不調(diào)用fork,而是直接調(diào)用exec執(zhí)行!這意味著當(dāng)前shell的代碼執(zhí)行到exec后,代碼被替換成了exec要執(zhí)行的程序,自然地,后續(xù)的shell腳本不會(huì)得到執(zhí)行,因?yàn)閟hell本身都被替換掉了。
clipboard.png
上圖的env實(shí)際并不準(zhǔn)確,因?yàn)閑nv不是build-in命令,讀者可自行腦補(bǔ)
嗯,光是從理論去理解,或許沒那么好消化,不如動(dòng)手“實(shí)作+思考”來的印象深刻哦。
問題一:寫兩個(gè)簡單的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”
然后,分別跑如下參數(shù)來觀察結(jié)果:
$ ./1.sh fork
$ ./1.sh source
$ ./1.sh exec
問題二:用env設(shè)置環(huán)境變量后,運(yùn)行的腳本中又調(diào)用了其他腳本,這個(gè)環(huán)境變量還會(huì)繼承下去嗎?