關於 `fork`、子程序和“子 shell”

關於 `fork`、子程序和“子 shell”

這篇文章基本上是一篇文章的後續先前的問題我的。

從這個問題的答案中,我意識到我不僅不太理解「子shell」的整個概念,而且更一般地說,我不理解fork-ing 和子進程之間的關係。

我曾經認為當進程X執行 a時fork,a新的Y創建的進程的父進程是X,但根據該問題的答案,

[a] subshel​​l 並不是一個全新的進程,而是現有進程的一個分支。

這裡的意思是「分叉」不是(或不會導致)「一個全新的過程」。

我現在很困惑,太困惑了,事實上,無法提出一個連貫的問題來直接消除我的困惑。

然而,我可以提出一個可能間接帶來啟發的問題。

根據 ,由於zshall(1)每當$ZDOTDIR/.zshenv新的實例啟動時都會獲取來源,因此任何導致創建「全新的 [zsh] 進程」的zsh命令都會導致無限倒退。$ZDOTDIR/.zshenv另一方面,在文件中包含以下任一行$ZDOTDIR/.zshenv不會不是導致無限回歸:

echo $(date; printenv; echo $$) > /dev/null    #1
(date; printenv; echo $$)                      #2

我發現透過上述機制引發無限回歸的唯一方法是在文件中包含類似以下1 的$ZDOTDIR/.zshenv行:

$SHELL -c 'date; printenv; echo $$'            #3

我的問題是:

  1. #1上面標記的命令#2與標記帳戶#3的這種行為差異有何不同?

  2. 如果在 中建立的 shell#1#2稱為“子 shell”,那麼那些類似於呼叫產生的 shell 是什麼#3

  3. 是否有可能根據 Unix 進程的“理論”(因為缺乏更好的詞)來合理化(並可能概括)上述經驗/軼事發現?

最後一個問題的動機是能夠確定提前時間(即不訴諸實驗)如果將哪些命令包含在中,它們會導致無限回歸$ZDOTDIR/.zshenv


1我在上面的各個範例中使用的特定命令順序date; printenv; echo $$並不是太重要。它們恰好是其輸出可能有助於解釋我的“實驗”結果的命令。 (然而,我確實希望這些序列包含多個命令,原因如下這裡.)

答案1

因為,根據 zshall(1),每當 zsh 的新實例啟動時,都會取得 $ZDOTDIR/.zshenv

如果你在這裡專注於「開始」這個詞,你會過得更好。的作用fork()是創建另一個進程從目前進程所在的位置開始。它克隆一個現有的進程,唯一的差異是 的回傳值fork。該文件使用「開始」表示從頭開始進入程式。

您的範例 #3 運行$SHELL -c 'date; printenv; echo $$',從頭開始一個全新的進程。它將經歷普通的啟動行為。例如,您可以透過交換另一個 shell 來說明這一點:運行bash -c ' ... '而不是zsh -c ' ... '.在這裡使用並沒有什麼特別的$SHELL

範例#1 和#2 運行子shell。 shellfork本身並在該子進程內執行您的命令,然後在子進程完成時繼續執行自己的命令。


您的問題#1 的答案如下:範例 3 從一開始就執行一個全新的 shell,而其他兩個則是執行子 shell。啟動行為包括載入.zshenv

他們特別指出這種行為的原因(這可能是導致您困惑的原因)是該文件(與其他一些文件不同)在互動式和非互動式 shell 中載入。


對於你的問題#2:

如果在 #1 和 #2 中創建的 shell 被稱為“子 shell”,那麼那些類似於 #3 生成的 shell 被稱為“子 shell”?

如果你想要一個名字,你可以稱它為“子殼”,但實際上它沒什麼。它與從 shell 啟動的任何其他進程沒有什麼不同,無論是相同的 shell、不同的 shell 還是cat.


對於你的問題#3:

