io複用與epoll模型詳解

unix下的五種io模型

1.阻塞式io
該io模型使得調用方阻塞等待數據到達,直到數據從內核拷貝到用戶空間後才返回。
2.非阻塞式io
該io模型不會阻塞,當內核沒有可讀的數據時,調用該函數會返回一個錯誤。當內核有數據可讀時,會等待數據從內核拷貝到用戶空間而後返回。
3.io複用
該io模型下進程阻塞在select/poll上,select/pool自己持有多個io描述符,當任何一個描述符觸發了你註冊的事件時,返回進程,咱們能夠經過select/poll得到當前須要處理的io描述符,進行處理。
注:io複用通常和非阻塞io一塊兒使用,緣由是咱們不但願在處理任何一個io描述符時進入阻塞狀態,而若是使用阻塞io很難避免這一點。(考慮當io可讀時,經過循環讀取io中全部的數據,阻塞io總會阻塞在最後一次read上)與此對應的,咱們可使用多線程+阻塞io模型來處理多個io,這兩種模型的優劣與合適的場景見後。
4.信號驅動式io
利用unix系統的信號機制通知進程有數據可讀。
5.異步io
異步io模型將數據從內核複製到用戶空間後才通知用戶。html

前四種模型是同步io模型,進程須要阻塞等待數據從內核拷貝到用戶空間。異步io模型不須要阻塞等待數據拷貝。linux

描述符讀就緒條件:

1.套接字接收緩衝區中的字節數大於等於套接字接收緩衝區低水位標記的當前大小。低水位標記能夠經過SO_RCVLOWAT套接字選項設置該套接字的低水位標記,對於TCP和UDP而言該值默認爲1。
2.鏈接半關閉,及接收到了FIN。這時read會返回0。
3.監聽套接字已完成的鏈接數不爲0。(但對該套接字調用accept可能阻塞。緣由以下:在監聽套接字爲阻塞模式下,accept調用在已完成隊列爲空時會阻塞。若是一已完成鏈接的套接字出發了監聽套接字可讀,可是在accept以前客戶端發送RST報文致使服務器內核從已完成隊列裏刪除了這一項,此時已完成隊列爲空,accept阻塞。所以io複用中監聽套接字也要是非阻塞的。)
4.套接字上有錯誤待處理,這時read返回-1,並將errno設置爲對應錯誤。ios

描述符寫就緒條件:

1.套接字發送緩衝區可用空間字節數大於等於套接字發送緩衝區低水位標記的當前大小,若是套接字是非阻塞的,咱們調用write函數則會返回一正值。低水位標記能夠經過SO_SNDLOWAT套接字選項設置套接字的低水位標記,對TCP和UDP而言該值一般默認爲2048。
2.該鏈接的寫半部關閉,對這樣的套接字寫會產生SIGPIPE信號。(通常咱們將SIGPIPE信號設置爲忽略,這樣寫操做將會返回-1並將errno設置爲EPIPE。)
3.非阻塞模式的connect套接字完成鏈接或者產生錯誤。
4.該套接字上有錯誤待處理。api

描述符異常就緒條件:

咱們僅關注帶外數據到達。數組

io複用模型之間的比較

linux上最多見的幾種io複用模型有:select,poll和epoll。
缺點1:select可以監視的文件描述符數量存在最大限值,一般是1024。
缺點2:採用輪詢方式掃描文件描述符,文件描述符越多,性能越差。
缺點3:每次select都須要將句柄數據結構從用戶空間複製到內核空間,開銷很大。
缺點4:select採用數組記錄監視的文件描述符,每次返回以後須要遍歷查找觸發的事件。
select採用水平觸發模式。緩存

poll採用鏈表結構保存文件描述符,所以沒有了最大監視限制,解決了缺點1。但其餘缺點依然存在。服務器

epoll解決了以上問題,每次註冊新的事件時,epoll會將fd拷貝進內核,而不是在epoll_wait時候從新拷貝。解決了缺點1,3。
epoll在內核中維護了一顆紅黑樹結構以存儲監視的fd,而且爲fd讀寫事件註冊回調函數,當fd讀寫事件發生時調用回調函數,會將對應的fd加入到一個雙向鏈表中。所以epoll_wait不須要輪詢fd,只須要定時去查看就緒雙向鏈表,當有觸發fd時就將對應的結構拷貝到用戶空間並返回。所以解決了缺點2,4。數據結構

