Linux五種IO模型淺談

http://www.ywnds.com/?p=10504

文件描述符

我們知道Linux的內核將所有外部設備都可以看做一個文件來操作。那麼我們對與外部設備的操作都可以看做對文件進行操作。我們對一個文件的讀寫,都通過調用內核提供的系統調用,內核給我們返回一個文件描述符(file descriptor,簡稱fd)。我們通過ls -l  /proc/${pid}/fd/ 可以看到進程${pid}佔用的所有描述符。而對一個socket的讀寫也會有相應的描述符,稱爲socket FD(socket描述符),描述符就是一個數字,指向內核中一個結構體;文件路徑,數據區,等一些屬性。而我們的應用程序對文件的讀寫就通過對描述符的讀寫完成。

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。 

進程切換

現代多任務系統,通俗點說就是把CPU的時間進行分片了,在特定的時間片內處理一個特定的系統進程(指單核CPU)。就這樣,在多個進程之間利用CPU時間分片來回處理就是我們看到的多任務處理。

當然,爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復以前掛起的某個進程的執行。這種行爲被稱爲進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:

1. 保存處理機上下文,包括程序計數器和其他寄存器。

2. 更新PCB信息。

3. 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。

4. 選擇另一個進程執行,並更新其PCB。

5. 更新內存管理的數據結構。

6. 恢復處理機上下文。

PS:總而言之就是很耗資源

緩存IO和直接IO

緩存IO又被稱作標準IO,大多數文件系統的默認IO 操作都是緩存IO。在Linux的緩存IO機制中,操作系統會將IO的數據緩存在文件系統的頁緩存(page cache)中,也就是說,數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。

緩存IO的缺點:

數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的CPU以及內存開銷是非常大的。

系統如何調用I/O操作

系統調用是如何完成一個I/O操作的呢? Linux將內存分爲內核區和用戶區;Linux內核給我們管理所有的硬件資源,應用程序通過調用系統調用和內核交互,達到使用硬件資源的目的;應用程序通過系統調用read()發起一個讀操作,這時候內核創建一個文件描述符,並通過驅動程序向硬件發送讀指令,並將讀的數據放在這個描述符對應結構體的緩存區。但這個結構體是在內核內存區的。所以需要將這個數據複製一份寫入到用戶區進程空間內,這樣完成了一次讀操作。

Linux提供的read/write系統調用,都是一個阻塞函數。這樣我們的應用進程在發起read/write系統調用時,就必須阻塞,就進程被掛起(進程睡眠)而等待文件描述符的讀就緒。

文件描述符讀就緒和寫就緒

讀就緒:就是這個文件描述符的接收緩衝區中的數據字節數大於等於套接字接收緩衝區低水位標記的當前大小。

寫就緒:該描述符發送緩衝區的可用空間字節數大於等於描述符發送緩衝區低水位標記的當前大小(如果是socket fd,說明上一個數據已經發送完成)。

接收低水位標記和發送低水位標記:由應用程序指定,比如應用程序指定接收低水位爲64個字節,那麼接收緩衝區有64個字節,纔算fd讀就緒。

有沒有辦法能讓我們在進行Socket I/O操作時,不讓我們的應用進程阻塞。於是對高效的處理I/O的需求就越緊迫了。在人們不斷探索中,時至今日一見發明了很多種處理I/O問題的方式,從形式上劃分的話,可歸類爲5大模型:阻塞、非阻塞、多路複用、信號驅動和異步I/O(AIO)

再說一下IO發生時涉及的對象和步驟。對於一個網絡IO (這裏我們以read舉例),它會涉及到兩個系統對象,一個是調用這個IO的process (or thread),另一個就是系統內核(kernel)。當一個read操作發生時,它會經歷兩個階段:

1.等待數據準備 (Waiting for the data to be ready)

2.將數據從內核拷貝到進程中(Copying the data from the kernel to the process)

記住這兩點很重要,因爲這些IO模型的區別就是在兩個階段上各有不同的情況。

五中IO模型闡述

1)阻塞式I/O

在Linux中,默認情況下所有的socket都是阻塞,一個典型的讀操作流程大概是這樣:


