努力理解管道和子shell中的重定向:非常感謝程式碼解釋
請考慮來自終端會話(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”,但我不明白為什麼結果文件中沒有任何內容。我對發生的事情的理解如下:
- 首先,fd 3 被設置為
stdout
. 我確信這發生在管道執行之前,因為否則管道中的任何命令都無法訪問 fd 3,這將導致“錯誤描述符”錯誤消息。- 管道不是一個簡單的命令,因此會產生一個子shell 來執行它。子shell 繼承父shell 的執行環境,包括文件描述符和重定向。$$ 1 $$
- 管道中的每個命令也在其自己的子 shell 中執行$$ 2 $$,再次繼承執行環境和文件描述符。
echo
的輸出被重定向到 fd 3,而 fd 3 又從之前被複製stdout
,總之導致echo
的輸出出現在stdout
(輸出到 fd 3,它到 fd 1,這是標準輸出)。- 但我不明白為什麼
echo
’s 的輸出沒有進入結果文件。從bash 手冊(強調我的):管道中每個命令的輸出通過管道連接到下一個命令的輸入。也就是說,每個命令都讀取前一個命令的輸出。此連接在命令指定的任何重定向之前執行。
我理解這一點,即在設置或應用重定向之前
echo
,應該將 ’ 的輸出連接到cat
’ 的輸入。但如果這是真的,那麼在命令執行後結果文件將存在(並包含“測試”)。所以我的理解顯然是錯誤的。>&3
有人可以解釋我錯過了什麼嗎?
更新,基於 AB 和 Gilles 在下面的出色回答,並提供進一步的解釋
**我擔心的根源是我在上面第 3 項中所寫的。**它只是不那樣工作。另見吉爾斯的回答。
AB 是第一個提供答案的人(見下文)。但是,我需要一些時間來理解它。因此,我將解釋一些段落,以便更容易理解。
- 該行的最後一部分:
3>&1
首先完成:指向終端輸出的 fd 1 被複製到 fd 3。這意味著 fd 1 和 fd 3 現在都指向終端輸出。它們是相同的,可以互換使用。- 在 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 的系統上發生相同的行為。
- 該行的最後一部分:
3>&1
首先完成:指向終端的fd 1 被複製為**fd 3(通常使用dup2(2)
系統呼叫)。- 在分叉之前,通常使用
pipe(2)
系統呼叫在下一個可用的fd上創建一個管道:假設 4 和 5。然後準備過程分叉到 futureecho
和 futurecat
。proto-echo dups 5 到 1(“覆蓋”它指向:終端),關閉 5 和 execsecho
,proto-cat dups2() 4 到 0,關閉 4 和 execscat
。fd 3 到處都是繼承的。>&3
與第 1 條相反:它將fd 3(指向終端)複製到fd 1。因此管道的寫入端已被替換,現在已關閉(dup2(2)
說:“如果文件描述符newfd以前打開過,它會靜默在重新使用之前關閉。”)。任何東西都不會寫入管道。終端接收test
並顯示它。- 並行
cat
打開並截斷目標文件result
並開始從管道讀取。這會觸發 EOF,pipe(7)
因為寫入端是/已關閉:cat
命令結束。- 主 shell 程序沒有剩餘子程序:執行結束
結果:
test
在終端和空result
文件上。