IO多路复用的优点

IO多路复用最大的优点在于 : 相比于阻塞式IO一个线程维护一个连接,其能够通过一个选择器查询线程处理很多个连接,使得系统不需要创建多余的线程,也不需要去维护这些线程,从而很大程度上减少了系统的开销。但是相对而言,在单线程上的工作效率会明显降低。

IO多路复用的三种实现方式

  • select
  • poll
  • epoll

    select函数接口

    1. #include <sys/select.h>
    2. #include <sys/time.h>
    3. #define FD_SETSIZE 1024
    4. #define NFDBITS (8 * sizeof(unsigned long))
    5. #define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
    6. // 数据结构 (bitmap)
    7. typedef struct {
    8. unsigned long fds_bits[__FDSET_LONGS];
    9. } fd_set;
    10. // API
    11. int select(
    12. int max_fd,
    13. fd_set *readset,
    14. fd_set *writeset,
    15. fd_set *exceptset,
    16. struct timeval *timeout
    17. ) // 返回值就绪描述符的数目
    18. FD_ZERO(int fd, fd_set* fds) // 清空集合
    19. FD_SET(int fd, fd_set* fds) // 将给定的描述符加入集合
    20. FD_ISSET(int fd, fd_set* fds) // 判断指定描述符是否在集合中
    21. FD_CLR(int fd, fd_set* fds) // 将给定的描述符从文件中删除

    select使用示例

    int main() {
    /*
     * 这里进行一些初始化的设置,
     * 包括socket建立,地址的设置等,
     */
    fd_set read_fs, write_fs;
    struct timeval timeout;
    int max = 0;  // 用于记录最大的fd,在轮询中时刻更新即可
    // 初始化比特位
    FD_ZERO(&read_fs);
    FD_ZERO(&write_fs);
    int nfds = 0; // 记录就绪的事件,可以减少遍历的次数
    while (1) {
      // 阻塞获取
      // 每次需要把fd从用户态拷贝到内核态
      nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
      // 每次需要遍历所有fd,判断有无读写事件发生
      for (int i = 0; i <= max && nfds; ++i) {
        if (i == listenfd) {
           --nfds;
           // 这里处理accept事件
           FD_SET(i, &read_fd);//将客户端socket加入到集合中
        }
        if (FD_ISSET(i, &read_fd)) {
          --nfds;
          // 这里处理read事件
        }
        if (FD_ISSET(i, &write_fd)) {
           --nfds;
          // 这里处理write事件
        }
      }
    }
    

    select缺点

  • 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

    poll函数接口

    poll与select相比,只是没有fd的限制,其它基本一样

    #include <poll.h>
    // 数据结构
    struct pollfd {
      int fd;                         // 需要监视的文件描述符
      short events;                   // 需要内核监视的事件
      short revents;                  // 实际发生的事件
    };
    // API
    int poll(struct pollfd fds[], nfds_t nfds, int timeout);
    

    poll使用示例

    // 先宏定义长度
    #define MAX_POLLFD_LEN 4096  
    int main() {
    /*
     * 在这里进行一些初始化的操作,
     * 比如初始化数据和socket等。
     */
    int nfds = 0;
    pollfd fds[MAX_POLLFD_LEN];
    memset(fds, 0, sizeof(fds));
    fds[0].fd = listenfd;
    fds[0].events = POLLRDNORM;
    int max  = 0;  // 队列的实际长度,是一个随时更新的,也可以自定义其他的
    int timeout = 0;
    int current_size = max;
    while (1) {
      // 阻塞获取
      // 每次需要把fd从用户态拷贝到内核态
      nfds = poll(fds, max+1, timeout);
      if (fds[0].revents & POLLRDNORM) {
          // 这里处理accept事件
          connfd = accept(listenfd);
          //将新的描述符添加到读描述符集合中
      }
      // 每次需要遍历所有fd,判断有无读写事件发生
      for (int i = 1; i < max; ++i) {     
        if (fds[i].revents & POLLRDNORM) { 
           sockfd = fds[i].fd
           if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
              // 这里处理read事件
              if (n == 0) {
                  close(sockfd);
                  fds[i].fd = -1;
              }
           } else {
               // 这里处理write事件     
           }
           if (--nfds <= 0) {
              break;       
           }   
        }
      }
    }
    

    poll缺点

  • 每次调用poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

    epoll函数接口

    #include <sys/epoll.h>
    // 数据结构
    // 每一个epoll对象都有一个独立的eventpoll结构体
    // 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
    // epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
    struct eventpoll {
      /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
      struct rb_root  rbr;
      /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
      struct list_head rdlist;
    };
    // API
    int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程
    

    epoll使用示例

    int main(int argc, char* argv[])
    {
     /*
     * 在这里进行一些初始化的操作,
     * 比如初始化数据和socket等。
     */
      // 内核中创建ep对象
      epfd=epoll_create(256);
      // 需要监听的socket放到ep中
      epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    
      while(1) {
        // 阻塞获取
        nfds = epoll_wait(epfd,events,20,0);
        for(i=0;i<nfds;++i) {
            if(events[i].data.fd==listenfd) {
                // 这里处理accept事件
                connfd = accept(listenfd);
                // 接收新连接写到内核对象中
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            } else if (events[i].events&EPOLLIN) {
                // 这里处理read事件
                read(sockfd, BUF, MAXLINE);
                //读完后准备写
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            } else if(events[i].events&EPOLLOUT) {
                // 这里处理write事件
                write(sockfd, BUF, n);
                //写完后准备读
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
      }
      return 0;
    }
    

    epoll缺点

  • epoll只能工作在linux下

    epoll LT 与 ET模式的区别

  • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

  • LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作
  • ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,或者遇到EAGAIN错误

    epoll应用

  • redis

  • nginx

    select/poll/epoll之间的区别

    IO多路复用 - 图1IO多路复用 - 图2