當用戶進程調用了recvfrom這個系統調用,內核就開始了IO的第一個階段:準備數據,對於網絡IO來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候內核就要等待足夠的數據到來。而在用戶進程這邊,整個進程會被阻塞。當內核一直等到數據準備好了,它就會將數據從內核中拷貝到用戶內存(copy socket data from kenel space to user space.),然後內核返回結果,用戶進程才解除阻塞的狀態,重新運行起來。所以,阻塞式IO的特點就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被阻塞了。

幾乎所有的程序員第一次接觸到的網絡編程都是從listen()、send()、recv() 等接口開始的,這些接口都是阻塞型的。使用這些接口可以很方便的構建服務器/客戶機的模型。下面是一個簡單地“一問一答”的服務器。


我們注意到,大部分的socket接口都是阻塞型的。所謂阻塞型接口是指系統調用(一般是IO接口)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。

實際上,除非特別指定,幾乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。這給網絡編程帶來了一個很大的問題,如在調用send()的同時,線程將被阻塞,在此期間,線程將無法執行任何運算或響應任何的網絡請求。

一個簡單的改進方案是在服務器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。具體使用多進程還是多線程,並沒有一個特定的模式。傳統意義上,進程的開銷要遠遠大於線程,所以如果需要同時爲較多的客戶機提供服務,則不推薦使用多進程;如果單個服務執行體需要消耗較多的CPU資源,譬如需要進行大規模或長時間的數據運算或文件訪問,則進程較爲安全。通常,使用pthread_create ()創建新線程,fork()創建新進程。

我們假設對上述的服務器 / 客戶機模型,提出更高的要求,即讓服務器同時爲多個客戶機提供一問一答的服務。那麼就會出現這個情況,如果有連接,則創建新線程,並在新線程中提供爲前例同樣的問答服務。這樣一來,多線程的服務器模型似乎完美的解決了爲多個客戶機提供問答服務的要求,但其實並不盡然。如果要同時響應成百上千路的連接請求,則無論多線程還是多進程都會嚴重佔據系統資源,降低系統對外界響應效率,而線程與進程本身也更容易進入假死狀態。

很多程序員可能會考慮使用“線程池”或“連接池”。“線程池”旨在減少創建和銷燬線程的頻率,其維持一定合理數量的線程,並讓空閒的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,儘量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種數據庫等。但是,“線程池”和“連接池”技術也只是在一定程度上緩解了頻繁調用IO接口帶來的資源佔用。而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應並不比沒有池的時候效果好多少。所以使用“池”必須考慮其面臨的響應規模,並根據響應規模調整“池”的大小。

對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。總之,多線程模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,可以用非阻塞接口來嘗試解決這個問題。

2)非阻塞式I/O

Linux下,可以通過設置socket使其變爲非阻塞式IO。當對一個非阻塞式IO的socket執行讀操作時,流程是這個樣子:


從圖中可以看出,當用戶進程發出read操作時,如果內核中的數據還沒有準備好,那麼它並不會阻塞用戶進程,不要將進程睡眠,而是立刻返回一個error。從用戶進程角度講,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作,一旦內核中的數據準備好了,並且又再次收到了用戶進程的system call,那麼它馬上就將數據拷貝到了用戶內存,然後返回。

所以,在非阻塞式IO中,用戶進程其實是需要不斷的主動詢問內核數據準備好了沒有。

非阻塞的接口相比於阻塞型接口的顯著差異在於,在被調用之後立即返回。使用如下的函數可以將某句柄fd設爲非阻塞狀態。

fcntl( fd, F_SETFL, O_NONBLOCK );

就是在I/O請求時加上O_NONBLOCK一類的標誌,立刻返回,但第二階段依然需要工作進程參與庫函數把內核空間數據複製到用戶空間,第二階段依舊阻塞。

下面將給出只用一個線程,但能夠同時從多個連接中檢測數據是否送達,並且接受數據的模型。


在非阻塞狀態下,recv()接口在被調用後立即返回,返回值代表了不同的含義。如在本例中,

* recv()返回值大於0,表示接受數據完畢,返回值即是接受到的字節數;

