天天看點

dup 與 dup2 的作用

dup 與 dup2 的作用

dup和dup2也是兩個非常有用的調用,它們的作用都是用來複制一個檔案的描述符。它們經常用來重定向程序的stdin、stdout和stderr。這兩個函數的原型如下所示:

#include

    int dup( int oldfd );

    int dup2( int oldfd, int targetfd )

    利用函數dup,我們可以複制一個描述符。傳給該函數一個既有的描述符,它就會傳回一個新的描述符,這個新的描述符是傳給它的描述符的拷貝。這意味着,這兩個描述符共享同一個

[url=http://www.pcdog.com/special/1002/index.html]資料結構[/url]

。例如,如果我們對一個檔案描述符執行lseek操作,得到的第一個檔案的位置和第二個是一樣的。下面是用來說明dup函數使用方法的代碼片段:

    int fd1, fd2;

    ...

fd2 = dup( fd1 );

    需要注意的是,我們可以在調用fork之前建立一個描述符,這與調用dup建立描述符的效果是一樣的,子程序也同樣會收到一個複制出來的描述符。

dup2函數跟dup函數相似,但dup2函數允許調用者規定一個有效描述符和目标描述符的id。dup2函數成功傳回時,目标描述符(dup2函數的第

二個參數)将變成源描述符(dup2函數的第一個參數)的複制品,換句話說,兩個檔案描述符現在都指向同一個檔案,并且是函數第一個參數指向的檔案。下面

我們用一段代碼加以說明:

     int oldfd;

    oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );

    dup2( oldfd, 1 );

    close( oldfd );

本例中,我們打開了一個新檔案,稱為“app_log”,并收到一個檔案描述符,該描述符叫做fd1。我們調用dup2函數,參數為oldfd和1,這會

導緻用我們新打開的檔案描述符替換掉由1代表的檔案描述符(即stdout,因為标準輸出檔案的id為1)。任何寫到stdout的東西,現在都将改為寫

入名為“app_log”的檔案中。需要注意的是,dup2函數在複制了oldfd之後,會立即将其關閉,但不會關掉新近打開的檔案描述符,因為檔案描述

符1現在也指向它。

    下面我們介紹一個更加深入的示例代碼。回憶一下本文前面講的指令行管道,在那裡,我們将ls –1指令的标準輸出作為标準輸入連接配接到wc –l指令。接下來,我們就用一個C程式來加以說明這個過程的實作。代碼如下面的示例代碼3所示。

在示例代碼3中,首先在第9行代碼中建立一個管道,然後将應用程式分成兩個程序:一個子程序(第13–16行)和一個父程序(第20–23行)。接下來,

在子程序中首先關閉stdout描述符(第13行),然後提供了ls

–1指令功能,不過它不是寫到stdout(第13行),而是寫到我們建立的管道的輸入端,這是通過dup函數來完成重定向的。在第14行,使用dup2

函數把stdout重定向到管道(pfds[1])。之後,馬上關掉管道的輸入端。然後,使用execlp函數把子程序的映像替換為指令ls

–1的程序映像,一旦該指令執行,它的任何輸出都将發給管道的輸入端。

現在來研究一下管道的接收端。從代碼中可以看出,管道的接收端是由父程序來擔當的。首先關閉stdin描述符(第20行),因為我們不會從機器的鍵盤等标

準裝置檔案來接收資料的輸入,而是從其它程式的輸出中接收資料。然後,再一次用到dup2函數(第21行),讓stdin變成管道的輸出端,這是通過讓文

件描述符0(即正常的stdin)等于pfds[0]來實作的。關閉管道的stdout端(pfds[1]),因為在這裡用不到它。最後,使用

execlp函數把父程序的映像替換為指令wc -1的程序映像,指令wc -1把管道的内容作為它的輸入(第23行)。

示例代碼3:利用C實作指令的流水線操作的代碼

     1:       #include

     2:       #include

     3:       #include

     4:

     5:       int main()

     6:       ...{

     7:         int pfds[2];

     8:

     9:         if ( pipe(pfds) == 0 ) ...{

     10:

     11:           if ( fork() == 0 ) ...{

     12:

     13:             close(1);

     14:             dup2( pfds[1], 1 );

     15:             close( pfds[0] );

     16:             execlp( "ls", "ls", "-1", NULL );

     17:

     18:           } else ...{

     19:

     20:             close(0);

     21:             dup2( pfds[0], 0 );

     22:             close( pfds[1] );

     23:             execlp( "wc", "wc", "-l", NULL );

     24:

     25:           }

     26:

     27:         }

     28:

     29:         return 0;

     30:       }

     在該程式中,需要格外關注的是,我們的子程序把它的輸出重定向的管道的輸入,然後,父程序将它的輸入重定向到管道的輸出。這在實際的應用程式開發中是非常有用的一種技術。

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

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是程式運作所在的終端, 這個應該通過一種方法獲得。實踐證明這種方法

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

目前我就想到這兩種方法, 不知道你有什麼好的想法? 有的話希望告訴我:)

繼續閱讀