是否有可能根據 Unix 進程的“理論”(因為缺乏更好的詞)來合理化(並可能概括)上述經驗/軼事發現?

fork建立一個帶有新 PID 的新進程,該進程從該進程停止的地方開始並行運行。exec用從某處載入的新程式取代目前正在執行的程式碼,並從頭開始執行。當您產生一個新程式時,首先您fork自己,然後是exec子程式中的該程式。這是適用於任何地方的過程的基本理論,無論是殼內部還是殼外部。

子 shell 是forks,您執行的每個非內建指令都會導致 aforkexec


請注意,$$擴展為父 shell 的 PID在任何 POSIX 相容的 shell 中,因此無論如何您可能都無法獲得預期的輸出。另請注意,zsh 無論如何都會積極優化子 shell 執行,並且通常exec是最後一個命令,或者如果所有命令在沒有它的情況下都是安全的,則根本不會產生子 shell。

測試你的直覺的一個有用命令是:

strace -e trace=process -f $SHELL -c ' ... '

...這會將您在新 shell 中執行的命令的所有與進程相關的事件(而不是其他事件)列印到標準錯誤。您可以查看新進程中運行和不運行的內容以及exec發生的位置。

另一個可能有用的命令是pstree -h,它將列印並突出顯示當前進程的父進程樹。您可以在輸出中看到您的層數。

答案2

當手冊中說命令.zshenv是「來源」時,這表示它們是在運行它們的 shell 中執行的。它們不會引起對 的調用fork(),因此它們不會產生子 shell。您的第三個範例明確運行一個子 shell,調用 調用fork(),從而無限遞歸。我相信,這應該(至少部分)回答你的第一個問題。

  1. 命令 1 和 2 中沒有「創建」任何內容,因此沒有任何東西可以被稱為任何東西 - 這些命令是在採購 shell 的上下文中運行的。

  2. 概括而言,「呼叫」shell 例程或程式與「採購」shell 例程或程式之間的差異 - 後者通常僅適用於 shell 命令/腳本,而不適用於外部程式。 「取得」shell 腳本通常是透過. <scriptname>而不是或來./<scriptname>完成的/full/path/to/script- 請注意取得指令開頭的「點-空間」序列。也可以使用 呼叫 Sourcing source <scriptname>,該source指令是 shell 內部指令。

答案3

fork,假設一切順利,回傳兩次。一個返回在父進程中(具有原始進程 ID),另一個返回在新子進程中(不同的進程 ID,但在其他方面與父進程共享許多共同點)。此時,子進程可以exec(3)執行某些操作,這將導致一些「新」二進位檔案載入到該進程中,儘管子進程不需要這樣做,並且可以運行已透過父進程加載的其他程式碼(例如 zsh 函數) 。因此,fork如果「全新」被認為是指透過exec(3)系統呼叫載入的東西,則 a 可能會也可能不會導致「全新」進程。

提前猜測哪些指令會導致無限倒退是很棘手的。除了 fork-calling-fork 情況(又稱「forkbomb」)之外,另一個簡單的情況是透過一些命令的簡單函數包裝器

function ssh() {
   ssh -o UseRoaming=no "$@"
}

相反,可能應該寫成

function ssh() {
  =ssh -o UseRoaming=no "$@"
}

command ssh ...避免ssh函數的無限函數調用,調用ssh該函數的函數調用...這絕不涉及fork,因為函數調用是 ZSH 進程內部的,但會愉快地發生到無窮大,直到該單一函數遇到某個限制ZSH進程。

strace像往常一樣,可以方便地準確揭示任何命令涉及哪些系統調用(特別是這裡fork,也許還有一些exec調用); shell 可以使用或類似的方式進行偵錯-x,以顯示 shell 內部正在執行的操作(例如函數呼叫)。如需更多閱讀,Stevens 在《Unix 環境中的高階程式設計》中有幾章與新進程的創建和處理有關。

相關內容