歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
 Linux教程網 >> Linux基礎 >> 關於Linux >> 服務器編程中的文件描述符

服務器編程中的文件描述符

日期:2017/8/19 9:49:20      编辑:關於Linux
linux系統下一切皆文件,通過虛擬文件系統(VFS)的機制將所有底層屏蔽掉,用戶可以通過統一的接口來實現對不同驅動的操作,對於每一個文件需要一個引用來指示,此時文件描述符應用而生,文件描述符類似於widows下的handle,對於文件的大部分操作都是通過這個描述符來操作的,例如read,write。對於每一個文件描述符,內核使用三種數據結構來管理。

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

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

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

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

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

(b) 當前文件位移量。(即為lseek函數所操作的值)

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

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

\

經過上述文件系統的三層封裝,每層負責不同的職責,從上到下第一層用於標識文件,第二層用於管理進程獨立數據,第三層管理文件系統元數據,直接關聯一個文件。這種分層思想的一個優點就是上層可以復用下層的結構。可能有多個文件描述符項指向同一個文件表項,也可以有多個文件表項指向同一個V節點。

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

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

同時如果fork之後調用exec,所有的文件描述符繼續保持打開狀態。這點可以用來給Exec後的程序傳遞某些文件描述符。

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

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

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

//在程序開頭先”占用”一個文件描述符

int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
…………
//然後當出現EMFILE錯誤的時候處理這個錯誤
peerlen = sizeof(peeraddr);
 connfd = accept4(listenfd, (struct sockaddr*)&peeraddr, &peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);

if (connfd == -1)
 {
 if (errno == EMFILE)
{
close(idlefd);
idlefd = accept(listenfd, NULL, NULL);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
 continue;
}
else
ERR_EXIT("accept4");
 }
 
Copyright © Linux教程網 All Rights Reserved