IO模型

UNIX Network Programming第六章節(jié)

前序概念:

文件描述符(File Descriptor)

文件描述符在形式上是一個非負(fù)整數(shù)。實(shí)際上,它是一個索引值,指向內(nèi)核為每一個進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時(shí),內(nèi)核向進(jìn)程返回一個文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于Unix、Linux這樣的操作系統(tǒng)。

用戶空間(User Space) Vs 內(nèi)核空間(Kernal Space)

在操作系統(tǒng)的定義當(dāng)中,內(nèi)核程序運(yùn)行在內(nèi)核空間,而用戶的應(yīng)用程序運(yùn)行在用戶控件。兩個實(shí)際上是虛擬概念,只是邏輯上的劃分,真正的RAM其實(shí)不存在這個區(qū)分。用戶空間就像是一個沙盒,它限制了程序可以操作的內(nèi)存范圍,從而保護(hù)其他應(yīng)用以及操作系統(tǒng)內(nèi)核(資源,硬件)不受影響。而內(nèi)核空間則對外提供了系統(tǒng)調(diào)用方法使應(yīng)用獲取一些操作系統(tǒng)級別資源。

6.1 簡介

在第五章中,我們看到TCP客戶端同時(shí)處理兩個輸入:TCP socket和標(biāo)準(zhǔn)輸入。在客戶端調(diào)用fgets被阻塞的以后,服務(wù)器進(jìn)程被kill掉了,我們就會遇到問題。服務(wù)器正確的發(fā)送一個FIN到客戶端TCP,但是因?yàn)榭蛻舳诉M(jìn)程被阻塞住于從標(biāo)準(zhǔn)輸入讀的過程,它看不到這個EOF,直到它從socket中讀為止(可能要很久)。當(dāng)一個或者多個I/O條件就緒時(shí), 我們需要內(nèi)核能夠通知我們(比如輸入已經(jīng)可讀,描述符(descriptor)可以承載更多輸出)。這個能力我們叫做I/O多路復(fù)用, 由select和poll來提供。我們還有一個新的POSIX版本,叫做pselect。

對于進(jìn)程等待一系列的事件,有些系統(tǒng)提供了更為先進(jìn)的方式。輪詢(poll)設(shè)備是其中一種機(jī)制。這個機(jī)制在第14章會進(jìn)行詳述。

I/O 多路復(fù)用在網(wǎng)絡(luò)應(yīng)用中通常有以下一些場景:

  • 當(dāng)一個客戶端處理多個描述符(通常是交互式輸入和網(wǎng)絡(luò)socekt)
  • 可能但是少見的情況,一個客戶端同時(shí)處理多個socket,在16.5章中結(jié)合網(wǎng)絡(luò)客戶端有例子
  • TCP服務(wù)端同時(shí)處理一個監(jiān)聽socket和已經(jīng)連接的socket, 如6.8中的例子
  • 服務(wù)端處理多個服務(wù)或者多個協(xié)議,比如13.5章中提到的inetd守護(hù)進(jìn)程

I/O多路復(fù)用并不局限于網(wǎng)絡(luò)編程,很多重要應(yīng)用也會用到這些技巧

6.2 I/O模型

在Unix中,有五種不同的I/O模型,我們來具體看一下他們的區(qū)別

  • Blocking I/O 阻塞式I/O
  • Nonblocking I/O 非阻塞式I/O
  • I/O multiplexing (select and poll) I/O多路復(fù)用
  • signal driven I/O (SIGIO) 信號驅(qū)動I/O
  • asynchronous I/O (the POSIX aio_functions) 異步I/O

在本文提到的所有例子中,對于一個輸入操作,通常有兩個不同的階段 等待數(shù)據(jù)就緒 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中

對于socket上的輸入操作, 第一步通常包含等待數(shù)據(jù)從網(wǎng)絡(luò)傳輸。當(dāng)數(shù)據(jù)包到達(dá)時(shí),它會被拷貝到內(nèi)核中的一段緩沖區(qū)中。第二步是將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到我們應(yīng)用的緩沖區(qū)中。

