在Socket编程时,为了处理大量客户的连接请求,需要使用非阻塞I/O和端口复用,select、poll和epoll是Linux API提供的I/O复用方式。其实在*nix下的网络并发方法向来不缺,比如典型的Apache模型(Process Per Connection,简称PPC),TPC(Thread Per Connection)模型,这两种模型思想类似,就是利用了多进程、多线程概念,让进来的每一个连接去干别的事情去。但是连接多了以后,首先需要较大的内存,且进程/线程切换开销会非常大,因此这类模型能接受的最大连接数都不会太高。
Linux 2.6中加入了epoll之后(据说Windows下使用的是IOCP,但是我没使用过),在高性能服务器领域中得到广泛的应用,主要原因就是高效。在讲epoll之前,我们先总结下select、poll,因为epoll其实也就是他们的增强版本,比如select是一个系统调用,而epoll是个模块,由三个系统调用组成,内核中由文件系统实现。
select
select的第一个参数为fdset集合中最大描述符值加1,select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被SET的描述符进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回,当轮询一遍后没有任何事件发生,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行下一次轮询,并将临时结果写到用户空间,然后返回。select返回后,需要逐一检查关注的描述符是否被SET(事件是否发生)。
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
1 | 参数 描述 |
为了维护fd_set类型的参数,会使用下面四个宏:FD_SET(), FD_CLR(), FD_ZERO() 和 FD_ISSET()。
1 | typedef struct fd_set |
select 函数监视的文件描述符分3类,分别是writefds,readfds、和exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者except)、或者超时(timeout指定等待的时间,timeout== NULL表示永远等待,timeout == 0表示不等待、立即返回,其他表示等待的时间)。当 select 函数返回后,可以通过遍历 fdset ,来找到就绪的描述符。
select 的一个优点就是跨平台,缺点就是单个进程能够监视的文件描述符的数量存在最大限制,linux下一般为1024,Windows下好像无此限制,虽然可以修改这一限制,但是这样也会造成效率低下。
运行过程:
1 | int sock; |
注意:每次select 有数据要遍历全部socket,每次select之前要重置fds的值。
poll
poll函数类似于 select,可用于任何类型的文件描述符,与 select 不同,poll不是为每个状态(可读性、可写性和异常状态)构造一个描述符集,而是构造一个pollfd 结构数组向内核传递需要关注的事件,故没有描述符个数的限制,每个数组元素指定一个描述符编号以及对其所关心的状态,pollfd中的events字段和revents字段分别用于标示关注的事件和发生的事件。
poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,poll返回后,需要对pollfd中的每个元素检查其revents值,来判断事件是否发生。
返回值:
-1:有错误产生
0:超时时间到,而且没有描述符有状态变化
>0:有状态变化的描述符个数
1 | int poll(struct pollfd fdarray[],nfds_t nfds,int timeout); |
1 | struct pollfd{ |
应将每个数组元素的events成员设置为下图所示的值。通过这些值告诉内核我们对该描述符关心的是什么。返回时,内核设置revents成员,以说明对于该描述符已经发生了什么事件。(注意,poll没有更改events成员,这与select不同,select修改其参数以指示哪一个描述符已准备好了。)
poll的events和revents标志:
timeout == -1 永远等待。当所指定的描述符中的一个已准备好,或捕捉到一个信号时则返回。如果捕捉到一个信号,则poll返回-1,errno设置为EINTR timeout == 0 不等待 timeout > 0 等待timeout毫秒,如果已超时但是还没有一个描述符准备好,则返回值是0。
运行过程(与select相似):
1 | struct pollfd fds[IN_FILES]; |
epoll
epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait 检查事件,epoll_wait 的第二个参数用于存放结果。
epoll与select、poll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。
epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像poll、select那样进行轮询检查。
系统调用:
1 | //创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。 |
1 | /* |
struct epoll_event结构如下:
1 | typedef union epoll_data { |
`
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
1 | /* |
运行过程:
1 | for (n = 0; n < nfds; ++n) { |
可简单归结为:
1 | int fd = epoll_create(xxA); //xxA可监听的socket |
epoll_wait返回的都是有效数据,可直接从struct epoll_events[]中获取事件,效率高。每次取事件后,要重新注册此socket的事件epoll(epoll_ctl)。
参考资料:
《UNIX环境高级编程》
http://zh.wikipedia.org/wiki/Select_(Unix)
http://zh.wikipedia.org/wiki/Epoll
http://www.cnblogs.com/xuxm2007/archive/2011/08/15/2139809.html