* recv()返回0,表示連接已經正常斷開;

* recv()返回-1,且errno等於EAGAIN,表示recv操作還沒執行完成;

* recv()返回-1,且errno不等於EAGAIN,表示recv操作遇到系統錯誤errno;

可以看到服務器線程可以通過循環調用recv()接口,可以在單個線程內實現對所有連接的數據接收工作。但是上述模型絕不被推薦。因爲,循環調用recv()將大幅度推高CPU 佔用率;此外,在這個方案中recv()更多的是起到檢測“操作是否完成”的作用,實際操作系統提供了更爲高效的檢測“操作是否完成“作用的接口,例如select()多路複用模式,可以一次檢測多個連接是否活躍。

3I/O多路複用

I/O多路複用(IO multiplexing)同阻塞式I/O本質上是一樣的,有些地方也稱這種IO方式爲事件驅動IO(event driven IO),因爲利用新的select()、poll()、epoll()等系統調用(函數),由操作系統來負責輪詢操作。select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個函數會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:


當用戶進程調用了select,那麼整個進程會被阻塞,而同時,內核會“監視”所有select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從內核拷貝到用戶進程。

這個圖和阻塞式IO的圖其實並沒有太大的不同,事實上還更差一些。因爲這裏需要使用兩個系統調用(select和recvfrom),而阻塞式IO只調用了一個系統調用(recvfrom)。但是用select的優勢在於它可以同時處理多個connection。(多說一句:所以,如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。)

在多路複用模型中,對於每一個socket,一般都設置成爲非阻塞,但是,如上圖所示,整個用戶的process其實是一直被阻塞的。只不過process是被select這個函數阻塞,而不是被socket IO給阻塞。因此select()與非阻塞IO類似。

下面將重新模擬上例中從多個客戶端接收數據的模型。


這種模型的特徵在於每一個執行週期都會探測一次或一組事件,一個特定的事件會觸發某個特定的響應。我們可以將這種模型歸類爲“事件驅動模型”。相比其他模型,使用select()的事件驅動模型只用單線程(進程)執行,佔用資源少,不消耗太多CPU,同時能夠爲多客戶端提供服務。如果試圖建立一個簡單的事件驅動的服務器程序,這個模型有一定的參考價值。

但這個模型依舊有着很多問題。首先select()接口並不是實現“事件驅動”的最好選擇。因爲當需要探測的句柄值較大時,select()接口本身需要消耗大量時間去輪詢各個句柄。很多操作系統提供了更爲高效的接口,如Linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。如果需要實現更高效的服務器程序,類似epoll這樣的接口更被推薦。遺憾的是不同的操作系統特供的epoll接口有很大差異,所以使用類似於epoll的接口實現具有較好跨平臺能力的服務器會比較困難。其次,該模型將事件探測和事件響應夾雜在一起,一旦事件響應的執行體龐大,則對整個模型是災難性的。幸運的是,有很多高效的事件驅動庫可以屏蔽上述的困難,常見的事件驅動庫有libevent庫,還有作爲libevent替代者的libev庫。這些庫會根據操作系統的特點選擇最合適的事件探測接口,並且加入了信號(signal) 等技術以支持異步響應,這使得這些庫成爲構建事件驅動模型的不二選擇。

4)信號驅動I/O模型

信號驅動I/O是一種不常用的I/O模型,首先我們允許套接口進行信號驅動I/O,並安裝一個信號處理函數sigaltion,調用sigaltion系統調用,當內核中IO數據就緒時以SIGIO信號通知請求進程,請求進程再把數據從內核讀入到用戶空間,這一步是阻塞的。


5)異步IO(Asynchronous I/O,AIO)

Linux下的異步IO其實用得不多,從內核2.6版本纔開始引入。先看一下它的流程:


工作進程調用I/O庫函數epoll,工作進程不會因爲I/O操作而阻塞,也不需要輪詢,待I/O操作完成後會通知請求者。(異步I/O實現比較複雜但是Nginx支持磁盤異步I/O);LINUX2.6內核纔開始支持。

