set,env,export,source,exec傻傻分不清楚?

你是否被下面的幾個問題困擾過,甚至至今沒法真正理解?git

  1. 什麼是export,何時用export,爲何有時用了export還要source
  2. 爲何用env來設置環境變量,不用export,有什麼好處?
  3. sourceexec有什麼區別?

本文試圖經過普及unix進程、環境變量等概念,讓讀者真真理解這些shell命令的本質,知道這些命令的使用場合。github

clipboard.png

首先,先對這些命令作一個解釋,若是讀者能徹底理解,那麼本文也許對你幫助不大。shell

  • set設置了當前shell進程的本地變量,本地變量只在當前shell的進程內有效,不會被子進程繼承和傳遞。
  • env僅爲將要執行的子進程設置環境變量
  • export將一個shell本地變量提高爲當前shell進程的環境變量,從而被子進程自動繼承,可是export的變量沒法改變父進程的環境變量。
  • source運行腳本的時候,不會啓用一個新的shell進程,而是在當前shell進程環境中運行腳本。
  • exec運行腳本或命令的時候,不會啓用一個新的shell進程,而且exec後續的腳本內容不會獲得執行,即當前shell進程結束了。

在這些表述中,反覆提到進程環境變量的概念。若是但願深刻理解其中的含義,還必須理解進程的相關概念。編程

進程和環境變量

進程是一個程序執行的上下文集合,這個集合包括程序代碼、數據段、堆棧、環境變量、內核標識進程的數據結構等。一個進程能夠生成另外一個進程,生成的進程稱爲子進程,那麼相應的就有父進程,所謂子子孫孫無窮盡也。子進程父進程處會繼承一些遺傳因素,其中就包括本文的主題環境變量。環境變量是一組特殊的字符型變量,因爲具備繼承性質,環境變量也常常用於父子進程傳遞參數用,這一點在shell編程中尤其突出。bash

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參數設置環境變量ui

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

做爲父進程而言,能夠經過waitpid()函數等待子進程退出,並得到退出狀態。spa

clipboard.png

進程可經過setenvputenv更改本身的環境變量,但環境變量的繼承只能單向,即從父進程繼承給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-shellexport的變量在父shell中是看不到的。有什麼辦法可讓一個腳本中的export印象到父進程的環境變量呢?

答案是使用source執行腳本,source的用法以下:

source ./test.sh

若是用source執行腳本,意味着fork和exec不會被調用,當前shell直接對test.sh解釋執行。這樣的話,若是此時test.sh中有export(即putenv),那麼將會改變當前shell的環境變量。

export如此好用,可是問題是它幾乎會影響到其後的全部命令,有沒有辦法能夠在運行某個命令時,臨時啓用某個環境變量,而不影響後面的命令呢?

答案是使用envenv的用法以下:

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設置環境變量後,運行的腳本中又調用了其餘腳本,這個環境變量還會繼承下去嗎?

相關文章
相關標籤/搜索