shell十三問之6:exec跟source差在哪?


這次讓我們從CU shell版的一個實例帖子來談起吧: (論壇改版後,原鏈接已經失效)

例中的提問原文如下:

帖子提問:

cd /etc/aa/bb/cc可以執行 但是把這條命令放入shell腳本後,shell腳本不執行! 這是什麼原因?

意思是:運行shell腳本,並沒有移動到/etc/aa/bb/cc目錄。

我當時如何回答暫時別去深究,先讓我們瞭解一下進程 (process)的概念好了。

首先,我們所執行的任何程序,都是父進程(parent process)產生的一個 子進程(child process),子進程在結束後,將返回到父進程去。 此現象在Linux中被稱為fork

(為何要稱為fork呢? 嗯,畫一下圖或許比較好理解...^_^)

當子進程被產生的時候,將會從父進程那裡獲得一定的資源分配、及 (更重要的是)繼承父進程的環境。

讓我們回到上一章所談到的"環境變量"吧: 所謂環境變量其實就是那些會傳給子進程的變量。 簡單而言, "遺傳性"就是區分本地變量與環境變量的決定性指標。 然而,從遺傳的角度來看,我們不難發現環境變量的另一個重要特徵: 環境變量只能從父進程到子進程單向傳遞。 換句話說:在子進程中環境如何變更,均不會影響父進程的環境。

接下來,在讓我們瞭解一下shell腳本(shell script)的概念. 所謂shell script 講起來很簡單,就是將你平時在shell prompt輸入的多行 command line, 依序輸入到一個文件文件而已。

再結合以上兩個概念(process + script),那應該不難理解如下的這句話的意思了: 正常來說,當我們執行一個shell script時,其實是先產生一個sub-shell的子進程, 然後sub-shell再去產生命令行的子進程。 然則,那讓我們回到本章開始時,所提到的例子在重新思考:

帖子提問:

cd /etc/aa/bb/cc可以執行 但是把這條命令放入shell腳本後,shell腳本不執行! 這是什麼原因?

意思是:運行shell腳本,並沒有移動到/etc/aa/bb/cc目錄。

我當時的答案是這樣的:

因為,我們一般跑的shell script是用sub-shell去執行的。 從process的概念來看,是 parent process產生一個child process去執行, 當child結束後,返回parent, 但parent的環境是不會因child的改變而改變的。 所謂的環境變量元數很多,如effective id(euid),variable, working dir等等... 其中的working dir($PWD) 正是樓主的疑問所在: 當用sub-shell來跑script的話,sub-shell的$pwd會因為cd而變更, 但返回primary shell時,$PWD是不會變更的。

能夠瞭解問題的原因及其原理是很好的,但是? 如何解決問題,恐怕是我們更應該感興趣的是吧?

那好,接下來,再讓我們瞭解一下source命令好了。 當你有了fork的概念之後,要理解soruce就不難:

所謂source,就是讓script在當前shell內執行、 而不是產生一個sub-shell來執行。 由於所有執行結果均在當前shell內執行、而不是產生一個sub-shell來執行。

因此, 只要我們原本單獨輸入的script命令行,變成source命令的參數, 就可輕而易舉地解決前面提到的問題了。

比方說,原本我們是如此執行script的:

$ ./my_script.sh

現在改成這樣既可:

$ source ./my_script.sh

或者:

$ . ./my_script.sh

說到這裡,我想,各位有興趣看看/etc底下的眾多設定的文件, 應該不難理解它們被定義後,如何讓其他script讀取並繼承了吧?

若然,日後,你有機會寫自己的script, 應也不難專門指定一個設定的文件以供不同的script一起"共用"了... ^_^

okay,到這裡,若你搞懂forksource的不同, 那接下來再接受一個挑戰:

exec又與source/fork有何不同呢?

哦...要了解exec或許較為複雜,尤其是扯上File Decscriptor的話... 不過,簡單來說:

exec 也是讓script在同一個進程上執行,但是原有進程則被結束了。 簡言之,原有進程能否終止,就是execsource/fork的最大差異了。

嗯,光是從理論去理解,或許沒那麼好消化, 不如動手"實踐+思考"來得印象深刻哦。

下面讓我們為兩個簡單的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

好了,別忘了仔細比較輸出結果的不同及背後的原因哦... 若有疑問,歡迎提出來一起討論討論~~~~

happy scripting! ^_^