Blocking I/O Model

最為常見的I/O模型就是阻塞式I/O,例如上面提到的例子。所有的socket默認(rèn)都是阻塞的。在下圖中,我們使用一個socket中的數(shù)據(jù)報(bào)文進(jìn)行示例:


figure_6.1.png

我們在例子中使用UDP而不是TCP,因?yàn)閿?shù)據(jù)"準(zhǔn)備好"的概念比較簡單: 收到的數(shù)據(jù)報(bào)文是否完整。TCP則更為復(fù)雜,會有一些額外的變量,比如socket的low-water mark等。

在本文中的例子中,我們同樣也把recvfrom當(dāng)做一個系統(tǒng)調(diào)用,因?yàn)槲覀円獏^(qū)分應(yīng)用進(jìn)程和內(nèi)核。無論recvfrom是怎樣實(shí)現(xiàn)的,通常會從"在應(yīng)用中運(yùn)行"切換到"在內(nèi)核中運(yùn)行",一段時(shí)間以后再返回應(yīng)用。

在上圖中,進(jìn)程調(diào)用recvfrom,系統(tǒng)調(diào)用不會返回,直到數(shù)據(jù)報(bào)文到達(dá)并且被拷貝到應(yīng)用緩沖區(qū)中,或者出現(xiàn)錯誤。最常見的錯誤就是系統(tǒng)調(diào)用被信號量中斷。我們的進(jìn)程從調(diào)用recvfrom開始直到獲得返回整個周期會被阻塞。當(dāng)recvfrom成功返回以后,我們的程序開始處理數(shù)據(jù)報(bào)文。

Nonblocking I/O Model

如果我們將socket設(shè)置為非阻塞,我們告訴內(nèi)核"當(dāng)一個I/O操作只有把進(jìn)程sleep才能完成的時(shí)候,不要讓進(jìn)程sleep,而是返回一個錯誤"。在第十六章中會詳細(xì)描述,但是下圖是一個關(guān)于非阻塞概念的總結(jié):


figure_6.2.png

前三次我們調(diào)用recvfrom并沒有返回?cái)?shù)據(jù),內(nèi)核直接返回一個EWOULDBLOCK 錯誤。第四次我們調(diào)用recvfrom時(shí)一個數(shù)據(jù)報(bào)文已經(jīng)就緒,拷貝到我們的應(yīng)用緩沖區(qū)中,recvfrom返回成功,然后我們處理數(shù)據(jù)。

當(dāng)應(yīng)用像這樣對應(yīng)非阻塞描述符進(jìn)行循環(huán)調(diào)用recvfrom的操作叫做輪詢。應(yīng)用一直輪詢內(nèi)核查看是否有操作準(zhǔn)備就緒。這通常十分消耗CPU時(shí)間片,但事實(shí)這個模型偶爾也使用在專門提供某一功能的系統(tǒng)中。

I/O Multiplexing Model

在I/O多路復(fù)用模型中,我們使用select或者poll,并且阻塞在這兩個系統(tǒng)調(diào)用中的一個中,而不是真正的I/O系統(tǒng)調(diào)用。如下圖所示:


figure_6.3.png

我們阻塞在select系統(tǒng)調(diào)用中,等待有可讀的socket數(shù)據(jù)報(bào)文。當(dāng)select返回socket可讀時(shí),我們再調(diào)用recvfrom將數(shù)據(jù)報(bào)文拷貝到應(yīng)用緩沖區(qū)中。

和阻塞式I/O相比,似乎看不出有什么優(yōu)勢,并且能很明顯看出I/O復(fù)用的劣勢就是使用select需要兩次系統(tǒng)調(diào)用而不是一次。當(dāng)多個描述符(descriptor)準(zhǔn)備就緒的時(shí)候,我們會在后面看到使用select的優(yōu)勢,

另一個很相近的I/O模型是在阻塞式I/O上使用多線程。這個模型跟上面所說的多路復(fù)用很像。只不多路復(fù)用模型使用select阻塞在多個文件描述符上,而多線程阻塞模型使用多個線程(每個描述符一個),每個線程都可以使用阻塞式系統(tǒng)調(diào)用,比如recvfrom。

