看了Epoll模型的一篇博客,对其进行总结,原文请见http://blog.163.com/huchengsz@126/blog/static/73483745201181824629285/
1. select函数的缺点
select用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在Linux2.6中是1024,搜索内核源代码的include/linux/posix_types.h:#define __FD_SETSIZE 1024
如果想要同时检测1025个句柄的可读状态是不可能用select实现的,或者同时检测1025个句柄的可写状态也是不可能的。其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,select要检测的句柄数越多就会越费时。
2. Epoll的优点
<1>支持一个进程打开大数目的socket描述符(FD)
select一个进程所打开的FD是有一定限制的,一是可以选择修改这个宏然后重新编译内核,会带来网络效率的下降,二是可以选择多进程的解决方案(传统的Apache方案)。Epoll支持的FD上限是最大可以打开文件的数目,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
<2>IO效率不随FD数目增加而线性下降
select/poll另一个致命弱点就是当有一个很大的socket集合,由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但Epoll只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,只有"活跃"的socket才会主动的去调用callback函数。在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。
<3>使用mmap加速内核与用户空间的消息传递。
无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,Epoll是通过内核于用户空间mmap同一块内存实现的。
3. Epoll的工作模式
Epoll有2种工作方式:LT和ET。
LT(level triggered)是缺省的工作方式,同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。所以,这种模式编程出错误可能性要小一点,传统的select/poll都是这种模型的代表。
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(on
4. Epoll的使用方法
Epoll的操作共4个 API:epoll_create, epoll_ctl, epoll_wait和close。
首先通过epoll_create(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后将通过这个句柄来进行操作。在用完之后,用close()来关闭epoll句柄。之后在网络主循环里面,每一帧的调用 epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:nfds = epoll_wait(kdpfd, events, max_events, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个 epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。 max_events是当前需要监听的所有socket句柄数。最后一个timeout是epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件发生,为任意正整数的时候表示等这么长的时间。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
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 结构如下:
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 事件有两种模型:
Edge Triggered (ET) 边缘触发 只有数据到来,才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发 只要有数据都会触发。
02 for (n = 0; n < nfds; ++n)
03 {
04 if (events[n].da
05 { //如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。
06 client = accept (listener, ( struct sockaddr *) &local, &addrlen);
07 if (client < 0)
08 {
09 perror ( "accept");
10 continue;
11 }
12 setnonblocking (client); // 将新连接置于非阻塞模式
13 ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。
14 //注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
15 //如果有写操作的话,这个时候epoll是不会返回事件的,
16 //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
17 ev.da
18 if (epoll_ctl (kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
19 {
20 /*
21 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,
22 这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通
23 过EPOLL_CTL_MOD来改变一个事件的监听方式.
24 */
25 fprintf (stderr, "epoll set insertion error: fd=%d", client);
26 return - 1;
27 }
28 }
29 else // 如果不是主socket的事件的话,则代表是一个用户socket的事件,
30 do_use_fd (events[n].da
31 }
Epoll使用示例:
02 {
03 int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS,EPOLL_TIME_OUT); //等待EPOLL时间的发生,相当于监听,
04 //至于相关的端口,需要在初始化EPOLL的时候绑定。
05 if (nfds <= 0)
06 continue;
07 m_bOnTimeChecking = FALSE;
08 G_CurTime = time ( NULL);
09 for ( int i = 0; i < nfds; i ++)
10 {
11 try
12 {
13 if (m_events[i].da
14 //建立新的连接。由于我们新采用了SOCKET连接,所以基本没用。
15 {
16 On
17 }
18 else if (m_events[i].da
19 //建立新的连接。
20 {
21 On
22 }
23 else if (m_events[i].events & EPOLLIN) //如果是已经连接的用户,并且收到数据,那么进行读入。
24 {
25 On
26 }
27
28 On
29 }
30 catch ( int)
31 {
32 PRINTF ( "CATCH捕获错误 \n ");
33 continue;
34 }
35 }
36 m_bOnTimeChecking = TRUE;
37 On
38 }