用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從內核的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何阻塞。然後,內核會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,內核會給用戶進程發送一個Signal,告訴它read操作完成了,在這整個過程中,進程完全沒有被阻塞。用異步IO實現的服務器如近年比較流行的Nginx服務器。異步IO是真正非阻塞的,它不會對請求進程產生任何的阻塞,因此對高併發的網絡服務器實現至關重要。

總結的IO模型有5種之多:阻塞IO,非阻塞IO,IO複用,信號驅動IO,異步IO。如下圖:


到目前爲止,已經將五個IO模型都介紹完了。但是我們還經常會聽到這麼一個概念就是“同步IO(synchronous IO)”和“異步IO(Asynchronous)”,對於這兩個概念,POSIX的定義是這樣子的:

*一個同步的輸入/輸出操作會導致請求進程被阻塞,直到完成/輸出操作完成;

*一個異步輸入/輸出操作不會導致請求進程被阻塞;

兩者的區別就在於同步IO做”IO operation”的時候會將process阻塞。按照這個定義,之前所述的阻塞IO,非阻塞IO,多路複用IO都屬於同步IO。有人可能會說,非阻塞式IO並沒有被阻塞啊。這裏有個非常“狡猾”的地方,定義中所指的”IO operation”是指真實的IO操作,就是例子中的recvfrom這個系統調用。非阻塞式IO在執行recvfrom這個系統調用的時候,如果內核的數據沒有準備好,這時候不會阻塞進程。但是當內核中數據準備好的時候,recvfrom會將數據從內核拷貝到用戶內存中,這個時候進程是被阻塞了,在這段時間內進程是被阻塞的。而異步IO則不一樣,當進程發起IO操作之後,就直接返回再也不理睬了,直到內核發送一個信號,告訴進程說IO完成。在這整個過程中,進程完全沒有被阻塞。

 

所以前四種都屬於同步IO。阻塞IO不必說了。非阻塞IO ,IO請求時加上O_NONBLOCK一類的標誌位,立刻返回,IO沒有就緒會返回錯誤,需要請求進程主動輪詢不斷髮IO請求直到返回正確。IO複用同非阻塞IO本質一樣,不過利用了新的select系統調用,由內核來負責本來是請求進程該做的輪詢操作。看似比非阻塞IO還多了一個系統調用開銷,不過因爲可以支持多路IO,纔算提高了效率。信號驅動IO,調用sigaltion系統調用,當內核中IO數據就緒時以SIGIO信號通知請求進程,請求進程再把數據從內核讀入到用戶空間,這一步是阻塞的。異步IO,如定義所說,不會因爲IO操作阻塞,IO操作全部完成才通知請求進程。

C10K問題

Select() & Poll()

Select()調用是I/O多路複用模型下的產物,I/O多路複用實際上是一種複合I/O模型,即利用類似於select的調用對阻塞或非阻塞式I/O的一個集合進行監控,epoll跟select都能提供多路I/O複用的解決方案。在現在的Linux內核裏有都能夠支持,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般操作系統均有實現。

經典的select處理方式是這樣的:調用者將需要監控的I/O句柄(FD)放入一個數組中,將這個數組傳遞給select調用,並設定監控何種事件,這時select會阻塞調用進程;當有I/O事件發生時,select就在數組中給發生了事件的那些I/O句柄做一個標記後返回;之後,調用者便輪詢這個數組,發現被打了標記的便進行相應的處理,並去掉這個標記以備下次使用。這樣,對於服務器程序來說,一個進程或線程就可以處理很多客戶端的讀寫請求了。不過select有一個限制,就是傳遞給它的I/O句柄最多不能超過1024個。於此人們就引入了poll這個調用,poll調用本質上和select沒有區別,只是在I/O句柄數理論上沒有上限了,原因是它是基於鏈表來存儲的,但是同樣有缺點:

1)大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。

2)poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

