Linux檔案,檔案描述符以及dup()和dup2()

來源:互聯網
上載者:User

一.Linux中檔案

可以分為4種:普通檔案、目錄檔案、連結檔案和裝置檔案。

1、普通檔案

   是使用者日常使用最多的檔案,包括文字檔、shell指令碼、二進位的可執行和各種類型的資料。

        ls -lh 來查看某個檔案的屬性,可以看到有類似 -rw-r--r-- ,值得注意的是第一個符號是 - ,這樣的檔案在Linux中就是普通檔案。這些檔案一般是用一些相關的應用程式建立,比像工具、文檔工具、歸檔工具... .... 或 cp工具等。這類檔案的刪除方式是用rm 命令;

2、目錄檔案

    在linux中,目錄也是檔案,它們是包含檔案名稱和子目錄名以及指向那些檔案和子目錄的指標

當我們在某個目錄下執行,看到有類似 drwxr-xr-x ,這樣的檔案就是目錄,目錄在Linux是一個比較特殊的檔案。注意它的第一個字元是d。建立目錄的命令可以用 mkdir 命令,或cp命令,cp可以把一個目錄複寫為另一個目錄。刪除用rm 或rmdir命令。

3、連結檔案

連結檔案類似於Windows中的“捷徑”。

是通過ln -s 源檔案名稱 新檔案名稱   來建立的。

4、裝置檔案

包括兩種,塊裝置檔案,另一種是字元裝置檔案

       塊裝置檔案是指資料的讀寫,它們是以塊為單位的裝置,如硬碟光碟機

       字元裝置主要是指序列埠的介面裝置,如網卡等。

二、檔案描述符 

1、檔案描述符及其作用

  核心(kernel)利用檔案描述符(file descriptor)來訪問檔案。檔案描述符是非負整數。開啟現存檔案或建立檔案時,核心會返回一個檔案描述符。讀寫檔案也需要使用檔案描述符來指定待讀寫的檔案。 對於 Linux 而言,所有對裝置和檔案的操作都使用檔案描述符來進行的。檔案描述符是一個非負的整數,它是一個索引值,並指向核心中每個進程開啟檔案的記錄表。當開啟一個現存檔案或建立一個新檔案時,核心就向進程返回一個檔案描述符;當需要讀寫檔案時,

也需要把檔案描述符作為參數傳遞給相應的函數。

通常,一個進程啟動時,都會開啟 3 個檔案:標準輸入、標準輸出和標準出錯處理。這3 個檔案分別對應檔案描述符為 0、1和2也就是宏替換 STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,鼓勵讀者使用這些宏替換)。

查看LINUX預設的檔案描述符,總共有1024個,對於大多數情況下是夠用的:

# ulimit -n

查看進程id

#ps aux

擷取某進程檔案描述符

cd /proc/[pid]/fd
[pid] 是對應的進程的pid.


#cd /proc/1473/fd

#sysctl -a | grep fs.file

nr就是已經用的

參考:百科http://baike.baidu.com/view/1303430.htm

三.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行)。
範例程式碼:利用C實現命令的流水線操作的代碼
      #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
     
            int main()
           {
              int pfds[2];
     
              if ( pipe(pfds) == 0 ) {   //建立一個管道
     
                if ( fork() == 0 ) {   //子進程
     
                 close(1);     //關閉stdout描述符
                 dup2( pfds[1], 1 );   //把stdout重新導向到管道(pfds[1])
                 close( pfds[0] );    //關掉管道的輸入端
                  execlp( "ls", "ls", "-1", NULL ); //把子進程的映像替換為命令ls –1的進程映像
     
                } else {     //父進程
     
                 close(0);     //關閉stdin描述符
                  dup2( pfds[0], 0 );   //讓stdin變成管道的輸出端
                 close( pfds[1] );    //關閉管道的stdout端(pfds[1])
                  execlp( "wc", "wc", "-l", NULL ); //把父進程的映像替換為命令wc -1的進程映像
     
               }
     
            }
     
              return 0;
           }

在該程式中,需要格外關注的是,我們的子進程把它的輸出重新導向的管道的輸入,然後,父進程將它的輸入重新導向到管道的輸出。
這在實際的應用程式開發中是非常有用的一種技術。
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是程式運行所在的終端, 這個應該通過一種方法獲得。實踐證明這種方法
是可行的,但是我總感覺有些不妥,不知道為什麼,可能一些潛在的問題還沒出現。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.