1. 程式人生 > >Linux管道程式設計技術:dup函式,dup2函式,open函式詳解

Linux管道程式設計技術:dup函式,dup2函式,open函式詳解

(一)dup函式,dup2()函式
1.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);
(二)open函式
open 函式用於開啟和建立檔案。以下是 open 函式的簡單描述
#include <fcntl.h>
int open(const char *pathname, int oflag, ... );
返回值:成功則返回檔案描述符,否則返回 -1

對於 open 函式來說,第三個引數(...)僅當建立新檔案時才使用,用於指定檔案的訪問許可權位(access permission bits)。pathname 是待開啟/建立檔案的路徑名(如 C:/cpp/a.cpp);oflag 用於指定檔案的開啟/建立模式,這個引數可由以下常量(定義於 fcntl.h)通過邏輯或構成。

O_RDONLY      只讀模式
O_WRONLY      只寫模式
O_RDWR        讀寫模式

開啟/建立檔案時,至少得使用上述三個常量中的一個。以下常量是選用的:

O_APPEND       每次寫操作都寫入檔案的末尾
O_CREAT        如果指定檔案不存在,則建立這個檔案
O_EXCL         如果要建立的檔案已存在,則返回 -1,並且修改 errno 的值
O_TRUNC        如果檔案存在,並且以只寫/讀寫方式開啟,則清空檔案全部內容
O_NOCTTY       如果路徑名指向終端裝置,不要把這個裝置用作控制終端。
O_NONBLOCK     如果路徑名指向 FIFO/塊檔案/字元檔案,則把檔案的開啟和後繼 I/O設定為非阻塞模式(nonblocking mode)

以下三個常量同樣是選用的,它們用於同步輸入輸出

O_DSYNC        等待物理 I/O 結束後再 write。在不影響讀取新寫入的資料的前提下,不等待檔案屬性更新。
O_RSYNC        read 等待所有寫入同一區域的寫操作完成後再進行
O_SYNC         等待物理 I/O 結束後再 write,包括更新檔案屬性的 I/O

open 返回的檔案描述符一定是最小的未被使用的描述符。
    如果 NAME_MAX(檔名最大長度,不包括'\0')是 14,而我們想在當前目錄下建立檔名長度超過 14 位元組的檔案,早期的 System V 系統(如 SVR2)會截斷超出部分,只保留前 14 個位元組;而由 BSD 衍生的(BSD-derived)系統會返回錯誤資訊,並且把 errno 置為 ENAMETOOLONG。
    POSIX.1 引入常量 _POSIX_NO_TRUNC 用於決定是否截斷長檔名/長路徑名。如果_POSIX_NO_TRUNC 設定為禁止截斷,並且路徑名長度超過 PATH_MAX(包括 '\0'),或者組成路徑名的任意檔名長度超過 NAME_MAX,則返回錯誤資訊,並且把 errno 置為ENAMETOOLONG。

what is the difference between

fopen and open
fread and read
fwrite and write
open and creat