公佈欄

2016年12月19日 星期一

檔案描述符 (file descriptor) 簡單應用分析

回憶兩個月前寫作業時遇到檔案描述符的相關問題

沒錯 "兩個月前" 不過一直沒有機會發文,所以就拖到了現在

真的對不起 (鞠躬

以下直接進入正題


    exec /bin/sh
  $ ((sudo fdisk -l 3>&1 1>&2 2>&3 3>&- | tee /dev/fd/2) 3>&1 1>&2 2>&3 3>&-)\
    | cat >fdisk.log

在網路上看到這一段指令,相信應該很多人都不知道這段指令最後的結果是什麼

網路上很多文章都沒有很詳細的提到 redirct 和 pipe 的執行順序

直到回憶的朋友丟了一篇文章給我(網址附在文末),才有了大致上的了解

因此才有了這篇筆記

從檔案描述符 (file descriptor) 開始


在開始討論執行結果前

應該先來了解一下 file descriptor 是什麼?

很多人對其的理解: 0 是 stderr 1 是 stdout 2 是 stderr

這個是最基本的認識

不過我們應該考慮的是 "0 1 2" 代表是什麼?


檔案描述符在形式上是一個非負整數。
實際上,它是一個索引值,指向內核為每一個行程所維護的該行程開啟檔案的記錄表。當程式開啟一個現有檔案或者建立一個新檔案時,內核向行程返回一個檔案描述符。但是檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。
from wiki

事實上一般程式在開啟時,會自動開啟三個檔案描述符

第一個為輸入 (因此編號為0)

第二個為正常訊息輸出 (因此編號為1)

第三個為錯誤訊息輸出 (因此編號為2)

我們可以使用 lsof (list open file)指令查看

以最基本的 shell為例 (只列出相關訊息) $ lsof
COMMAND    PID    USER    FD    TYPE      DEVICE     SIZE/OFF      NODE    NAME
     sh    847    uesr    0u    VCHR        0,84     0t208433        84    /dev/pts/0      sh    847    uesr    1u    VCHR        0,84     0t208433        84    /dev/pts/0      sh    847    uesr    2u    VCHR        0,84     0t208433        84    /dev/pts/0

以下解釋各欄位意義:

COMMAND: 程序名稱(指令)

PID: 程序ID

USER: 執行此指令的使用者

FD:(列舉常見的,不過回憶不是很懂)
(1) cwd: current working directory
(2) rtd: root directory
(3) txt: program text (code and data)
(4) mem: memory-mapped file
(5) mmap: memory-mapped device
(6) 數字 + 英文字: <數字為 file descriptor 編號, 英文字為鎖定模式>
    (a) r : 只讀模式
    (b) w: 只寫模式
    (c) u : 讀寫模式

TYPE:(free BSD 中以V開頭的應為 virtual 的意思)
(1)DIR:表示目錄
(2)CHR:表示字符類型
(3)BLK:塊設備類型
(4)UNIX: UNIX 域 socket
(5)FIFO:先進先出 (First In First Out) 隊列
(6)IPv4:網際協議 (IP) socket

DEVICE: 設備號碼

SIZE/OFF: 文件大小/偏移量

NODE: inode 號碼

NAME: 打開的文件 (即 file descriptor 所指向的檔案)

什麼是 pipeline ?


管道 (pipeline) 的功用為在兩個 porcess擔任中介訊息傳遞者

在 shell 命令裡以 "|" 表示

具體功能為將其左方 process 的 1號 fd 導向 pipe

並將右方 process 的 0號 fd 導向 pipe

只不過前者是使用寫入功能,後者使用讀取功能


對於 linux 來說, pipeline 也是一種文件

我們一樣可以使用 lsof 來查看pipe

$ ls | more
^M Suspended
$ lsof
COMMAND    PID    USER    FD    TYPE   DEVICE            SIZE/OFF  NAME
     ls   3560    user    1u    PIPE   0xfffff800029b5730       0  ->0xfffff800029b55d0
   more   3561    user    0u    PIPE   0xfffff800029b55d0   65536  ->0xfffff800029b5730


管道的建立先於程式執行,另外當程式完成工作後,會自己結束,
並不會等待所有程式執行完後再一同結束

什麼是重導向 (redirection)



在瞭解上述內容後,我們來試著產生 stdout 以及 stderr

首先在一個目錄下建立一個文字檔,內容隨意 $ mkdir test
$ cd test
$ echo "hello world!" > test_file
$ cat test_file none
hello world!
cat: none: No such file or directory

我們可以合理推測第一行為 stdout 第二行為 stderr
我們用以下操作來驗證: $ mkdir test
$ cat test_file none 1>/dev/null
cat: none: No such file or directory
$ cat test_file none 2>/dev/null
hello world!

這裡說明一個特別的文件 "/dev/null"

網路上經常將其稱為黑洞

任何輸出導向 /dev/null 即為不顯示也不導向任何文件

我們再來看個稍微複雜的操作: $ cat test_file none 1>/dev/null 2>&1
$ cat test_file none 2>&1 1>/dev/null
cat: none: No such file or directory

以上操作說明了 redirection 是有順序性的

"2>&1" 的意思為將 fd 2指向 fd 1所指向的文件

在第一行指令時, fd 1 和 2 都指向 /dev/null

因此在終端上是沒有輸出的

關於 redirection
  1. 建議 redirection 符號 (< 、 >) 前後不要有空格,以免被shell誤判
  2. > 符號如果左方沒有特別指明,默認為 fd 2
  3. < 符號如果右方沒有特別指明,默認為 fd 0

回到最初的問題


現在我們開始來分析一開始的那行命令

file descriptor 有繼承性,從命令列來解讀時,
括號外對FD做的更動會影響括號內的所有porcess

分析重導向命令時建議畫一個表格,這樣比較易懂

表格的上方回憶習慣標示process名稱

左方標示FD編號

另外為了方便閱讀,FD 預設指向以 "terminal" 表示

* 表示未使用

而 pipe 由左至右分別命名為 pipe1, pipe2, ....

初始狀態                                              
fdiskteecat
FD 0terminalterminalterminal
FD 1terminalterminalterminal
FD 2terminalterminalterminal
FD 3* * *


  $ ((sudo fdisk -l 3>&1 1>&2 2>&3 3>&- | tee /dev/fd/2) 3>&1 1>&2 2>&3 3>&-) \
    | cat >fdisk.log
 

括號外先執行,pipe 又優先於 redirection:
                                             
fdiskteecat
FD 0terminalterminalpipe2
FD 1pipe2pipe2fdisk.log
FD 2terminalterminalterminal
FD 3* * *

將標示完的部分消除:
  $ ((sudo fdisk -l 3>&1 1>&2 2>&3 3>&- | tee /dev/fd/2) 3>&1 1>&2 2>&3 3>&-)
 

其中 3>&1 1>&2 2>&3 3>&- 的意思為 FD 1 和 FD 2 對調
<FD 3 指向 FD 1內容,FD 1 指向 FD 2內容,FD 2 指向 FD 3內容,最後將 FD 3 關閉>
                                             
fdiskteecat
FD 0terminalterminalpipe2
FD 1terminalterminalfdisk.log
FD 2pipe2pipe2terminal
FD 3* * *

處理 pipe1:
  $ sudo fdisk -l 3>&1 1>&2 2>&3 3>&- | tee /dev/fd/2
 

                                             
fdiskteecat
FD 0terminalpipe1pipe2
FD 1pipe1terminalfdisk.log
FD 2pipe2pipe2terminal
FD 3* * *

接下來的命令比較特別
  $ sudo fdisk -l 3>&1 1>&2 2>&3 3>&-
    tee /dev/fd/2

/dev/fd/2 表示該程序的 FD 2

而 tee 對檔案描述符的操作為 "新增一個 FD 指向後面接的參數(一般為檔案),並將 FD 1內容複製一份過去"

以下操作可以當作參考:
$ ls -R / | tee test.txt
^M Suspended
$ lsof
COMMAND    PID    USER    FD    TYPE   DEVICE            SIZE/OFF  NAME
     ls    877    user    1u    PIPE   0xfffff80002684a18       0  ->0xfffff800026848b8
    tee    878    user    0u    PIPE   0xfffff800026848b8   16384  ->0xfffff80002684a18
    tee    878    user    1u    VCHR     0,84             0t48901  /dev/pts/0
    tee    878    user    2u    VCHR     0,84             0t48901  /dev/pts/0
    tee    878    user    3w    VREG     0,78     424016351330304  / (/dev/ada0p2)

                                             
fdiskteecat
FD 0terminalpipe1pipe2
FD 1pipe2※terminalfdisk.log
FD 2pipe1pipe2terminal
FD 3* ※pipe2 *


※ 得到的內容是一樣的,也就是說 fdisk 的 stderr 一樣會導向 cat 的 stdin

如果就輸出位置來討論的話,可以得到以下結果:

terminal: fdisk 的錯誤訊息、cat 的錯誤訊息

fdisk.log: fdisk 的正常訊息、fdisk 的錯誤訊息、tee 的錯誤訊息

redirection 拆解順序
  1. 括弧外
  2. pipe
  3. fd 重導向

參考資料:
    http://wiki.bash-hackers.org/howto/redirection_tutorial
    http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/lsof.html

沒有留言:

張貼留言