Unix 重新導向跟 2>&1

這幾天因為研究 L4D2 的專屬伺服器架設,看到 Srcds 網站針對確保 Srcds 服務程序能隨時保持高優先權,在 crontab 底下使用 renice 的方式每五分鐘調整一次程式的優先權,其指令中
renice -20 `cat /home/yoursrcdspath/srcds.pid` >/dev/null 2>&1

最後面的「>/dev/null 2>&1」為 Unix 的重新導向技巧。雖然大約知道是什麼,不過實際上的原理我卻一直不明白,大約查了一下,疑問變更多。於是我決定來搞清楚它的來龍去脈,這篇文章就是這樣來的。

首先,我們先說明一下這段語法的作用,「>/dev/null 2>&1」實際上在這裡會將左邊程式的執行結果丟給「/dev/null」,然後不管程式有什麼錯誤,也會一併丟給 null。熟悉 Unix 的人大概知道,「/dev/null」是 Unix 底下的垃圾筒+黑洞。它不帶任何意義,所有丟進去的東西都會被吃掉然後不見,反正你不想看到的東西丟給 null 就對了。而「>」的意思其實是,將「>」左邊輸出的東西重新導向到右邊去。但不止是這樣而已,系統將標準輸入輸出分成三個:標準輸入 (stdin)、標準輸出 (stdout)、以及標準錯誤輸出 (stderr),它們的 fd (file descriptor, 檔案描述子) 分別是 0、1、2。當「>」左邊未指定任何東西時,它會讀取左方程式的標準輸出 (也就是 fd=1) 重新導向給右邊的東西,但是你也可以指定要重新導向的 fd (也就是說「>abc.txt」會等於「1>abc.txt」)。所以可以想見 2>&1 的意思應該是把 fd=2,也就是標準錯誤輸出重新導向給 &1。而這邊的 &1 指的其實就是 fd=1。這邊似乎有點混淆視聽的感覺,為什麼「>」前面的 fd 不需要指定 &,後面的 fd 卻又要加 & 呢?我沒找到確切的原因,但是根據這篇討論,我認為有可能是因為「>」左邊只接受 fd,但右邊所接收的語法卻應該是檔案名稱。因此若用「2>1」其結果會變成「將標準錯誤輸出重新導向給檔案名稱為 "1" 的檔案」,所以我們需要用 & 來告訴系統後面的 "1" 指的是 fd。

接下來,有趣的東西來了。根據「man bash」裡 REDIRECTION 段落裡的說明,重新導向的順序是由左至右。也就是說「>/dev/null 2>&1」會先處理「>/dev/null」再處理「2>&1」。如果聽到這裡你覺得怪的話,再告訴你一件很幹的事。用「2>&1 >/dev/null」並不會將 stderr 導到 /dev/null。「man bash」裡的說明如下:
Note that the order of redirections is significant. For example, the
command

ls > dirlist 2>&1

directs both standard output and standard error to the file dirlist,
while the command

ls 2>&1 > dirlist

directs only the standard output to file dirlist, because the standard
error was duplicated as standard output before the standard output was
redirected to dirlist.

我看到這段的時候重複讀了好幾遍,確定我他媽的沒誤解原文的意思。「ls > dirlist 2>&1」的 stdout 不是先被導到 dirlist 去了嗎?後面再把 stderr 導到 stdout 也會輸出到 dirlist 去,這感覺上像是前面 stdout 的導向到後面仍然有作用。但是「ls 2>&1 > dirlist」先把 stderr 導到 stdout 之後,後面再把 stdout 導向 dirlist,這時 stderr 卻不會跟著被導過去。我對於這點相當困惑,於是在網路上找了一下。發現這篇文章,裡面的回答提到,在對 stdout 做導向時,系統所做的事大約會是這樣:
close(1);
fd=open("results.txt", O_WRONLY, 0);

系統必需先把原本已經打開的 fd=1 關閉,然後再將要導入的檔案開啟,新開啟的檔案就會被指定為 fd=1。這時候再遇到後面 stderr 的導向時,系統會這麼做:
close(2);
fd=dup(1);

一樣是先把 fd=2 關閉,但是這時候要導向的是另一個 fd,所以基本上系統做的事情只是把 stdout 的 fd 複製一份給 stderr。由於 fd 是指向相同的地方,所以這時任何寫入 stderr 的資料當然也會進入已經開啟的 dirlist 裡。那麼倒過來為什麼不行?因為倒過來的話我們會先把 stderr 指向 stdout,但是當 stdout 被導向 dirlist 時會捨棄舊的 fd=1,開啟 dirlist 為新的 fd。這時 stderr 指向的卻還是舊的 stdout 的 fd,因此並不會寫入 dirlist。

由以上看來,系統在做導向的時候會依序把要導入的檔案打開 (若為 fd 則直接複製該 fd),導向完成後才會開始執行程式的輸出。這點完全符合 REDIRECTION 這段的第一句話:
Before a command is executed, its input and output may be redirected using a special notation interpreted by the shell.

重新導向是 Unix 系統上很有力的一個工具,和 pipeline 結合使用可以使得下指令變得很靈活。但是要進行較複雜的運用時就必須更深入了解其中的原理,否則很容易就會出錯了。

留言

