首页 > 代码库 > select、poll和epoll

select、poll和epoll

在单线程并发服务器中,select/poll/epoll可以高效的处理多个连接的数据,下面具体分析三者的区别。

1. select函数

函数原型:

int select(

    int nfds,  //fdset集合中最大描述符值加1

    fd_set *readfds,  //读事件文件描述符数组

    fd_set *writefds,  //写事件文件描述符数组

    fd_set *exceptfds, //错误事件文件描述符数组

    struct timeval *timeout //超时事件,该结构被内核修改,其值为超时剩余时间

);

 

其中 fdset是一个位数组,其大小限制为_FD_SETSIZE(1024)位数组的每一位代表的是其对应的描述符是否需要被检查。

常用的操作有

FD_ZERO(&set); /*将set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd在fdset集合中的状态是否变化返回整型,当检测到fd状态发生变化时返回真,否则,返回假(0)*/     
select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被set的描述符进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回;当轮询一遍后没有任何事件发生时,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行一次轮询,并将临时结果写到用户空间,然后返回。
 
2. poll
poll()系统调用是System V的多元I/O解决方案。
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组

pollfd结构体定义如下:
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中返回。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,我们可以设置events为POLLIN | POLLOUT。在poll返回时,我们可以检查对应于文件描述符请求的events结构体的revents中的标志。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

3. select/poll特点

传统的select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。
poll的执行分三部分:
(1)将用户传入的pollfd数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个O(n)操作。
(2)查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。 查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是O(n),这里面不包括等待时间。
(3)将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是O(n)。
4. epoll机制
Linux 2.6内核完全支持epollepoll的IO效率不随FD数目增加而线性下降。
epoll包括三个系统调用:epoll_create, epoll_ctl, epoll_wait。
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,内核实现中epoll是根据每个fd上面的callback函数实现的。只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会。
如果所有的socket基本上都是活跃的---比如一个高速LAN环境,过多使用epoll,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
int epoll_create(int size)

该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。

int epoll_ctl(
    int epfd,//由 epoll_create 生成的epoll专用的文件描述符
    int op,  //要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、
               //EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除
    int fd,   //关联的文件描述符
    struct epoll_event *event//指向epoll_event的指针
 )
用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
如果调用成功返回0,不成功返回-1
int epoll_wait(
    int epfd,//由epoll_create 生成的epoll专用的文件描述符
    struct epoll_event * events,//用于回传代处理事件的数组
    int maxevents,//每次能处理的事件数
    int timeout
//等待I/O事件发生的超时值
//为0的时候表示马上返回,
//为-1的时候表示一直等下去,直到有事件为止 //任意正整数的时候表示等这么长的时间,如果一直没有事件,则在超时后返回 //一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率 //如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率 )
5. epoll的优点:
<1>支持一个进程打开大数目的socket描述符(FD);
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候可以:
     (1) 可以修改这个宏然后重新编译内核,不过资料也同时指出,这样也会带来网络效率的下降
     (2) 可以选择多进程的解决方案,不过虽然linux上创建进程的代价比较下,但是仍旧是不可忽视的,所以也不是很完美的方案
epoll没有这样的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,具体数组可以查看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内核。如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
<3>使用mmap加速内核与用户空间的消息传递;
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,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次握手的数据包队列长度),也可以根据你平台内存大小动态调整。