下面詳細說說epoll的ET和LT模式。
LT模式及水平觸發模式,只要fd當前處於可讀/可寫狀態,則每次epoll_wait都會觸發對應的事件,所以一旦咱們沒有要寫的數據,就要從epoll中取消關注寫事件,防止無效觸發。
ET模式及邊沿觸發模式,對於讀來講,---update---,若是在此次你沒有讀完全部數據,就只有等下次有新數據到來才能繼續讀。這在業務層面可能會產生bug,假如客戶發過來一個請求,等獲得響應後才繼續發送數據,這時因爲這個請求在本次讀取過程當中沒有讀取完畢,服務器沒法識別本請求,故沒法產生響應,這樣服務器和客戶端停滯在這裏。對於寫來講,---update---。能夠看出來,ET模式應該使用非阻塞套接字,在每次讀時讀完全部數據,每次寫時要麼填滿發送緩衝區,要麼寫完當前全部數據。ET模式的寫事件觸發像是一種惰性觸發,不須要你在沒有要寫的數據時取消關注寫事件。
這裏看下面這個問題:https://www.zhihu.com/questio...
說明:不管是ET模式仍是LT模式,epoll內部維護的可讀/可寫狀態是同樣的,只是觸發的準則不一樣。(這裏的觸發能夠理解爲將fd相關結構加入就緒雙向列表)所以當一個可讀事件被觸發,其返回的狀態多是可讀可寫的,可是本次fd相關結構加入就緒列表這一動做不是因爲可寫事件而發生。對方斷開鏈接,可讀事件被觸發是預料以內的,可是返回的狀態是可讀可寫,爲何此時可寫呢?請看描述符寫就緒條件第2/4條,此時它的狀態確實是可寫,儘管本次返回不是因爲其可寫而觸發。
2020年3月11日更新:發現對ET模式什麼時候觸發的理解仍是不夠,準備查詢資料或者源代碼。多線程

再看第二個問題:LT模式可使用阻塞套接字麼?
見:https://www.zhihu.com/questio...
LT模式下也不能使用阻塞套接字,緣由在上述問題的回答中不少人都回答了。io複用返回的可讀不是肯定性的。(監聽套接字完成隊列非空,返回可讀,結果在讀事件處理以前客戶端發送RST致使該隊列中的鏈接關閉,這時候用accept去接收阻塞的監聽套接字就會阻塞。另外套接字接收緩衝區有數據到達,觸發可讀,結果在讀事件處理以前,經過校驗的方式發現接收緩衝區的數據有誤,因而丟棄該數據,這時候read一個阻塞套接字仍舊會阻塞。)對於寫來講卻是能夠根據接收緩衝區的低水位標記來發送數據,只要發送的數據小於該標記的值,理論上就不會阻塞,可是有沒有什麼其餘的坑目前不知道。
綜上,LT模式下也要使用非阻塞套接字。app

api

頭文件:<sys/epoll.h>
int epoll_create(int size);
Linux 2.6.8以後size參數被忽略(可是必須被設置爲大於0)。返回值是一個文件描述符,該描述符持有一個epoll實例,以後的epoll函數都依據於本描述符使用該epoll實例。當不須要時須要調用close函數關閉該文件描述符。
返回值:成功則返回非負文件描述符,失敗則返回-1並設置errno。
注:kernel 2.6.27加入了一個新函數epolL_create1,該函數能夠對返回的文件描述符設置O_CLOEXEC選項。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event\* 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_data_t data;
};

向epfd持有的epoll實例添加,修改或刪除關注的文件描述符,第二個參數op指示了本次要作的操做:
EPOLL_CTL_ADD 添加
EPOLL_CTL_MOD 修改
EPOLL_CTL_DEL 刪除
第三個參數是對應的文件描述符,第四個參數記錄了具體關注的事件以及用戶定義的數據。具體關注的事件見:http://www.man7.org/linux/man...
重點看這幾個:
EPOLLIN 可讀

EPOLLOUT 可寫
這兩個最經常使用,咱們很少介紹。

EPOLLRDHUP(since Linux 2.6.17)
當本端收到FIN字節或者本端調用shutdown(SHUT_RD)觸發該事件,這個狀況能夠用read返回0檢測出來。同時也會觸發EPOLLIN。聽說不是全部內核都支持。

EPOLLPRI
帶外數據到達觸發該選項。

EPOLLET
觸發ET模式,epoll默認是LT模式的。

