2018年12月31日 星期一

重新導向 (2>&1)

To redirect both stdout and stderr to a truncated file, I know to use:
cmd &> file.txt

cmd >>file.txt 2>&1
Bash executes the redirects from left to right as follows:
  1. >>file.txt: Open file.txt in append mode and redirect stdout there.
  2. 2>&1: Redirect stderr to "where stdout is currently going". In this case, that is a file opened in append mode. In other words, the &1 reuses the file descriptor which stdout currently uses.

Source http://ibookmen.blogspot.com/2010/11/unix-2.html

這幾天因為研究 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 結合使用可以使得下指令變得很靈活。但是要進行較複雜的運用時就必須更深入了解其中的原理,否則很容易就會出錯了。

沒有留言: