home products tech support contact us

 Linux 技術支援    ⇒   基礎篇    進階篇    補腦篇    指令索引    中⇒ENG
版權所有, 引用請註明出處

File descriptor

1.0 簡介 file descriptor (檔案描述符)
       fd 與重定向
       目錄〝/proc/<PID>/fd〞與 fd
1.1 exec 和 fd 重定向
           exec X>FILE :fd X 重定向檔案
           exec X>&Y :fd X 重定向到 fd Y
           exec X< FILE :檔案重定向到 fd X
           exec X<&Y :fd Y 重定向到 fd X
           exec fd X<> FILE :檔案重定向 fd X 並讀寫
           X>&- 或 X<&- :關閉 fd X
1.2誰偷走了 stdin ?


1.0 File Descriptor
有天讀到坊間某一書籍在講解 Shell Script 舉一例可一行一行的讀取檔案的內容,其範例如下:

(範例 ex1.sh)
$ cat ex1.sh
#!/bin/bash
# read〝FILE.txt〞 line by line
while read -r line #←此行是我看起來怪怪的地方之一,〝read〞是讀鍵盤怎可讀檔案?
do
echo $line
done <FILE.txt #←此行是我看起來怪怪的地方之二,〝done <FILE〞是什麼東東???

上例雖只有兩三行但語法怪怪的,個人才疏學淺看不太懂,作者對原理也沒多作說明(大概也是抄來的?)。
好吧!天下文章一大抄看不懂也沒關係,我也是想抄過來套在我的應用,因上例經測試可正常的工作。

我的應用也很簡單只是想每列出一行要按任一鍵才會讀一下行,於是我再加一行指令改寫如下。

(範例 ex2.sh)
$ cat ex2.sh
#!/bin/bash
while read -r line
do
echo $line
read -p "Press any key to continue" -n 1 #←我增加的行
done <FILE.txt

怪了經我加工後的程式 ex2.sh 怎不能正常工作??!兇手是誰?是我看起來怪怪的地方?還是我自行加的那行???

於是我當起鍵盤柯南,誓言一定要把兇手抓起來繩之以法。

挑燈夜戰好幾天, 兇手終於被我抓到,那個兇手就叫〝File Descriptor〞簡稱〝fd〞(中文有人叫〝檔案描述符〞)。

原例 (ex1.sh) 中可一行一行的讀取檔案是 shell 為了簡化語法隱藏了 file descriptor 的相關操作,而我改寫的範例 (ex2.sh)不能正常工作因 shell 隱藏的 file descriptor 操作偷走了 stdin。

國內的書籍有提及〝file descriptor〞是少之又少,就算有點到也沒搔到癢處, 於是把這幾天研究的心得寫下來,一方當自己的備忘另一方面或許其他人遇到同樣問題時可減少摸索,尤其是 shell scripts 中要讀取檔案和要求鍵盤輸入的應用,利用 file descriptor 或許可不動用到 awksed 即可完成任務。

〝file descriptor〞簡單的說就是當 Unix -like 的 OS 在讀取檔案時會把每一檔案編號,此編號是 Kernel 用來追蹤 process 開啟檔案輸出/輸入的索引。

例如用瀏覽器瀏覽此文章時至少已開了20 個檔案(html 檔和多個圖檔),這些檔案會被編號 (例如 100,101,102 ....) 這些當檔案索引的數字就是 file descriptor (fd),但維護這些 file descriptor 是 Kernel 的事一般的使用者不用也不必知道。

^ back on top ^

fd 與重定向
有三個 fd 檔案是永遠開啟的那就是 stdin(鍵盤)、stdout(螢幕)、stderr(錯誤訊息)且 POSIX 標準保留這 3 個檔案的 fd 編號各為 0~2 供使用者操作重定向(redirection)的應用。

fd Number Name Function
0 stdin 標準輸入
1 stdout 標準輸出
2 stderr 標準錯誤

重定向實際上就是把三個永不關閉的 fd 0~2 重定向到其他地方(大部分為檔案或另一 fd ) 。

  Function Example Example note
COMMAND 1> stdout 重定向 echo '123' >fileA fd 1 輸往檔案
COMMAND 1>> stdout 累加重定向 seq 100 200 >> fileA fd 1 累加輸往檔案
COMMAND 2> stderr 重定向 find / -name '*.conf' 2>/dev/null fd 2 輸往檔案
COMMAND 2>> stderr 累加重定向 seq 1 10 >>fileA fd 2 累加輸往檔案
COMMAND 0< stdin 重定向 cat < fileA fd 0 由檔案取代

輸出重定向的語法為 COMMAND [fd]>,其中如省略 fd,預設為 1。
而輸入重定向語法為 COMMAND [fd]<, 如省略 fd,預設為 0 。

重定向也可改變原輸出到 stdout 改為 stderr 反之也可把 stderr 改到 stdout。
其語法為 X>&Y (X 為原 fd, Y 為重定向後的 fd,,X 如省略預設為 1)。以 stderr(2) 重定向到 stdout(1) 為例寫成〝2>&1〞。

  Function Example
2>&1 stderr(2) 重定向 stdout(1) ls -R /home > fileA 2>&1
1>&2 stdout 重定向 stderr find / -name '*readme.txt' 1>&2 2>/dev/null

由於 stdin、stdout、stderr( fd 0~2)這三個檔案是永遠開啟的可直接使用,但如 fd 大於 3 一般要用exec 來開啟。

^ back on top ^

目錄〝/proc/<PID>/fd〞與 fd
process 運行時會產生一 PID ,相對的會映射 fd 到目錄〝/proc/<PID>/fd〞(目錄中的〝<PID>〞為 process 的 PID 編號)此目錄可觀察 file descriptor 的使用情形。

例:
$ seq 1 1000000
1
2
3
4 Ctrl+Z ←按 <Ctrl+Z>暫停

[1]+ Stopped                seq 1 100000000 ←程式被 stoped 了
$ jobs -p ←列出被我們暫停的指令的 PID
$ 2373 ←剛那指令〝seq 1 1000000〞 PID 為 2373
ls -lgG /proc/2373/fd/ ←列出 /proc/<PID>/fd 來觀察 fd 使用情形
total 0
lrwx------ 1 64 2015-04-26 22:28 0 -> /dev/tty1
lrwx------ 1 64 2015-04-26 22:28 1 -> /dev/tty1
lrwx------ 1 64 2015-04-26 22:28 2 -> /dev/tty1

上例中在目錄〝/proc/<PID>/fd/〞內共有 3 個檔案就是 fd,各別為〝0〞、〝1〞和〝2〞, 而這三檔又分別連結到〝/dev/tty1〞(如在圖形介面測試可能為〝/dev/pts/N〞 )。

也就是說上例的 stdin (fd 0)、stdout (fd 1)和 stderr (fd 2) 都是 tty (終端機)或 /dev/pts/N (虛擬終端機)。

Ok 我們把上述的實驗改寫成 seq 1 1000000 > fileA 2>&1 再觀察,其結果如下。
lrwx------ 1 64 2015-04-26 15:04 0 -> /dev/tty1
l-wx------ 1 64 2015-04-26 15:04 1 -> /home/basalt/fileA
l-wx------ 1 64 2015-04-26 15:04 2 -> /home/basalt/fileA

上例的 stdin (fd 0)一樣是 tty,但 stdout (fd 1)和 stderr (fd 2) 都變成〝fileA〞也就是 stdout 和 stderr 都輸往〝fileA〞這一檔案。

所以如某指令可能經管線重定向被搞的暈頭轉向時,觀察目錄〝/proc/<PID>/fd/〞所提供的 file descriptor 資訊就可一目了然。

所以如再改寫成 seq 1 100 > fileB >&2 ,運算完時檔案〝fileB〞的內容是空的,為什麼呢?自己觀察〝/proc/<PID>/fd/〞吧!

^ back on top ^



  1.1 exec 和 fd 重定向
扣除 fd 0(stdin)、fd 1(stdout) 和 fd 2(stderr) 和系統保留的 fd 10~255,一般的使用者建議只可開啟並使用 fd 3 ~ 9。
(fd 255 一般留給 shell script,而 process substitution 可能會用到 fd 63 或 fd 62,故系統自行運用的 fd 10~255 最好不要拿來用以免相沖。

但要使用 fd 3~9 要用 exec,在 processexec 功能為關掉 parent process 直接跑 child-process。而 exec 另一重要的功能為 fd 重定向。

重定向有兩種,一種為 fd 重定向到另一 fd ,另一為 fd 重定向到檔案各說明如下:
綜合其各組合可能的用法如下:

^ back on top ^

1.2 誰偷走了 stdin ?
回頭觀察我覺得詭異的範例 ex1.sh ,執行時其目錄〝/proc/<PID>/fd〞觀察結果如下:
lr-x------ 1 64 2015-04-26 14:45 0 -> /home/basalt/FILE.txt ←stdin 變檔案
lrwx------ 1 64 2015-04-26 14:45 1 -> /dev/tty1
lrwx------ 1 64 2015-04-26 14:45 10 -> /dev/tty1 ←多開啟了 fd 10
lrwx------ 1 64 2015-04-26 14:45 2 -> /dev/tty1
lr-x------ 1 64 2015-04-26 14:45 255 -> /home/basalt/ex1.sh

因 stdin 變成了檔案,又多開啟了 fd 10,故可大膽推測遇到 ex1.sh 的〝done < FILE〞這類語法時 shell 會自作聰明把 stdin 來源變檔案,還原被 shell 隱藏的 file descriptor 相關操作推論如下。
exec 10<&0 #←shell 隱藏操作的部分 (備份 fd 0 到 fd10)
exec < FILE.txt #←shell 隱藏操作的部分(stdin=file)

while read -r line
do
echo $line
done #←原始指令為 done<FILE.txt

exec 0<&10 #←shell 隱藏的部分 (從 fd 10 復原 fd 0)

因〝done < FILE〞 把 stdin 變成了檔案,因 stdin 被檔案盜走了,所以我改寫的 ex2.sh 用指令 read 不能再讀取鍵盤的原因在此!,改寫後即可讀檔案又可讀 stdin (鍵盤)範例 如下。 (ex6.sh)

$ cat ex6.sh
#!/bin/bash
# read〝FILE.txt〞 line by line

exec 7<FILE.txt # ← fd 7=FILE.txt)

while read -u 7 line #← read 讀取由 stdin 改為 fd 7)
do
echo $line
read -p "Press any key to continue" -n 1
done

有天在網路上無意看到不少人也遇到 stdin 被偷走但不知所以故上網發文求救,比較經典的案例此網友原是想寫個 shell script 來列出工作目錄內的檔案然後詢問要不要刪除,但不知原因不能工作而 po 文討救兵。

此有問題的 shell script 程式如下:
$ cat ex7.sh
#!/bin/bash
while read file_name
do
rm -iv $file_name
done < <(ls)

如果 user 已熟 fd 相關操作了,應能幫忙此網友到找到原因 (溫馨提示,最後那個〝<(ls)〞是 process substitution)
(修正後可工作的程式參考 [註 1.1a])


^ back on top ^





[註1.1] 範例來源 Advanced Bash-Scripting Guide

[註1.1a]

$ cat ex8.sh
#!/bin/bash
exec 3<&0
while read file_name
do
    rm -iv $file_name <&3
done < <(ls)

# 如要排除此目錄內還有目錄而產生錯誤訊息,最後一行可改如下
# done < <(ls -F | grep -v '/$')