徹底搞懂shell的高級I/O重定向


bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.html


基本的重定向功能想必都理解。本文對shell環境下的IO重定向稍作深入,相信看完后,能夠徹底理解 >file 2>&1 。

文件描述符(file description,fd)

文件描述符是IO重定向中的重要概念。文件描述符使用數字表示,它指明了數據的流向特征。

軟件設計認為,程序應該有一個數據來源、數據出口和報告錯誤的地方。在Linux系統中,它們分別使用描述符0、1、2來表示,這3個描述符默認的目標文件(設備)分別是/dev/stdin、/dev/stdout、/dev/stderr,它們分別是各個終端字符設備的軟鏈接。

[root@mariadb ~]# ll /dev/std*
lrwxrwxrwx 1 root root 15 Apr  2 07:57 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Apr  2 07:57 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Apr  2 07:57 /dev/stdout -> /proc/self/fd/1

[root@mariadb ~]# ll /proc/self/fd/
total 0
lrwx------ 1 root root 64 Apr  6 03:53 0 -> /dev/pts/2
lrwx------ 1 root root 64 Apr  6 03:53 1 -> /dev/pts/2
lrwx------ 1 root root 64 Apr  6 03:53 2 -> /dev/pts/2
lr-x------ 1 root root 64 Apr  6 03:53 3 -> /proc/14038/fd

在Linux中,每一個進程打開時都會自動獲取3個文件描述符0、1和2,分別表示標准輸入、標准輸出、和標准錯誤,如果要打開其他文件,則文件描述符必須從3開始標識。對於我們人為要打開的描述符,建議使用9以內的描述符,超過9的描述符可能已經被系統內部分配給其他進程。

文件描述符說白了就是系統為了跟蹤這個打開的文件而分配給它的一個數字,這個數字和文件綁定在一起,數據流入描述符的時候也表示流入文件。

而Linux中萬物皆文件,這些文件都可以分配描述符,包括套接字。

程序在打開文件描述符的時候,有三種可能的行為:從描述符中讀、向描述符中寫、可讀也可寫。從lsof的FD列可以看出程序打開這個文件是為了從中讀數據,還是向其中寫數據,亦或是既讀又寫。例如,tail命令監控文件時,就是打開文件從中讀數據的(3r的r是read,w是write,u是read and write)。

[root@mariadb ~]# lsof -n | grep "/a.sh" | column -t                 
tail  13563  root  3r  REG  8,2  182  69632966  /root/a.sh

文件描述符的復制(duplicate)

文件描述符的復制表示復制文件描述符到另一個文件描述符中以作其副本。使用"&"進行復制。

[n]<&word :將文件描述符n復制於word 代表的文件或描述符。可以理解為文件描述符n重用word代表的文件或描述符,即word原來對應哪個文件,現在n作為它的副本也對應這個文件。n不指定則默認為0(標准輸入就是0),表示標准輸入也將輸入到word所代表的文件或描述符中。 

[n]>&word :將文件描述符n復制於word 代表的文件或描述符。可以理解為文件描述符n重用word代表的文件或描述符,即word原來對應哪個文件,現在n作為它的副本也對應這個文件。n不指定則默認為1(標准輸出就是1),表示標准輸出也將輸出到word所代表的文件或描述符中。

例如,3>&1表示fd=3復制於fd=1,而fd=1目前的重定向目標文件是/dev/stdout(fd=1指向與輸出設備是默認的),因此fd=3也重定向到/dev/stdout,以后進程將數據寫入fd=3的時候,將直接輸出到屏幕。這里的3>&1等價於3>&/dev/stdout。如果用"復制"來理解,就是fd=3是當前fd=1的一個副本,即指向/dev/stdout設備。如果后面改變了fd=1的輸出目標(如file1),由於fd=3的目標仍然是/dev/stdout,所以可以拿fd=3來還原fd=1使其目標變回/dev/stdout。

(fd=1) --> /dev/stdout
  |
 3>&1
 \|/
(fd=3) --> /dev/stdout

關於文件描述符的duplicate

在操作系統(或C)中,對於實體文件的文件描述符來說,文件描述符是用來描述它所指向的實體文件的。例如fd=5指向文件a.txt。復制(duplicate)實際上是執行dup()函數,表示創建另一個文件描述符(例如fd=6),指向同一個底層對象,例如指向同一個實體文件。這時fd=5和fd=6都將指向a.txt。

在shell中,我們將文件描述符和實體文件的關聯關系(或者稱為指向的關系)稱為重定向,其實用更底層的指向關系更容易理解。例如,"3>&1"表示復制fd=1,使得fd=3和fd=1都指向同一個對象,也就是stdout。

