dup()和dup2()解惑

1. 文件描述符在內核中數據結構

    在具體說dup/dup2之前, 我認爲有必要先了解一下文件描述符在內核中的形態。

一個進程在此存在期間,會有一些文件被打開,從而會返回一些文件描述符,從shell

中運行一個進程,默認會有3個文件描述符存在(0、1、2), 0與進程的標準輸入相關聯,

1與進程的標準輸出相關聯,2與進程的標準錯誤輸出相關聯,一個進程當前有哪些打開

的文件描述符可以通過/proc/進程ID/fd目錄查看。 下圖可以清楚的說明問題:

  進程表項
————————————————

   fd標誌 文件指針
      _____________________
fd 0:|________|____________|————> 文件表
fd 1:|________|____________|
fd 2:|________|____________|
fd 3:|________|____________|
     |     …….         |
     |_____________________|

                圖1
       
文件表中包含:文件狀態標誌、當前文件偏移量、v節點指針,這些不是本文討論的

重點,我們只需要知道每個打開的文件描述符(fd標誌)在進程表中都有自己的文件表

項,由文件指針指向。

2. dup/dup2函數

APUE和man文檔都用一句話簡明的說出了這兩個函數的作用:複製一個現存的文件描述符。

#include <unistd.h>

int dup(int oldfd);

int dup2(int oldfd, int newfd);

從圖1來分析這個過程,當調用dup函數時,內核在進程中創建一個新的文件描述符,此

描述符是當前可用文件描述符的最小數值,這個文件描述符指向oldfd所擁有的文件表項。

  進程表項
————————————————

   fd標誌 文件指針
      _____________________
fd 0:|________|____________|                   ______
fd 1:|________|____________|—————-> |      |
fd 2:|________|____________|                  |文件表|
fd 3:|________|____________|—————-> |______|
     |     …….         |
     |_____________________|

                圖2:調用dup後的示意圖

如圖2 所示,假如oldfd的值爲1, 當前文件描述符的最小值爲3, 那麼新描述符3指向

描述符1所擁有的文件表項。

dup2和dup的區別就是可以用newfd參數指定新描述符的數值,如果newfd已經打開,則

先將其關閉。如果newfd等於oldfd,則dup2返回newfd, 而不關閉它。dup2函數返回的新

文件描述符同樣與參數oldfd共享同一文件表項。

APUE用另外一個種方法說明了這個問題:

實際上,調用dup(oldfd);

等效與
        fcntl(oldfd, F_DUPFD, 0)

而調用dup2(oldfd, newfd);

等效與
        close(oldfd);
        fcntl(oldfd, F_DUPFD, newfd);

3. CGI中dup2

寫過CGI程序的人都清楚,當瀏覽器使用post方法提交表單數據時,CGI讀數據是從標準

輸入stdin, 寫數據是寫到標準輸出stdout(c語言利用printf函數)。按照我們正常的理

解,printf的輸出應該在終端顯示,原來CGI程序使用dup2函數將STDOUT_FINLENO(這個

宏在unitstd.h定義,爲1)這個文件描述符重定向到了連接套接字。

dup2(connfd, STDOUT_FILENO); /*實際情況還涉及到了管道,不是本文的重點*/

如第一節所說, 一個進程默認的文件描述符1(STDOUT_FILENO)是和標準輸出stdout相

關聯的,對於內核而言,所有打開的文件都通過文件描述符引用,而內核並不知道流的

存在(比如stdin、stdout),所以printf函數輸出到stdout的數據最後都寫到了文件描述

符1裏面。至於文件描述符0、1、2與標準輸入、標準輸出、標準錯誤輸出相關聯,這

只是shell以及很多應用程序的慣例,而與內核無關。

用下面的流圖可以說明問題:(ps: 雖然不是流圖關係,但是還是有助於理解)

printf -> stdout -> STDOUT_FILENO(1) -> 終端(tty)

printf最後的輸出到了終端設備,文件描述符1指向當前的終端可以這麼理解:

STDOUT_FILENO = open("/dev/tty", O_RDWR);

使用dup2之後STDOUT_FILENO不再指向終端設備, 而是指向connfd, 所以printf的

輸出最後寫到了connfd。是不是很優美?:)

4. 如何在CGI程序的fork子進程中還原STDOUT_FILENO

如果你能看到這裏,感謝你的耐心, 我知道很多人可能感覺有點複雜, 其實

複雜的問題就是一個個小問題的集合。所以弄清楚每個小問題就OK了,第三節中

說道,STDOUT_FILENO被重定向到了connfd套接字, 有時候我們可能想在CGI程序

中調用後臺腳本執行,而這些腳本中難免會有一些輸入輸出, 我們知道fork之後,

子進程繼承了父進程的所有文件描述符,所以這些腳本的輸入輸出並不會如我們願

輸出到終端設備,而是和connfd想關聯了,這個顯然會擾亂網頁的輸出。那麼如何

恢復STDOUT_FILENO和終端關聯呢?

方法1:在dup2之前保存原有的文件描述符,然後恢復。

代碼實現如下:

savefd = dup(STDOUT_FILENO); /*savefd此時指向終端*/

dup2(connfd, STDOUT_FILENO);   /*STDOUT_FILENO(1) 被重新指向connfd*/

…..  /*處理一些事情*/

dup2(savefd, STDOUT_FILENO);  /*STDOUT_FILENO(1) 恢復指向savefd*/

很遺憾CGI程序無法使用這種方法, 因爲dup2這些不是在CGI程序中完成的,而是在

web server中實現的,修改web server並不是個好主意。

方法2: 追本溯源,打開當前終端恢復STDOUT_FILENO。

分析第三節的流圖, STDOUT_FILENO是如何和終端關聯的? 我們重頭做一遍不就行

了, 代碼實現如下:

ttyfd = open("/dev/tty", O_RDWR);

dup2(ttyfd, STDOUT_FILENO);

close(ttyfd);

/dev/tty是程序運行所在的終端, 這個應該通過一種方法獲得。實踐證明這種方法

是可行的,但是我總感覺有些不妥,不知道爲什麼,可能一些潛在的問題還沒出現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章