服務(wù)器編程中對于文件的操作詳解

  linux系統(tǒng)下一切皆文件,通過虛擬文件系統(tǒng)(vfs)的機制將所有底層屏蔽掉,用戶可以通過統(tǒng)一的接口來實現(xiàn)對不同驅(qū)動的操作,對于每一個文件需要一個引用來指示,此時文件描述符應(yīng)用而生,文件描述符類似于widows下的handle,對于文件的大部分操作都是通過這個描述符來操作的,例如read,write。對于每一個文件描述符,內(nèi)核使用三種數(shù)據(jù)結(jié)構(gòu)來管理。

(1) ?每個進程在進程表中都有一個記錄項,每個記錄項中有一張打開文件描述符表,可將其視為一個矢量,每個描述符占用一項。與每個文件描述符相關(guān)聯(lián)的是:

  (a)? 文件描述符標志。 (當(dāng)前只定義了一個文件描述符標志FD_CLOEXEC)

  (b)? 指向一個文件表項的指針。

(2)? 內(nèi)核為所有打開文件維持一張文件表。每個文件表項包含:

  (a)? 文件狀態(tài)標志(讀、寫、增寫、同步、非阻塞等 )。

  (b)? 當(dāng)前文件位移量。(即為lseek函數(shù)所操作的值)

  (c)? 指向該文件v節(jié)點表項的指針。

(3)? 每個打開文件(或設(shè)備)都有一個 v 節(jié)點結(jié)構(gòu)。 v節(jié)點包含了文件類型和對此文件進行各種操作的函數(shù)的指針信息。對于大多數(shù)文件, v 節(jié)點還包含了該文件的 i 節(jié)點(索引節(jié)點)。這些信息是在打開文件時從盤上讀入內(nèi)存的,所以所有關(guān)于文件的信息都是快速可供使用的。例如, i 節(jié)點包含了文件的所有者、文件長度、文件所在的設(shè)備、指向文件在盤上所使用的實際數(shù)據(jù)塊的指針等等點。

