Linux文件,文件描述符以及dup()和dup2()
一.Linux中文件
可以分為4種:普通文件、目錄文件、鏈接文件和設(shè)備文件。
1、普通文件
?? 是用戶日常使用最多的文件,包括文本文件、shell腳本、二進(jìn)制的可執(zhí)行和各種類型的數(shù)據(jù)。
??? ??? ls -lh 來查看某個文件的屬性,可以看到有類似 -rw-r--r-- ,值得注意的是第一個符號是 - ,這樣的文件在Linux中就是普通文件。這些文件一般是用一些相關(guān)的應(yīng)用程序創(chuàng)建,比如圖像工具、文檔工具、歸檔工具... .... 或 cp工具等。這類文件的刪除方式是用rm 命令;
2、目錄文件
????在linux中,目錄也是文件,它們是包含文件名和子目錄名以及指向那些文件和子目錄的指針
當(dāng)我們在某個目錄下執(zhí)行,看到有類似 drwxr-xr-x ,這樣的文件就是目錄,目錄在Linux是一個比較特殊的文件。注意它的第一個字符是d。創(chuàng)建目錄的命令可以用 mkdir 命令,或cp命令,cp可以把一個目錄復(fù)制為另一個目錄。刪除用rm 或rmdir命令。
3、鏈接文件
鏈接文件類似于Windows中的“快捷方式”。
是通過ln -s 源文件名 新文件名?? 來創(chuàng)建的。
4、設(shè)備文件
包括兩種,塊設(shè)備文件,另一種是字符設(shè)備文件
? ? ? ?塊設(shè)備文件是指數(shù)據(jù)的讀寫,它們是以塊為單位的設(shè)備,如硬盤光驅(qū)
? ? ? ?字符設(shè)備主要是指串行端口的接口設(shè)備,如網(wǎng)卡等。
二、文件描述符?1、文件描述符及其作用
??內(nèi)核(kernel)利用文件描述符(file descriptor)來訪問文件。文件描述符是非負(fù)整數(shù)。打開現(xiàn)存文件或新建文件時,內(nèi)核會返回一個文件描述符。讀寫文件也需要使用文件描述符來指定待讀寫的文件。 對于 Linux 而言,所有對設(shè)備和文件的操作都使用文件描述符來進(jìn)行的。文件描述符是一個非負(fù)的整數(shù),它是一個索引值,并指向內(nèi)核中每個進(jìn)程打開文件的記錄表。當(dāng)打開一個現(xiàn)存文件或創(chuàng)建一個新文件時,內(nèi)核就向進(jìn)程返回一個文件描述符;當(dāng)需要讀寫文件時,
也需要把文件描述符作為參數(shù)傳遞給相應(yīng)的函數(shù)。
通常,一個進(jìn)程啟動時,都會打開 3 個文件:標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)出錯處理。這3 個文件分別對應(yīng)文件描述符為 0、1和2也就是宏替換 STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,鼓勵讀者使用這些宏替換)。
查看LINUX默認(rèn)的文件描述符,總共有1024個,對于大多數(shù)情況下是夠用的:
# ulimit -n
查看進(jìn)程id
#ps aux
獲取某進(jìn)程文件描述符
cd /proc/[pid]/fd
[pid] 是對應(yīng)的進(jìn)程的pid.
#cd /proc/1473/fd
#sysctl -a | grep fs.file
nr就是已經(jīng)用的
參考:百科http://baike.baidu.com/view/1303430.htm
三.dup和dup2
dup和dup2也是兩個非常有用的調(diào)用,它們的作用都是用來復(fù)制一個文件的描述符。
它們經(jīng)常用來重定向進(jìn)程的stdin、stdout和stderr。
這兩個函數(shù)的 原形如下:
#include <unistd.h>
int dup( int oldfd );
int dup2( int oldfd, int targetfd )
利用函數(shù)dup,我們可以復(fù)制一個描述符。傳給該函數(shù)一個既有的描述符,它就會返回一個新的描述符,
這個新的描述符是傳給它的描述符的拷貝。這意味著,這兩個描述符共享同一個數(shù)據(jù)結(jié)構(gòu)。例如,
如果我們對一個文件描述符執(zhí)行l(wèi)seek操作,得到的第一個文件的位置和第二個是一樣的。
下面是用來說明dup函數(shù)使用方法的代碼片段:
int fd1, fd2;
? ? ...
fd2 = dup( fd1 );
需要注意的是,我們可以在調(diào)用fork之前建立一個描述符,這與調(diào)用dup建立描述符的效果是一樣的,
子進(jìn)程也同樣會收到一個復(fù)制出來的描述符。
dup2函數(shù)跟dup函數(shù)相似,但dup2函數(shù)允許調(diào)用者規(guī)定一個有效描述符和目標(biāo)描述符的id。dup2函數(shù)成功返回時,
目標(biāo)描述符(dup2函數(shù)的第二個參數(shù))將變成源描述符(dup2函數(shù)的第一個參數(shù))的復(fù)制品,換句話說,
兩個文件描述符現(xiàn)在都指向同一個文件,并且是函數(shù)第一個參數(shù)指向的文件。下面我們用一段代碼加以說明: ? ? ?
int oldfd;
oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );
dup2( oldfd, 1 );
close( oldfd );
本例中,我們打開了一個新文件,稱為“app_log”,并收到一個文件描述符,該描述符叫做fd1。我們調(diào)用dup2函數(shù),
參數(shù)為oldfd和1,這會導(dǎo)致用我們新打開的文件描述符替換掉由1代表的文件描述符(即stdout,因?yàn)闃?biāo)準(zhǔn)輸出文件的id為1)。
任何寫到stdout的東西,現(xiàn)在都將改為寫入名為“app_log”的文件中。
需要注意的是,dup2函數(shù)在復(fù)制了oldfd之后,會立即將其關(guān)閉,但不會關(guān)掉新近打開的文件描述符,因?yàn)槲募枋龇?現(xiàn)在也指向它。
下面我們介紹一個更加深入的示例代碼。回憶一下本文前面講的命令行管道,在那里,我們將ls –1命令的標(biāo)準(zhǔn)輸出作為標(biāo)準(zhǔn)輸入
連接到wc –l命令。接下來,我們就用一個C程序來加以說明這個過程的實(shí)現(xiàn)。代碼如下面的示例代碼3所示。?
在示例代碼3中,首先在第9行代碼中建立一個管道,然后將應(yīng)用程序分成兩個進(jìn)程:一個子進(jìn)程(第13–16行)
和一個父進(jìn)程(第20–23行)。接下來,在子進(jìn)程中首先關(guān)閉stdout描述符(第13行),然后提供了ls –1命令功能,
不過它不是寫到stdout(第13行),而是寫到我們建立的管道的輸入端,這是通過dup函數(shù)來完成重定向的。在第14行,
使用dup2 函數(shù)把stdout重定向到管道(pfds[1])。之后,馬上關(guān)掉管道的輸入端。然后,使用execlp函數(shù)把子進(jìn)程的
映像替換為命令ls –1的進(jìn)程映像,一旦該命令執(zhí)行,它的任何輸出都將發(fā)給管道的輸入端。
現(xiàn)在來研究一下管道的接收端。從代碼中可以看出,管道的接收端是由父進(jìn)程來擔(dān)當(dāng)?shù)摹J紫汝P(guān)閉stdin描述符(第20行),
因?yàn)槲覀儾粫臋C(jī)器的鍵盤等標(biāo)準(zhǔn)設(shè)備文件來接收數(shù)據(jù)的輸入,而是從其它程序的輸出中接收數(shù)據(jù)。然后,再一次用到dup2函數(shù)(第21行),
讓stdin變成管道的輸出端,這是通過讓文件描述符0(即常規(guī)的stdin)等于pfds[0]來實(shí)現(xiàn)的。關(guān)閉管道的stdout端(pfds[1]),
因?yàn)樵谶@里用不到它。最后,使用 execlp函數(shù)把父進(jìn)程的映像替換為命令wc -1的進(jìn)程映像,命令wc -1把管道的內(nèi)容作為它的輸入(第23行)。
示例代碼:利用C實(shí)現(xiàn)命令的流水線操作的代碼
? ? ? #include <stdio.h>
? ? ?#include <stdlib.h>
? ? ?#include <unistd.h>
? ? ?
? ? ? ? ? ? int main()
? ? ? ? ? ?{
? ? ? ? ? ? ? int pfds[2];
? ? ?
? ? ? ? ? ? ? if ( pipe(pfds) == 0 ) { ? //建立一個管道
? ? ?
? ? ? ? ? ? ? ? if ( fork() == 0 ) { ? //子進(jìn)程
? ? ?
? ? ? ? ? ? ? ? ?close(1); ? ? //關(guān)閉stdout描述符
? ? ? ? ? ? ? ? ?dup2( pfds[1], 1 ); ? //把stdout重定向到管道(pfds[1])
? ? ? ? ? ? ? ? ?close( pfds[0] ); ? ?//關(guān)掉管道的輸入端
? ? ? ? ? ? ? ? ? execlp( "ls", "ls", "-1", NULL ); //把子進(jìn)程的映像替換為命令ls –1的進(jìn)程映像
? ? ?
? ? ? ? ? ? ? ? } else { ? ? //父進(jìn)程
? ? ?
? ? ? ? ? ? ? ? ?close(0); ? ? //關(guān)閉stdin描述符
? ? ? ? ? ? ? ? ? dup2( pfds[0], 0 ); ? //讓stdin變成管道的輸出端
? ? ? ? ? ? ? ? ?close( pfds[1] ); ? ?//關(guān)閉管道的stdout端(pfds[1])
? ? ? ? ? ? ? ? ? execlp( "wc", "wc", "-l", NULL ); //把父進(jìn)程的映像替換為命令wc -1的進(jìn)程映像
? ? ?
? ? ? ? ? ? ? ?}
? ? ?
? ? ? ? ? ? }
? ? ?
? ? ? ? ? ? ? return 0;
? ? ? ? ? ?}
在該程序中,需要格外關(guān)注的是,我們的子進(jìn)程把它的輸出重定向的管道的輸入,然后,父進(jìn)程將它的輸入重定向到管道的輸出。
這在實(shí)際的應(yīng)用程序開發(fā)中是非常有用的一種技術(shù)。
1. 文件描述符在內(nèi)核中數(shù)據(jù)結(jié)構(gòu)
? ? 在具體說dup/dup2之前, 我認(rèn)為有必要先了解一下文件描述符在內(nèi)核中的形態(tài)。
一個進(jìn)程在此存在期間,會有一些文件被打開,從而會返回一些文件描述符,從shell
中運(yùn)行一個進(jìn)程,默認(rèn)會有3個文件描述符存在(0、1、2), 0與進(jìn)程的標(biāo)準(zhǔn)輸入相關(guān)聯(lián),
1與進(jìn)程的標(biāo)準(zhǔn)輸出相關(guān)聯(lián),2與進(jìn)程的標(biāo)準(zhǔn)錯誤輸出相關(guān)聯(lián),一個進(jìn)程當(dāng)前有哪些打開
的文件描述符可以通過/proc/進(jìn)程ID/fd目錄查看。 下圖可以清楚的說明問題:
進(jìn)程表項(xiàng)
————————————————
fd標(biāo)志 文件指針
? ? ? ?_____________________
fd 0:|________|____________|------------> 文件表
fd 1:|________|____________|
fd 2:|________|____________|
fd 3:|________|____________|
? ? ? | ? ? ....... ? ? ? ? |
? ? ? |_____________________|
? ? ? ? ? ? ? ? 圖1
文件表中包含:文件狀態(tài)標(biāo)志、當(dāng)前文件偏移量、v節(jié)點(diǎn)指針,這些不是本文討論的
重點(diǎn),我們只需要知道每個打開的文件描述符(fd標(biāo)志)在進(jìn)程表中都有自己的文件表
項(xiàng),由文件指針指向。
2. dup/dup2函數(shù)
APUE和man文檔都用一句話簡明的說出了這兩個函數(shù)的作用:復(fù)制一個現(xiàn)存的文件描述符。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
從圖1來分析這個過程,當(dāng)調(diào)用dup函數(shù)時,內(nèi)核在進(jìn)程中創(chuàng)建一個新的文件描述符,此
描述符是當(dāng)前可用文件描述符的最小數(shù)值,這個文件描述符指向oldfd所擁有的文件表項(xiàng)。
進(jìn)程表項(xiàng)
————————————————
fd標(biāo)志 文件指針
? ? ? ?_____________________
fd 0:|________|____________| ? ? ? ? ? ? ? ? ? ______
fd 1:|________|____________|----------------> | ? ? ?|
fd 2:|________|____________| ? ? ? ? ? ? ? ? ?|文件表|
fd 3:|________|____________|----------------> |______|
? ? ? | ? ? ....... ? ? ? ? |
? ? ? |_____________________|
? ? ? ? ? ? ? ? 圖2:調(diào)用dup后的示意圖
如圖2 所示,假如oldfd的值為1, 當(dāng)前文件描述符的最小值為3, 那么新描述符3指向
描述符1所擁有的文件表項(xiàng)。
dup2和dup的區(qū)別就是可以用newfd參數(shù)指定新描述符的數(shù)值,如果newfd已經(jīng)打開,則
先將其關(guān)閉。如果newfd等于oldfd,則dup2返回newfd, 而不關(guān)閉它。dup2函數(shù)返回的新
文件描述符同樣與參數(shù)oldfd共享同一文件表項(xiàng)。
APUE用另外一個種方法說明了這個問題:
實(shí)際上,調(diào)用dup(oldfd);
等效與
? ? ? ? fcntl(oldfd, F_DUPFD, 0)
而調(diào)用dup2(oldfd, newfd);
等效與
? ? ? ? close(oldfd);
? ? ? ? fcntl(oldfd, F_DUPFD, newfd);
3. CGI中dup2
寫過CGI程序的人都清楚,當(dāng)瀏覽器使用post方法提交表單數(shù)據(jù)時,CGI讀數(shù)據(jù)是從標(biāo)準(zhǔn)
輸入stdin, 寫數(shù)據(jù)是寫到標(biāo)準(zhǔn)輸出stdout(c語言利用printf函數(shù))。按照我們正常的理
解,printf的輸出應(yīng)該在終端顯示,原來CGI程序使用dup2函數(shù)將STDOUT_FINLENO(這個
宏在unitstd.h定義,為1)這個文件描述符重定向到了連接套接字。
dup2(connfd, STDOUT_FILENO); /*實(shí)際情況還涉及到了管道,不是本文的重點(diǎn)*/
如第一節(jié)所說, 一個進(jìn)程默認(rèn)的文件描述符1(STDOUT_FILENO)是和標(biāo)準(zhǔn)輸出stdout相
關(guān)聯(lián)的,對于內(nèi)核而言,所有打開的文件都通過文件描述符引用,而內(nèi)核并不知道流的
存在(比如stdin、stdout),所以printf函數(shù)輸出到stdout的數(shù)據(jù)最后都寫到了文件描述
符1里面。至于文件描述符0、1、2與標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯誤輸出相關(guān)聯(lián),這
只是shell以及很多應(yīng)用程序的慣例,而與內(nèi)核無關(guān)。
用下面的流圖可以說明問題:(ps: 雖然不是流圖關(guān)系,但是還是有助于理解)
printf -> stdout -> STDOUT_FILENO(1) -> 終端(tty)
printf最后的輸出到了終端設(shè)備,文件描述符1指向當(dāng)前的終端可以這么理解:
STDOUT_FILENO = open("/dev/tty", O_RDWR);
使用dup2之后STDOUT_FILENO不再指向終端設(shè)備, 而是指向connfd, 所以printf的
輸出最后寫到了connfd。是不是很優(yōu)美?:)
4. 如何在CGI程序的fork子進(jìn)程中還原STDOUT_FILENO
如果你能看到這里,感謝你的耐心, 我知道很多人可能感覺有點(diǎn)復(fù)雜, 其實(shí)
復(fù)雜的問題就是一個個小問題的集合。所以弄清楚每個小問題就OK了,第三節(jié)中
說道,STDOUT_FILENO被重定向到了connfd套接字, 有時候我們可能想在CGI程序
中調(diào)用后臺腳本執(zhí)行,而這些腳本中難免會有一些輸入輸出, 我們知道fork之后,
子進(jìn)程繼承了父進(jìn)程的所有文件描述符,所以這些腳本的輸入輸出并不會如我們愿
輸出到終端設(shè)備,而是和connfd想關(guān)聯(lián)了,這個顯然會擾亂網(wǎng)頁的輸出。那么如何
恢復(fù)STDOUT_FILENO和終端關(guān)聯(lián)呢?
方法1:在dup2之前保存原有的文件描述符,然后恢復(fù)。
代碼實(shí)現(xiàn)如下:
savefd = dup(STDOUT_FILENO); /*savefd此時指向終端*/
dup2(connfd, STDOUT_FILENO); ? /*STDOUT_FILENO(1) 被重新指向connfd*/
..... /*處理一些事情*/
dup2(savefd, STDOUT_FILENO); /*STDOUT_FILENO(1) 恢復(fù)指向savefd*/
很遺憾CGI程序無法使用這種方法, 因?yàn)閐up2這些不是在CGI程序中完成的,而是在
web server中實(shí)現(xiàn)的,修改web server并不是個好主意。
方法2: 追本溯源,打開當(dāng)前終端恢復(fù)STDOUT_FILENO。
分析第三節(jié)的流圖, STDOUT_FILENO是如何和終端關(guān)聯(lián)的? 我們重頭做一遍不就行
了, 代碼實(shí)現(xiàn)如下:
ttyfd = open("/dev/tty", O_RDWR);
dup2(ttyfd, STDOUT_FILENO);
close(ttyfd);
/dev/tty是程序運(yùn)行所在的終端, 這個應(yīng)該通過一種方法獲得。實(shí)踐證明這種方法
是可行的,但是我總感覺有些不妥,不知道為什么,可能一些潛在的問題還沒出現(xiàn)。
總結(jié)
以上是生活随笔為你收集整理的Linux文件,文件描述符以及dup()和dup2()的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 搬砖多少钱啊?
- 下一篇: 修改CentOS yum源