介绍:
   
在说select、poll和epoll之前,先说下IO多路复用机制,简单来说,就是服务端通过一个线程(或者进程)来维护多个Socket连接,多个连接共用一个阻塞对象,线程只需要在这个阻塞对象上等待,无需再轮询多个连接。当任何一个连接上有数据可以处理时,操作系统就会通知进程,进程就阻塞的状态返回,开始进行业务处理。
    select、poll和epoll都是IO多路复用机制的常见方式,通过这种方式,可以监控多个文件描述符,一旦某个描述符就绪,操作系统就能够通知进程进行相应的读写操作。
    
    select、poll和epoll都是同步IO。
   
    
    
    select:
   
    select函数允许内核挂起当前进程,当一个或多个IO事件发生后,再唤醒该进程。
    
    select的定义如下:
   
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
    
     参数说明:
    
    
    maxfd指待测试的文件描述符个数,它的值是待测试的最大文件描述符加 1。(因为文件描述符是从0开始的)
    
    readset是读描述符集合,代表内核可以在这些描述符上检测数据是否可读。
    
    writeset是写描述符集合,代表内核可以在这些描述符上检测数据是否可写。
    
    exceptset是异常描述符集合,代表内核可以在这些描述符上检测数据是否有异常发生。
    
    timeout指的是等待描述符上数据就绪的超时时间,是一个结构体,,定义如下:
   
struct timeval {
  long   tv_sec; /* seconds */
  long   tv_usec; /* microseconds */
};
如果把该参数设置为null,则表示一直等待,直到有数据就绪。如果把tv_sec和 tv_usec都设置为0,则表示不等待,还可以设置非0的值,表示等待的时间。
    该方法的返回值:
    
    如果有就绪的文件描述符,则返回就绪的文件描述符的数目,若超时则为0,若出错则为-1。
   
    select的缺点:
    
    每次调用select,都需要把fd的集合从用户态拷贝到内核态。而且select能够支持的文件描述符数量太小,默认只有1024个。
   
    
    
    poll:
   
    poll和select的功能类似,两个接口描述fd的方式不同,poll使用的是pollfd,而select使用的是fd_set。poll和select只是编程接口上的区别,内核的本质实现其实是差不多的。但是克服了select的文件描述符限制的缺陷,比select使用的更广。
    
    poll的定义如下:
   
int poll(struct pollfd *fds, unsigned long nfds, int timeout); 
函数需要三个入参,第一个参数是pollfd 的数组。pollfd的定义如下:
struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
 };
    fd代表文件描述符,events代表描述符上待检测的文件类型,可以表示多个不同的事件,revents存放每次检测完的结果。
    
    nfds描述的是数组 fds 的大小,简单说,就是向 poll 申请的事件检测的个数。
    
    timeout也是代表的等待的超时时间,如果大于0,表示等待指定的毫秒数后返回,等于0表示立即返回,小于0表示永远等待。
   
    函数返回值:
    
    如果有就绪的文件描述符,则返回就绪的文件描述符的数目,若超时则为0,若出错则为-1。
   
    
    
    epoll:
   
    epoll是对select和poll的改进,在面对大量文件描述符时,性能表现非常出色。
    
    epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait。
    
    
     epoll_create:
    
   
int epoll_create(int size);
int epoll_create1(int flags);
    epoll_create方法创建一个epoll实例,epoll实例用来调用epoll_ctl和epoll_wait。
    
    返回值: 若创建成功则返回一个大于0的值,表示epoll实例;若返回-1表示出错
    
    
     epoll_ctl:
    
   
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    调用epoll_ctl方法可以往epoll实例中增加或删除监控的事件,入参有四个,epfd是一个epoll实例描述字,就是通过epoll_create创建的epoll实例。op表示增加还是删除一个监控事件,fd是注册的事件的文件描述符。event表示的是注册的事件类型。
    
    返回值: 若成功返回0;若返回-1表示出错。
    
    
     epoll_wait:
    
   
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    epoll_wait和select、poll功能类似,等待事件的就绪,调用后进程会被挂起。
    
    入参有四个,第一个参数epfd是 epoll 实例描述字,events代表返回给用户空间需要处理的 I/O 事件,maxevents表示 epoll_wait 可以返回的最大事件值,timeout也是代表超时时间,如果为-1表示不超时,如果为0则是立即返回。
    
    返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1。
   
    select和poll提供的是水平触发(level-triggered)机制,而epoll除了提供水平触发机制外,还提供了性能更强的边缘触发(edge-triggered)机制。
    
    水平触发:当检测到某描述符事件就绪,服务器端需要不断的从epoll_wait中苏醒,直到内核缓冲区的数据被读取完成。水平触发是epoll默认的方式。
    
    边缘触发:当检测到某描述符事件就绪,服务器端只需要从epoll_wait中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
   
    如果用的是水平触发方式,当内核通知文件描述符可以读写时,接下来还可以继续检测它的状态,判断是否可以继续读写。
    
    但如果使用的是边缘触发方式,内核只会通知一次可以读写,而且还不知道能读写多少内容,所以收到通知后一般是去循环读取,以免失去读写机会。
    
    一般来说,边缘触发方式能够减少epoll_wait的系统调用,所以效率比水平触发要高。
   
epoll是Linux内核为处理大批量文件描述符而对poll进行的改进,算是select和poll的增强版本。在大量并发并且少量连接活跃的场景中能显著提高CPU的利用率,被称为解决C10K的利器。
 
