【面試】徹底理解 IO 多路複用

目錄

1、什麼是 IO 多路複用?
2、爲什麼出現 IO 多路複用機制?
3、IO 多路複用的三種實現方式
4、select 函數接口
5、select 使用示例
6、select 缺點
7、poll 函數接口
8、poll 使用示例
9、poll 缺點
10、epoll 函數接口
11、epoll 使用示例
12、epoll 缺點
13、epoll LT 與 ET 模式的區別
14、epoll 應用
15、select/poll/epoll 之間的區別
16、IO 多路複用完整代碼實現
17、高頻面試題

1、什麼是 IO 多路複用

「定義」

2、爲什麼有 IO 多路複用機制?

沒有 IO 多路複用機制時,有 BIO、NIO 兩種實現方式,但有一些問題

同步阻塞(BIO)

// 僞代碼描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  fds.append(client_fd)
  for (fd in fds) {
    // recv阻塞(會影響上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}
// 僞代碼描述
while(1) {
  // accept阻塞
  client_fd = accept(listen_fd)
  // 開啓線程read數據(fd增多導致線程數增多)
  new Thread func() {
    // recv阻塞(多線程不影響上面的accept)
    if (recv(fd)) {
      // logic
    }
  }  
}

同步非阻塞(NIO)

setNonblocking(listen_fd)
// 僞代碼描述
while(1) {
  // accept非阻塞(cpu一直忙輪詢)
  client_fd = accept(listen_fd)
  if (client_fd != null) {
    // 有人連接
    fds.append(client_fd)
  } else {
    // 無人連接
  }  
  for (fd in fds) {
    // recv非阻塞
    setNonblocking(client_fd)
    // recv 爲非阻塞命令
    if (len = recv(fd) && len > 0) {
      // 有讀寫數據
      // logic
    } else {
       無讀寫數據
    }
  }  
}

IO 多路複用(現在的做法)

fds = [listen_fd]
// 僞代碼描述
while(1) {
  // 通過內核獲取有讀寫事件發生的fd,只要有一個則返回,無則阻塞
  // 整個過程只在調用select、poll、epoll這些調用的時候纔會阻塞,accept/recv是不會阻塞
  for (fd in select(fds)) {
    if (fd == listen_fd) {
        client_fd = accept(listen_fd)
        fds.append(client_fd)
    } elseif (len = recv(fd) && len != -1) { 
      // logic
    }
  }  
}

3、IO 多路複用的三種實現方式


4、select 函數接口

#include <sys/select.h>
#include <sys/time.h>

#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)

// 數據結構 (bitmap)
typedef struct {
    unsigned long fds_bits[__FDSET_LONGS];
} fd_set;

// API
int select(
    int max_fd, 
    fd_set *readset, 
    fd_set *writeset, 
    fd_set *exceptset, 
    struct timeval *timeout
)                              // 返回值就緒描述符的數目

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)    // 將給定的描述符從文件中刪除

5、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事件
      }
    }
  }

6、select 缺點

7、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);

8、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;       
         }   
      }
    }
  }

9、poll 缺點

10、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 則阻塞進程

11、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;
}

12、epoll 缺點

13、epoll LT 與 ET 模式的區別

14、epoll 應用

15、select/poll/epoll 之間的區別

16、完整代碼示例

https://github.com/caijinlin/learning-pratice/tree/master/linux/io

17、高頻面試題

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KO0ogu-gTBF2fQiYC4rcDw