事情看似解決的不錯,可是互聯網越來越發達了,上網的人也越來越多了,網絡服務器在處理數以萬計的客戶端連接時,經常會出現效率低下甚至完全癱瘓的局面,這就是非常著名的C10K問題。C10K問題的特點是:一個設計不良好的程序,其性能和連接數,以及機器性能的關係往往是非線性的。換句話說,如果沒有考慮C10K問題,一個經典的基於select或poll的程序在舊機器上能很好地處理1000連接,它在2倍性能的新機器上往往處理不了2000併發。這是因爲大量的操作的消耗與當前連接數n成線性相關,從而導致單個任務的的資源消耗和當前任務的關係會是O(n)。那麼服務器程序同時對數以萬計的網絡I/O事件進行處理所積累下來的資源消耗會相當可觀,結果就是系統吞吐量不能和機器性能匹配。爲了解決這個問題,必須改變I/O複用的策略。

爲了解決上述這個問題,大神們就發明了epoll、kqueue和/dev/poll這三套利器。其中epoll是Linux的方案,kqueue是freebSD的方案,/dev/poll是最爲古老的solaris的方案,使用難度依次遞增。這些方案几乎不約而同地做了兩件事:

1)是避免每次調用select或poll時內核用於分析參數建立事件等待結構的開銷,取而代之的是維護一個長期的事件關注表,應用程序通過句柄修改這個列表和鋪貨I/O事件。

2)是避免了select或poll返回後,應用程序掃描整個句柄表的開銷,取而代之的是直接返回具體的事件列表。這樣,就徹底擺脫了具體操作的消耗與當前連接數n的線性關係,從而極大地提高了服務器的處理能力。

Epoll()

epoll是Linux內核爲處理大批量文件描述符而作了改進的poll,是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。

Epoll的優點

1)支持一個進程打開大數目的socket描述符

select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是1024。對於那些需要支持的上萬連接數目的服務器來說顯然太少了。Epoll雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接。舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max查看,一般來說這個數目和系統內存關係很大。不過這是理論上的,原因就是一個進程所能創建的,或者說能夠使用的I/O句柄數是有限制的。默認情況下,Linux允許一個進程對多擁有1024個I/O句柄。雖然可以修改爲無限制模式。但是這個不建議,如果要處理上萬併發連接的話,最好採用多進程模式,這樣不但可以充分利用CPU資源,還可以保證系統的整體穩定性。

2)IO效率不隨FD數目增加而線性下降

傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網絡延時,任一時間只有部分的socket是“活躍”的,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對“活躍”的socket進行操作—這是因爲在內核實現中epoll是根據每個fd上面的callback函數實現的。那麼,只有“活躍”的socket纔會主動的去調用 callback函數,其他idle狀態socket則不會,在這點上,epoll實現了一個“僞”AIO,因爲這時候推動力在os內核。在一些 benchmark中,如果所有的socket基本上都是活躍的—比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。

3)使用mmap加速內核與用戶空間的消息傳遞

這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就很重要,在這點上,epoll是通過內核與用戶空間mmap同一塊內存實現的。不管是什麼方案,都是避免不了內核向用戶空間傳遞消息,那麼避免不必要的數據拷貝就不失爲一個絕妙的辦法。mmap就能夠做到,因爲mmap可以使得內核空間和用戶空間的虛擬內存塊映射爲同一個物理內存塊。從而不需要數據拷貝,內核空間和用戶空間就可以訪問到相同的數據。

最後,epoll可以支持內核微調,不過不能把這個優點完全歸epoll所有,這是整個Linux系統的優點,賦予你微調內核的能力,就是procfs文件系統,對內核進行微調的開放接口。

Epoll的工作模型

epoll有兩種工作模式,即ET(邊沿觸發)和LT(水平觸發),還可以稱爲事件觸發和條件觸發。所謂邊沿觸發就是當狀態有變化時,也就是發生了某種事件就發出通知,而水平觸發就是當處於某種狀態,也可以說是具備某種條件就發出通知。

LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。

ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。

ET和LT的區別就在這裏體現,LT事件不會丟棄,而是隻要讀buffer裏面有數據可以讓用戶讀,則不斷的通知你。而ET則只在事件發生之時通知。可以簡單理解爲LT是水平觸發,而ET則爲邊緣觸發。LT模式只要有事件未處理就會觸發,而ET則只在高低電平變換時(即狀態從1到0或者0到1)觸發。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章