0x01 什么是標準輸入輸出
準確來講,該問題應(yīng)該是什么是標準輸入、標準輸出、標準錯誤輸出。
在linux下,它們其實就是三個文件描述符,其中標準輸入是0,標準輸出是1,標準錯誤輸出是2。
該定義我們也可以在各個編程語言中看到。
比如,這是rust標準輸入、標準輸出、標準錯誤輸出的定義:
其中,libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO對應(yīng)的值分別為:
比如,這是go標準輸入、標準輸出、標準錯誤輸出的定義:
其中,syscall.Stdin, syscall.Stdout, syscall.Stderr對應(yīng)的值分別為:
其他編程語言中也有類似的定義,感興趣的話可以自己看下。
0x02 為什么是0/1/2
這只是一種約定,linux內(nèi)核并不會對0/1/2文件描述符做任何特殊處理,之所以定義為0/1/2,是因為進程文件描述符是從0開始依次遞增的,所以就用了前三個。
0x03 為什么要有這種約定
這是因為,只有有了這種約定,linux世界的各個組件之間才能相互合作。
比如,在terminal emulator,即終端模擬器,中啟動bash子進程時,因為知道bash的標準輸入/輸出/錯誤輸出分別是0/1/2,那它就可以把bash子進程的0/1/2文件描述符都指向自己,這樣當bash做標準輸入輸出操作時,它其實都是在和terminal emulator交互。
又比如,當bash要執(zhí)行某個程序時,因為它知道目標程序所使用的編程語言,把標準輸入/輸出/錯誤輸出分別定義為0/1/2,這樣當我們要求bash把目標程序的標準輸入/輸出/錯誤輸出重定向到其他文件時,bash只需要修改目標程序進程的0/1/2文件描述符的文件指向就好了,非常簡單。
諸如此類。
0x04 為什么是三個文件描述符而不是一個
上面我們講到,bash進程的0/1/2文件描述符都指向terminal emulator,其實這個說法并不準確,它們真正指向的是terminal emulator向內(nèi)核分配的pty數(shù)據(jù)通道的slave端,這個在上篇文章?為什么Ctrl-C會中斷當前運行程序 中有詳細講過。
但不管怎樣,bash進程的0/1/2文件描述符,都是指向內(nèi)核中的同一個文件實例,而該文件實例最終又指向了terminal emulator。
同樣,bash中運行的其他程序,默認情況下,其進程的0/1/2文件描述符,是繼承自bash的,所以也同樣都指向了同一個terminal emulator。
既然進程的0/1/2文件描述符,默認都指向同一個terminal emulator,那為什么不只用一個文件描述符來表示它們的標準輸入/輸出/錯誤輸出,而是要用三個呢,這不是浪費了進程的文件描述符嗎?
原因也很簡單,這樣做更靈活。
雖然默認情況下,進程的0/1/2都指向了同一個terminal emulator,但它給了我們一種能力,使我們可以把進程的標準輸入/輸出/錯誤輸出指向不同的文件。
比如,我們在bash中執(zhí)行 ./hello > a.log,就會把hello程序的標準輸出重定向到了a.log里,而標準輸入和標準錯誤輸出,還是指向的原來的terminal emulator。
0x05 什么是文件描述符
在linux世界里,一切皆文件。
比如我們創(chuàng)建一個socket,創(chuàng)建一個epoll實例,又或者是打開一個普通文件,所有的這些操作創(chuàng)建的目標對象,在內(nèi)核里最終都是以一個file實例來表示的,該file實例會存放到進程的一個文件數(shù)組中,而該數(shù)組的下標,就是文件描述符,即fd。
該fd值,會隨著我們使用的系統(tǒng)調(diào)用,返回給用戶程序,當用戶程序想對目標文件進行各種操作時,執(zhí)行該操作對應(yīng)的系統(tǒng)調(diào)用,把fd值再傳給內(nèi)核,這樣內(nèi)核就可以根據(jù)該fd找到對應(yīng)的file實例,進而就可以執(zhí)行對應(yīng)的操作了。
0x06 0/1/2具體指向哪里
一個進程的0/1/2文件描述符具體指向什么文件,是受執(zhí)行環(huán)境及命令參數(shù)影響的,下面我們就舉幾個常見的例子,再配合一些圖,來看看0/1/2具體指向哪里。
0x07 在terminal emulator中執(zhí)行hello程序
上圖中,我們在terminal emulator中執(zhí)行hello程序,該操作產(chǎn)生的各種數(shù)據(jù),是按圖中實線箭頭方向流動的。
我們在terminal emulator中輸入./hello命令,該命令沿著內(nèi)核pty數(shù)據(jù)通道,到達bash的標準輸入。
bash從標準輸入中讀取./hello命令,然后調(diào)用fork函數(shù),新建一個子進程,用于執(zhí)行hello程序。
hello程序執(zhí)行時,會先向標準輸出寫hello字符串,然后再向標準錯誤輸出寫world字符串。
這兩個字符串會沿著內(nèi)核pty數(shù)據(jù)通道,到達terminal emulator的pty master fd。
terminal emulator從pty master fd中讀取這些字符串,并顯示在界面上。
這種情況下,hello進程的0/1/2文件描述符,都是指向內(nèi)核pty數(shù)據(jù)通道的slave端,并且通過該pty數(shù)據(jù)通道和terminal emulator交互。
0x08 在ssh中執(zhí)行hello程序
上圖中,我們先用ssh命令登陸到機器2,然后再在terminal emulator中輸入./hello命令,圖中實線表示該操作產(chǎn)生數(shù)據(jù)的流動方向。
當我們在terminal emulator中輸入./hello命令后,該命令會沿著內(nèi)核pty數(shù)據(jù)通道,到達ssh進程的標準輸入。
ssh進程從標準輸入中讀取到./hello命令,然后將其寫到socket fd里。
然后,該命令會沿著socket fd指向的tcp連接,到達機器2的對應(yīng)socket端。
在機器2上,sshd進程從它的socket fd中讀取到./hello命令,然后將其寫到pty master fd中。
這樣,該命令又會沿著機器2的內(nèi)核pty數(shù)據(jù)通道,到達bash進程的標準輸入。
機器2上的bash進程,從標準輸入中讀到該命令,然后調(diào)用fork函數(shù),創(chuàng)建一個子進程,用于執(zhí)行hello程序。
hello程序執(zhí)行時,會寫hello到標準輸出,寫world到標準錯誤輸出,這兩個字符串又會沿著機器2的內(nèi)核pty數(shù)據(jù)通道,到達sshd進程的pty master fd。
sshd進程從pty master fd中讀取到hello進程輸出的內(nèi)容,并寫到socket fd里。
該數(shù)據(jù)又沿著socket fd指向的tcp連接,最終會到達機器1對應(yīng)的socket端。
機器1中的ssh進程,從socket fd里讀取到hello程序的輸出內(nèi)容,并將其寫到標準輸出。
該數(shù)據(jù)會沿著機器1的內(nèi)核pty數(shù)據(jù)通道,到達terminal emulator的pty master fd。
terminal emulator從pty master fd中讀取到對應(yīng)的數(shù)據(jù),最終將其顯示在界面上。
以上就是這張圖的完整流程。
在這種情況下,hello進程的0/1/2文件描述符,指向的都是機器2上的pty數(shù)據(jù)通道的slave端,該端會再經(jīng)過一系列的鏈路,最終和機器1上的terminal emulator連接起來。
也就是說,我們在機器1上的terminal emulator中輸入內(nèi)容,最終會被機器2上的hello進程從標準輸入讀出來,而機器2上hello進程的各種輸出,最終會被傳送到機器1上的terminal emulator進程,并在其界面上顯示出來。
0x09 在ssh中執(zhí)行hello程序并重定向標準輸出
這種情況和上面講的情況基本類似,只是額外把hello進程的標準輸出重定向到了a.log文件,這里就不多講了,具體可見圖中內(nèi)容。
編輯:黃飛
?
評論
查看更多