还是先思考几个问题:

1.select、poll、epoll是什么?作用是什么?为什么用它?

2.select、poll、epoll原理。

3.对比优缺点。

4.epoll如何实现高并发服务器。

 

在解释这几个问题之前,我们来先看一下,如果没有这几个复用管理,我们之前对tcp的套接字socket fd如何管理的呢?

如果有100条tcp连接,那么我们要高效的监控并读写这100条连接:

单线程:

  阻塞模式下单线程几乎是不可能完成监控+读写的。

  非阻塞模式下,单线程不停的遍历所有的socket fd来判断现在是否可以读写,这里处理的效率是非常低的,即使没有读写,线程也要不停的遍历,耗cpu。

多线程:

  阻塞模式下多线程处理,那么tcp连接非常多的时候,你的线程也会非常多,线程上下文切换也会非常频繁,效率低下。

  非阻塞模式也是一样,比阻塞模式效率更低,因为即使没有读写,也会空跑。

 

那么为了解决管理socket fd,内核为我们提供了管理,(select 、poll、epoll),好了在这种背景下我们引出了这几个多路复用。

 

第一个问题:

1.select、poll、epoll是什么?作用是什么?为什么用它?

其实在我们上面的分析中不难找出答案,如果你读懂了上面的分析的话。

是什么?

  多路复用。

作用是什么?为什么用它?

  内核帮应用程序管理大量socket fd状态。应用程序只需要调用系统提供的api即可,状态维护在内核中,降低开发难度。(当然你了解原理后,觉得自己实现的会更好,效率和安全性更高,你可以选择自己实现)

 

第二个问题:

2.select、poll、epoll原理。

select原理:

  支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。

select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。

api接口:  

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

  int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);// 阻塞函数,主要是使用该函数

  void FD_CLR(int fd, fd_set *set);
  int FD_ISSET(int fd, fd_set *set);
  void FD_SET(int fd, fd_set *set);
  void FD_ZERO(fd_set *set);

 

 poll原理:(跟select和epoll相比较其实是个比较鸡肋的东西,了解一下就好,主要是epoll和select要深入下)

  poll只是在select基础上对支持的最大fd数量做了改进,内核存储的是fd相应结构链表。从来解决最大数量问题,但是比较积累。不改变每次都遍历的方式,其实只改支持数量,也不会改善太多。

 

api接口:

  struct pollfd {

    int fd; /* file descriptor */
    short events; /* requested events */
    short revents; /* returned events */
  };

   int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  

epoll原理:

  内核中红黑树+双向链表

  红黑树存所有关心事件,双向链表存放准备就绪事件。每次获取可读写事件查询双向链表即可。

api接口:

  int epoll_create(int size);
  int epoll_create1(int flags);

  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  参数介绍:

    epfd:file descriptor epfd (epoll_create返回值)

    op:EPOLL_CTL_ADD\EPOLL_CTL_MOD\EPOLL_CTL_DEL

    fd:需要监控的文件描述符

    epoll_event:

      typedef union epoll_data {

        void *ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
      } epoll_data_t;

      

      struct epoll_event {
        uint32_t events; /* Epoll events */
        epoll_data_t data; /* User data variable */
      };

  

  int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

  int close(int fd);

  

第三个问题

3.对比优缺点:

select缺点:

  int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);// 阻塞函数,主要是使用该函数

  1)select的实现,在内核中它是被定义成一个数组的结构,数组肯定有大小,数组的大小被默认定义为1024,默认情况下只能支持1024个socket的管理。

  当然,可以修改内核中的这个个数宏,增大支持个数。

  2)select返回后,内核管理的这个fd_set会清空,下次调用select依然需要用户态拷贝数据(fd_set) 到内核态。

  3)select返回后,依然需要遍历fd_set来判断期望的socket fd是否发生我们关心的事件。

 

poll改善:

  poll内核用链表替换数组来解决支持fd上限1024问题。

  但高并发情况下select和poll支持并发有限,eg:c10k(1w左右并发)

 

为了解决并发问题,内核重新设计了机制,也就是epoll来解决高并发问题。

 

epoll通过红黑树+双向链表就解决上面的三个问题

  红黑树存放fd,fd上有事件发生时,通过回调把事件添加到双向链表中,epoll_wait获取双向链表中的事件。

 

第四个问题:

4.epoll如何实现高并发服务器。

 这个我改日再剖析,代码会更新在我的github上。