【IO专题】select, poll, epoll

本问题对应的项目链接:高频题回答模版训练

PS:这几个属于问的挺多的高频题,如果有条件,select, epoll 可以深入了解一下

1、I/O多路复用讲一下,有什么优缺点?

2、介绍一下select,poll和epoll,他们区别知道嘛?讲一下,它们算同步还是异步io?

3、关于 select, epoll 的应用,你了解哪些?(就是哪些工具用了这些机制)

共以下 1 个回答

  • 秊 永久会员 2023年7月10日 下午4:57

    1、I/O多路复用讲一下,有什么优缺点?

    I/O 多路复用是指,用一个线程同时监听多个文件描述符。它的主要思想是将多个 I/O 事件注册到一个统一的事件循环中,然后通过调用系统调用,常见的有 select,poll,epoll,等待其中任意一个事件发生。一旦有事件发生,系统调用就会返回并告诉应用程序哪些事件已经就绪,应用程序可以根据返回的结果进行相应的处理。

    它的优点很明显,使用单线程就可以监听多个 IO 事件,避免了线程切换的开销。同时利用了操作系统提供的系统调用,效率很高。但它不能充分利用多核处理器,并且使用不同的系统调用,优缺点也不同。

    2、介绍一下select,poll和epoll,他们区别知道嘛?讲一下,它们算同步还是异步io?

    select 就是将在用户态对文件描述符数组的遍历操作,交给操作系统在内核态去进行。操作系统会筛选出可以进行读写的文件描述符并做个标记,然后将其「个数」通过函数返回值告知用户层,用户层此时再遍历这个文件描述符数组,找出做过标记的文件描述符即可。

    select 最早实现了一个线程处理多个文件描述符的功能,减少了系统调用的开销。但是整个过程会发生文件描述符数组从用户态到内核态的来回拷贝,高并发场景下资源消耗多。并且 select 最多只能监听 1024 个文件描述符。

    poll 相较于 select,主要是取消了 select 只能监听 1024 个文件描述符的限制,也就是无限制。

    epoll 针对 select,poll 的缺点做了相对应的改进。比如 epoll 会在内核态中保存文件描述符的集合,避免每次调用时都拷贝;并且 epoll 只会将发生 IO 事件的文件描述符返回给用户,省去了用户层的遍历操作;除此之外,epoll 将原先对文件描述符集合的轮询优化为了基于 IO 事件的回调,整体效率不会随着文件描述符的增多而降低,效率远高于 select 和 poll。不过 epoll 只能在 linux 下使用。

    它们都算是同步 IO,因为程序在执行这三种系统调用函数时,都需要阻塞等待直到 I/O 操作完成。

    3、关于 select, epoll 的应用,你了解哪些?(就是哪些工具用了这些机制)

    比如 Redis 是单线程却还那么快,原因就是 Redis 借用了 Linux 的 IO 多路复用机制。该机制中的内核会有多个监听套接字和已连接套接字,同时内核会一直监听这些套接字的连接请求或数据请求,一旦有请求到达,就会将其交给 Redis 线程处理。

    其它的比如 Nginx,Netty 这种高性能网络应用框架,底层都会用到 IO 多路复用来提高性能。

    4、demo

    select 使用示例

    // 数据结构 (bitmap)
    typedef struct {
        unsigned long fds_bits[__FDSET_LONGS];
    } fd_set;
    
    // API
    FD_ZERO(int fd, fd_set* fds)   // 清空集合
    FD_SET(int fd, fd_set* fds)    // 将给定的描述符加入集合
    FD_ISSET(int fd, fd_set* fds)  // 判断指定描述符是否在集合中 
    FD_CLR(int fd, fd_set* fds)    // 将给定的描述符从文件中删除  
    
    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事件
          }
        }
      }
    
    

    poll 使用示例

    // 数据结构
    struct pollfd {
        int fd;                         // 需要监视的文件描述符
        short events;                   // 需要内核监视的事件
        short revents;                  // 实际发生的事件
    };
    
    // 先宏定义长度
    #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;       
             }   
          }
        }
      }
    
    

    epoll 使用示例

    // 数据结构: 每一个epoll对象都有一个独立的eventpoll结构体, 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
    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); // 负责把 socket 增加、删除到内核红黑树
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); // 负责检测可读队列,没有可读 socket 则阻塞进程
    
    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;
    }
    
    0 赞同 0 条回复

# 回答此问题

后才能回答