?服務(wù)器編程中對于文件的操作詳解

  經(jīng)過上述文件系統(tǒng)的三層封裝,每層負責(zé)不同的職責(zé),從上到下第一層用于標識文件,第二層用于管理進程獨立數(shù)據(jù),第三層管理文件系統(tǒng)元數(shù)據(jù),直接關(guān)聯(lián)一個文件。這種分層思想的一個優(yōu)點就是上層可以復(fù)用下層的結(jié)構(gòu)。可能有多個文件描述符項指向同一個文件表項,也可以有多個文件表項指向同一個V節(jié)點。

  如果兩個獨立的進程打開了同一個文件,打開此文件的每個進程都得到一個文件表項,但是兩個文件表項的V節(jié)點指針指向相同的V節(jié)點,這樣的安排使得每個進程都有他自己的對該文件的當(dāng)前位移量,且支持不同的打開方式(O_RDONLY, O_WRONLY, ORDWR)。

  當(dāng)一個進程通過fork創(chuàng)建出子進程后,此時父,子進程內(nèi)的文件描述符共享同一個文件表項,也就是說父子進程的文件描述符的指向相同。一般我們會在fork后關(guān)閉掉各自不需要的fd,例如父子進程通過pipe或socketpair進行通信,往往會close掉自己不需要讀(或?qū)?的一端。只有在沒有文件描述符引用當(dāng)前文件表項的時候,close操作才真正銷毀當(dāng)前文件表項數(shù)據(jù)結(jié)構(gòu),有點類似于引用計數(shù)的思想。這也是網(wǎng)絡(luò)編程中close和shutdown函數(shù)的區(qū)別,前者只有在最后一個使用該socket的句柄的進程關(guān)閉的時候才真正斷開連接,而后者毫不商量直接斷開一側(cè)連接。但是在多線程的環(huán)境中,由于父子線程共享地址空間,此時文件描述符共同擁有,只有一份,所以也就不能在線程內(nèi)close掉自己不需要的fd,否則會導(dǎo)致其它需要該fd的線程也受影響。因為父,子進程內(nèi)打開的文件描述符共享同一個文件表項,所以在某些系統(tǒng)的服務(wù)器編程中,如果采用preforking模型(服務(wù)器預(yù)先派生多個子進程,在每個子進程監(jiān)聽listenfd來accept連接)就會導(dǎo)致驚群現(xiàn)象的發(fā)生,服務(wù)器派生的多個子進程各自調(diào)用accept并因而均被投入睡眠,當(dāng)?shù)谝粋€客戶連接到達時,盡管只有一個進程獲得連接,但是所有進程都被喚醒,這樣導(dǎo)致性能受損。參見UNP P657。

  同時如果fork之后調(diào)用exec,所有的文件描述符繼續(xù)保持打開狀態(tài)。這可以用來給exec后的程序傳遞某些文件描述符。同時文件描述符標志FD_CLOEXEC 就是用來關(guān)閉exec時繼續(xù)保持開放的文件描述符的選項。

  也可以通過dup或fcntl顯式復(fù)制一個文件描述符,他們指向相同的文件表項。通過dup2將文件描述符復(fù)制到制定數(shù)值。

  每個進程都有一個文件描述符表,進程間獨立,兩個進程之間的文件描述符并無直接關(guān)系,所以在進程內(nèi)可以直接傳遞文件描述符,但是如果跨越進程傳遞就失去了意義,unix可以通過sendmsg/recvmsg進行專門的文件描述符的傳遞(參見書UNP 15.7節(jié))。每個進程的前三個文件描述符分別對應(yīng)標準輸入,標準輸出,標準錯誤。但是一個進程可打開的文件描述符數(shù)量是有限制的,如果打開的文件描述符太多會出現(xiàn)”Too many open files”的問題。在網(wǎng)絡(luò)服務(wù)器中,通過listenfd調(diào)用調(diào)用accept時,體現(xiàn)為產(chǎn)生EMFILE錯誤,這主要是因為文件描述符是系統(tǒng)的一個重要資源,系統(tǒng)資源是有盡的,系統(tǒng)對單一進程文件描述符限制默認值一般是1024,使用ulimit -n命令可以查看。當(dāng)然也可以調(diào)高進程文件描述符數(shù)目,但這是治標不治本的方法,因為處理高并發(fā)服務(wù)時,服務(wù)器資源有限,難免資源枯竭。

  當(dāng)結(jié)合epoll的水平觸發(fā)方式來監(jiān)聽lisenfd的連接時,大量socket連接涌來如果不處理會塞滿TCP的連接隊列,listenfd會一直產(chǎn)生可讀事件,將服務(wù)器陷入忙等待,用C++開源網(wǎng)絡(luò)庫muduo作者陳碩的做法是事先準備一個空閑的文件描述符,當(dāng)產(chǎn)生EMFILE錯誤時就先關(guān)閉這個空閑文件,獲得一個文件描述符名額,再accept拿到一個socket連接的文件描述符,隨后立刻close,這樣就優(yōu)雅的斷開了與客戶端的連接,最后重新打開空閑文件,把”坑”填上,以備再次出現(xiàn)這種情況時使用。

 1 //在程序開頭先”占用”一個文件描述符 2  3 int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC); 4 ………… 5  6 //然后當(dāng)出現(xiàn)EMFILE錯誤的時候處理這個錯誤 7  8 peerlen = sizeof(peeraddr); 9 connfd = accept4(listenfd,  (struct sockaddr*)&peeraddr, &peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);10 11 if (connfd == -1)12 {13     if (errno == EMFILE)14     {15         close(idlefd);16         idlefd = accept(listenfd, NULL, NULL);17         close(idlefd);18         idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);19         continue;20     }21     else22         ERR_EXIT("accept4");23 }

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點贊8 分享