首页 > 代码库 > select, poll, epoll区别详解(一)
select, poll, epoll区别详解(一)
1 select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
1.1 参数说明
第一个参数nfds为fdset集合中最大描述符值加1,fdset是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位代表其对应的描述符是否需要被检查。第二三四个参数表示需要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需要重新初始化fdset。
第五个参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。
select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被SET的描述符调用进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回;当轮询一遍后没有任何事件发生时,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行一次轮询,并将临时结果写到用户空间,然后返回。
select返回后,需要逐一检查关注的描述符是否被SET(事件是否发生).
2 poll函数
struct pollfd { int fd; //文件描述符 short events; //要求检测的事件掩码 short revents; //返回的事件掩码 } typedef unsigned long nfds_t; int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.1 参数说明
poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制。pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。第一个参数用于存放需要检测其状态的socket描述符;每当调用这个函数之后,系统不会清空这个数组,操作起来比较方便;特别是对于socket连接比较多的情况下,在一定程度上可以提高处理的效率;这一点与select()函数不同,调用select()函数之后,select() 函数会清空它所检测的socket描述符集合,导致每次调用select()之前都必须把socket描述符重新加入到待检测的集合中;因 此,select()函数适合于只检测一个socket描述符的情况,而poll()函数适合于大量socket描述符的情况.
poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
poll返回后,需要对pollfd中的每个元素检查其revents值,来得知事件是否发生。
第二个参数用于标记数组fds中的结构体元素的总数量。
第三个参数是poll函数调用阻塞的时间,单位为毫秒。
2.2 返回值
>0: 数组fds中准备好读、写或出错状态的那些socket描述符的总数量.==0: 数组fds中没有任何socket描述符准备好读、写,或出错;此时poll超时,超时时间是timeout毫秒;换句话说,如果所检测的 socket描述符上没有任何事件发生的话,那么poll()函数会阻塞timeout所指定的毫秒时间长度之后返回,如果timeout==0,那么 poll() 函数立即返回而不阻塞,如果timeout==INFTIM,那么poll() 函数会一直阻塞下去,直到所检测的socket描述符上的感兴趣的事件发 生是才返回,如果感兴趣的事件永远不发生,那么poll()就会永远阻塞下去.
-1: poll函数调用失败,同时会自动设置全局变量errno.
如果待检测的socket描述符为负值,则对这个描述符的检测就会被忽略,也就是不会对成员变量events进行检测,在events上注册的事件也会被忽略,poll()函数返回的时候,会把成员变量revents设置为0,表示没有事件发生。
另外,poll() 函数不会受到socket描述符上的O_NDELAY标记和O_NONBLOCK标记的影响和制约,也就是说,不管socket是阻塞的还是非阻塞 的,poll()函数都不会收到影响;而select()函数则不同,select()函数会受到O_NDELAY标记和O_NONBLOCK标记的影 响,如果socket是阻塞的socket,则调用select()跟不调用select()时的效果是一样的,socket仍然是阻塞式TCP通信,相反,如果socket是非阻塞的socket,那么调用select()时就可以实现非阻塞式TCP通信。
所以poll() 函数的功能和返回值的含义与 select() 函数的功能和返回值的含义是完全一样的,两者之间的差别就是内部实现方式不一样,select()函数基本上可以在所有支持文件描述符操作的系统平台上运 行(如:Linux 、Unix 、Windows、MacOS等),可移植性好,而poll()函数则只有个别的的操作系统提供支持(如:SunOS、Solaris、AIX、HP提供 支持,但是Linux不提供支持),可移植性差。
2.3 事件标记
经常检测的事件标记: POLLIN/POLLRDNORM(可读)、POLLOUT/POLLWRNORM(可写)、POLLERR(出错)。如果是对一个描述符上的多个事件感兴趣的话,可以把这些常量标记之间进行按位或运算。例如:对socket描述符fd上的读、写、异常事件感兴趣,就可以这样做:struct pollfd fds;
fds[nIndex].events=POLLIN | POLLOUT | POLLERR;
当 poll()函数返回时,要判断所检测的socket描述符上发生的事件,可以这样做:
struct pollfd fds;
检测可读TCP连接请求:
if((fds[nIndex].revents & POLLIN) == POLLIN){//接收数据/调用accept()接收连接请求}
检测可写:
if((fds[nIndex].revents & POLLOUT) == POLLOUT){//发送数据}
检测异常:
if((fds[nIndex].revents & POLLERR) == POLLERR){//异常处理}
3 epoll函数
3.1 参数说明
epoll主要包含epoll_create, epoll_ctl以及epoll_wait三个函数。 epoll_create函数创建epoll文件描述符,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好epoll句柄后,会占用一个fd值,在linux下可以通过"cat /proc/进程id/fd/" 查看。因此在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。使用例子:int epfd = epoll_create(int size);
epoll_ctl事件注册函数,控制对指定描述符fd执行op操作。
第一个参数是epoll_create()的返回值;
第二个参数表示动作,使用如下三个宏来表示:
EPOLL_CTL_ADD //注册新的fd到epfd中;
EPOLL_CTL_MOD //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL //从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事件。events可以是下面几个宏的集合:
EPOLLIN //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT //表示对应的文件描述符可以写;
EPOLLPRI //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR //表示对应的文件描述符发生错误;
EPOLLHUP //表示对应的文件描述符被挂断;
EPOLLET //将epoll设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT //只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,
//需要再次把这个socket加入到epoll队列里。
当对方关闭连接(FIN), EPOLLERR,都可以认为是一种EPOLLIN事件,在read的时候分别有0,-1两个返回值。
使用例子:
struct epoll_event ev;
ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符
ev.events=EPOLLIN|EPOLLET; //设置要处理的事件类型
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //注册epoll事件
epoll_wait等待epfd上的io事件,最多返回maxevents个事件。
第一个参数是epoll_create()的返回值,即生成的epoll专用的文件描述符;
第二个参数是用于回传代处理事件的数组;
第三个参数是每次能处理的事件数,用于告诉内核这个events有多大,maxevents的值不能大于创建epoll_create()时的size;
第四个参数是等待I/O事件发生的超时时间。单位是毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞。
epoll_wait运行的原理是:等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的socket fd和事件类型放入到events数组中。并且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd, EPOLL_CTL_MOD, listenfd, &ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。
3.2 返回值
epoll_create返回一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。epoll_ctl执行成功时返回0; 失败返回-1.
epoll_wait返回需要处理的事件数目,若返回0则表示已超时。
3.3 与select/poll的区别
在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。epoll与select、poll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。其次,epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。
epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像poll、select那样进行轮询检查。
3.4 epoll的优点
1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也 不是一种完美的方案。2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
3. 支持电平触发和边沿触发(只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发)两种方式,理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
4. mmap加速内核与用户空间的信息传递。epoll是通过内核与用户空间mmap同一块内存,避免了无谓的内存拷贝。
3.5 epoll的模型
EPOLL事件有两种模型 Level Triggered (LT) 和 Edge Triggered (ET):LT(level triggered,水平触发模式)是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。
select, poll, epoll区别详解(一)
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。