Linux下的管道編程技術-dup函數和dup2函數

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

#include <unistd.h>    

 int dup( int oldfd );    

 int dup2( int oldfd, int targetfd )  
    利用函數dup,我們可以複製一個描述符。傳給該函數一個既有的描述符,它就會返回一個新的描述符,這個新的描述符是傳給它的描述符的拷貝。這意味着,這兩個描述符共享同一個數據結構。例如,如果我們對一個文件描述符執行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 <stdio.h>     

2:       #include <stdlib.h>     

3:       #include <unistd.h>     

 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 <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是程序運行所在的終端, 這個應該通過一種方法獲得。實踐證明這種方法 
 
是可行的,但是我總感覺有些不妥,不知道爲什麼,可能一些潛在的問題還沒出現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章