epoll函数及三种I/O复用函数的对比

 

epoll函数

#include <sys/epoll.h>
int epoll_create(int size)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)

Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll 。

1  epoll的使用方法

    Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll,epoll是linux特有的I/O复用函数 。

epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:
所用到的数据结构:
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 */

};
结构体epoll_event 被用于注册所感兴趣的事件和回传所发生就绪(待处理)的事件,而epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可能的取值为:

 

EPOLLIN:表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读;
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符有事件发生;

所用到的函数:
1)、epoll_create函数
函数声明:int epoll_create(int size)
size参数其实并不起作用,它只是给内核一个提示,告诉它事件表需要多大,该函数的返回值标识着这个内核的epoll资源,所有对epoll的操作都是基于该返回值的。

  epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select那样每次都要重复传入文件描述符集或事件集。epoll_create创建返回的描述符用来唯一表示内核中的这个事件表
2)、epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符
op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功则返回0,不成功则返回-1并设置errno。
3)、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout) //返回发生事件数。
它在一段超时时间内等待一组文件描述符上的事件。

成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。

重点理解:epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(有epfd参数指定)中复制到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到就绪事件。而不像select和poll的数组参数那样既用于传入用户注册的时间,有用于输出检测到的就绪事件。
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传就绪(代处理)事件的数组
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;

1 首先通过epoll_create(int maxfds)来创建一个epoll的句柄,其中maxfds为你的epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作都将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄
2 之后在你的网络主循环里面,调用epoll_wait(int epfd, epoll_event events, int max_events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait函数操作成功之后,events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout参 数指示 epoll_wait的超时条件,为0时表示马上返回;为-1时表示函数会一直等下去直到有事件返回;为任意正整数时表示等这么长的时间,如果一直没有事 件,则会返回。一般情况下如果网络主循环是单线程的话,可以用-1来等待,这样可以保证一些效率,如果是和主循环在同一个线程的话,则可以用0来保证主循 环的效率。epoll_wait返回之后,应该进入一个循环,以便遍历所有的事件

2 Epoll模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤如下:

(a) 使用epoll_create()函数创建文件描述,设定可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket的描述符ListenSock,并将该描述符设定为非阻塞模式,调用Listen()函数在该套接字上侦听有无新的连接请求,在epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()来注册事件,最后启动网络监视线程
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工作方式为epoll_ET。
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。

3 epoll的工作模式

令人高兴的是,linux2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦的是epoll有2种工作方式:LT和ET
LT(level triggered)是缺省的工作方式并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered) 是高速工作方式,只支持no-block socket。 在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll_wait告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的 就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如 你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html,在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了

EPOLLONESHOT事件

    为了保证socket连接在任一时刻只被一个线程(进程)处理,可以使用epoll的EPOLLONESHOT事件。

     对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常时间,且只触发一次。除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket是,其他线程是不可能有机会处理该socket的。

 

4 三种I/O复用(select poll epoll)的微小区别

1  select的参数fd_set没有将文件描述符和事件绑定,只提供了三种类型的文件描述符集来分别表示输入和输出可读、可写及异常处理,导致它不能处理更多类型时间。
   另一方面,内核对fd_set集合的在线修改,应用程序下次调用select前不得不重置三个描述符集合。
2 poll的参数类型pollfd则把描述符和时间都定义其中,任何时间同一处理。从而使编程接口简洁得多。
   并且内核每次修改的pollfd结构体重的revents成员,而events成员保持不便,因此下次调用epoll是应用程序无需重置pollfd类型的事件集参数。
3 由于每次select和poll调用都返回整个用户注册事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符 的时间复杂度为O(n)
4 epoll_wait系统调用的events参数仅返回就绪的时间,这使得应用程序索引就绪文件描述符的时间复杂度为O(1)

5  为什么select落后

首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在我用的内核中,该值是1024,搜索内核源代码得到:
include/linux/posix_types.h:
#define __FD_SETSIZE 1024 //centos6.5
也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者同时检测1025个句柄的可写状态也是不可能的。
其次,内核中实现select是使用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数的执行时间与FD_SET中句柄的个数有一个比例关系,即select要检测的句柄数越多就会越费时。当然,在前文中我并没有提及poll方法,事实上用select的朋友一定也试过poll,我个人觉得select和poll大同小异,个人偏好于用select而已。

6  epoll的优点

<1> 支持一个进程打开大数目的socket描述符(FD)

select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置, 默认值是1024。对于那些需要支持上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样 会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步高效,所以这也不是一种完美的方案。                                                                                               不过epoll 没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于select 所支持的1024。举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

<2> IO效率不随FD数目增加而线性下降

传统select/poll的另一个致命弱点就是当你拥有一个很大的socket集合,由于网络得延时,使得任一时间只有部分的socket是"活跃" 的而select/poll每次调用都会线性扫描全部的集合(轮询的方式),导致效率呈现线性下降。                                                                      但是epoll不存在这个问题它只会对"活跃"的socket进 行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的(回调方式),内核检测到见许的文件描述符是,就出发回调函数,回调函数将该文件描述符上对应的时间插入内核就绪事件队列,内核最后在适当的时机将该就绪时间队列中的内容拷贝到用户空间。但是,当活动连接比较多时,epoll_wait的效率未必比select和poll高,因为此时回调函数被触发的平凡。所以,epoll_wait适合连接数多,但活动连接数较少的情况

<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网卡驱动架构。

 

posted @ 2014-05-02 21:42  botou  阅读(537)  评论(0编辑  收藏  举报