Signal-Driven I/O Model

我們也可以使用信號量,在描述符就緒時(shí)讓內(nèi)核使用SIGIO信號通知我們。我們稱之為信號驅(qū)動,如下圖所示:


figure_6.4.png

我們首先啟用信號驅(qū)動I/O的socket,然后使用sigaction系統(tǒng)調(diào)用安裝一個信號處理器。sigaction調(diào)用會立刻返回,我們的程序繼續(xù)執(zhí)行而不會阻塞。當(dāng)數(shù)據(jù)報(bào)文可讀,內(nèi)核為我們的應(yīng)用生成SIGIO信號。我們可以在信號處理器中調(diào)用recvfrom讀取數(shù)據(jù)報(bào)文,然后通知主程序數(shù)據(jù)可以被處理;也可以通知主程序來讀取數(shù)據(jù)報(bào)文。

無論我們?nèi)绾翁幚硇盘?,這個模型的優(yōu)勢在于我們無須等待數(shù)據(jù)報(bào)文到來。主程序可以繼續(xù)執(zhí)行,只需要等待被信號通知,數(shù)據(jù)可讀甚至已經(jīng)可以處理。

Asynchronous I/O Model

異步I/O在POSIX規(guī)范中定義,早期不同版本中的實(shí)時(shí)函數(shù)經(jīng)過演變已經(jīng)達(dá)成一致??偠灾?,這些函數(shù)告訴內(nèi)核開始進(jìn)行I/O操作,并且當(dāng)整個操作(包括將數(shù)據(jù)從內(nèi)核拷貝到應(yīng)用緩沖區(qū))完成以后通知我們。異步模型和信號驅(qū)動模型最主要的區(qū)別是在信號模型中,內(nèi)核告訴我們什么時(shí)候可以進(jìn)行操作,異步模型告訴我們什么時(shí)候I/O完成。如下圖所示:


figure_6.5.png

我們調(diào)用aio_read(POSIX異步模型函數(shù)以aio_或者lio_打頭)并且將描述符,緩沖區(qū)指針,緩沖區(qū)大小(前三個參數(shù)和read相同),文件偏移量(file offset, 類似于lseek) 和 當(dāng)整個操作完成以后如何通知我們傳參給內(nèi)核。aio_read立刻返回,我們的程序也不會阻塞在等待I/O完成。我們假設(shè)在這個例子中當(dāng)操作完成后讓內(nèi)核生成一些信號。和信號驅(qū)動不同的是,信號直到數(shù)據(jù)被拷貝到應(yīng)用緩沖區(qū)中以后才會被生成。.

此時(shí)(很久以前了),只有部分系統(tǒng)支持POSIX 異步I/O。我們并不確定某個系統(tǒng)在socket中是否支持異步I/O。這里只是為了跟信號驅(qū)動I/O做個比較。

I/O Model對比

下圖是5中不同I/O模型的對比。前四種模型主要的區(qū)別在第一階段(等待數(shù)據(jù)),而第二階段是相同的:進(jìn)程阻塞在recvfrom調(diào)用中,等待數(shù)據(jù)從內(nèi)核拷貝到應(yīng)用緩存中。異步模型則處理了兩階段的任務(wù)。


figure_6.6.png

POSIX 定義了兩種概念:

  • 同步I/O操作導(dǎo)致請求進(jìn)程阻塞,直到I/O操作完成。
  • 異步I/O操作不導(dǎo)致請求進(jìn)程阻塞

從這個定義來講,前四種模型在第二階段都會阻塞,所以屬于同步I/O,只有最后一種I/O屬于異步I/O。

Select, Poll和Epoll

這三者都屬于I/O多路復(fù)用,我們來對這三者進(jìn)行比較,這里參考了這篇文章。

當(dāng)使用非阻塞I/O socket來設(shè)計(jì)高性能網(wǎng)絡(luò)程序的時(shí)候,架構(gòu)師需要決定選用哪種輪詢方法來監(jiān)控socket生成的事件。不同方式的適用場景并不一樣,選擇正確的方式來滿足應(yīng)用的需求非常重要。

