bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.html
子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,這不同於特殊變量"$$"值,因為"$$"在大多數情況下都會從父shell中繼承。
何時產生子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。
[root@xuexi ~]# echo $BASHPID # 當前BASHPID 65230 [root@xuexi ~]# let a=$BASHPID # bash內置命令,不進入子shell [root@xuexi ~]# echo $a 65230
[root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# cd | expr $BASHPID # 管道使得任何命令都進入進程組,會進入子shell 65603
這時候的子shell的作用是為bash內置命令提供執行環境。
②.執行bash命令本身時。
顯然它會進入子shell環境,它的絕大多數環境都是新配置的,因為會加載一些環境配置文件。事實上fork出來的bash子進程內容完全繼承父shell,但因重新加載了環境配置項,所以子shell沒有繼承普通變量,更准確的說是覆蓋了從父shell中繼承的變量。不妨試試在/etc/bashrc文件中定義一個變量,再在父shell中export名稱相同值卻不同的環境變量,然后到子shell中看看該變量的值為何?
[root@xuexi ~]# echo "var=55" >>/etc/bashrc [root@xuexi ~]# export var=66 [root@xuexi ~]# bash [root@xuexi ~]# echo $var 55
由結果55可知,執行bash時加載的/etc/bashrc中的變量覆蓋了父bash中的導出的環境變量值66。
其實執行bash命令,既可以認為進入了子shell,也可以認為沒有進入子shell。在執行bash命令后從變量$BASH_SUBSHELL的值為0可以認為它沒有進入子shell。但從執行bash命令后進入了新的shell環境來看,它有其父bash進程,且$BASHPID值和父shell不同,所以它算是進入了子shell。
[root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# bash [root@xuexi ~]# echo $BASHPID 65534
其實,執行bash命令更應該被認為是進入了一個完全獨立的、全新的shell環境,而不應該認為是進入了片面的子shell環境。
此外,執行bash命令,"$$"不會繼承父shell的值。
③.執行shell腳本時。
腳本中第一行總是"#!/bin/bash"或者直接"bash xyz.sh",這和上面的執行bash進入子shell其實是一回事,都是使用bash命令進入子shell。只不過此時的bash命令和情況②中直接執行bash命令所隱含的選項不一樣,所以繼承和加載的shell環境也不一樣。事實也確實如此,它僅只繼承父shell的某些環境變量,其余環境一概初始化。
另外,執行shell腳本相比於直接執行bash命令,還多了一個動作:腳本執行完畢后自動退出子shell。
[root@xuexi ~]# cat b.sh #!/bin/bash echo $BASHPID [root@xuexi ~]# echo $BASHPID 65534 [root@xuexi ~]# ./b.sh 65570
此外,shell腳本中的"$$"不繼承父shell的值。
④.執行shell函數時。
其實shell函數就是命令,它和bash內置命令的情況一樣。直接執行時不會進入子shell,但放在管道后會進入子shell。
[root@xuexi ~]# fun_test (){ echo $BASHPID; } # 定義一個函數,輸出BASHPID變量的值 [root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# fun_test # 說明執行函數不會進入子shell 65230 [root@xuexi ~]# cd | fun_test # 但放在管道后會進入子shell 65605
⑤.執行非bash內置命令時。
例如執行cp命令、grep命令等,它們直接fork一份bash進程,然后使用exec加載程序替代該子bash。此類子進程會繼承所有父bash的環境。但嚴格地說,這已經不是子shell,因為exec加載的程序已經把子bash進程替換掉了,這意味着丟失了很多bash環境。在bash文檔中,直接稱呼這種環境為"單獨的環境",和子shell的概念類似。
[root@xuexi ~]# let a=$BASHPID # let是內置命令 [root@xuexi ~]# echo $a 65230 [root@xuexi ~]# echo $BASHPID # echo是非內置命令,結果是不進入子shell 65230
⑥.命令替換。
當命令行中包含了命令替換部分時,將開啟一個子shell先執行這部分內容,再將執行結果返回給當前命令。因為這次的子shell不是通過bash命令進入的子shell,所以它會繼承父shell的所有變量內容。這也就解釋了"echo $(echo $$)"中"$$"的結果是當前bash的pid號,而不是子shell的pid號,但"echo $(echo $BASHPID)"卻和父bash進程的pid不同,因為它不是使用bash命令進入的子shell。
[root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# echo $(echo $BASHPID) # 使用命令替換$()進入子shell 65612
⑦.使用括號()組合一系列命令。
例如(ls;date;echo haha),獨立的括號將會開啟一個子shell來執行括號內的命令。這種情況等同於情況⑤。
[root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# (echo $BASHPID) # 使用括號()的命令組合進入子shell 65613
⑧.放入后台運行的任務。
它不僅是一個獨立的子進程,還是在子shell環境中運行的。例如"echo hahha &"。
[root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# echo $BASHPID & # 放入后台運行的任務進入子shell [1] 65614 [root@xuexi ~]# 65614 [1]+ Done echo $BASHPID
⑨.進程替換。
既然是新進程了,當然進入子shell執行。例如"cat <(echo haha)"。
[root@xuexi ~]# echo $BASHPID 65230 [root@xuexi ~]# cat <(echo $BASHPID) # 進程替換"<()"進入子shell 65616
再說明"$$"的繼承問題。除了直接執行bash命令和shell腳本這兩種子shell,其他進入子shell的情況都會繼承父shell的值。前面也已經說了,其實shell腳本和直接執行bash命令開啟子shell的方式是一樣的,它們都不會繼承"$$"值,可以根據上述實驗自行測試。
需要說明的是,子shell的環境設置不會粘滯到父shell環境,無論是使用export還是source,它們都只能的概念都是父shell到子shell,不是也不會是從子shell到父shell。也就是說子shell的變量等不會影響父shell。
最后,建議同時閱讀另一篇文章:bash啟動時環境配置流程,此文中詳細解釋了bash啟動時加載哪些配置文件。
知道了子shell的概念,想必對shell解釋器和shell就理解的差不多。於是,下面這張經典的圖中為什么出現shell層也很容易理解。說白了,SHELL就是提供了執行環境和解析了命令,並使解析后的命令運行起來。