select、poll、epoll的原理

一、select
select函数
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

参数和返回值

    maxfd:监视对象文件描述符数量。
    readset: 将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值。
    writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值。
    exceptset: 将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值。
    timeout: 调用select后,为防止陷入无限阻塞状态,传递超时信息。
    返回值:错误返回-1,超时返回0。因关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
 
四个宏定义
    FD_ZERO(fd_set* fdset): 将fd_set变量的所有位初始化为0。
    FD_SET(int fd, fd_set* fdset):在参数fdset指向的变量中注册文件描述符fd的信息。
    FD_CLR(int fd, fd_set* fdset):参数fdset指向的变量中清除文件描述符fd的信息。
    FD_ISSET(int fd, fd_set* fdset): 若参数fdset指向的变量中包含文件描述符fd的信息,则返回真。

select方法底层

我们可以把三个参数都看成三个数组:读事件数组,写事件数组,异常事件数组
我们需要维护这三个数组,这三个数组存的都是fd,socket的文件描述符
在timeout时间里,当有其中一个文件描述符有读写的io事件发生,内核就会把这三个数组都返回给我们用户
并且用户必须无差别轮询判断,到底哪个io发生了读写事件,再去进行对应的操作(accpet从数组中添加fd,disconnected从数组中删除fd等...)
然后我们再去把三个数组交给内核去管理
缺点
  1. 每次都要把这三个数组从用户态拷贝到内核态
  2. 每次都要无差别轮询这三个数组。我们如果有500个连接,那么就得轮询500次
  3. 支持最多1024个连接。
  4. 只有水平触法一种方式
 

 
二、poll
poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 
参数与返回值
   1)第一个参数:一个结构数组,struct pollfd结构如下:
  struct pollfd{
      int fd;              //文件描述符
      short events;    //请求的事件
      short revents;   //返回的事件
  };
    
    2)第二个参数, 要监视的描述符的数目
    3)第三个参数,超时时间
    4)返回值,返回的事件个数,=0超时,-1失败
 
与select相同,我们需要管理 fds链表,这个结构体里面存着我们感兴趣的事件和我们的对应的文件描述符
同样我们需要无差别地轮询 fds链表。但是没有最大链接数限制了
缺点:
  1. 与select不同的是不受最大连接数限制了
  2. 还是需要去无差别轮询,从内核态拷贝到用户态,再从内核态到用户态。效率低

三、epoll
 
int epoll_create(int size);
参数返回值:
1.size:最大连接数,即内核最大监听数目
2.返回值:epoll_fd epoll文件描述符
 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数返回值:
1.epfd:epoll_create获取到的文件描述符
2.op:操作,有三个宏定义
    EPOLL_CTL_ADD:注册新的fd到epfd中;(添加到红黑树上)
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;(譬如读/写事件修改)
    EPOLL_CTL_DEL:从epfd中删除一个fd;(从红黑树上剔除)
3.fd:要操作的文件描述符(server_fd和accpet_fd)
4.event:
    struct epoll_event {
        __uint32_t events; /* Epoll events */            epoll事件
        epoll_data_t data; /* User data variable */  用户传参
    };
    
    typedef union epoll_data {
        void *ptr;    // 用户自定义数据结构
        int fd;        // 文件描述符
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
5.返回值:0成功 -1失败
 
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
参数返回值
1.返回值:本次通知返回的事件数目
2.epfd:epoll_create返回值
3.events:返回的所有事件
4.maxevents:用户设置内核最大事件返回数目
5.timeout:超时,-1不超时
 
与前两个io多路复用不同的是,所有的用户要交给内核管理的文件描述符,都存在于内核,
当文件描述符有事件发生时,内核哪个文件描述符发生的events事件给用户,而不是将所有的文件描述符的所有events事件都交给用户
好处是:
  1. 用户不需要再无差别轮询,遍历的events数组都是发生了事件的
  2. 不需要频繁地从用户态拷贝大量文件描述符到内核态,再从内核态拷贝到用户态
  3. epoll底层内核用的是红黑树,效率极高
  4. epoll提供水平模式(用户不处理events会不断触发事件)和边缘触发模式(用户不处理本次事件,本次事件内核不再提醒)
  5. select和poll每次要拷贝数组到内核中。而epoll,所有的event都存在于内核的红黑树上,用户通过epoll_ctl接口来操作红黑树上的节点
 
用户通过epoll_create获取ep_fd,然后用户通过epoll_ctl来操作,用户填充epoll_event结构体,结构体中由epoll_data中的void* ptr给用户提供传参(一般传入一个结构体,结构体中有回调方法)。
epoll_ctl提供三种操作:注册/修改/删除
epoll_wait等待内核返回events事件
 
    
 
内核的epoll数据结构:红黑树+双向链表
每当我们用epoll_ctl注册时,都会在红黑树上添加一个event节点(红黑树极快定位哪个节点发生了读写事件)
当一个红黑树上的节点发生了事件,会被添加到双向链表中,拷贝给用户。
 
总结:
1)select和poll每次都要用户无差别轮询到底哪个发生了读写事件,内核只管通知,不会告诉用户到底哪个事件就绪了。无差别轮询:fd越多,遍历次数越多,效率越低
2)select和poll每次都要将数组从用户拷贝到内核中,用户态/内核态来回切换,内存拷贝,效率极低。
3)poll用的是链表形式,而select则用的是数组形式,select的监听数目受限制,最大1024,而poll不受到限制
4)epoll的所有fd都交给内核去管理,用户通过epoll_ctl接口去操作红黑树上的节点,内核返回的events都是就绪的events。效率因为充斥大量未就绪事件受影响
5)epoll由于所有fd都交给了内核去管理,少了用户态向内核态数据的拷贝,效率提高不少
 
虽然在epoll的性能最好,但是在连接数非常少并且连接都十分活跃的情况下,select/poll的性能可能比epoll高。因为epoll的通知机制需要很多函数的回调
 
最后附上一张epoll调用原理
 
 
 
 
 
 
 
 
发布了23 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/TanJiaLiang_/article/details/104287895