本章(第三章)內容其實和第二章內容,都是第一章內容的延伸。第二章內容是第一章內容的延伸,本章內容則是第一章內容再往底層方面的延伸,也是面試中考察網絡方面知識時,可能會問到的幾個點。
select、poll、epoll都是I/O多路複用的機制。I/O多路複用就是通過一種機制,一個進程可以監視多個文件描述符,一旦某個描述符就緒(讀就緒或寫就緒),能夠通知程序進行相應的讀寫操作 。
但是,select,poll,epoll本質還是同步I/O(I/O多路複用本身就是同步IO)的範疇,因爲它們都需要在讀寫事件就緒後線程自己進行讀寫,讀寫的過程阻塞的。而異步I/O的實現是系統會把負責把數據從內核空間拷貝到用戶空間,無需線程自己再進行阻塞的讀寫,內核已經准備完成。
一、Select機制
API簡介
linux系統中/usr/include/sys/select.h文件中對select方法的定義如下:
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Check the first NFDS descriptors each in READFDS (if not NULL) for read
readiness, in WRITEFDS (if not NULL) for write readiness, and in EXCEPTFDS
(if not NULL) for exceptional conditions. If TIMEOUT is not NULL, time out
after waiting the interval specified therein. Returns the number of ready
descriptors, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
int __nfds是fd_set中最大的描述符+1,當調用select時,內核態會判斷fd_set中描述符是否就緒,__nfds告訴內核最多判斷到哪一個描述符。
__readfds、__writefds、__exceptfds都是結構體fd_set,fd_set可以看作是一個描述符的集合。 select函數中存在三個fd_set集合,分別代表三種事件,readfds表示讀描述符集合,writefds表示讀描述符集合,exceptfds表示異常描述符集合。當對應的fd_set = NULL時,表示不監聽該類描述符。
timeval __timeout用來指定select的工作方式,即當文件描述符尚未就緒時,select是永遠等下去,還是等待一定的時間,或者是直接返回
函數返回值int表示: 就緒描述符的數量,如果爲-1表示産生錯誤 。
運行機制
Select會將全量fd_set從用戶空間拷貝到內核空間,並注冊回調函數, 在內核態空間來判斷每個請求是否准備好數據 。select在沒有查詢到有文件描述符就緒的情況下,將一直阻塞(I/O多路服用中提過:select是一個阻塞函數)。如果有一個或者多個描述符就緒,那麽select將就緒的文件描述符置位,然後select返回。返回後,由程序遍曆查看哪個請求有數據。
Select的缺陷
- 每次調用select,都需要把fd集合從用戶態拷貝到內核態,fd越多開銷則越大;
- 每次調用select都需要在內核遍曆傳遞進來的所有fd,這個開銷在fd很多時也很大
- select支持的文件描述符數量有限,默認是1024。參見/usr/include/linux/posix_types.h中的定義:
# define __FD_SETSIZE 1024
二、Poll機制
API簡介
linux系統中/usr/include/sys/poll.h文件中對poll方法的定義如下:
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
/* Poll the file descriptors described by the NFDS structures starting at
FDS. If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
an event to occur; if TIMEOUT is -1, block until an event occurs.
Returns the number of file descriptors with events, zero if timed out,
or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
__fds參數時Poll機制中定義的結構體pollfd,用來指定一個需要監聽的描述符。結構體中fd爲需要監聽的文件描述符,events爲需要監聽的事件類型,而revents爲經過poll調用之後返回的事件類型,在調用poll的時候,一般會傳入一個pollfd的結構體數組,數組的元素個數表示監控的描述符個數。
__nfds和__timeout參數都和Select機制中的同名參數含義類似
運行機制
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構代替select的fd_set(網上講:類似于位圖)結構,其他的本質上都差不多。所以Poll機制突破了Select機制中的文件描述符數量最大爲1024的限制。
Poll的缺陷
Poll機制相較于Select機制中,解決了文件描述符數量上限爲1024的缺陷。但另外兩點缺陷依然存在:
- 每次調用poll,都需要把fd集合從用戶態拷貝到內核態,fd越多開銷則越大;
- 每次調用poll,都需要在內核遍曆傳遞進來的所有fd,這個開銷在fd很多時也很大
三、Epoll機制
Epoll在Linux2.6內核正式提出,是基于事件驅動的I/O方式。相對于select來說,epoll沒有描述符個數限制;使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,通過內存映射,使其在用戶空間也可直接訪問,省去了拷貝帶來的資源消耗。
API簡介
linux系統中/usr/include/sys/epoll.h文件中有如下方法:
/* Creates an epoll instance. Returns an fd for the new instance.
The "size" parameter is a hint specifying the number of file
descriptors to be associated with the new instance. The fd
returned by epoll_create() should be closed with close(). */
extern int epoll_create (int __size) __THROW;
/* Manipulate an epoll instance "epfd". Returns 0 in case of success,
-1 in case of error ( the "errno" variable will contain the
specific error code ) The "op" parameter is one of the EPOLL_CTL_*
constants defined above. The "fd" parameter is the target of the
operation. The "event" parameter describes which events the caller
is interested in and any associated user data. */
extern int epoll_ctl (int __epfd, int __op, int __fd,
struct epoll_event *__event) __THROW;
/* Wait for events on an epoll instance "epfd". Returns the number of
triggered events returned in "events" buffer. Or -1 in case of
error with the "errno" variable set to the specific error code. The
"events" parameter is a buffer that will contain triggered
events. The "maxevents" is the maximum number of events to be
returned ( usually size of "events" ). The "timeout" parameter
specifies the maximum wait time in milliseconds (-1 == infinite).
This function is a cancellation point and therefore not marked with
__THROW. */
extern int epoll_wait (int __epfd, struct epoll_event *__events,
int __maxevents, int __timeout);
epoll_create函數:創建一個epoll實例並返回,該實例可以用于監控__size個文件描述符
epoll_ctl函數:向epoll中注冊事件,該函數如果調用成功返回0,否則返回-1。
- __epfd爲epoll_create返回的epoll實例
- __op表示要進行的操作
- __fd爲要進行監控的文件描述符
- __event要監控的事件
epoll_wait函數:類似與select機制中的select函數、poll機制中的poll函數,等待內核返回監聽描述符的事件産生。該函數返回已經就緒的事件的數量,如果爲-1表示出錯。
- __epfd爲epoll_create返回的epoll實例
- __events數組爲 epoll_wait要返回的已經産生的事件集合
- __maxevents爲希望返回的最大的事件數量(通常爲__events的大小)
- __timeout和select、poll機制中的同名參數含義相同
運行機制
epoll操作過程需要上述三個函數,也正是通過三個函數完成Select機制中一個函數完成的事情,解決了Select機制的三大缺陷。epoll的工作機制更爲複雜,我們就解釋一下,它是如何解決Select機制的三大缺陷的。
- 對于第一個缺點,epoll的解決方案是:它的fd是共享在用戶態和內核態之間的,所以可以不必進行從用戶態到內核態的一個拷貝,大大節約系統資源。至于如何做到用戶態和內核態,大家可以查一下“mmap”,它是一種內存映射的方法。
- 對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把當前線程輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把當前線程挂一遍(這一遍必不可少),並爲每個fd指定一個回調函數。當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表。那麽當我們調用epoll_wait時,epoll_wait只需要檢查鏈表中是否有存在就緒的fd即可,效率非常可觀。
- 對于第三個缺點,fd數量的限制,也只有Select存在,Poll和Epoll都不存在。由于Epoll機制中只關心就緒的fd,它相較于Poll需要關心所有fd,在連接較多的場景下,效率更高。在1GB內存的機器上大約是10萬左右,一般來說這個數目和系統內存關系很大。
工作模式
相較于Select和Poll,Epoll內部還分爲兩種工作模式: LT水平觸發(level trigger)和ET邊緣觸發(edge trigger)。
- LT模式: 默認的工作模式,即當epoll_wait檢測到某描述符事件就緒並通知應用程序時,應用程序可以不立即處理該事件;事件會被放回到就緒鏈表中,下次調用epoll_wait時,會再次通知此事件。
- ET模式: 當epoll_wait檢測到某描述符事件就緒並通知應用程序時,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應並通知此事件。
由于上述兩種工作模式的區別,LT模式同時支持block和no-block socket兩種,而ET模式下僅支持no-block socket。即epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個fd的阻塞I/O操作把多個處理其他文件描述符的任務餓死。ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。
Epoll的優點
- 使用內存映射技術,節省了用戶態和內核態間數據拷貝的資源消耗;
- 通過每個fd定義的回調函數來實現的,只有就緒的fd才會執行回調函數。I/O的效率不會隨著監視fd的數量的增長而下降;
- 文件描述符數量不再受限;
四、Select、Poll、Epoll機制的對比
下圖主流I/O多路複用機制的benchmark:
當並發fd較小時,Select、Poll、Epoll的響應效率想差無幾,甚至Select和Poll更勝一籌。但是當並發連接(fd)較多時,Epoll的優勢便真正展現出來。
下面一張表格總結三種模式的區別:
通過上述的一些總結,希望我們對I/O多路複用的Select、Poll、Epoll機制有一個更深刻的認識。也要明白爲什麽epoll會成爲Linux平台下實現高性能網絡服務器的首選I/O多路複用機制。
五、Epoll的使用場景
上面的文章中已經不斷介紹了Epoll機制的優勢,又提到它是Linux平台下實現高性能網絡服務器的首選I/O複用機制。實際工作中,我們在哪裏會用到它?怎麽用呢?
比如下面代碼,就是我們使用高性能網絡框架Netty實現IM項目中對于netty的bossGroup和workerGroup以及serverChannel的配置
String os = System.getProperty("os.name");
if(os.toLowerCase().startsWith("win") || os.toLowerCase().startsWith("mac")){
// 點開NioEventLoopGroup的源碼,對于這個類是這麽注釋的
// MultithreadEventLoopGroup implementations which is used for NIO Selector based Channel
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup(4);
}else{
// 點開EpollEventLoopGroup的源碼,對于這個類是這麽注釋的
// EventLoopGroup which uses epoll under the covers. Because of this it only works on linux.
bossGroup = new EpollEventLoopGroup(1);
workerGroup = new EpollEventLoopGroup(4);
}
bootStrap = new ServerBootstrap();
bootStrap.group(bossGroup,workerGroup);
if(os.toLowerCase().startsWith("win") || os.toLowerCase().startsWith("mac")) {
// NioServerSocketChannel implementation which uses NIO selector based implementation to accept new connections.
bootStrap.channel(NioServerSocketChannel.class);
}else{
// ServerSocketChannel implementation that uses linux EPOLL Edge-Triggered Mode for maximal performance.
// 注意看注釋中的“linux EPOLL Edge-Triggered Mode”,linux下ET模式的Epoll機制
bootStrap.channel(EpollServerSocketChannel.class);
}
看完這些,我們對Select、Poll、Epoll的了解是不是更多了一點。
至此,我們高性能IO模型分析的三篇文章已完結。如果能幫助到你,點個贊再走呗!