1. 程式人生 > >理解Linux中子shell的概念

理解Linux中子shell的概念

是什麼子shell

子shell的概念貫穿整個shell,寫shell指令碼時更是不可不知。所謂子shell,即從當前shell環境新開一個shell環境,這個新開的shell環境就稱為子shell(subshell),而開啟子shell的環境稱為該子shell的父shell。子shell和父shell的關係其實就是子程序和父程序的關係,只不過子shell和父shell是關聯的程序是bash程序。

子shell會從父shell中繼承很多環境,如變數、命令全路徑、檔案描述符、當前工作目錄、陷阱等等,但子shell有很多種型別,不同型別的子shell繼承的環境不相同。可以使用$BASH_SUBSHELL變數來檢視從當前程序開始的子shell層數,$BASHPID檢視當前所處BASH的PID,這不同於特殊變數"KaTeX parse error: Can't use function '\"' in math mode at position 6: "值,因為\̲"̲

"會從父程序繼承。

何時產生子shell

要解釋清楚子shell以及產生何種型別的子shell,需要搞清楚Linux中如何產生子程序。Linux上建立子程序的方式有三種:一種是fork出來的程序,一種是exec出來的程序,一種是clone出來的程序。此處無需關心clone,因為它用來實現Linux中的執行緒。

(1).fork是複製程序,它會複製當前程序的副本(不考慮寫時複製的模式),以適當的方式將這些資源交給子程序。所以子程序掌握的資源和父程序是一樣的,包括記憶體中的內容,所以也包括環境變數和變數。但父子程序是完全獨立的,它們是一個程式的兩個例項。

(2).exec是載入另一個應用程式,替代當前執行的程序,也就是說在不建立新程序的情況下載入一個新程式。exec還有一個動作:在程序執行完畢後,退出exec所在的shell環境。

所以為了保證程序安全,若要形成新的且獨立的子程序,都會先fork一份當前程序,然後在fork出來的子程序上呼叫exec來載入新程式替代該子程序。例如在bash下執行cp命令,會先fork出一個bash,然後再exec載入cp程式覆蓋子bash程序變成cp程序。

再來說明子shell的問題。一般fork出來的子程序,內容和父程序是一樣的(包括變數),例如執行cp命令時也能獲取到父程序的變數。但是cp命令在哪裡執行呢?執行cp命令敲入回車後,當前的bash程序fork出一個子bash,然後子bash通過exec載入cp程式替代子bash。這算是進入了子shell嗎?更通用的問題是:什麼情況下會進入子shell環境,什麼時候不進入子shel環境呢?

判斷是否進入了子shell的方式非常簡單,執行"echo $BASHPID",如果該值和父bash程序的pid值不同,則表示進入了子shell。在shell中是否進入子shell的情況可以分為幾種:

①.執行bash內建命令時.

bash內建命令是非常特殊的,父程序不會建立子程序來執行這些命令,而是直接在當前bash環境中執行。但如果將內建命令放在管道後,則此內建命令將和管道左邊的程序同屬於一個程序組,所以仍然會建立子shell。

[[email protected] ~]# echo $BASHPID   # 當前BASHPID
65230
[[email protected] ~]# let a=$BASHPID   # bash內建命令,不進入子shell
[[email protected] ~]# echo $a
65230
[[email protected] ~]# echo $BASHPID
65230
[[email protected] ~]# cd | expr $BASHPID      # 管道使得任何命令都進入程序組,會進入子shell   
65603

②.執行bash命令本身時。

這是一個很巧合的命令。bash命令本身是bash內建命令,在當前shell環境下執行內建命令本不會建立子shell,也就是說不會有獨立的bash程序出現,而實際結果則表現為新的bash是一個子程序。其中一個原因是執行bash命令會載入各種環境配置項,為了父bash的環境得到保護而不被覆蓋,所以應該讓其以子shell的方式存在。雖然fork出來的bash子程序內容完全繼承父shell,但因重新載入了環境配置項,所以子shell沒有繼承普通變數,更準確的說是覆蓋了從父shell中繼承的變數。不妨試試在/etc/bashrc檔案中定義一個變數,再在父shell中export名稱相同值卻不同的環境變數,然後到子shell中看看該變數的值為何?

[[email protected] ~]# echo "var=55" >>/etc/bashrc
[[email protected] ~]# export var=66
[[email protected] ~]# bash
[[email protected] ~]# echo $var
55

