先看一道shell命令
java -jar snapshot.jar > snapshot.log 2>&1 &
寫Java的朋友一定對上面的命令很熟悉,相信大部分人都知道>表示的是重定向,那麽什麽是重定向?2>&1又是什麽意思?
要從根兒上說明這個問題,我們有必要好好理解一下「文档描述符」的概念。
文档描述符
1.1. 什麽是文档描述符
文档描述符(File descripter)就是一個整數,這個整數唯一標識了操作系統中某個被打開的“文档”。
1.2. 文档與I/O
說到“文档”必然離不開I/O。
很多人搞不明白I/O到底應該怎麽理解,字面上就是輸入/輸出罷了,但是站在不同角度,其表示的含義也不一樣。
站在計算機的角度,I/O表示的是計算機與外界的交互,交互的對象是硬件設備,輸入輸出自然也指的是和硬件之間的輸入輸出。
站在程序的角度,I/O的含義更寬泛,操作系統與所有能被當作文档的對象之間的交互就是I/O。
Linux的哲學思想是「一切皆文档」,文档(file,例如foo.txt)、管道(pipe)、網路(socket),甚至列印機、顯示器、磁盤以及命令行(terminal)都算是文档。
文档描述符標識的就是這些文档。
文档描述符這個術語通常出現在Unix或類Unix系統中,比如Linux、MacOS以及BSD等。
在Windows系統中,他有另外一個響當當(或者臭名昭著)的名字——句柄(File handle)。
文档描述符原理
2.1. 進程私有文档描述符表
每個進程可以打開多個文档,所以每個進程都會有一個私有的文档描述符表(file descriptors table)。
注:下文稱file descriptors table中的每一個條目爲file descriptor,稱file descriptor中的整數爲fd。
需要注意的是,每個進程的fd 0,1,2已經被佔用(下文會有解釋),之後分配的每個進程的fd從3開始。
進程級的描述符表的每一個條目記錄了當前進程所有打開的文档的文档描述符,進程之間相互獨立,例如一個進程使用了文档描述符99,另一個進程也可以用99。
fd只是個數字而已,操作系統肯定需要根據這個數字來找到實際對應的文档。換句話說,file descripter肯定指向了某個表示真實文档的數據結構,或者能夠再次根據這個數據結構來找到真實文档。
這個數據結構就是「全局文档表」。
2.2. 全局文档表
全局文档表(global file table),顧名思義,就是所有進程共享的一個數據結構。
當用戶進程向内核發起一個針對文档的system call(比如open())時,内核將
允許進程訪問;
向全局文档表(global file table)中插入一個條目,並向進程返回一個指向該條目的一個file descriptor;
進程將file descriptor插入到file descriptors table,並返回其在file descriptors table中的下標,也就是fd。
其實,根據global file table並不能直接找到對應文档進行操作,還需要根據其中的指針找到inode table的數據結構,進而再找到最終文档。但是這個技術細節對我們認識文档描述符沒有什麽作用,於是按下不表了。
2.3. 爲什麽需要文档描述符
進程進行系統調用的時候,内核爲什麽不直接返回指向文档的指針呢?反而多此一舉加了個fd來引用文档。
原因是爲了防止用戶空間的程序隨意讀寫操作系統内核的文档對象。
如果内核直接返回内核中文档對象的地址給進程,進程便可以繞過内核,肆意對該文档進行操作,這樣一來用戶空間和内核空間的劃分便如同虛設。
有了文档描述符之後,由於global file table處於内核空間中,用戶即使擁有fd,也無法得到實際文档對象的地址,除非把fd作爲系統調用的參數來使用,如此一來,控制權又回到了内核手中,也便達到了權限控制的目的。
標準輸入/標準輸出/標準錯誤
前面說到,進程的文档描述符表的前3項已經被默認使用了。
0:標準輸入(stdin)
1:標準輸出(stdout)
2:標準錯誤(stderr)
這些名詞怎麽理解?
我們在Java中使用new Scanner(System.in)接收從鍵盤的輸入,使用System.out.println()向顯示器寫數據,對應C語言分別是scanf()和printf()。
需要明確的是,函數並非直接使用鍵盤和顯示器,而是使用了標準輸入和標準輸出。
說得再通俗一點就是,進程生來就有一個耳朵和兩張嘴,耳朵用來接受標準輸入裡的數據,一個嘴往標準輸出裡邊“說話”,另一張嘴往標準錯誤裡邊“吐槽”。
函數並不知道數據從哪裡來,也不關心數據要到哪裡去,它們只需要從標準輸入讀數據,向標準輸出、標準錯誤中寫數據就行了。
這就是抽象啊,朋友們!
默認情況下,操作系統會把所有鍵盤輸入都發送到標準輸入,會把從標準輸出、標準錯誤中讀到數據發送到顯示器。
爲了方便表示,下文不再將全局文档表畫出,而是用一張表來簡化表示文档描述符和數據流之間的對應關係。
接下來我們就可以解釋文章開頭提的問題了。
輸入輸出重定向
標準輸入/輸出/錯誤在描述表的位置雖然是固定的,但他們指向的數據流是可以改變的。
java -jar snapshot.jar > snapshot.log 2>&1 &
這條指令的意思就是將snapshot.jar程序用>運算符重定向標準輸出,由原本的指向顯示器改爲snapshot.log文档。
2>是用來重定向標準錯誤,因爲標準錯誤在描述符表中的fd就是2,同樣,其實重定向標準輸出也可以表示爲1>,不過一般簡寫爲>。
標準錯誤和標準輸出可以重定向到同一個地方,比如指令中的&1表示的就是標準輸出,2>&1的含義就是重定向標準錯誤到標準輸出表示的數據流中。