select、poll和epoll

I/O复用:

  在一个进程或者多个进程的需要多个I/O,不能阻塞在一个I/O上而停止不前,而是用到I/O复用。进程预先告知内核需要哪些I/O描述符,内核一旦发现指定的一个或多个I/O条件就绪,则通知进程进行相应操作,这就是I/O复用。

使用场合:

1、客户处理多个描述符(交互式输入和网络套接字)

2、TCP服务器既处理监听套接字,又处理连接套接字

3、一服务器既处理TCP又处理UDP

4、一服务器要处理多个服务或多个协议

select函数:

允许进程指示内核等待多个事件中的任何一个发生,且只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

其中:maxfdp1表示探测描述符中的最大值加一(因为描述符从0开始,其表示个数),后面三个参数依次表示读、写和异常描述符集,最后一个表示等待时间。timeout:NULL 永远等待;正数,等待一段时间后返回;0,不等待,检查描述符后立即返回。

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);  
                                              返回:若有就绪描述符则为其数目,若超时为0,出错为-1

struct timeval
{
   long tv_sec;  //seconds
   long tv_usec;      //microseconds
};

void FD_ZERO(fd_set *fdset);    //clear all bits in fdset
void FD_SET(int fd, fd_set *fdset);   //trun on the bit in fdset 
void FD_CLR(int fd, fd_set *fdset);  //turn off the bit for fd in fdset
void FD_ISSET(int fd, fd_set *fdset);  //is the bit for fd on in fdset 

poll函数

  与select函数大致相同,不同在于select描述符最大个数FD_SETSIZE,poll可更大。且传递的结构不同,poll对每个描述符管理起来,select分别用三个数组管理起来。timeout:INFTIM永远等待,0立即返回,正数等待指定毫秒数返回。

#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);  //返回:若有就需描述符则为其数目,超时为0,出错为-1
struct pollfd
{
  int fd;  //descriptor to check
  short events;  //events of interest on fd
  short revents;  //events that pccurred on fd
};

 总结:参考http://www.open-open.com/lib/view/open1410403215664.html#articleHeader0

select缺点:1、单进程可监视文件描述符最大限制1024个,可更改。但select采用轮询方式扫描文件描述符,文件描述符数量越多性能越差(Linux内核中:#define _FD_SETSIZE 1024)

2、内核、用户空间内存拷贝,select需要赋值大量的句柄数据结构,产生巨大开销;

3、select返回整个句柄数组,应用程序需要遍历数组查找就绪文件描述符;

4、select水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将其桃枝进程

poll:相比select只是数据结构发生变化,用一个结构体数组来表示监视的文件描述符,每一个结构存储监视的文件描述符和其监视事件,并在其中返回监视结果。其监视文件数量没有限制。但是其他缺点和select一样。

例如:服务器需要支持100万并发连接,在_FD_SETSIZE为1024的情况下,我们至少需要创建1K歌进程才能实现100万的并发连接,除进程间上下文切换的时间开销,从内核、用户空间的内存拷贝,数组轮询等都是系统难以承受和实现的。因此基于select模型的服务器,要达到10万级别的并发访问控制,是很难完成的。

epoll

  就上面例子中,select/poll都是服务器进程每次都把这100万个连接告诉操作系统(从用户赋值句柄数据结构到内核),让操作系统内核查询这些套接字上是否有事件发生,该过程资源消耗较大,因此select/poll一般只能处理几千的并发连接。

epoll的设计和实现与select完全不同。epoll通过Linux内核中申请一个建议的文件系统(B+树),吧原先的select/poll分为:

1、epoll_creat()简历一个epoll对象(epoll文件系统中为这个句柄对象分配资源)

2、epoll_ctl向epoll对象中添加监视的描述符;

3、epoll_wait收集发生的事件的连接;

epoll实现思路:

  当某一进程调用epoll_creat方法,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关

struct eventpoll{
    ....
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
    ....
};

   每一个epoll对象都有一个eventpoll结构体,用于存放通过epoll_ctl方法将epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此重复添加的事件也可以通过红黑树而高效的识别出来。

  所有添加到epoll中的事件都会与设备(网卡)驱动程序简历回调关系,当相应的事件发生时会调用这个回调方法。该回调方法在内核中叫ep_poll_callback,它将发生的事件添加到rdlist双链表中。

对于每一个事件都会建立epitem结构体:

struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

  当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件赋值到用户态,同时将时间数量返回。

原文地址:https://www.cnblogs.com/weiyi-mgh/p/6885280.html