使用Select 進(jìn)行輪詢

我們把傳統(tǒng)的socket稱為Berkeley sockets。它在上世紀(jì)80年代出現(xiàn),并且這個接口就沒有變過。只不過因?yàn)楫?dāng)時(shí)還沒有非阻塞I/O這個概念,它并沒有行程最初的規(guī)范。

開發(fā)人員需要初始化fd_set參數(shù)(描述符和監(jiān)聽事件等),然后調(diào)用select方法,大致流程如下:

fd_set fd_in, fd_out;
struct timeval tv;

// 重置sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );

// 監(jiān)控socket1的輸入事件
FD_SET( sock1, &fd_in );

// 監(jiān)控socket2的輸出事件
FD_SET( sock2, &fd_out );

//找出哪個socket值最大(select需要)
int largest_sock = sock1 > sock2 ? sock1 : sock2;

// 等待10秒
tv.tv_sec = 10;
tv.tv_usec = 0;

// 調(diào)用socket
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );

// 檢查select是否成功
if ( ret == -1 )
    // 報(bào)告錯誤并且中止
else if ( ret == 0 )
    // 超時(shí);沒有檢測到事件
else
{
    if ( FD_ISSET( sock1, &fd_in ) )
        // socket1輸入事件

    if ( FD_ISSET( sock2, &fd_out ) )
         // socket2輸出事件
}

設(shè)計(jì)select接口的時(shí)候,沒人會想到會有多線程應(yīng)用服務(wù)著成千上萬個連接所以select有很多設(shè)計(jì)缺陷,使得它在現(xiàn)在網(wǎng)絡(luò)應(yīng)用中并不是一個合格的輪詢機(jī)制。主要缺陷如下:

  • select修改了傳入的fd_sets使得他們不可以被重用,即使你不需要改變?nèi)魏螙|西 - 比如一個描述符接收到了數(shù)據(jù),并且需要接收更多數(shù)據(jù) - 整個set要么需要重新創(chuàng)建,要么使用FD_COPY從備存中恢復(fù)。上述過程在每次select調(diào)用中都會需要。
  • 為了找到哪個描述符發(fā)起了事件,我們需要手動遍歷set中的所有描述符,并且對每一個調(diào)用FD_ISSET方法。當(dāng)你有2000個描述符但是只有一個是活躍的,假設(shè)是最后一個...我們浪費(fèi)了太多CPU時(shí)間片。
  • 我們剛才提到了2000個描述符?但是select并不支持這么多,至少在Linux中,它把FD_SETSIZE這個常量設(shè)為了1024。有些操作系統(tǒng)可能支持使用某種黑科技重新定義這個大小,但是Linux不行~~
  • 我們不能在等待的時(shí)候從另一個線程中修改這個描述符set。假設(shè)一個線程正在執(zhí)行上述代碼,然后有一個清理線程認(rèn)為socket1等待輸入數(shù)據(jù)時(shí)間太長了,需要將socket1釋放出來。清理線程想關(guān)閉這個socket從而這個socket可以被重用來服務(wù)其他客戶端。但是這個socket目前在select等待的fd_set中。那么這個socket被關(guān)閉以后會發(fā)生什么?select手冊告訴我們一個不喜歡的答案:"如果被select監(jiān)控的文件描述符被另一個線程關(guān)閉了,那么結(jié)果不可預(yù)知..."
  • 如果另一個線程突然覺得通過socket1發(fā)送點(diǎn)什么,我們會有同樣的問題。除非select返回,不然沒有辦法開始監(jiān)控socket的輸出事件。
  • 事件等待時(shí)候的選擇十分有限;例如:要判斷遠(yuǎn)程socket是否關(guān)閉,我們需要 a) 監(jiān)控它的輸入, b)真正嘗試從socket讀取數(shù)據(jù)來判斷是否關(guān)閉(read會返回0)。如果你正好想從socket讀數(shù)據(jù)的話這還好,但是如果你只是發(fā)送文件并不關(guān)注輸入呢?
  • select 還有額外的開銷,你需要傳入描述符列表來計(jì)算最大描述符代表的數(shù)字。

當(dāng)然操作系統(tǒng)開發(fā)者明白這些缺點(diǎn),并且在設(shè)計(jì)輪詢方法的時(shí)候處理了大部分。你或許會問,為什么我們還要使用select?為什么不把它放在電腦博物館中?主要原因有兩個,這個原因或許對你來說很重要,或許你根本無需關(guān)心:

  1. 第一個原因是移植性,select已經(jīng)使用了很久,我們周邊支持網(wǎng)絡(luò)和非阻塞socket的設(shè)備會有select的實(shí)現(xiàn),甚至可能根本沒有輪詢方法。
  2. 第二個原因,select可以以ns的精度處理超時(shí),poll和epoll的精度在ms。對于桌面應(yīng)用或者服務(wù)器系統(tǒng)來說這不是個問題,但是對于實(shí)時(shí)嵌入式系統(tǒng)來說可能很有用。比如用來控制關(guān)閉核反應(yīng)堆的系統(tǒng),對于這種系統(tǒng)我們需要確保絕對安全...

上面的例子或許是僅有的我們必須使用select的情況。但是如果應(yīng)用不會超過一定量的socket,比如200個,poll和select的性能差異并不顯著,選擇哪個主要還是看個人偏好或者其他原因。

使用Poll進(jìn)行輪詢

poll is a newer polling method which probably was created immediately after someone actually tried to write the high performance networking server. It is much better designed and doesn’t suffer from most of the problems which select has. In the vast majority of cases you would be choosing between poll and epoll/libevent.

poll是一個更新的輪詢方法,很可能在有人真正著手開發(fā)高性能網(wǎng)絡(luò)服務(wù)器的時(shí)候就被創(chuàng)造出來。它設(shè)計(jì)的更為完善,并且解決了select的大多數(shù)問題。大多數(shù)情況下,我們需要在poll和epoll/libevent之間做出選擇。

To use poll, the developer needs to initialize the members of struct pollfd structure with the descriptors and events to monitor, and call the poll(). A typical workflow looks like that:

開發(fā)者需要用描述符和監(jiān)控的事件來初始化struct pollfd,然后調(diào)用poll方法。一個常見的工作流如下:

// The structure for two events
struct pollfd fds[2];

// 監(jiān)控socket1的輸入
fds[0].fd = sock1;
fds[0].events = POLLIN;

// 監(jiān)控socket2的輸出
fds[1].fd = sock2;
fds[1].events = POLLOUT;

// 等待10秒
int ret = poll( &fds, 2, 10000 );
// 檢查輪詢有沒有成功
if ( ret == -1 )
    // 報(bào)告錯誤并且中止
else if ( ret == 0 )
    // 超時(shí);沒有檢測到事件
else
{
    // If we detect the event, zero it out so we can reuse the structure
    //如果檢測到事件,將其置為0,我們可以重用這個結(jié)構(gòu)
    if ( pfd[0].revents & POLLIN )
        pfd[0].revents = 0;
        // socket1的輸入事件

    if ( pfd[1].revents & POLLOUT )
        pfd[1].revents = 0;
        // socket2的輸出事件 
}

poll主要是為了修復(fù)select的問題,所以有以下優(yōu)點(diǎn):

  • 對于描述符沒有1024這硬限制

  • 沒有修改傳入的pollfd中的數(shù)據(jù)。所以這個數(shù)據(jù)在poll調(diào)用中可以不斷重用,只要將產(chǎn)生事件的描述符事件的輸出設(shè)置為0。IEEE規(guī)范中提到"在每個pollfd結(jié)構(gòu)中,poll方法應(yīng)該清除輸出"

  • 允許更細(xì)粒度的事件控制。比如它可以檢測到遠(yuǎn)端的關(guān)閉,而無需監(jiān)控讀事件。

它也有一些缺點(diǎn),比如poll在vista之前的windows系統(tǒng)中就不存在;在vista以及以上的系統(tǒng)中,它叫做WSAPoll。

