http://www.cppfans.org/1417.html
http://blog.lucode.net/linux/epoll-tutorial.html
現如今,網絡通訊中用epoll(linux)和IOCP(windows)幾乎是大家津津樂道的東西,不為別的,就因為高效,所以大家喜歡用。IOCP的基礎東西已經講過了,可翻閱《IOCP淺析》 《IOCP淺析[二]——IOCP出現的意義和函數接口》.
什么是epoll?
epoll是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量並發連接中只有少量活躍的情況下的系統CPU利用率,因為它會復用文件描述符集 合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新准備要被偵聽的文件描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符 集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的電平觸發 (Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。Linux2.6內核中對/dev/epoll設備的訪問的封裝(system epoll)。
這個使我們開發網絡應用程序更加簡單,並且更加高效。
為什么要使用epoll?
同樣,我們在linux系統下,影響效率的依然是I/O操作,linux提供給我們select/poll/epoll等多路復用I/O方式(kqueue暫時沒研究過),為什么我們對epoll情有獨鍾呢?原因如下:
1.文件描述符數量的對比。
epoll並沒有fd(文件描述符)的上限,它只跟系統內存有關,我的2G的ubuntu下查看是20480個,輕松支持20W個fd。可使用如下命令查看:
cat /proc/sys/fs/file-max
再來看select/poll,有一個限定的fd的數量,linux/posix_types.h頭文件中
#define __FD_SETSIZE 1024
2.效率對比。
當然了,你可以修改上述值,然后重新編譯內核,然后再次寫代碼,這也是沒問題的,不過我先說說select/poll的機制,估計你馬上會作廢上面修改枚舉值的想法。
select/poll會因為監聽fd的數量而導致效率低下,因為它是輪詢所有fd,有數據就處理,沒數據就跳過,所以fd的數量會降低效率;而epoll只處理就緒的fd,它有一個就緒設備的隊列,每次只輪詢該隊列的數據,然后進行處理。(先簡單講一下,第二篇還會詳細講解)
3.內存處理方式對比。
不管是哪種I/O機制,都無法避免fd在操作過程中拷貝的問題,而epoll使用了mmap(是指文件/對象的內存映射,被映射到多個內存頁上),所以同一塊內存就可以避免這個問題。
btw:TCP/IP協議棧使用內存池管理sk_buff結構,你還可以通過修改內存池pool的大小,畢竟linux支持各種微調內核。
epoll的工作方式
epoll分為兩種工作方式LT和ET。
LT(level triggered) 是默認/缺省的工作方式,同時支持 block和no_block socket。這種工作方式下,內核會通知你一個fd是否就緒,然后才可以對這個就緒的fd進行I/O操作。就算你沒有任何操作,系統還是會繼續提示fd已經就緒,不過這種工作方式出錯會比較小,傳統的select/poll就是這種工作方式的代表。
ET(edge-triggered) 是高速工作方式,僅支持no_block socket,這種工作方式下,當fd從未就緒變為就緒時,內核會通知fd已經就緒,並且內核認為你知道該fd已經就緒,不會再次通知了,除非因為某些操作導致fd就緒狀態發生變化。如果一直不對這個fd進行I/O操作,導致fd變為未就緒時,內核同樣不會發送更多的通知,因為only once。所以這種方式下,出錯率比較高,需要增加一些檢測程序。
LT可以理解為水平觸發,只要有數據可以讀,不管怎樣都會通知。而ET為邊緣觸發,只有狀態發生變化時才會通知,可以理解為電平變化。
如何使用epoll?
使用epoll很簡單,只需要
#include <sys/epoll.h>
有三個關鍵函數:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_events* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
當然了,不要忘記關閉函數.
============分割線==============
這篇就講到這里了,下面兩篇主要是函數介紹,效率分析,例子。
轉載請注明:C++愛好者博客 » 淺析epoll-為何多路復用I/O要使用epoll
前一篇大致講了一下epoll是個什么東西,優點等內容,這篇延續上一篇的內容,主要是分析epoll的函數,epoll高性能的深入分析。
epoll的三大函數
1.創建epoll fd函數
int epoll_create(int size);
epoll_create()創建一個epoll的事例,通知內核需要監聽size個fd。size指的並不是最大的后備存儲設備,而是衡量內核內部結構大小的一個提示。當創建成功后,會占用一個fd,所以記得在使用完之后調用close(),否則fd可能會被耗盡。
Note:自從Linux2.6.8版本以后,size值其實是沒什么用的,不過要大於0,因為內核可以動態的分配大小,所以不需要size這個提示了。
創建還有另外一個函數
int epoll_create1(int flag);
這個函數是在linux 2.6.27中加入的,當你在看陳碩的muduo時可以看到這個函數,其實它和epoll_create差不多,不同的是epoll_create1函數的參數是flag,當flag是0時,表示和epoll_create函數完全一樣,不需要size的提示了。
當flag = EPOLL_CLOEXEC,創建的epfd會設置FD_CLOEXEC
當flag = EPOLL_NONBLOCK,創建的epfd會設置為非阻塞
一般用法都是使用EPOLL_CLOEXEC.
Note:關於FD_CLOEXEC,現在網上好多都說的有點問題,我翻閱了一些資料,請教了一些人,大約明白它的意思了。
它是fd的一個標識說明,用來設置文件close-on-exec狀態的。當close-on-exec狀態為0時,調用exec時,fd不會被關閉;狀態非零時則會被關閉,這樣做可以防止fd泄露給執行exec后的進程。關於exec的用法,大家可以去自己查閱下,或者直接man exec。
2.epoll事件的注冊函數
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
select是在監聽時告訴內核要監聽的事件,而epoll_ctl是先注冊需要監聽的事件。
第一個參數epfd,為epoll_create返回的的epoll fd。
第二個參數op表示操作值。有三個操作類型,
EPOLL_CTL_ADD // 注冊目標fd到epfd中,同時關聯內部event到fd上 EPOLL_CTL_MOD // 修改已經注冊到fd的監聽事件 EPOLL_CTL_DEL // 從epfd中刪除/移除已注冊的fd,event可以被忽略,也可以為NULL
第三個參數fd表示需要監聽的fd。
第四個參數event表示需要監聽的事件。
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
event參數是一個枚舉的集合,可以用” | “來增加事件類型,枚舉如下:
EPOLLIN:表示關聯的fd可以進行讀操作了。
EPOLLOUT:表示關聯的fd可以進行寫操作了。
EPOLLRDHUP(since Linux 2.6.17):表示套接字關閉了連接,或者關閉了正寫一半的連接。
EPOLLPRI:表示關聯的fd有緊急優先事件可以進行讀操作了。
EPOLLERR:表示關聯的fd發生了錯誤,epoll_wait會一直等待這個事件,所以一般沒必要設置這個屬性。
EPOLLHUP:表示關聯的fd掛起了,epoll_wait會一直等待這個事件,所以一般沒必要設置這個屬性。
EPOLLET:設置關聯的fd為ET的工作方式,epoll的默認工作方式是LT。
EPOLLONESHOT (since Linux 2.6.2):設置關聯的fd為one-shot的工作方式。表示只監聽一次事件,如果要再次監聽,需要把socket放入到epoll隊列中。
3.epoll等待事件函數
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
上面兩個函數的參數含義:
第一個參數:表示epoll_wait等待epfd上的事件
第二個參數:events指針攜帶有epoll_data_t數據
第三個參數:maxevents告訴內核events有多大,該值必須大於0
第四個參數:timeout表示超時時間(單位:毫秒)
epoll_pwait(since linux 2.6.19)允許一個應用程序安全的等待,直到fd設備准備就緒,或者捕獲到一個信號量。其中sigmask表示要捕獲的信號量。
函數如果等待成功,則返回fd的數字;0表示等待fd超時,其他錯誤號請查看errno
函數到這里就講完了,下一篇會寫一個例子給大家看下這些函數是如何使用的。
============
epoll支持水平觸發和邊緣觸發,理論上來說邊緣觸發性能更高,但是使用更加復雜,因為任何意外的丟失事件都會造成請求處理錯誤。Nginx就使用了epoll的邊緣觸發模型。
這里提一下水平觸發和邊緣觸發就緒通知的區別,這兩個詞來源於計算機硬件設計。它們的區別是只要句柄滿足某種狀態,水平觸發就會發出通知;而只有當句柄狀態改變時,邊緣觸發才會發出通知。例如一個socket經過長時間等待后接收到一段100k的數據,兩種觸發方式都會向程序發出就緒通知。假設程序從這個socket中讀取了50k數據,並再次調用監聽函數,水平觸發依然會發出就緒通知,而邊緣觸發會因為socket“有數據可讀”這個狀態沒有發生變化而不發出通知且陷入長時間的等待。
因此在使用邊緣觸發的 api 時,要注意每次都要讀到 socket返回 EWOULDBLOCK為止。 否則netstat 的recv-q會持續增加
===============
通常來說,et方式是比較危險的方式,如果要使用et方式,那么,應用程序應該 1、將socket設置為non-blocking方式 2、epoll_wait收到event后,read或write需要讀到沒有數據為止,write需要寫到沒有數據為止(對於non-blocking socket來說,EAGAIN通常是無數據可讀,無數據可寫的返回狀態);
我們最近遇到一個問題,就是由於在使用epoll的過程中,緩沖區的數據沒有讀完,造成后續的通信失敗。
表現現象就是,使用netstat -an觀察時,這個socket的recv-q值不為0.