Linux下select, poll和epoll IO模型的详解
http://blog.csdn.net/tianmohust/article/details/6677985
?
?
一).Epoll 介紹
Epoll 可是當(dāng)前在 Linux 下開發(fā)大規(guī)模并發(fā)網(wǎng)絡(luò)程序的熱門人選, Epoll 在 Linux2.6 內(nèi)核中正式引入,和 select 相似,其實都?I/O 多路復(fù)用技術(shù)而已?,并沒有什么神秘的。其實在 Linux 下設(shè)計并發(fā)網(wǎng)絡(luò)程序,向來不缺少方法,比如典型的 Apache 模型( Process Per Connection ,簡稱 PPC ), TPC ( Thread Per Connection )模型,以及 select 模型和 poll 模型,那為何還要再引入? Epoll 這個東東呢?那還是有得說說的 …
二). 常用模型的缺點
如果不擺出來其他模型的缺點,怎么能對比出 Epoll 的優(yōu)點呢。
① PPC/TPC 模型
這兩種模型思想類似,就是讓每一個到來的連接一邊自己做事去,別再來煩我?。只是 PPC 是為它開了一個進(jìn)程,而 TPC 開了一個線程。可是別煩我是有代價的,它要時間和空間啊,連接多了之后,那么多的進(jìn)程 / 線程切換,這開銷就上來了;因此這類模型能接受的最大連接數(shù)都不會高,一般在幾百個左右。
② select 模型
1. 最大并發(fā)數(shù)限制,因為一個進(jìn)程所打開的 FD (文件描述符)是有限制的,由 FD_SETSIZE 設(shè)置,默認(rèn)值是 1024/2048 ,因此 Select 模型的最大并發(fā)數(shù)就被相應(yīng)限制了。自己改改這個 FD_SETSIZE ?想法雖好,可是先看看下面吧 …
2. 效率問題, select 每次調(diào)用都會線性掃描全部的 FD 集合,這樣效率就會呈現(xiàn)線性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢來,什么?都超時了。
3. 內(nèi)核 / 用戶空間 內(nèi)存拷貝問題,如何讓內(nèi)核把 FD 消息通知給用戶空間呢?在這個問題上 select 采取了內(nèi)存拷貝方法。
總結(jié)為:1.連接數(shù)受限?? 2.查找配對速度慢?3.數(shù)據(jù)由內(nèi)核拷貝到用戶態(tài)
③ poll 模型
基本上效率和 select 是相同的, select 缺點的 2 和 3 它都沒有改掉。
三). Epoll 的提升
把其他模型逐個批判了一下,再來看看 Epoll 的改進(jìn)之處吧,其實把 select 的缺點反過來那就是 Epoll 的優(yōu)點了。
①. Epoll 沒有最大并發(fā)連接的限制,上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于 2048,?一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大?,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看。
②. 效率提升, Epoll 最大的優(yōu)點就在于它只管你“活躍”的連接?,而跟連接總數(shù)無關(guān),因此在實際的網(wǎng)絡(luò)環(huán)境中, Epoll 的效率就會遠(yuǎn)遠(yuǎn)高于 select 和 poll 。
③. 內(nèi)存拷貝, Epoll 在這點上使用了“共享內(nèi)存?”,這個內(nèi)存拷貝也省略了。
?四). Epoll 為什么高效
Epoll 的高效和其數(shù)據(jù)結(jié)構(gòu)的設(shè)計是密不可分的,這個下面就會提到。
首先回憶一下 select 模型,當(dāng)有 I/O 事件到來時, select 通知應(yīng)用程序有事件到了快去處理,而應(yīng)用程序必須輪詢所有的 FD 集合,測試每個 FD 是否有事件發(fā)生,并處理事件;代碼像下面這樣:
[cpp] view plaincopy
Epoll 不僅會告訴應(yīng)用程序有I/0 事件到來,還會告訴應(yīng)用程序相關(guān)的信息,這些信息是應(yīng)用程序填充的,因此根據(jù)這些信息應(yīng)用程序就能直接定位到事件,而不必遍歷整個FD 集合。
[cpp] view plaincopy
五). Epoll 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)
前面提到 Epoll 速度快和其數(shù)據(jù)結(jié)構(gòu)密不可分,其關(guān)鍵數(shù)據(jù)結(jié)構(gòu)就是:
[cpp] view plaincopy
可見 epoll_data 是一個 union 結(jié)構(gòu)體 , 借助于它應(yīng)用程序可以保存很多類型的信息 :fd 、指針等等。有了它,應(yīng)用程序就可以直接定位目標(biāo)了。
六). 使用 Epoll
既然 Epoll 相比 select 這么好,那么用起來如何呢?會不會很繁瑣啊 … 先看看下面的三個函數(shù)吧,就知道 Epoll 的易用了。?
int?epoll_create(int?size); ?
生成一個? Epoll 專用的文件描述符,其實是申請一個內(nèi)核空間,用來存放你想關(guān)注的 socket fd 上是否發(fā)生以及發(fā)生了什么事件。 size 就是你在這個 Epoll fd 上能關(guān)注的最大 socket fd 數(shù),大小自定,只要內(nèi)存足夠。
int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event?); ?
控制某個? Epoll 文件描述符上的事件:注冊、修改、刪除。其中參數(shù) epfd 是 epoll_create() 創(chuàng)建 Epoll 專用的文件描述符。相對于 select 模型中的 FD_SET 和 FD_CLR 宏。
int?epoll_wait(int?epfd,struct?epoll_event?*?events,int?maxevents,int?timeout); ?
等待 I/O 事件的發(fā)生;參數(shù)說明:
epfd: 由?epoll_create()?生成的 Epoll 專用的文件描述符;
epoll_event: 用于回傳代處理事件的數(shù)組;
maxevents: 每次能處理的事件數(shù);
timeout: 等待 I/O 事件發(fā)生的超時值;
返回發(fā)生事件數(shù)。
相對于 select 模型中的 select 函數(shù)。
七). 例子程序
下面是一個簡單 Echo Server 的例子程序,麻雀雖小,五臟俱全,還包含了一個簡單的超時檢查機(jī)制,簡潔起見沒有做錯誤處理。
[cpp] view plaincopy?
3、epoll
3.1、poll(select)的限制
????? Poll函數(shù)起源于SVR3,最初局限于流設(shè)備,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模型與此類例。內(nèi)核必須遍歷所有監(jiān)視的描述符,而應(yīng)用程序也必須遍歷所有描述符,檢查哪些描述符已經(jīng)準(zhǔn)備好。當(dāng)描述符成百上千時,會變得非常低效——這是select(poll)模型低效的根源所在。考慮這些情況,2.6以后的內(nèi)核都引進(jìn)了epoll模型。
3.2、核心數(shù)據(jù)結(jié)構(gòu)與接口
Epoll模型由3個函數(shù)構(gòu)成,epoll_create、epoll_ctl和epoll_wait。
3.2.1創(chuàng)建epoll實例(Creating a New Epoll Instance)
???? epoll環(huán)境通過epoll_create函數(shù)創(chuàng)建:
???? #include <sys/epoll.h>
????? int epoll_create (int size)
????? 調(diào)用成功則返回與實例關(guān)聯(lián)的文件描述符,該文件描述符與真實的文件沒有任何關(guān)系,僅作為接下來調(diào)用的函數(shù)的句柄。size是給內(nèi)核的一個提示,告訴內(nèi)核將要監(jiān)視的文件描述符的數(shù)量,它不是最大值;但是,傳遞合適的值能夠提高系統(tǒng)性能。發(fā)生錯誤時,返回-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環(huán)境或從epoll環(huán)境移除文件描述符。
?
代碼 #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結(jié)構(gòu)中的events字段,表示對該文件描述符所關(guān)注的事件,它的取值如下:
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結(jié)構(gòu)中的fd是epoll高效的根源所在,當(dāng)描述符準(zhǔn)備好。應(yīng)用程序不用遍歷所有描述符,而只用檢查發(fā)生事件的描述符。
將一個描述符加入epoll環(huán)境:
?
代碼 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);
等待事件的產(chǎn)生,類似于select()調(diào)用。參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個events有多大,這個 maxevents的值不能大于創(chuàng)建epoll_create()時的size,參數(shù)timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有 說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回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;
}
//只需要檢查發(fā)生事件的文件描述符,而不需要遍歷所有描述符
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;
????????//?設(shè)置好event之后,將這個新的event通過epoll_ctl加入到epoll的監(jiān)聽隊列里面
????????????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);
?
補(bǔ)充部分:
select()系統(tǒng)調(diào)用提供一個機(jī)制來實現(xiàn)同步多元I/O:
?
| #include?<sys/time.h> |
調(diào)用select()將阻塞,直到指定的文件描述符準(zhǔn)備好執(zhí)行I/O,或者可選參數(shù)timeout指定的時間已經(jīng)過去。
監(jiān)視的文件描述符分為三類set,每一種對應(yīng)等待不同的事件。readfds中列出的文件描述符被監(jiān)視是否有數(shù)據(jù)可供讀取(如果讀取操作完成則不會阻塞)。writefds中列出的文件描述符則被監(jiān)視是否寫入操作完成而不阻塞。最后,exceptfds中列出的文件描述符則被監(jiān)視是否發(fā)生異常,或者無法控制的數(shù)據(jù)是否可用(這些狀態(tài)僅僅應(yīng)用于套接字)。這三類set可以是NULL,這種情況下select()不監(jiān)視這一類事件。
select()成功返回時,每組set都被修改以使它只包含準(zhǔn)備好I/O的文件描述符。例如,假設(shè)有兩個文件描述符,值分別是7和9,被放在readfds中。當(dāng)select()返回時,如果7仍然在set中,則這個文件描述符已經(jīng)準(zhǔn)備好被讀取而不會阻塞。如果9已經(jīng)不在set中,則讀取它將可能會阻塞(我說可能是因為數(shù)據(jù)可能正好在select返回后就可用,這種情況下,下一次調(diào)用select()將返回文件描述符準(zhǔn)備好讀取)。
第一個參數(shù)n,等于所有set中最大的那個文件描述符的值加1。因此,select()的調(diào)用者負(fù)責(zé)檢查哪個文件描述符擁有最大值,并且把這個值加1再傳遞給第一個參數(shù)。
timeout參數(shù)是一個指向timeval結(jié)構(gòu)體的指針,timeval定義如下:
| #include?<sys/time.h> struct?timeval?{ long?tv_sec;?/* seconds */ long?tv_usec;?/*?10E-6 second?*/ }; |
如果這個參數(shù)不是NULL,則即使沒有文件描述符準(zhǔn)備好I/O,select()也會在經(jīng)過tv_sec秒和tv_usec微秒后返回。當(dāng)select()返回時,timeout參數(shù)的狀態(tài)在不同的系統(tǒng)中是未定義的,因此每次調(diào)用select()之前必須重新初始化timeout和文件描述符set。實際上,當(dāng)前版本的Linux會自動修改timeout參數(shù),設(shè)置它的值為剩余時間。因此,如果timeout被設(shè)置為5秒,然后在文件描述符準(zhǔn)備好之前經(jīng)過了3秒,則這一次調(diào)用select()返回時tv_sec將變?yōu)?。
如果timeout中的兩個值都設(shè)置為0,則調(diào)用select()將立即返回,報告調(diào)用時所有未決的事件,但不等待任何隨后的事件。
文件描述符set不會直接操作,一般使用幾個助手宏來管理。這允許Unix系統(tǒng)以自己喜歡的方式來實現(xiàn)文件描述符set。但大多數(shù)系統(tǒng)都簡單地實現(xiàn)set為位數(shù)組。FD_ZERO移除指定set中的所有文件描述符。每一次調(diào)用select()之前都應(yīng)該先調(diào)用它。
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 */
設(shè)計良好的代碼應(yīng)該永遠(yuǎn)不使用FD_CLR,而且實際情況中它也確實很少被使用。
FD_ISSET測試一個文件描述符是否指定set的一部分。如果文件描述符在set中則返回一個非0整數(shù),不在則返回0。FD_ISSET在調(diào)用select()返回之后使用,測試指定的文件描述符是否準(zhǔn)備好相關(guān)動作:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
因為文件描述符set是靜態(tài)創(chuàng)建的,它們對文件描述符的最大數(shù)目強(qiáng)加了一個限制,能夠放進(jìn)set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,這個值是1024。本章后面我們還將看到這個限制的衍生物。
返回值和錯誤代碼
select()成功時返回準(zhǔn)備好I/O的文件描述符數(shù)目,包括所有三個set。如果提供了timeout,返回值可能是0;錯誤時返回-1,并且設(shè)置errno為下面幾個值之一:
EBADF,給某個set提供了無效文件描述符。
EINTR,等待時捕獲到信號,可以重新發(fā)起調(diào)用。
EINVAL,參數(shù)n為負(fù)數(shù),或者指定的timeout非法。
ENOMEM,不夠可用內(nèi)存來完成請求。
--------------------------------------------------------------------------------------------------------------
poll()系統(tǒng)調(diào)用是System V的多元I/O解決方案。它解決了select()的幾個不足,盡管select()仍然經(jīng)常使用(多數(shù)還是出于習(xí)慣,或者打著可移植的名義):
?
| #include?<sys/poll.h> int?poll?(struct?pollfd?*fds,?unsigned?int?nfds,?int?timeout); |
和select()不一樣,poll()沒有使用低效的三個基于位的文件描述符set,而是采用了一個單獨的結(jié)構(gòu)體pollfd數(shù)組,由fds指針指向這個組。pollfd結(jié)構(gòu)體定義如下:
| #include?<sys/poll.h> struct?pollfd?{ int?fd;?/* file descriptor */ short?events;?/* requested events to watch */ short?revents;?/* returned events witnessed */ }; |
每一個pollfd結(jié)構(gòu)體指定了一個被監(jiān)視的文件描述符,可以傳遞多個結(jié)構(gòu)體,指示poll()監(jiān)視多個文件描述符。每個結(jié)構(gòu)體的events域是監(jiān)視該文件描述符的事件掩碼,由用戶來設(shè)置這個域。revents域是文件描述符的操作結(jié)果事件掩碼。內(nèi)核在調(diào)用返回時設(shè)置這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN,有數(shù)據(jù)可讀。
POLLRDNORM,有普通數(shù)據(jù)可讀。
POLLRDBAND,有優(yōu)先數(shù)據(jù)可讀。
POLLPRI,有緊迫數(shù)據(jù)可讀。
POLLOUT,寫數(shù)據(jù)不會導(dǎo)致阻塞。
POLLWRNORM,寫普通數(shù)據(jù)不會導(dǎo)致阻塞。
POLLWRBAND,寫優(yōu)先數(shù)據(jù)不會導(dǎo)致阻塞。
POLLMSG,SIGPOLL消息可用。
此外,revents域中還可能返回下列事件:
POLLER,指定的文件描述符發(fā)生錯誤。
POLLHUP,指定的文件描述符掛起事件。
POLLNVAL,指定的文件描述符非法。
這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。POLLIN | POLLPRI等價于select()的讀事件,POLLOUT | POLLWRBAND等價于select()的寫事件。POLLIN等價于POLLRDNORM | POLLRDBAND,而POLLOUT則等價于POLLWRNORM。
例如,要同時監(jiān)視一個文件描述符是否可讀和可寫,我們可以設(shè)置events為POLLIN | POLLOUT。在poll返回時,我們可以檢查revents中的標(biāo)志,對應(yīng)于文件描述符請求的events結(jié)構(gòu)體。如果POLLIN事件被設(shè)置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設(shè)置,則文件描述符可以寫入而不導(dǎo)致阻塞。這些標(biāo)志并不是互斥的:它們可能被同時設(shè)置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。timeout參數(shù)指定等待的毫秒數(shù),無論I/O是否準(zhǔn)備好,poll都會返回。timeout指定為負(fù)數(shù)值表示無限超時;timeout為0指示poll調(diào)用立即返回并列出準(zhǔn)備好I/O的文件描述符,但并不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。
返回值和錯誤代碼
成功時,poll()返回結(jié)構(gòu)體中revents域不為0的文件描述符個數(shù);如果在超時前沒有任何事件發(fā)生,poll()返回0;失敗時,poll()返回-1,并設(shè)置errno為下列值之一:
EBADF,一個或多個結(jié)構(gòu)體中指定的文件描述符無效。
EFAULT,fds指針指向的地址超出進(jìn)程的地址空間。
EINTR,請求的事件之前產(chǎn)生一個信號,調(diào)用可以重新發(fā)起。
EINVAL,nfds參數(shù)超出PLIMIT_NOFILE值。
ENOMEM,可用內(nèi)存不足,無法完成請求。
以上內(nèi)容來自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
Epoll的優(yōu)點:
1.支持一個進(jìn)程打開大數(shù)目的socket描述符(FD)
??? select 最不能忍受的是一個進(jìn)程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯然太少了。這時候你一是可以選擇修改這個宏然后重新編譯內(nèi)核,不過資料也同時指出這樣會帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案),不過雖然linux上面創(chuàng)建進(jìn)程的代價比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。不過? epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于2048,舉個例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
2.IO效率不隨FD數(shù)目增加而線性下降
??? 傳統(tǒng)的select/poll另一個致命弱點就是當(dāng)你擁有一個很大的socket集合,不過由于網(wǎng)絡(luò)延時,任一時間只有部分的socket是"活躍"的,但是select/poll每次調(diào)用都會線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進(jìn)行操作---這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有"活躍"的socket才會主動的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會,在這點上,epoll實現(xiàn)了一個"偽"AIO,因為這時候推動力在os內(nèi)核。在一些? benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環(huán)境,epoll并不比select/poll有什么效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
3.使用mmap加速內(nèi)核與用戶空間的消息傳遞。
??? 這點實際上涉及到epoll的具體實現(xiàn)了。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話,一定不會忘記手工 mmap這一步的。
4.內(nèi)核微調(diào)
??? 這一點其實不算epoll的優(yōu)點了,而是整個linux平臺的優(yōu)點。也許你可以懷疑linux平臺,但是你無法回避linux平臺賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運行時期動態(tài)調(diào)整這個內(nèi)存pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個參數(shù)(TCP完成3次握手的數(shù)據(jù)包隊列長度),也可以根據(jù)你平臺內(nèi)存大小動態(tài)調(diào)整。更甚至在一個數(shù)據(jù)包面數(shù)目巨大但同時每個數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)卡驅(qū)動架構(gòu)。
########################################################
select/epoll的特點
select的特點:select 選擇句柄的時候,是遍歷所有句柄,也就是說句柄有事件響應(yīng)時,select需要遍歷所有句柄才能獲取到哪些句柄有事件通知,因此效率是非常低。但是如果連接很少的情況下, select和epoll的LT觸發(fā)模式相比, 性能上差別不大。
這 里要多說一句,select支持的句柄數(shù)是有限制的, 同時只支持1024個,這個是句柄集合限制的,如果超過這個限制,很可能導(dǎo)致溢出,而且非常不容易發(fā)現(xiàn)問題, TAF就出現(xiàn)過這個問題, 調(diào)試了n天,才發(fā)現(xiàn):)當(dāng)然可以通過修改linux的socket內(nèi)核調(diào)整這個參數(shù)。
epoll的特點:epoll對于句柄事件的選擇不是遍歷的,是事件響應(yīng)的,就是句柄上事件來就馬上選擇出來,不需要遍歷整個句柄鏈表,因此效率非常高,內(nèi)核將句柄用紅黑樹保存的。相比于select,epoll最大的好處在于它不會隨著監(jiān)聽fd數(shù)目的增長而降低效率。因為在內(nèi)核中的select實現(xiàn)中,它是采用輪詢來處理的,輪詢的fd數(shù)目越多,自然耗時越多。并且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE ? ?1024
表示select最多同時監(jiān)聽1024個fd,當(dāng)然,可以通過修改頭文件再重編譯內(nèi)核來擴(kuò)大這個數(shù)目,但這似乎并不治本。
對于epoll而言還有ET和LT的區(qū)別,LT表示水平觸發(fā),ET表示邊緣觸發(fā),兩者在性能以及代碼實現(xiàn)上差別也是非常大的。
epoll的LT和ET的區(qū)別
LT:水平觸發(fā),效率會低于ET觸發(fā),尤其在大并發(fā),大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現(xiàn)問題。LT模式服務(wù)編寫上的表現(xiàn)是:只要有數(shù)據(jù)沒有被獲取,內(nèi)核就不斷通知你,因此不用擔(dān)心事件丟失的情況。
ET:邊緣觸發(fā),效率非常高,在并發(fā),大流量的情況下,會比LT少很多epoll的系統(tǒng)調(diào)用,因此效率高。但是對編程要求高,需要細(xì)致的處理每個請求,否則容易發(fā)生丟失事件的情況。
epoll相關(guān)API
epoll的接口非常簡單,一共就三個函數(shù):
1. int epoll_create(int size);
創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大。這個參數(shù)不同于select()中的第一個參數(shù),給出最大監(jiān)聽的fd+1的值。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數(shù),它不同與select()是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊要監(jiān)聽的事件類型。第一個參數(shù)是epoll_create()的返回值,第二個參數(shù)表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數(shù)是需要監(jiān)聽的fd,第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事,struct epoll_event結(jié)構(gòu)如下:
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 :表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產(chǎn)生,類似于select()調(diào)用。參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個events有多大,這個 maxevents的值不能大于創(chuàng)建epoll_create()時的size,參數(shù)timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時。
示例代碼
epoll服務(wù)器
?
?
客戶端測試代碼:
?
?
總結(jié)
以上是生活随笔為你收集整理的Linux下select, poll和epoll IO模型的详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Apache与Nginx网络模型
- 下一篇: linux 其他常用命令