由結果55可知,執行bash時載入的/etc/bashrc中的變數覆蓋了父bash中的匯出的環境變數值66。

其實執行bash命令,既可以認為進入了子shell,也可以認為沒有進入子shell。從bash是內建命令的角度來考慮,它不會進入子shell,這一點在執行bash命令後從變數
$BASH_SUBSHELL的值為0可以驗證出來。但從執行bash命令後進入了新的shell環境來看,它有其父bash程序,且$BASHPID值和父shell不同,所以它算是進入了子shell。

[[email protected] ~]# echo $BASHPID
65230
[[email protected] ~]# bash
[[email protected] ~]# echo $BASHPID
65534

③.執行shell指令碼時。

指令碼中第一行總是"#!/bin/bash"或者直接"bash xyz.sh",這和上面的執行bash進入子shell其實是一回事,都是使用bash命令進入子shell。只不過此時的bash命令和情況②中直接執行bash命令所隱含的選項不一樣,所以繼承和載入的shell環境也不一樣。事實也確實如此,它僅只繼承父shell的某些環境變數,其餘環境一概初始化。

另外,執行shell指令碼相比於直接執行bash命令,還多了一個動作:指令碼執行完畢後自動退出子shell。

[[email protected] ~]# cat b.sh 
#!/bin/bash
echo $BASHPID

[[email protected] ~]# echo $BASHPID
65534
[[email protected] ~]# ./b.sh 
65570

④.執行shell函式時。

其實shell函式就是命令,它和bash內建命令的情況一樣。直接執行時不會進入子shell,但放在管道後會進入子shell。

[[email protected] ~]# fun_test (){ echo $BASHPID; }   # 定義一個函式,輸出BASHPID變數的值
[[email protected] ~]# echo $BASHPID 
65230
[[email protected] ~]# fun_test      # 說明執行函式不會進入子shell
65230
[[email protected] ~]# cd | fun_test   # 但放在管道後會進入子shell
65605
<font color="ff0000">⑤.執行非bash內建命令時。</font>

例如執行cp命令、grep命令等,它們直接fork一份bash程序,然後使用exec載入程式替代該子bash。此類子程序會繼承所有父bash的環境。但嚴格地說,這已經不是子shell,因為exec載入的程式已經把子bash程序替換掉了,這意味著丟失了很多bash環境。在bash文件中,直接稱呼這種環境為"單獨的環境",和子shell的概念類似。

[[email protected] ~]# let a=$BASHPID   # let是內建命令
[[email protected] ~]# echo $a
65230
[[email protected] ~]# echo $BASHPID    # echo是非內建命令,結果是不進入子shell
65230

⑥.命令替換($())。

當命令列中包含了命令替換部分時,將開啟一個子shell先執行這部分內容,再將執行結果返回給當前命令。因為這次的子shell不是通過bash命令進入的子shell,所以它會繼承父shell的所有變數內容。這也就解釋了"echo $(echo $$)“中”$$"的結果是當前bash的pid號,而不是子shell的pid號,但"echo $(echo $BASHPID)"卻和父bash程序的pid不同,因為它不是使用bash命令進入的子shell。

[[email protected] ~]# echo $BASHPID
65230
[[email protected] ~]# echo $(echo $BASHPID)      # 使用命令替換$()進入子shell
65612

⑦.使用括號()組合一系列命令。

例如(ls;date;echo haha),獨立的括號將會開啟一個子shell來執行括號內的命令。這種情況等同於情況⑤。

[[email protected] ~]# echo $BASHPID
65230
[[email protected] ~]# (echo $BASHPID)  # 使用括號()的命令組合進入子shell
65613

⑧.放入後臺執行的任務。

它不僅是一個獨立的子程序,還是在子shell環境中執行的。例如"echo hahha &"。

[[email protected] ~]# echo $BASHPID
65230
[[email protected] ~]# echo $BASHPID &   # 放入後臺執行的任務進入子shell
[1] 65614
[[email protected] ~]# 65614

[1]+  Done                    echo $BASHPID 

⑨.程序替換。

既然是新程序了,當然進入子shell執行。例如"cat <(echo haha)"。

[[email protected] ~]# echo $BASHPID
65230

[[email protected] ~]# cat <(echo $BASHPID)    # 程序替換"<()"進入子shell
65616

需要說明的是,子shell的環境設定不會粘滯到父shell環境,也就是說子shell的變數等不會影響父shell。