還有poll的超時(shí)精度在ms級別,這大多時(shí)候也都不是問題。

但是下面兩個問題我們需要注意:

  • 像select一樣,我們?nèi)匀恢挥型耆闅v整個列表,檢查所有的輸出才能知道哪個文件描述符觸發(fā)了事件。更糟糕的是,在內(nèi)核空間也是如此。內(nèi)核需要遍歷整個文件描述符列表找到哪些socket被監(jiān)控,然后再次遍歷整個列表來設(shè)置事件。
  • 像select一樣,無法動態(tài)修改set或者關(guān)閉正在被輪詢的socket

對于大多數(shù)客戶端網(wǎng)絡(luò)應(yīng)用甚至服務(wù)端來說,這些都不是問題 - 唯一的例外是類似P2P應(yīng)用,需要處理成千上萬開啟的連接。這樣的話我們應(yīng)該選擇poll而不是select。在以下情況下,我們甚至應(yīng)該使用poll而不是更新的epoll:

  • 我們需要支持的不僅僅是Linux,并且不想使用epoll的包裝方法,比如libevent(epoll只適用于Linux)
  • 應(yīng)用需要同時(shí)監(jiān)控的socket不超過1000個(用epoll沒有明顯好處)
  • 應(yīng)用需要監(jiān)控超過1000個socket,但是連接都非常短。
  • 在有線程等待事件的時(shí)候,其他并不會修改這個事件。

使用epoll輪詢

epoll是Linux中最新,最好的輪詢方式。但是也不是那么新,2002年被加入到內(nèi)核中。它與poll和select都不同,在內(nèi)核中保存了當(dāng)前被監(jiān)控的描述符和關(guān)聯(lián)的事件,并且暴露了增刪改的API。

我們需要預(yù)先準(zhǔn)備很多步驟來使用epoll:

  • 調(diào)用epoll_create創(chuàng)建epoll描述符
  • 使用需要的event和上下文數(shù)據(jù)指針初始化struct epoll,上下文可以是任何東西,epoll將它直接傳給返回的事件類型。這里我們將一個指針存儲到Connection類中
  • 調(diào)用epoll_ctl(EPOLL_CTL_ADD)將描述符添加到監(jiān)控set中
  • 調(diào)用epoll_wait等待20個事件,然后保存到存儲空間中。不像之前的方法,這個調(diào)用接收空的數(shù)據(jù)結(jié)構(gòu),然后使用觸發(fā)的事件進(jìn)行填充。例如,如果總共有200個描述符,其中5個有待定事件,epoll_wait會返回5,只有前5個pevent結(jié)構(gòu)會被初始化。如果有50個有待定事件,第一次處理時(shí)前20個會被拷貝,剩下的30個會留在隊(duì)列中(不會丟失)。
  • 遍歷返回的東西,這個遍歷會很短,因?yàn)橹挥杏|發(fā)的事件會被返回。

一個典型的流程如下:

// 創(chuàng)建epoll描述符。在應(yīng)用中只需要一個,并且用來監(jiān)控所有的socket
//方法參數(shù)現(xiàn)在被忽略,隨便賦值
int pollingfd = epoll_create( 0xCAFE ); 

if ( pollingfd < 0 )
//報(bào)告錯誤

//初始化epoll數(shù)據(jù)結(jié)構(gòu),后面可能有更多事件加進(jìn)來
struct epoll_event ev = { 0 };

//將connection類實(shí)例和事件關(guān)聯(lián),這里我們可以關(guān)聯(lián)任何東西
//這里是我們自己加的,epoll不使用這個信息,存儲了一個connection類的指針,pConnection1

ev.data.ptr = pConnection1;

//監(jiān)控輸入,不要在事件以后重新給描述符賦值
ev.events = EPOLLIN | EPOLLONESHOT;

//添加描述符到監(jiān)控列表,即使其他線程在epoll_wait中等待,我們可以正確添加
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
    // 報(bào)告錯誤

//等待直到有20個事件
struct epoll_event pevents[ 20 ];