EPOLLHUP
文件描述符綁定的套接字被掛斷,有如下幾種狀況:
1.本地調用shutdown(SHUT_RDWR)
2.本地調用shutdown(SHUT_WR)而且收到了FIN。
3.接收到了對端發送的RST。
注:被epoll處理的套接字不能被close掉,否則會被epoll識別出來並返回錯誤。
本項的說明來自連接:https://blog.csdn.net/zhouguo... 不保證正確性,需進一步測試。

EPOLLERR
套接字出錯,例如向一個遠端close的套接字寫數據。
linux manual 建議能夠不將EPOLLHUP和EPOLLERR加入監聽,由於能夠根據讀寫返回值以及其餘信息判斷出這些標註的狀況。

返回值:成功則返回0,出錯返回-1並設置errno。

BUGS
在kernel 2.6.9以前,調用epoll_ctl解除文件描述符的監視時,第三個參數不可被設置爲nullptr,儘管這種狀況下這個參數沒有意義。

int epoll_wait(int epfd, struct epoll_event\* events, int maxevents, int timeout);
阻塞等待註冊在epfd綁定的epoll上的事件,參數timeout表示最長阻塞的時間,單位爲ms,在這以前有時間發生則馬上返回,若是阻塞時間達到timeout且沒有事件發生,也會返回。timeout設置爲-1表示不設置此項,默認永遠阻塞。
發生事件對應的epoll_event會被填充在events中。
epoll_wait返回有三種狀況:
1.有事件發生。
2.timeout被設置且時間到達。
3.被信號中斷。
注:使用epoll_pwait能夠設置阻塞期間阻塞的信號。

返回值:無出錯返回值大於等於0,出錯則返回-1並設置errno。

example
#include <iostream>
#include <sys/epoll.h>
#include <cassert>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <vector>
#include <string>
#include <unistd.h>
#include <errno.h>

struct tconnection {
    int fd;
    std::string recv_content;
};

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(listenfd > 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    int result = inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
    assert(result > 0);
    server_addr.sin_port = htons(12345);
    server_addr.sin_family = AF_INET;

    socklen_t len = sizeof(server_addr);
    result = bind(listenfd, (struct sockaddr*)&server_addr, len);
    assert(result == 0);
    result = listen(listenfd, 1024);
    assert(result == 0);

    int epfd = epoll_create(10);
    assert(epfd >= 0);
    epoll_event ev;
    ev.events = EPOLLIN;
    tconnection* ptr = new tconnection();
    ptr->fd = listenfd;
    ev.data.ptr = ptr;
    result = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
    assert(result == 0);

    std::vector<epoll_event> evs;
    evs.resize(1024);
    while(true) {
      int ready = epoll_wait(epfd, &*evs.begin(), evs.size(), 5000);
      std::cout << "epoll wake up." <<std::endl;
      assert(ready != -1);
      for(int i = 0; i < ready; ++i) {
        tconnection* ptr = (tconnection*)evs[i].data.ptr;
        if(ptr->fd == listenfd) {
          int clientfd = accept(listenfd, nullptr, nullptr);
          std::cout << "new connection.\n";
          assert(clientfd > 0);
          tconnection* ptr = new tconnection();
          ptr->fd = clientfd;
          ev.data.ptr = ptr;
          result = epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
          assert(result == 0);
          continue;
        }
        if(evs[i].events & (EPOLLIN)) {
          char buf[1025];
          int n = -1;
          while((n = read(ptr->fd, buf, 1024)) > 0) {
            buf[n] = '\0';
            ptr->recv_content.append(buf);
            std::cout << "fd : " << ptr->fd << " get " << buf << std::endl;
          }
          assert(n >= 0);
          if(n == 0) {
            result = write(ptr->fd, ptr->recv_content.c_str(), ptr->recv_content.size());
            assert(result >= 0);
            result = epoll_ctl(epfd, EPOLL_CTL_DEL, ptr->fd, &ev);
            assert(result == 0);
            result = close(ptr->fd);
            assert(result == 0);
            delete ptr;
            std::cout << "connection close." <<std::endl;
          }
        }
      }
    }
    close(epfd);
    return 0;
}

注:這個例子僅爲了說明epoll api的使用,嚴格意義上並非正確的程序(可能阻塞在不少地方)。本例中使用了阻塞套接字(非阻塞套接字須要額外提供輸入和輸出緩衝,比較麻煩),且沒有關注可寫事件,直接在可讀事件中讀數據並緩存,當讀到0時將緩衝的數據write回對端。

相關文章
相關標籤/搜索