Folay寫道…
很不錯的說明耶!
意思是已經開好了新的目標(dirlist),
但stderr並不知道這個目標的存在(仍指向stdout),所以沒東西會被寫到dirlist。
是這樣子嗎?
淺光寫道…
歡迎 : )
對,可以這麼說。
Unknown寫道…
獲益良多

Thx!!
Unknown寫道…
終於搞懂 2>&1 是幹嘛的了
感謝原PO
ijon寫道…
那為什麼不用「&> /dev/null」呢?這樣不是 stdout 跟 stderr 都會到 /dev/null?
淺光寫道…
To jeul: 語法不同而已。我很少用 Shell Script,在你提出 &> 之前我並不曉得這個語法,查到的資料幾乎都是用 2>&1。
後來稍微查了一下,&>似乎是 bash 專用的語法,並非的標準 POSIX 語法,以此看來 2>&1 是較通用的。
Unknown寫道…
但在C language中, &foo好像是代表pointer foo所儲存的記憶體位置, 在linux是不是這樣就不太確定
Unknown寫道…
作者已經移除這則留言。
Unknown寫道…
因為對linux知識有限, 以下只是我用C的pointer作為基本而對linux 這句command的個人猜想:

使用pointer資料儲存, 會分成2部份, 一個是放置實體記憶的位置, 另一個是儲存了這個實體記憶所在的位置資訊. 例如第一個內容可以是文字像是standard output, 第二個就是放置這內容的地址, 例如ff00aabc, 而這個第二個儲存位置的本身地址是abcd1234:

address content
ff00aabc : standard output
eeee1111 : standard error
abcd1234 : ff00aabc

當第一句是使用實體記憶位置ff00aabc, 那下一句無論怎樣改abcd1234位置當中的內容, 都不會影響上一句的結果, 因為上句是直接指向了 how are you, 而不是先指向abcd1234去尋求下一步指向的內容

但如果上一句是指向abcd1234, 那第二句只需要改動abcd1234當中的內容, 例如eeee1111:

abcd1234 : eeee1111

那第一句本來指向standard output, 會改變成指向standard error

例如有一個不存的檔案non-exist.txt
ls non-exist.txt
你會看到錯誤
ls
你會看到檔案被列出
ls non-exist.txt > log
cat log
你會看到內容為空的log
ls non-exist.txt > log 1>&2
你會看到錯誤被記錄在log

意思就是第一句 non-exist.txt > log 可能是在用某個儲存了non-exist.txt內容地址的位置, 暫稱為X, 派給log成為其內容, 而這裡是指standard output, 即是1
第二句 因為 1>&2 令上面的X儲存了1地址的內容換成2, 所以當全句完成, 執行的時侯第一句就會指向X, 從X拿出指各的位置, 因為改成了2, 所以放到log的內容變成了standard error
淺光寫道…
To Gorp Mar: 嗨,感謝你的回應,不過你關於 C 旗標(pointer)的說法有點容易引發誤會,&foo 在 C 語言中的作用為讀取變數 foo 的記憶體位址,與 foo 本身是不是旗標無關。

我也不知道 shell 所用的語法是否有受 C 的旗標語法影響,不過若是以旗標來說,要讀取/設定該位址的變數值時應是使用 *foo 來存取該旗標位址的變數。也許有可能因為命令列中的 * 代表萬用字元所以才用 &,但我覺得還是不要過度解讀比較好。

另外你的例子看起來有點混亂... 「當第一句是使用實體記憶位置ff00aabc, 那下一句無論怎樣改abcd1234位置當中的內容, 都不會影響上一句的結果」這句我不太明白你的指是什麼?若是以指令來看 ls > file1 2>&1,第一句指的是 ls > file1 嗎?「那下一句無論怎樣改abcd1234位置當中的內容」指的是 2>&1 嗎? @@

然後 non-exist.txt 的例子中,「意思就是第一句 non-exist.txt > log 可能是在用某個儲存了non-exist.txt內容地址的位置」,由於 non-exist.txt 並不存在,因此也就不會有某個儲存了 non-exist.txt 內容位址的位置,X 實際上並不存在。再者,此例中的檔案 log 執行後會是空的,並不會如你所說有記錄錯誤。這是因為指令中原本就只有指定 log 作為 stdout,stderr 從頭到尾都沒有變動過,而 ls non-exist.txt 所產生的錯誤當然就是導到原本的 stderr 了。(且最後的 1>&2 又會將 stdout 重新指定到 stderr,實際上無論 ls non-exist.txt 執行的結果為何都不會進到 log 中)
NULL寫道…
作者已經移除這則留言。
NULL寫道…
很棒的說明!!

不過文章消化了好一陣子才看懂囧

希望可以換個簡單或是懶人的方式說明,這樣更能幫助像我這種新手了解,不過也有可能是我中文程度比較差XDD

還是非常感謝大大的說明,目前找到最棒的說明,非您陌屬了!!
淺光寫道…
NULL:
嗨,感謝,當初其實也是一時興起寫下的,雖然有盡量用詳細簡單的方式來說明,不過可能還是需要有點基礎才會覺得比較好懂。真的要讓新手了解的話可能要另外重寫一篇了 ^^"
Unknown寫道…
謝解說!
匿名表示…
很棒的解說,謝謝!
第一次沒認真看,真的好複雜阿,看來要很認真看才能搞懂

這個網誌中的熱門文章

薰衣草茶實驗中...

用 HTML mailto 屬性指定標題與信件內容