1. 程式人生 > >網路IO-select,poll,epoll分析

網路IO-select,poll,epoll分析

背景介紹

select,poll,epoll都是IO多路複用的機制。I/O多路複用就是通過一種機制,一個程序可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。

其實在對select,poll和epoll進行分析前,需要對linux系統產生的五種網路模式簡單介紹,但是由於我主要學習Java,對linux不臺熟悉,而且在剛開始學習網路IO時過多的糾結在同步與非同步阻塞與非阻塞上,因此在日後的學習中不再區分這些概念,對於linux系統的5種網路模式也一樣不再深究,只是區分不同網路模式在Java上的表現是否相同.現在學習select,poll和epoll也是因為了解到Netty解決了epoll的一個bug,順便深入學習一下.

select

函式介紹

函式名

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

引數及返回值介紹

函式引數介紹如下:

  1. 第一個引數maxfdp1指定待測試的檔案描述符個數,它的值是待測試的最大檔案描述符加1(因此把該引數命名為maxfdp1),檔案描述符0、1、2…maxfdp1-1均將被測試,因為檔案描述符是從0開始的.
  2. 中間的三個引數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指標。fd_set是檔案描述符集合
    .
  3. timeout是等待時間,告知核心等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。

實現原理

睡眠等待邏輯

  1. 將等待讀事件的fd_set從使用者空間拷貝到核心空間
  2. 遍歷fd_set,若某個檔案描述符有讀事件,直接返回;若檔案描述符沒有讀事件,則為當前process構建一個wait_entry節點,然後插入到被監控socket的sleep_list中
    process封裝為wait_entry後掛在sk的sleep_list上
  3. 遍歷fd_set結束後仍未返回,當前process睡眠

喚醒邏輯

  1. 若fd_set中有某個socket有資料可讀,則會喚醒該socket的sleep_list中正在睡眠的process
  2. process被喚醒後,再次遍歷fd_set,此時在fd_set必定有至少一個fd有讀事件

缺點

  1. 每次呼叫select()必須將三個fd_set從使用者空間拷貝到核心空間
  2. 為了減少資料拷貝帶來的效能損壞,核心對被監控的fd_set集合大小做了限制,並且這個是通過巨集控制的,大小不可改變(限制為1024)
  3. process被喚醒後,必須要再次遍歷fd_set,才能確定是哪個socket上有資料可讀

poll-雞肋

函式介紹

int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

pollfd封裝了檔案描述符等待的事件,相當於使用pollfd代替fd_set

優缺點

  1. 解決了select()的1024問題
  2. 其餘兩個關乎效能的問題沒有解決

epoll

解決問題思路

在計算機行業中,有兩種解決問題的思想:

[1] 電腦科學領域的任何問題, 都可以通過新增一個中間層來解決
[2] 變集中(中央)處理為分散(分散式)處理

我們就按照上述的兩個思想嘗試解決select剩下的兩個問題

變集中為分散解決fd_set拷貝問題

每次select都要將fd_set從使用者空間拷貝到核心空間中,但是fd_set的改變次數相較於select的執行次數是非常少的,因此我們可以將select()中的複製fd_set和遍歷fd_set兩個邏輯分開,每個邏輯由一個函式完成.(其實就是將fd_set的複製從集中複製變為分散增加)
epoll引入了int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)用於新增和刪除fd集合,具體的遍歷及等待邏輯是在int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)中完成

新增中間層解決process被喚醒後遍歷fd_set的問題

當process從睡眠中被喚醒時,雖然此時fd_set中至少會有一個fd有讀事件,但仍然需要再次遍歷fd_set,當fd_set很大時,遍歷的大多操作是無用的,因此如果我們能將就緒的fd放在單獨的列表中,就可以避免無效遍歷.

如何才能將就緒的fd放在單獨的ready_list中?
select()中是將process封裝為wait_entry放在socket的sleep_list中,如果我們將一個callback函式封裝為wait_entry,此函式的邏輯是將當前socket放置在ready_list中.那麼當socket有讀事件時,便會執行該函式,將socket放入ready_list

process的睡眠與喚醒問題?
有了ready_list後,process的睡眠和喚醒時機便很明顯.當ready_list為空時睡眠,ready_list非空時喚醒

如和將ready_list串聯在一起?
個人感覺是通過int epoll_create(int size);返回的fd將一切串聯在一起的.即process是睡眠在epoll_create()返回的fd上,根據ready_list的空與非空將process睡眠與喚醒.

函式介紹

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

int epoll_create(int size);

建立epoll的fd作為中間層並返回
這裡寫圖片描述

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

將被監聽的socket拷貝至核心空間,並且將callback封裝為wait_entry掛在socket的sleep_list上.

新增兩個被監聽的socket後
這裡寫圖片描述
再新增2個被監聽的socket後
這裡寫圖片描述

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

若呼叫epoll_wait()時無就緒的fd,則當前process會睡眠在中間層fd的sleep_list中
這裡寫圖片描述
若一段時間後,sk1和sk2都有讀事件到達,則分別會執行sk1和sk2的callback,將sk1和sk2放入中間層fd的read_list中,同時喚醒睡眠在中間層fd的process
這裡寫圖片描述
process被喚醒後,遍歷read_list,此時read_list中全是有讀事件的sk,不會產生無用遍歷

總結

  1. 此篇部落格只是介紹了下epoll解決select存在問題的大致思路,具體情況還請查詢相關資料
  2. 無epoll的ET和LT模式介紹.

參考