1. 程式人生 > >Linux下set,env,export,source,exec深入解析

Linux下set,env,export,source,exec深入解析

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

  1. 什麼是export,什麼時候用export,為什麼有時用了export還要source
  2. 為什麼用env來設定環境變數,不用export,有什麼好處?
  3. sourceexec有什麼區別?

本文試圖通過普及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引數設定環境變數

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

作為父程序而言,可以通過waitpid()函式等待子程序退出,並獲得退出狀態。

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設定環境變數後,執行的指令碼中又呼叫了其他指令碼,這個環境變數還會繼承下去嗎?