再例如,cat <&1表示fd=0復制於fd=1上,而此時fd=1的重定向文件是/dev/stdout,所以fd=0也指向這個/dev/stdout文件,而cat從fd=0中讀取標准輸入,於是/dev/stdout既是標准輸入設備,也是標准輸出設備,也就是說進程從/dev/stdout(屏幕)接受輸入,輸入后再直接輸出到/dev/stdout。以下是結果:

[root@mariadb ~]# cat <&1
q   # 進入交互式,輸入數據
q   # 直接輸出

最后需要說明的是一種特殊情況,如果是>&word,且word不是一個數值,比如 echo haha >&/tmp/a.log ,那么>&word&>word是等價的,都表示>word 2>&1。參考man bash的"Redirecting Standard Output and Standard Error"段落。

重定向順序很重要:">file 2>&1"和"2>&1 >file"

想必很多人都知道>file 2>&1的作用,它等價於&>file,表示標准輸出和標准錯誤都重定向到file中。那它和2>&1 >file有什么區別呢?

首先解釋>file 2>&1。這里分兩個過程:先打開file,再將fd=1重定向到file文件上,這樣file文件就成了標准輸出的輸出目標;之后再將fd=2復制於fd=1,而fd=1此時已經重定向到file文件上,因此fd=2也重定向到file上。所以,最終的結果是標准輸出重定向到file上,標准錯誤也重定向到file上。

再解釋2>&1 >file。這里也分兩個過程:先將fd=2復制於fd=1,而此時fd=1重定向的文件是默認的/dev/stdout,所以fd=2也重定向到/dev/stdout;之后再將fd=1重定向到file文件上。也就是說,這里的標准錯誤和標准輸出仍然是分開輸出的,只不過是使用/dev/stdout替代了/dev/stderr,使用file替代了/dev/stdout。所以,最終的結果是標准錯誤輸出到/dev/stdout,即屏幕上,而標准輸出將輸出到file文件中。

可以使用下面的命令來測試2>&1 >file。第一個ls命令是正確的,結果輸出到/tmp/a.log中,第二個ls命令是錯誤的,結果將直接輸出到屏幕上。

[root@mariadb ~]# ls /boot 2>&1 >/tmp/a.log
[root@mariadb ~]# ls sjdfk 2>&1 >/tmp/a.log
ls: cannot access sjdfk: No such file or directory

改變當前shell環境的重定向目標

如果在命令中直接改變重定向的位置,那么命令執行結束的時候描述符會自動還原。正如上面的ls /boot 2>&1 >/tmp/a.log命令,在ls執行結束后,fd=2還原回默認的/dev/stderr,fd=1還原回默認的/dev/stdout。

但是我們可以通過exec程序直接在當前的shell環境下改變重定向目標,只有在當前shell退出的時候才會釋放描述符的綁定。

例如:下面的命令將標准錯誤fd=2指向fd=3對應的文件上。

exec 2>&3

因此,我們可能在一段程序執行結束后,需要將描述符還原到原來的位置,並關閉不再需要的描述符。畢竟描述符也是資源,是有限的(ulimit -n)。

關閉文件描述符

[n]>&-
[n]<&-

關閉文件描述符的方式是將 [n]>&word 和 [n]<&word 中的word使用符號"-",這表示釋放fd=n描述符,且關閉其指向的文件。

打開文件

 [n]<> filename :打開filename,並指定其文件描述符為n,該描述符是可讀、可寫的描述符。若不指定n則默認為0,若filename文件不存在,則先創建filename文件。

例如:

[root@mariadb ~]# exec 3<> /tmp/a.log
[root@mariadb ~]# lsof -n | grep "/a.log" | column -t 
bash  13637  root  3u  REG  8,2  292018  69632965  /tmp/a.log

如果再exec 1>&3將fd=1復制於fd=3,那么/tmp/a.log就成了標准輸出的目標。

文件描述符的移動

文件描述符的移動表示將文件描述符1移動到描述符2上,同時關閉文件描述符1。

 [n]>&digit- :將文件描述符digit代表的輸出文件移動到n上,並關閉digit值的描述符。

 [n]<&digit- :將文件描述符digit代表的輸入文件移動到n上,並關閉digit值的描述符。

例如:

[root@mariadb ~]# exec 3<> /tmp/a.log
[root@mariadb ~]# lsof -n | grep "/a.log" | column -t 
bash  13637  root  3u  REG  8,2  292018  69632965  /tmp/a.log
[root@mariadb
~]# exec 1>&3- # 將3移動到1上,關閉3 [root@mariadb ~]# lsof -n | grep "/a.log" | column -t # 在另一個bash窗口查看 bash 13637 root 1u REG 8,2 292018 69632965 /tmp/a.log