// 等待10秒
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
// 檢查epoll是否成功
if ( ret == -1 )
    // 報(bào)告錯誤并且中止
else if ( ret == 0 )
    // 超時(shí);沒有檢測到事件
else
{
    // 檢查是否監(jiān)測到事件
    for ( int i = 0; i < ret; i++ )
    {
        if ( pevents[i].events & EPOLLIN )
        {
            // 返回connection指針
            Connection * c = (Connection*) pevents[i].data.ptr;
            c->handleReadEvent();
         }
    }
}

看到上面的實(shí)現(xiàn),我們可以明白epoll的缺點(diǎn)。它用起來比較復(fù)雜。并且和poll相比需要更多庫的調(diào)用。

epoll和poll/select相比有很多好處:

  • epoll返回觸發(fā)事件的描述符列表,而不需要遍歷整個描述符列表
  • 我們可以給監(jiān)控的事件附加更有意義的上下文而不是socket文件描述符。在我們的例子中,我們附加了一個class指針,可以直接進(jìn)行調(diào)用,而不需要再次進(jìn)行查詢
  • 我們可以在任何時(shí)候?yàn)楸O(jiān)控添加或者刪除socket,即使其他線程正在epoll_wait中。甚至還可以修改描述符事件而不會引發(fā)問題。并且實(shí)現(xiàn)的文檔非常齊全,這使我們在實(shí)現(xiàn)中有更多的靈活性
  • 因?yàn)閮?nèi)核知曉所有被監(jiān)控的描述符,它可以在描述符上注冊事件,即使沒有人在調(diào)用epoll_wait。這可以讓我們實(shí)現(xiàn)一些有趣的功能,比如邊沿觸發(fā)(edge trigger)。
  • 在epoll中,可以有多個線程調(diào)用epoll_wait等在相同的epoll隊(duì)列中,select和poll中就不可以。事實(shí)上,不僅僅是可以,而是在邊沿觸發(fā)模式中推薦的方式。

我們需要記住的是,epoll不是一個"更好的poll",和poll相比,它也有缺點(diǎn):

  • 改變事件flag(比如從READ到WRITE)需要一個epoll_ctl系統(tǒng)調(diào)用,如果使用poll,只需在用戶空間的一個簡單的bitmask操作。epoll將5000個socket從讀切換到寫則需要5000個系統(tǒng)調(diào)用和上下文切換(直到2014年,epoll_ctl還沒辦法做到批處理,每個描述符需要單個切換)。但是poll只需要在一個pollfd結(jié)構(gòu)中進(jìn)行循環(huán)
  • 當(dāng)一個accepted socket需要被加到set中,epoll同樣需要一個epoll_ctl,這就意味著每個新的連接socket都需要兩次系統(tǒng)調(diào)用(poll是一次)。如果我們的服務(wù)有很多短期的連接,并且只有很少的數(shù)據(jù)傳輸,epoll很可能可能需要更多時(shí)間進(jìn)行響應(yīng)
  • epoll是Linux專有,其他平臺有相似機(jī)制,但是也不盡相同。比如邊沿觸發(fā)機(jī)制
  • 高性能的處理邏輯更為復(fù)雜,而且更難調(diào)試,尤其是邊沿觸發(fā)更容易出現(xiàn)死鎖

因此我們應(yīng)當(dāng)在滿足以下全部情況的時(shí)候使用epoll:

  • 應(yīng)用使用了線程輪詢,通過多個線程處理多個連接。單線程應(yīng)用則不會利用到epoll的優(yōu)勢
  • 我們預(yù)期會有大量socket(多余1000)需要監(jiān)控,數(shù)量再小epoll沒有性能優(yōu)勢,再小的話epoll反而會更慢
  • 連接相對生命周期較長,正如之前所說,epoll在新連接只發(fā)送少量數(shù)據(jù),然后立即斷開的情況性能較差。因?yàn)閑poll需要額外的系統(tǒng)調(diào)用將描述符添加到epoll set中
  • 我們的應(yīng)用依賴Linux特殊的特性,或者我們可以提供epoll的包裝類
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容