Shell

努力理解管道和子shell中的重定向:非常感謝程式碼解釋

  • October 13, 2021

請考慮來自終端會話(Debian Buster,Bash 5.0)的以下日誌:

root@cerberus ~/scripts # rm -f result
root@cerberus ~/scripts # { { echo test; } | cat > result; }
root@cerberus ~/scripts # cat result
test
root@cerberus ~/scripts #

這裡沒什麼特別的,這是預期的行為,我理解。

但我不理解以下情況下的行為:

root@cerberus ~/scripts # rm -f result
root@cerberus ~/scripts # { { echo test >&3; } | cat > result; } 3>&1
test
root@cerberus ~/scripts # cat result
root@cerberus ~/scripts #

準確地說,我相信我理解為什麼在執行第二行時會輸出“test”,但我不明白為什麼結果文件中沒有任何內容。我對發生的事情的理解如下:

  1. 首先,fd 3 被設置為stdout. 我確信這發生在管道執行之前,因為否則管道中的任何命令都無法訪問 fd 3,這將導致“錯誤描述符”錯誤消息。
  2. 管道不是一個簡單的命令,因此會產生一個子shell 來執行它。子shell 繼承父shell 的執行環境,包括文件描述符和重定向。$$ 1 $$
  3. 管道中的每個命令也在其自己的子 shell 中執行$$ 2 $$,再次繼承執行環境和文件描述符。echo的輸出被重定向到 fd 3,而 fd 3 又從之前被複製stdout,總之導致echo的輸出出現在stdout(輸出到 fd 3,它到 fd 1,這是標準輸出)。
  4. 但我不明白為什麼echo’s 的輸出沒有進入結果文件。從bash 手冊(強調我的):

管道中每個命令的輸出通過管道連接到下一個命令的輸入。也就是說,每個命令都讀取前一個命令的輸出。此連接在命令指定的任何重定向之前執行。

我理解這一點,即在設置或應用重定向之前echo,應該將 ’ 的輸出連接到cat’ 的輸入。但如果這是真的,那麼在命令執行後結果文件將存在(並包含“測試”)。所以我的理解顯然是錯誤的。>&3

有人可以解釋我錯過了什麼嗎?

更新,基於 AB 和 Gilles 在下面的出色回答,並提供進一步的解釋

**我擔心的根源是我在上面第 3 項中所寫的。**它只是不那樣工作。另見吉爾斯的回答。

AB 是第一個提供答案的人(見下文)。但是,我需要一些時間來理解它。因此,我將解釋一些段落,以便更容易理解。

  1. 該行的最後一部分:3>&1首先完成:指向終端輸出的 fd 1 被複製到 fd 3。這意味著 fd 1 和 fd 3 現在都指向終端輸出。它們是相同的,可以互換使用。
  2. 在 fork 之前,通常使用系統呼叫在下一個可用的 fd 上創建一個管道pipe(2):假設 fd 4 和 fd 5。然後準備過程分叉為 future echo 和 future cat,執行以下步驟

: echo 像這樣工作:

fd 5 被複製到 fd 1 (覆蓋 fd 1 指向的位置:終端輸出)。這意味著 fd 1 現在與 fd 5 相同,並且它們可以互換使用。具體來說,fd 1 不再指向終端輸出,而是指向管道的寫入端。

在這個階段(但見下文), 的輸出echo將轉到管道的寫入端,因為echo寫入指向該寫入端的 fd 1。

因為我們不需要兩個文件描述符來處理同一件事,而且因為echo無論如何都要寫入 fd 1,所以 fd 5 現在被關閉了。

然後echo執行,但在設置了後面提到的附加重定向之後(參見 3.)。

b) 同樣地,fd 4 到 fd 0 的準備過程cat,即 fd 0 不再指向終端輸入,而是指向管道的接收端。在這個階段,for 的輸入cat將來自管道的接收端,因為cat從 fd 0 讀取,並且 fd 0 連接到接收端。因為我們不需要兩個文件描述符來處理同一件事,而且因為cat無論如何都是從 fd 0 讀取的,所以 fd 4 現在被關閉了。然後cat被執行。

雖然這一切都發生了,但 fd 3 到處都是繼承的。 3. >&3與第 1 條相反:它將 fd 3 複製到 fd 1。已創建 fd 3 以使其指向終端輸出,並由執行管道的子shell和執行各個管道命令的其他子shell繼承。

在步驟 2a) 中,fd 1 已指向管道的寫入側。但是現在,重定向>&3再次覆蓋 fd 1 並使其等於 fd 3,而 fd 3 又(仍然)指向終端輸出。這意味著 fd 1 不再指向管道的寫入端,而是指向終端輸出。這就是執行管道時終端上出現“test”的原因(請記住,echo始終寫入 fd 1,無論 fd 1 指向何處)。

另外,當 fd 1 被重定向“覆蓋”時,它的舊版本會被關閉(因為底層系統呼叫dup2(2)會這樣做)。由於其舊版本指向管道的寫入端,因此該寫入端現在已關閉。

因此,接收端,因此,cat不會接收任何數據。相反,他們會立即收到 EOF 通知。這就是為什麼cat不接收任何內容以及結果文件因此保持為空或被截斷的原因。

[ 旁注:我應該在重定向後關閉 fd 3 (也就是說,我們應該寫>&3 3>&-而不是>&3),因為echo- 如上所述 - 寫入 fd 1 並且對 fd 3 一無所知。但是,我的範例中缺少該部分,我想保留它以免分散實際問題的注意力)。]

那是因為 OP 的第 4 條,它的工作原理是這樣的,fd繼承了各種程序的創建/執行。我沒有寫所有發生 fork/exec 的地方。我當然會簡化其中的一些(使用內置命令……)。為 Linux 提供的文件連結,但應該在任何 POSIX 或類似 POSIX 的系統上發生相同的行為。

  1. 該行的最後一部分:3>&1首先完成:指向終端的fd 1 被複製為**fd 3(通常使用dup2(2)系統呼叫)。
  2. 在分叉之前,通常使用pipe(2)系統呼叫在下一個可用的fd上創建一個管道:假設 4 和 5。然後準備過程分叉到 futureecho和 future cat。proto-echo dups 5 到 1(“覆蓋”它指向:終端),關閉 5 和 execs echo,proto-cat dups2() 4 到 0,關閉 4 和 execs catfd 3 到處都是繼承的。
  3. >&3與第 1 條相反:它將fd 3(指向終端)複製到fd 1。因此管道的寫入端已被替換,現在已關閉(dup2(2)說:“如果文件描述符newfd以前打開過,它會靜默在重新使用之前關閉。”)。任何東西都不會寫入管道。終端接收test並顯示它。
  4. 並行cat打開並截斷目標文件result並開始從管道讀取。這會觸發 EOF,pipe(7)因為寫入端是/已關閉:cat命令結束。
  5. 主 shell 程序沒有剩餘子程序:執行結束

結果:test在終端和空result文件上。

引用自:https://unix.stackexchange.com/questions/672750