可見,fd=3移動到fd=1后,原本與fd=3關聯的/tmp/a.log已經關聯到fd=1上。

經典示例

(1). 示例一:

以下是《Advanced Bash-Scripting Guide》中的示例:

echo 1234567890 > File # (1).寫字符串到"File".
exec 3<> File          # (2).打開"File"並且給它分配fd 3.
read -n 4 <&3          # (3).只讀4 個字符.
echo -n . >&3          # (4).寫一個小數點.
exec 3>&-              # (5).關閉fd 3.
cat File               # (6).1234.67890

(1)向文件File中寫入幾個字符。

(2)打開文件File以備read/write,並分配fd=3給該文件。

(3)將fd=0復制於fd=3上,而fd=3的重定向目標為File,所以fd=0的目標也是File,即從File中讀取數據。這里讀取4個字符,由於read命令中沒有指定變量,因此分配給默認變量REPLY。注意,這個命令執行結束后,fd=0的重定向目標會變回/dev/stdin。

(4)將fd=1復制於fd=3上,而fd=3的重定向目標文件為File,所以fd=1的目標也是File,即數據寫入到File中。這里寫入一個小數點。注意,這個命令結束后,fd=1的重定向目標回變回/dev/stdout。

(5)關閉fd=3,這也會關閉其指向的文件File。

(6)File文件中已經寫入了一個小數點。如果此時執行echo $REPLY,將輸出"1234"。

(2). 示例二:關於描述符恢復、關閉

exec 6>&1                   # (1)
exec > /tmp/file.txt        # (2)
echo "---------------"      # (3)
exec 1>&6 6>&-              # (4)
echo "==============="      # (5)

(1)首先將fd=6復制於fd=1,此時fd=1的重定向目標為/dev/stdout,因此fd=6的重定向目標為/dev/stdout。
(2)將fd=1重定向到/tmp/file.txt文件。此后所有標准輸出都將寫入到/tmp/file.txt中。
(3)寫入數據。該數據將寫入到/tmp/file.txt中。
(4)將fd=1重新復制回fd=6,此時fd=6的重定向目標為/dev/stdout,因此fd=1將恢復到/dev/stdout上。最后將fd=6關閉。
(5)寫入數據,這段數據將輸出在屏幕上。

可能你會疑惑,為什么要先將fd=1復制於fd=6,再用fd=6來恢復fd=1,恢復的時候直接將fd=1重定向回/dev/stdout不就可以了嗎?

實際上,這里借用fd=6這個中轉描述符是為了方便操作。可以不用它,但是在恢復fd=1的重定向目標的時候,應該重定向到`/dev/{偽終端字符設備}`上,而不是/dev/stdout,因為/dev/stdout是軟鏈接,其目標指向/proc/self/fd/1,但該文件還是軟鏈接,它指向/dev/{偽終端字符設備}。同理/dev/stdin和/dev/stderr都一樣。

因此,如果你當前所在的終端如果是pts/2,那么可以使用下面的命令來實現上面同樣的功能:

exec > /tmp/file.txt
echo "---------------"
exec >/dev/pts/2
echo "==============="

exec >/dev/tty # 這樣更方便

如果不借用fd=6這個中轉描述符,你要先去獲取並記住當前shell所在的終端,很不方便。但可以使用/dev/tty這個文件來表示當前所在終端,這會方便的多。

但如果要恢復的不是終端相關的文件,那么可能就只能通過文件描述符的備份、還原來恢復了。

最后給張描述符復制、恢復的過程實例圖:

使用變量作為文件描述符

有時候一些特殊的需求下,可能想要使用變量來保存所分配的文件描述符,從而讓多個手動打開的文件夾描述符不至於前后混亂。

要用變量保存文件描述符,可以采用如下方式:

fd=3
eval "exec ${fd}<> /tmp/a.log"
lsof -n | grep a.log

在bash 4.1之后,bash自身提供了變量文件描述符的功能,只要在需要分配文件描述符的時候將原來的fd指定為 fdvar 即可創建這個變量,在分配文件描述符后會自動將其保存到變量fdvar中。使用這種模式時,文件描述符是從10開始分配的,所以fdvar是大於等於10的值。

exec {fd1}<> /tmp/a.log
echo $fd1 # 輸出:10

exec {fd2}<> /tmp/a.log
echo $fd2 # 輸出:11


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM