IO復用 知識點梳理

  • 2019 年 11 月 1 日
  • 筆記

IO復用

程序可以同時監聽多個fd

場景

  • 需要同時監聽多個socket
  • 需要同時監聽socket和用戶輸入
  • 需要同時處理監聽socket和連接socket
  • 需要同時處理TCP和UDP
  • 需要同時監聽多個端口

socket就緒

  • 可讀
  • 內核緩衝區收到的數據>=其低水位標誌
  • 對方關閉連接
  • socket上有新的連接請求
  • socket上有未處理的錯誤
  • 可寫
  • 發送緩衝區的位元組數大於等於SO_SNDLOWAT
  • 關閉寫操作
  • 非阻塞connect連接成功或失敗
  • socket上有未處理的錯誤
  • 異常
  • socket接收到帶外數據

XMind: ZEN – Trial Version

select

select API模型:

       int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 用三個fd數組分別表示要監聽可讀、可寫、異常事件的描述符數組。

readfds、writefds、exceptfds這三個數組既是輸入參數,也是輸出參數。 它們也用於內核空間想用戶空間傳遞就緒的文件描述符。

select使用模型:

          while(1) {                FD_ZERO(&rfds);                FD_ZERO(&wfds);                FD_SET(fd0, rfds); //                FD_SET(fd1, wfds); //                ret = select(MAX_FDS+1, &rfds, &wfds, &efds, &tv);                if (FD_ISSET(fd0, &rfds)) {                    ...                } else (FD_ISSET(fd1, &wfds)) {                    ...                }            }     

select調用時,會通過三個數組參數把要監聽的文件描述符(和對應的事件)傳遞給內核。

select因有就緒事件而返回時,內核再把相應就緒的文件描述符通過三個數組返回。

此時程序需要遍歷監聽文件描述符,判斷其是否在相應的就緒fds中。

缺陷

select模型有如下缺陷

  1. 因為只有一個函數,所以每次調用文件都需要把監聽的文件描述符這些數據從用戶空間拷貝到內核空間。
  2. 由於rfds、wfds、efds既是入參也是出參,所以每次調用select都需要重新設置三個數組。
  3. 索引就緒fd時,仍然需要遍歷所有監聽的fds,做FD_ISSET
  4. 由fd_set能容納的文件描述符數量限制為FD_SETSIZE,這個值被設置為1024
  5. select只支持 可讀、可寫、異常 三類事件。

poll

poll API原型:

     #include <poll.h>         int poll(struct pollfd fds[], nfds_t nfds, int timeout);               struct pollfd {             int    fd;       /* file descriptor */             short  events;   /* events to look for */             short  revents;  /* events returned */         };

poll函數講描述符和事件區關聯起來了。 同時講註冊到事件和就緒到事件區分開來。 內核只需要修改revents。

poll 使用:

int main() {      int ret = 0;      struct pollfd pollfds[1];      char buf[10];      pollfds[0].fd = 1;      pollfds[0].events = POLLIN;        while(1) {          memset(buf, 0, sizeof(buf));          ret = poll(pollfds, 2, 0);          if (pollfds[0].revents == POLLIN) {              scanf("%s", buf);              printf("buf:%sn", buf);          }      }      return 0;  }       

可以看出,相比select函數,poll把監聽描述符和事件到設置從for循環中移動出來了。

優缺點

相對select的改進

  1. 相對於select,不必每次調用前都重置一次監聽參數pollfds,
  2. 相對於select,能夠監聽更多都事件類型。

仍然存在都不足:

  1. 因為只有一個函數,所以每次調用文件都需要把監聽的文件描述符這些數據從用戶空間拷貝到內核空間。
  2. 索引就緒fd時,仍然需要遍歷所有監聽的fds,

epoll

epoll 是linux特有的API

epoll API

int epoll_create(int size);  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
           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 */             };

與select和poll只有一個函數不同,epoll API提供了一組函數來實現IO復用。

epoll 使用模型

    struct epoll_event event;      struct epoll_event *events; // 用戶保存內核返回的就緒事件      int efd;      int listenfd = 0; // 監聽標準輸入        efd = epoll_create(0);        event.data.fd = listenfd;      event.events = EPOLLIN | EPOLLET;      ret = epoll_ctl (efd, EPOLL_CTL_ADD, listenfd, &event);        while(1) {         int n, i;          n = epoll_wait(efd, events, MAXEVENTS, -1);          for (i = 0; i < n; i++) {              if (events[i].events & EPOLLIN) {                  // handle EpollIN, 可以從events[i].data.fd獲取文件描述符            }      } 

優缺點

epoll所做的改進

  1. epoll 提供了一整組函數, 將監聽數據(epoll_ctl)的傳遞和監聽本身(epoll_wait)分開了。調用epoll_wait時,不需要在將要監聽都文件描述符、事件等從用戶空間拷貝到內核空間。
  2. epoll事件返回了就緒到文件描述符和對應到事件,因而索引就緒事件時不需要遍歷所有監聽事件。事件複雜度變為O(1)。
  3. 在內部實現上,epoll使用回調取代了select和poll到輪詢機制,極大提升了性能。

其他

LT和ET

文件描述符的操作有LT和ET兩種方式,epoll兩種模式都支持。

  • LT:電平觸發
    epoll將就緒事件通知應用程序後,應用程序可以不用立即處理。 下次調用epoll_wait的時候,還會繼續通知。    
  • ET: 邊緣觸發
    epoll將就緒事件通知應用程序後,應用程序**必須**立即處理。 下次調用epoll_wait的時候,不會重複通知。    

舉個栗子,如果是ET模式,epoll_wait檢測到事件可讀性,通知應用程序後,應用程序就需要吧本次可讀讀數據都讀完。