Linux下select, poll和epoll IO模型的详解
http://blog.csdn.net/tianmohust/article/details/6677985
?
?
一).Epoll 介紹
Epoll 可是當前在 Linux 下開發大規模并發網絡程序的熱門人選, Epoll 在 Linux2.6 內核中正式引入,和 select 相似,其實都?I/O 多路復用技術而已?,并沒有什么神秘的。其實在 Linux 下設計并發網絡程序,向來不缺少方法,比如典型的 Apache 模型( Process Per Connection ,簡稱 PPC ), TPC ( Thread Per Connection )模型,以及 select 模型和 poll 模型,那為何還要再引入? Epoll 這個東東呢?那還是有得說說的 …
二). 常用模型的缺點
如果不擺出來其他模型的缺點,怎么能對比出 Epoll 的優點呢。
① PPC/TPC 模型
這兩種模型思想類似,就是讓每一個到來的連接一邊自己做事去,別再來煩我?。只是 PPC 是為它開了一個進程,而 TPC 開了一個線程。可是別煩我是有代價的,它要時間和空間啊,連接多了之后,那么多的進程 / 線程切換,這開銷就上來了;因此這類模型能接受的最大連接數都不會高,一般在幾百個左右。
② select 模型
1. 最大并發數限制,因為一個進程所打開的 FD (文件描述符)是有限制的,由 FD_SETSIZE 設置,默認值是 1024/2048 ,因此 Select 模型的最大并發數就被相應限制了。自己改改這個 FD_SETSIZE ?想法雖好,可是先看看下面吧 …
2. 效率問題, select 每次調用都會線性掃描全部的 FD 集合,這樣效率就會呈現線性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢來,什么?都超時了。
3. 內核 / 用戶空間 內存拷貝問題,如何讓內核把 FD 消息通知給用戶空間呢?在這個問題上 select 采取了內存拷貝方法。
總結為:1.連接數受限?? 2.查找配對速度慢?3.數據由內核拷貝到用戶態
③ poll 模型
基本上效率和 select 是相同的, select 缺點的 2 和 3 它都沒有改掉。
三). Epoll 的提升
把其他模型逐個批判了一下,再來看看 Epoll 的改進之處吧,其實把 select 的缺點反過來那就是 Epoll 的優點了。
①. Epoll 沒有最大并發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大于 2048,?一般來說這個數目和系統內存關系很大?,具體數目可以 cat /proc/sys/fs/file-max 察看。
②. 效率提升, Epoll 最大的優點就在于它只管你“活躍”的連接?,而跟連接總數無關,因此在實際的網絡環境中, Epoll 的效率就會遠遠高于 select 和 poll 。
③. 內存拷貝, Epoll 在這點上使用了“共享內存?”,這個內存拷貝也省略了。
?四). Epoll 為什么高效
Epoll 的高效和其數據結構的設計是密不可分的,這個下面就會提到。
首先回憶一下 select 模型,當有 I/O 事件到來時, select 通知應用程序有事件到了快去處理,而應用程序必須輪詢所有的 FD 集合,測試每個 FD 是否有事件發生,并處理事件;代碼像下面這樣:
[cpp] view plaincopy
Epoll 不僅會告訴應用程序有I/0 事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,因此根據這些信息應用程序就能直接定位到事件,而不必遍歷整個FD 集合。
[cpp] view plaincopy
五). Epoll 關鍵數據結構
前面提到 Epoll 速度快和其數據結構密不可分,其關鍵數據結構就是:
[cpp] view plaincopy
可見 epoll_data 是一個 union 結構體 , 借助于它應用程序可以保存很多類型的信息 :fd 、指針等等。有了它,應用程序就可以直接定位目標了。
六). 使用 Epoll
既然 Epoll 相比 select 這么好,那么用起來如何呢?會不會很繁瑣啊 … 先看看下面的三個函數吧,就知道 Epoll 的易用了。?
int?epoll_create(int?size); ?
生成一個? Epoll 專用的文件描述符,其實是申請一個內核空間,用來存放你想關注的 socket fd 上是否發生以及發生了什么事件。 size 就是你在這個 Epoll fd 上能關注的最大 socket fd 數,大小自定,只要內存足夠。
int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event?); ?
控制某個? Epoll 文件描述符上的事件:注冊、修改、刪除。其中參數 epfd 是 epoll_create() 創建 Epoll 專用的文件描述符。相對于 select 模型中的 FD_SET 和 FD_CLR 宏。
int?epoll_wait(int?epfd,struct?epoll_event?*?events,int?maxevents,int?timeout); ?
等待 I/O 事件的發生;參數說明:
epfd: 由?epoll_create()?生成的 Epoll 專用的文件描述符;
epoll_event: 用于回傳代處理事件的數組;
maxevents: 每次能處理的事件數;
timeout: 等待 I/O 事件發生的超時值;
返回發生事件數。
相對于 select 模型中的 select 函數。
七). 例子程序
下面是一個簡單 Echo Server 的例子程序,麻雀雖小,五臟俱全,還包含了一個簡單的超時檢查機制,簡潔起見沒有做錯誤處理。
[cpp] view plaincopy?
3、epoll
3.1、poll(select)的限制
????? Poll函數起源于SVR3,最初局限于流設備,SVR4取消了這種限制。總是來說,poll比select要高效一些,但是,它有可移植性問題,例如,windows就只支持select。
一個poll的簡單例子:
?
代碼 #include?<stdio.h>#include?<unistd.h>
#include?<sys/poll.h>
#define?TIMEOUT?5???????/*?poll?timeout,?in?seconds?*/
int?main?(void)
{
????????struct?pollfd?fds[2];
????????int?ret;
????????/*?watch?stdin?for?input?*/
????????fds[0].fd?=?STDIN_FILENO;
????????fds[0].events?=?POLLIN;
????????/*?watch?stdout?for?ability?to?write?(almost?always?true)?*/
????????fds[1].fd?=?STDOUT_FILENO;
????????fds[1].events?=?POLLOUT;
????????/*?All?set,?block!?*/
????????ret?=?poll?(fds,?2,?TIMEOUT?*?1000);
????????if?(ret?==?-1)?{
????????????????perror?("poll");
????????????????return?1;
????????}
????????if?(!ret)?{
????????????????printf?("%d?seconds?elapsed.\n",?TIMEOUT);
????????????????return?0;
????????}
????????if?(fds[0].revents?&?POLLIN)
????????????????printf?("stdin?is?readable\n");
????????if?(fds[1].revents?&?POLLOUT)
????????????????printf?("stdout?is?writable\n");
????????return?0;
}
?
?
???? select模型與此類例。內核必須遍歷所有監視的描述符,而應用程序也必須遍歷所有描述符,檢查哪些描述符已經準備好。當描述符成百上千時,會變得非常低效——這是select(poll)模型低效的根源所在。考慮這些情況,2.6以后的內核都引進了epoll模型。
3.2、核心數據結構與接口
Epoll模型由3個函數構成,epoll_create、epoll_ctl和epoll_wait。
3.2.1創建epoll實例(Creating a New Epoll Instance)
???? epoll環境通過epoll_create函數創建:
???? #include <sys/epoll.h>
????? int epoll_create (int size)
????? 調用成功則返回與實例關聯的文件描述符,該文件描述符與真實的文件沒有任何關系,僅作為接下來調用的函數的句柄。size是給內核的一個提示,告訴內核將要監視的文件描述符的數量,它不是最大值;但是,傳遞合適的值能夠提高系統性能。發生錯誤時,返回-1。
例子:
?
int?epfd;epfd?=?epoll_create?(100);??/*?plan?to?watch?~100?fds?*/
if?(epfd?<?0)
????????perror?("epoll_create");
?
3.2.2、控制epoll(Controlling Epoll)
通過epoll_ctl,可以加入文件描述符到epoll環境或從epoll環境移除文件描述符。
?
代碼 #include?<sys/epoll.h>int?epoll_ctl?(int?epfd,
???????????????int?op,
???????????????int?fd,
???????????????struct?epoll_event?*event);
struct?epoll_event?{
????????_?_u32?events;??/*?events?*/
????????union?{
????????????????void?*ptr;
????????????????int?fd;
????????????????_?_u32?u32;
????????????????_?_u64?u64;
????????}?data;
};
?
?
?
?
epfd為epoll_create返回的描述符。op表示對描述符fd采取的操作,取值如下:
EPOLL_CTL_ADD
Add a monitor on the file associated with the file descriptor fd to the epoll instance associated with epfd, per the events defined in event.
EPOLL_CTL_DEL
Remove a monitor on the file associated with the file descriptor fd from the epollinstance associated with epfd.
EPOLL_CTL_MOD
Modify an existing monitor of fd with the updated events specified by event.
epoll_event結構中的events字段,表示對該文件描述符所關注的事件,它的取值如下:
EPOLLET
Enables edge-triggered behavior for the monitor of the file .The default behavior is level-
triggered.
EPOLLHUP
A hangup occurred on the file. This event is always monitored, even if it’s not specified.
EPOLLIN
The file is available to be read from without blocking.
EPOLLONESHOT
After an event is generated and read, the file is automatically no longer monitored.A new event mask must be specified via EPOLL_CTL_MOD to reenable the watch.
EPOLLOUT
The file is available to be written to without blocking.
EPOLLPRI
There is urgent out-of-band data available to read.
而epoll_event結構中的fd是epoll高效的根源所在,當描述符準備好。應用程序不用遍歷所有描述符,而只用檢查發生事件的描述符。
將一個描述符加入epoll環境:
?
代碼 struct?epoll_event?event;int?ret;
event.data.fd?=?fd;?/*?return?the?fd?to?us?later?*/
event.events?=?EPOLLIN?|?EPOLLOUT;
ret?=?epoll_ctl?(epfd,?EPOLL_CTL_ADD,?fd,?&event);
if?(ret)
????????perror?("epoll_ctl");
?
?
?
3.2.3、等待事件(Waiting for Events with Epoll)
#include <sys/epoll.h>
int epoll_wait (int epfd,
??????????????? struct epoll_event *events,
??????????????? int maxevents,
??????????????? int timeout);
等待事件的產生,類似于select()調用。參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個 maxevents的值不能大于創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有 說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。
一個簡單示例:
?
代碼 #define?MAX_EVENTS????64struct?epoll_event?*events;
int?nr_events,?i,?epfd;
events?=?malloc?(sizeof?(struct?epoll_event)?*?MAX_EVENTS);
if?(!events)?{
????????perror?("malloc");
????????return?1;
}
nr_events?=?epoll_wait?(epfd,?events,?MAX_EVENTS,?-1);
if?(nr_events?<?0)?{
????????perror?("epoll_wait");
????????free?(events);
????????return?1;
}
//只需要檢查發生事件的文件描述符,而不需要遍歷所有描述符
for?(i?=?0;?i?<?nr_events;?i++)?{
????????printf?("event=%ld?on?fd=%d\n",
????????????????events[i].events,
????????????????events[i].data.fd);
????????/*
?????????*?We?now?can,?per?events[i].events,?operate?on
?????????*?events[i].data.fd?without?blocking.
?????????*/
}
free?(events);
?
?
3.2.4、epoll的典型用法
?
代碼 struct?epoll_event?ev,?*events;for(;;)?{
????nfds?=?epoll_wait(kdpfd,?events,?maxevents,?-1);
????for(n?=?0;?n?<?nfds;?++n)?{
????????if(events[n].data.fd?==?listener)?{
????????????//新的連接
????????????client?=?accept(listener,?(struct?sockaddr?*)?&local,
????????????????????????????&addrlen);
????????????if(client?<?0){
????????????????perror("accept");
????????????????continue;
????????????}
????????????setnonblocking(client);
????????????ev.events?=?EPOLLIN?|?EPOLLET;
????????????ev.data.fd?=?client;
????????//?設置好event之后,將這個新的event通過epoll_ctl加入到epoll的監聽隊列里面
????????????if?(epoll_ctl(kdpfd,?EPOLL_CTL_ADD,?client,?&ev)?<?0)?{
????????????????fprintf(stderr,?"epoll?set?insertion?error:?fd=%d0,
????????????????????????client);
????????????????return?-1;
????????????}
????????}
????????else
????????????do_use_fd(events[n].data.fd);
????}
}
?
?
3.3、綜合示例
?
代碼 //echo_epoll_server.c#include?"echo.h"
#include?<sys/epoll.h>
#include?<fcntl.h>
#define?EVENT_ARR_SIZE?20
#define?EPOLL_SIZE?????20
void?setnonblocking(
????int?sockfd
);
int
main(int?argc,?char?**argv)
{
????int????????i,??listenfd,?connfd,?sockfd,?epfd;
????ssize_t????????n;
????char????????????buf[MAXLINE];
????socklen_t????????clilen;
????struct?sockaddr_in????cliaddr,?servaddr;
????struct?epoll_event?ev,?evs[EVENT_ARR_SIZE];
????int???nfds;
????if((listenfd?=?socket(AF_INET,?SOCK_STREAM,?0))?<?0)
????????err_sys("create?socket?error!\n");
????setnonblocking(listenfd);
????epfd?=?epoll_create(EPOLL_SIZE);
????ev.data.fd?=?listenfd;
????ev.events?=?EPOLLIN?|?EPOLLET;
????if(epoll_ctl(epfd,?EPOLL_CTL_ADD,?listenfd,?&ev)?<?0)
????????err_sys("epoll_ctl?listenfd?error!\n");
????
????bzero(&servaddr,?sizeof(servaddr));
????servaddr.sin_family??????=?AF_INET;
????//servaddr.sin_addr.s_addr?=?INADDR_ANY;
????servaddr.sin_addr.s_addr?=?inet_addr("211.67.28.128");
????servaddr.sin_port????????=?htons(SERV_PORT);
????if(bind(listenfd,?(struct?sockaddr*)?&servaddr,?sizeof(servaddr))?<?0)
????????err_sys("bind?error!\n");
????if(listen(listenfd,?LISTENQ)?<?0)
????????err_sys("listen?error!\n");
????printf("server?is?listening....\n");
????for?(?;?;?)?{
????????if((nfds?=?epoll_wait(epfd,?evs,?EVENT_ARR_SIZE,?-1))?<?0)
????????????err_sys("epoll_wait?error!\n");
????????for(i?=?0;?i?<?nfds;?i++)
????????{
????????????????if(evs[i].data.fd?==?listenfd)
????????????????{
????????????????????clilen?=?sizeof(cliaddr);
????????????????????connfd?=?accept(listenfd,?(struct?sockaddr*)?&cliaddr,?&clilen);
????????????????????if(connfd?<?0)
????????????????????????continue;
????????????????????????
????????????????????setnonblocking(connfd);
????????????????????ev.data.fd?=?connfd;
????????????????????ev.events?=?EPOLLIN?|?EPOLLET;
????????????????????if?(epoll_ctl(epfd,?EPOLL_CTL_ADD,?connfd,?&ev)?<?0)
????????????????????????err_sys("epoll_ctl?connfd?error!\n");????????????
????????????????}
????????????????else?if(evs[i].events?&?EPOLLIN)
????????????????{
????????????????????sockfd?=?evs[i].data.fd;
????????????????????if?(sockfd?<?0)
????????????????????????continue;
????????????????????if?(?(n?=?read(sockfd,?buf,?MAXLINE))?==?0)?{
????????????????????????epoll_ctl(epfd,?EPOLL_CTL_DEL,?sockfd,?&ev);
????????????????????????close(sockfd);
????????????????????????evs[i].data.fd?=?-1;
????????????????????}?
????????????????????else?if(n?<?0)
????????????????????????err_sys("read?socket?error!\n");
????????????????????else
????????????????????{
????????????????????????printf("write?%d?bytes\n",?n);
????????????????????????write(sockfd,?buf,?n);
????????????????????}
????????????????}
????????????????else
????????????????????printf("other?event!\n");
????????}
????}
????return?0;
}
void?setnonblocking(
????int?sockfd
)
{
????int?flag;
????
????flag?=?fcntl(sockfd,?F_GETFL);
????if(flag?<?0)
????????????err_sys("fcnt(F_GETFL)?error!\n");
????flag?|=?O_NONBLOCK;
????if(fcntl(sockfd,?F_SETFL,?flag)?<?0)
????????err_sys("fcon(F_SETFL)?error!\n");
}
//echo.h
#include?<sys/types.h>
#include?<sys/socket.h>
#include?<netinet/in.h>
#include?<arpa/inet.h>
#include?<unistd.h>
#include?<stdlib.h>
#include?<string.h>
#include?<stdio.h>
#include?<errno.h>
#define?SERV_PORT?????9877
#define?MAXLINE????????4096
#define?LISTENQ????????5
void
err_sys(const?char?*fmt,?...);
ssize_t????????????????????????
readn(int?fd,?void?*vptr,?size_t?n);
?
補充部分:
select()系統調用提供一個機制來實現同步多元I/O:
?
| #include?<sys/time.h> |
調用select()將阻塞,直到指定的文件描述符準備好執行I/O,或者可選參數timeout指定的時間已經過去。
監視的文件描述符分為三類set,每一種對應等待不同的事件。readfds中列出的文件描述符被監視是否有數據可供讀取(如果讀取操作完成則不會阻塞)。writefds中列出的文件描述符則被監視是否寫入操作完成而不阻塞。最后,exceptfds中列出的文件描述符則被監視是否發生異常,或者無法控制的數據是否可用(這些狀態僅僅應用于套接字)。這三類set可以是NULL,這種情況下select()不監視這一類事件。
select()成功返回時,每組set都被修改以使它只包含準備好I/O的文件描述符。例如,假設有兩個文件描述符,值分別是7和9,被放在readfds中。當select()返回時,如果7仍然在set中,則這個文件描述符已經準備好被讀取而不會阻塞。如果9已經不在set中,則讀取它將可能會阻塞(我說可能是因為數據可能正好在select返回后就可用,這種情況下,下一次調用select()將返回文件描述符準備好讀取)。
第一個參數n,等于所有set中最大的那個文件描述符的值加1。因此,select()的調用者負責檢查哪個文件描述符擁有最大值,并且把這個值加1再傳遞給第一個參數。
timeout參數是一個指向timeval結構體的指針,timeval定義如下:
| #include?<sys/time.h> struct?timeval?{ long?tv_sec;?/* seconds */ long?tv_usec;?/*?10E-6 second?*/ }; |
如果這個參數不是NULL,則即使沒有文件描述符準備好I/O,select()也會在經過tv_sec秒和tv_usec微秒后返回。當select()返回時,timeout參數的狀態在不同的系統中是未定義的,因此每次調用select()之前必須重新初始化timeout和文件描述符set。實際上,當前版本的Linux會自動修改timeout參數,設置它的值為剩余時間。因此,如果timeout被設置為5秒,然后在文件描述符準備好之前經過了3秒,則這一次調用select()返回時tv_sec將變為2。
如果timeout中的兩個值都設置為0,則調用select()將立即返回,報告調用時所有未決的事件,但不等待任何隨后的事件。
文件描述符set不會直接操作,一般使用幾個助手宏來管理。這允許Unix系統以自己喜歡的方式來實現文件描述符set。但大多數系統都簡單地實現set為位數組。FD_ZERO移除指定set中的所有文件描述符。每一次調用select()之前都應該先調用它。
fd_set writefds;
FD_ZERO(&writefds);
FD_SET添加一個文件描述符到指定的set中,FD_CLR則從指定的set中移除一個文件描述符:
FD_SET(fd, &writefds); /* add 'fd' to the set */
FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */
設計良好的代碼應該永遠不使用FD_CLR,而且實際情況中它也確實很少被使用。
FD_ISSET測試一個文件描述符是否指定set的一部分。如果文件描述符在set中則返回一個非0整數,不在則返回0。FD_ISSET在調用select()返回之后使用,測試指定的文件描述符是否準備好相關動作:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
因為文件描述符set是靜態創建的,它們對文件描述符的最大數目強加了一個限制,能夠放進set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,這個值是1024。本章后面我們還將看到這個限制的衍生物。
返回值和錯誤代碼
select()成功時返回準備好I/O的文件描述符數目,包括所有三個set。如果提供了timeout,返回值可能是0;錯誤時返回-1,并且設置errno為下面幾個值之一:
EBADF,給某個set提供了無效文件描述符。
EINTR,等待時捕獲到信號,可以重新發起調用。
EINVAL,參數n為負數,或者指定的timeout非法。
ENOMEM,不夠可用內存來完成請求。
--------------------------------------------------------------------------------------------------------------
poll()系統調用是System V的多元I/O解決方案。它解決了select()的幾個不足,盡管select()仍然經常使用(多數還是出于習慣,或者打著可移植的名義):
?
| #include?<sys/poll.h> int?poll?(struct?pollfd?*fds,?unsigned?int?nfds,?int?timeout); |
和select()不一樣,poll()沒有使用低效的三個基于位的文件描述符set,而是采用了一個單獨的結構體pollfd數組,由fds指針指向這個組。pollfd結構體定義如下:
| #include?<sys/poll.h> struct?pollfd?{ int?fd;?/* file descriptor */ short?events;?/* requested events to watch */ short?revents;?/* returned events witnessed */ }; |
每一個pollfd結構體指定了一個被監視的文件描述符,可以傳遞多個結構體,指示poll()監視多個文件描述符。每個結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。revents域是文件描述符的操作結果事件掩碼。內核在調用返回時設置這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN,有數據可讀。
POLLRDNORM,有普通數據可讀。
POLLRDBAND,有優先數據可讀。
POLLPRI,有緊迫數據可讀。
POLLOUT,寫數據不會導致阻塞。
POLLWRNORM,寫普通數據不會導致阻塞。
POLLWRBAND,寫優先數據不會導致阻塞。
POLLMSG,SIGPOLL消息可用。
此外,revents域中還可能返回下列事件:
POLLER,指定的文件描述符發生錯誤。
POLLHUP,指定的文件描述符掛起事件。
POLLNVAL,指定的文件描述符非法。
這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。POLLIN | POLLPRI等價于select()的讀事件,POLLOUT | POLLWRBAND等價于select()的寫事件。POLLIN等價于POLLRDNORM | POLLRDBAND,而POLLOUT則等價于POLLWRNORM。
例如,要同時監視一個文件描述符是否可讀和可寫,我們可以設置events為POLLIN | POLLOUT。在poll返回時,我們可以檢查revents中的標志,對應于文件描述符請求的events結構體。如果POLLIN事件被設置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設置,則文件描述符可以寫入而不導致阻塞。這些標志并不是互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。timeout參數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定為負數值表示無限超時;timeout為0指示poll調用立即返回并列出準備好I/O的文件描述符,但并不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。
返回值和錯誤代碼
成功時,poll()返回結構體中revents域不為0的文件描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,并設置errno為下列值之一:
EBADF,一個或多個結構體中指定的文件描述符無效。
EFAULT,fds指針指向的地址超出進程的地址空間。
EINTR,請求的事件之前產生一個信號,調用可以重新發起。
EINVAL,nfds參數超出PLIMIT_NOFILE值。
ENOMEM,可用內存不足,無法完成請求。
以上內容來自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
Epoll的優點:
1.支持一個進程打開大數目的socket描述符(FD)
??? select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是2048。對于那些需要支持的上萬連接數目的IM服務器來說顯然太少了。這時候你一是可以選擇修改這個宏然后重新編譯內核,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多進程的解決方案(傳統的 Apache方案),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。不過? epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大于2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
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同一塊內存實現的。而如果你想我一樣從2.5內核就關注epoll的話,一定不會忘記手工 mmap這一步的。
4.內核微調
??? 這一點其實不算epoll的優點了,而是整個linux平臺的優點。也許你可以懷疑linux平臺,但是你無法回避linux平臺賦予你微調內核的能力。比如,內核TCP/IP協議棧使用內存池管理sk_buff結構,那么可以在運行時期動態調整這個內存pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的數據包隊列長度),也可以根據你平臺內存大小動態調整。更甚至在一個數據包面數目巨大但同時每個數據包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。
########################################################
select/epoll的特點
select的特點:select 選擇句柄的時候,是遍歷所有句柄,也就是說句柄有事件響應時,select需要遍歷所有句柄才能獲取到哪些句柄有事件通知,因此效率是非常低。但是如果連接很少的情況下, select和epoll的LT觸發模式相比, 性能上差別不大。
這 里要多說一句,select支持的句柄數是有限制的, 同時只支持1024個,這個是句柄集合限制的,如果超過這個限制,很可能導致溢出,而且非常不容易發現問題, TAF就出現過這個問題, 調試了n天,才發現:)當然可以通過修改linux的socket內核調整這個參數。
epoll的特點:epoll對于句柄事件的選擇不是遍歷的,是事件響應的,就是句柄上事件來就馬上選擇出來,不需要遍歷整個句柄鏈表,因此效率非常高,內核將句柄用紅黑樹保存的。相比于select,epoll最大的好處在于它不會隨著監聽fd數目的增長而降低效率。因為在內核中的select實現中,它是采用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。并且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE ? ?1024
表示select最多同時監聽1024個fd,當然,可以通過修改頭文件再重編譯內核來擴大這個數目,但這似乎并不治本。
對于epoll而言還有ET和LT的區別,LT表示水平觸發,ET表示邊緣觸發,兩者在性能以及代碼實現上差別也是非常大的。
epoll的LT和ET的區別
LT:水平觸發,效率會低于ET觸發,尤其在大并發,大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,因此不用擔心事件丟失的情況。
ET:邊緣觸發,效率非常高,在并發,大流量的情況下,會比LT少很多epoll的系統調用,因此效率高。但是對編程要求高,需要細致的處理每個請求,否則容易發生丟失事件的情況。
epoll相關API
epoll的接口非常簡單,一共就三個函數:
1. int epoll_create(int size);
創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。這個參數不同于select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd,第四個參數是告訴內核需要監聽什么事,struct epoll_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 */
};
events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生,類似于select()調用。參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個 maxevents的值不能大于創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。
示例代碼
epoll服務器
?
?
客戶端測試代碼:
?
?
總結
以上是生活随笔為你收集整理的Linux下select, poll和epoll IO模型的详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Apache与Nginx网络模型
- 下一篇: Apache+tomcat的整合